From 08abe9e704ead47703c60da729e900d713e0a299 Mon Sep 17 00:00:00 2001 From: semenshi-m Date: Thu, 28 May 2026 13:41:33 +0100 Subject: [PATCH 01/61] .NET: Add Foundry Toolbox MCP skills discovery sample (#6134) * feat: add Agent_Step26_FoundryToolboxMcpSkills sample Add a new sample demonstrating MCP-based skills discovery from a Foundry Toolbox endpoint using AgentSkillsProviderBuilder and AIContextProviders. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review comments for Step26 sample - Add Foundry-Features: Toolboxes=V1Preview header to MCP transport options, matching the Step25 pattern - Document skill://index.json prerequisite in README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 1 + ...gent_Step26_FoundryToolboxMcpSkills.csproj | 22 +++++ .../Program.cs | 93 +++++++++++++++++++ .../README.md | 32 +++++++ .../02-agents/AgentsWithFoundry/README.md | 1 + 5 files changed, 149 insertions(+) create mode 100644 dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj create mode 100644 dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs create mode 100644 dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index e2ae05c3b4..c6480cf02a 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -174,6 +174,7 @@ + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj new file mode 100644 index 0000000000..5243b24af0 --- /dev/null +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs new file mode 100644 index 0000000000..6efb8ce40e --- /dev/null +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Program.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Foundry Toolbox MCP Skills. +// +// Uses AgentSkillsProviderBuilder to discover MCP-based skills from a Foundry +// Toolbox endpoint and inject them as AIContextProviders so the agent can +// discover and use them at runtime. + +using System.Net.Http.Headers; +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using Microsoft.Agents.AI; +using ModelContextProtocol.Client; + +// --- Configuration --- +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; +string toolboxMcpServerUrl = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_MCP_SERVER_URL") + ?? throw new InvalidOperationException("FOUNDRY_TOOLBOX_MCP_SERVER_URL is not set."); + +// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. +// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid +// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. +TokenCredential credential = new DefaultAzureCredential(); + +using var httpClient = new HttpClient(new BearerTokenHandler(credential, "https://ai.azure.com/.default") +{ + InnerHandler = new HttpClientHandler(), +}); + +// --- Connect to the Foundry Toolbox MCP endpoint --- +await using McpClient mcpClient = await McpClient.CreateAsync( + new HttpClientTransport( + new HttpClientTransportOptions + { + Endpoint = new Uri(toolboxMcpServerUrl), + Name = "foundry_toolbox", + TransportMode = HttpTransportMode.StreamableHttp, + AdditionalHeaders = new Dictionary + { + ["Foundry-Features"] = "Toolboxes=V1Preview", + }, + }, + httpClient)); + +// --- Discover MCP-based skills --- +var skillsProvider = new AgentSkillsProviderBuilder() + .UseMcpSkills(mcpClient) + .Build(); + +// --- Create the agent --- +AIProjectClient aiProjectClient = new(new Uri(endpoint), credential); + +AIAgent agent = aiProjectClient.AsAIAgent( + options: new ChatClientAgentOptions + { + Name = "ToolboxMcpSkillsAgent", + ChatOptions = new() + { + ModelId = deploymentName, + Instructions = "You are a helpful assistant. Use available skills to answer the user.", + }, + AIContextProviders = [skillsProvider], + }); + +// --- Interactive prompt --- +Console.Write("User: "); +string? query = Console.ReadLine(); + +if (string.IsNullOrWhiteSpace(query)) +{ + Console.WriteLine("No input provided."); + return; +} + +Console.WriteLine($"Assistant: {await agent.RunAsync(query)}"); + +// --------------------------------------------------------------------------- +// DelegatingHandler: attaches a fresh Foundry bearer token to every request +// --------------------------------------------------------------------------- +internal sealed class BearerTokenHandler(TokenCredential credential, string scope) : DelegatingHandler +{ + private readonly TokenRequestContext _tokenContext = new([scope]); + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + AccessToken token = await credential.GetTokenAsync(this._tokenContext, cancellationToken).ConfigureAwait(false); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md new file mode 100644 index 0000000000..2e8efef8cc --- /dev/null +++ b/dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/README.md @@ -0,0 +1,32 @@ +# Foundry Toolbox MCP Skills + +This sample uses +`AgentSkillsProviderBuilder` to discover MCP-based skills from a Foundry Toolbox endpoint +and inject them as `AIContextProviders` so the agent can discover and use them at runtime. + +## What this sample demonstrates + +- Connecting to a Foundry toolbox's MCP endpoint via Streamable HTTP transport +- Injecting a fresh Azure AI bearer token (`https://ai.azure.com/.default`) on every MCP request +- Using `AgentSkillsProviderBuilder.UseMcpSkills(client)` to discover skills from the toolbox +- Injecting the discovered skills into `AIProjectClient.AsAIAgent(...)` via `AIContextProviders` + +## Prerequisites + +- A Microsoft Foundry project with a toolbox already configured +- The toolbox MCP endpoint must expose `skill://index.json` with `skill-md` entries (SEP-2640). If the resource is absent, the sample runs but the skills provider will be empty. +- Azure CLI installed and authenticated (`az login`) + +Set the following environment variables: + +```powershell +$env:AZURE_AI_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" +$env:AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4-mini" +$env:FOUNDRY_TOOLBOX_MCP_SERVER_URL="https://your-foundry-service.services.ai.azure.com/api/projects/your-project/toolboxes/your-toolbox/mcp?api-version=v1" +``` + +## Run the sample + +```powershell +dotnet run +``` diff --git a/dotnet/samples/02-agents/AgentsWithFoundry/README.md b/dotnet/samples/02-agents/AgentsWithFoundry/README.md index 3e203c1fc6..dda1e476f0 100644 --- a/dotnet/samples/02-agents/AgentsWithFoundry/README.md +++ b/dotnet/samples/02-agents/AgentsWithFoundry/README.md @@ -74,6 +74,7 @@ Some samples require extra tool-specific environment variables. See each sample | [Local MCP](./Agent_Step23_LocalMCP/) | Local MCP client with HTTP transport | | [Code interpreter file download](./Agent_Step24_CodeInterpreterFileDownload/) | Download container files generated by code interpreter | | [Foundry toolbox via MCP](./Agent_Step25_FoundryToolboxMcp/) | Use a Foundry Toolbox from a non-hosted agent via its MCP endpoint | +| [Foundry toolbox MCP skills](./Agent_Step26_FoundryToolboxMcpSkills/) | Use a Foundry Toolbox with MCP-based skills discovery (SEP-2640) via AIContextProviders | ## Running the samples From e6762ea876ef94d3c0d0c35e5de2dac5ebf0b994 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 28 May 2026 16:38:04 +0100 Subject: [PATCH 02/61] .NET: Fix render dupe and text input clear bugs, and improve guardrail error messaging (#6136) * Fix render dupe and text input clear bugs * Fix another text rendering issue and improve guardrails messaging * Address PR comments * Improve guardrail rendering and json error handling * Another tweak for input box render issue * Address PR comments --- .../ConsoleReactiveComponents/AnsiEscapes.cs | 89 ++++++++++++++++++ .../ConsoleReactiveComponents/TextPanel.cs | 48 ++++------ .../TextScrollPanel.cs | 30 +------ .../ConsoleReactiveComponent.cs | 10 ++- .../HarnessAppComponent.cs | 11 ++- .../Observers/PlanningOutputObserver.cs | 12 +-- .../OpenAIResponsesErrorObserver.cs | 90 ++++++++++++++++++- 7 files changed, 220 insertions(+), 70 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs index cf916938e7..6e5d51380d 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs @@ -72,6 +72,95 @@ public static class AnsiEscapes /// public static string ResetAttributes => "\x1b[0m"; + /// + /// Returns the visible (printed) length of a string after stripping ANSI escape sequences. + /// Escape sequences are zero-width on screen but occupy characters in the raw string. + /// + /// + /// This counts UTF-16 code units (chars) rather than terminal display cells. Emoji, + /// combining characters, variation selectors, and East Asian wide characters may be + /// measured incorrectly. For the console harness this is acceptable since content is + /// predominantly ASCII, and emoji are padded with surrounding spaces. + /// + public static int VisibleLength(string text) + { + if (string.IsNullOrEmpty(text)) + { + return 0; + } + + int length = 0; + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '\x1b' && i + 1 < text.Length && text[i + 1] == '[') + { + // Skip the ESC[ and all characters up to and including the final byte (0x40–0x7E). + i += 2; + while (i < text.Length && text[i] < 0x40) + { + i++; + } + + // i now points to the final byte of the escape sequence; the for-loop will advance past it. + } + else if (text[i] != '\n' && text[i] != '\r') + { + length++; + } + } + + return length; + } + + /// + /// Counts the number of physical terminal rows a text item will occupy, + /// accounting for both explicit newlines and terminal line wrapping. + /// + /// The text to measure. + /// The terminal width in columns. If <= 0, wrapping is ignored (1 row per logical line). + /// The number of physical rows the text occupies. + public static int CountPhysicalLines(string text, int terminalWidth) + { + if (string.IsNullOrEmpty(text)) + { + return 0; + } + + int physicalLines = 0; + int lineStart = 0; + + for (int i = 0; i <= text.Length; i++) + { + if (i == text.Length || text[i] == '\n') + { + if (terminalWidth <= 0) + { + // No wrapping — each logical line is one physical row + physicalLines += 1; + } + else + { + string logicalLine = text[lineStart..i]; + int visibleWidth = VisibleLength(logicalLine); + + physicalLines += visibleWidth == 0 + ? 1 + : (visibleWidth - 1) / terminalWidth + 1; + } + + lineStart = i + 1; + } + } + + // If text ends with a newline, don't count the trailing empty line + if (text[text.Length - 1] == '\n') + { + physicalLines--; + } + + return physicalLines; + } + private static int ConsoleColorToAnsi(ConsoleColor color) => color switch { ConsoleColor.Black => 30, diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextPanel.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextPanel.cs index 5692b58266..d63c243108 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextPanel.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextPanel.cs @@ -23,16 +23,18 @@ public record TextPanelProps : ConsoleReactiveProps public class TextPanel : ConsoleReactiveComponent { /// - /// Calculates the height (in lines) needed to render all items. + /// Calculates the height (in lines) needed to render all items, + /// accounting for terminal line wrapping at the specified width. /// /// The items to measure. - /// The total number of lines all items will occupy. - public static int CalculateHeight(IReadOnlyList items) + /// The terminal width in columns. When 0 or negative, wrapping is ignored. + /// The total number of physical lines all items will occupy. + public static int CalculateHeight(IReadOnlyList items, int terminalWidth = 0) { int total = 0; for (int i = 0; i < items.Count; i++) { - total += CountLines(items[i]); + total += AnsiEscapes.CountPhysicalLines(items[i], terminalWidth); } return total; @@ -47,13 +49,20 @@ public class TextPanel : ConsoleReactiveComponent 0 + ? Math.Max(1, (AnsiEscapes.VisibleLength(lines[j]) - 1) / props.Width + 1) + : 1; + Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + currentRow)); Console.Write(lines[j]); - currentRow++; + + currentRow += linePhysicalRows; + itemRow += linePhysicalRows; } } @@ -66,29 +75,4 @@ public class TextPanel : ConsoleReactiveComponent 0 ? lastItemLines - 1 : 0; // Update rendered count this._renderedCount = props.Items.Count; } - - private static int CountLines(string text) - { - if (string.IsNullOrEmpty(text)) - { - return 0; - } - - int count = 1; - for (int i = 0; i < text.Length; i++) - { - if (text[i] == '\n') - { - count++; - } - } - - // If text ends with a newline, don't count the trailing empty line - if (text[text.Length - 1] == '\n') - { - count--; - } - - return count; - } } diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveComponent.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveComponent.cs index d71436e5e6..fa923efdf8 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveComponent.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveComponent.cs @@ -13,6 +13,11 @@ public abstract class ConsoleReactiveComponent { } + /// + /// Gets the shared render lock across all component types to prevent ANSI escape sequence interleaving. + /// + protected static object RenderLock { get; } = new(); + /// /// Gets or sets the component's props as the base type. /// Used by parent components to set layout (X, Y, Width, Height) on children without @@ -40,7 +45,6 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive where TProps : ConsoleReactiveProps where TState : ConsoleReactiveState { - private readonly object _renderLock = new(); private TProps? _lastRenderedProps; private TState? _lastRenderedState; @@ -74,7 +78,7 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive /// public override void Render() { - lock (this._renderLock) + lock (RenderLock) { if (this.Props is null) { @@ -97,7 +101,7 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive /// public override void Invalidate() { - lock (this._renderLock) + lock (RenderLock) { this._lastRenderedProps = default; this._lastRenderedState = default; diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAppComponent.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAppComponent.cs index d100c9d081..b2104aa5fd 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAppComponent.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAppComponent.cs @@ -28,6 +28,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent /// Initializes a new instance of the class. @@ -341,7 +342,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent(collectedText); } - catch (JsonException ex) + catch (JsonException) { - await ux.WriteInfoLineAsync($"❌ Failed to parse planning response: {ex.Message}", ConsoleColor.Red); - await ux.WriteInfoLineAsync($"(raw response) {collectedText}", ConsoleColor.DarkYellow); + // JSON parsing failed — fall back to rendering as regular text output. + await ux.WriteTextAsync(collectedText).ConfigureAwait(false); return null; } if (planningResponse is null) { - await ux.WriteInfoLineAsync("(no structured response from agent)", ConsoleColor.DarkYellow); + // Null result — fall back to rendering as regular text output. + await ux.WriteTextAsync(collectedText).ConfigureAwait(false); return null; } @@ -118,7 +119,8 @@ public sealed class PlanningOutputObserver : ConsoleObserver return new List { this.BuildApprovalAction(question, session) }; } - await ux.WriteInfoLineAsync($"(unexpected response type: {planningResponse.Type})", ConsoleColor.DarkYellow); + // Unexpected type — fall back to rendering as regular text output. + await ux.WriteTextAsync(collectedText).ConfigureAwait(false); return null; } diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs index 9db00c9a65..8cf5abee57 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs @@ -2,6 +2,7 @@ #pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage. +using System.Text.Json; using Harness.Shared.Console.Observers; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; @@ -53,9 +54,94 @@ public sealed class OpenAIResponsesErrorObserver : ConsoleObserver case StreamingResponseIncompleteUpdate incompleteUpdate: string? reason = incompleteUpdate.Response?.IncompleteStatusDetails?.Reason?.ToString(); - string incompleteText = $"⚠️ Response incomplete: {reason ?? "unknown reason"}"; - await ux.WriteInfoLineAsync(incompleteText, ConsoleColor.Yellow); + if (string.Equals(reason, "content_filter", StringComparison.OrdinalIgnoreCase)) + { + string detail = GetContentFilterDetails(incompleteUpdate); + const string Message = "🛡️ The service's built-in content filter guardrails were triggered and the response was cut short."; + await ux.WriteInfoLineAsync( + string.IsNullOrEmpty(detail) ? Message : $"{Message}\n{detail}", + ConsoleColor.Yellow); + } + else + { + string incompleteText = $"⚠️ Response incomplete: {reason ?? "unknown reason"}"; + await ux.WriteInfoLineAsync(incompleteText, ConsoleColor.Yellow); + } + break; } } + + /// + /// Extracts content filter details from the serialized response JSON and returns + /// a formatted string showing which specific categories were triggered. + /// Returns if details cannot be extracted. + /// + private static string GetContentFilterDetails(StreamingResponseIncompleteUpdate incompleteUpdate) + { + try + { + var data = System.ClientModel.Primitives.ModelReaderWriter.Write(incompleteUpdate); + using var doc = JsonDocument.Parse(data.ToString()); + var root = doc.RootElement; + + // Navigate into the nested response object if present. + JsonElement responseElement = root.TryGetProperty("response", out var resp) ? resp : root; + + if (!responseElement.TryGetProperty("content_filters", out var filtersArray) + || filtersArray.ValueKind != JsonValueKind.Array) + { + return string.Empty; + } + + foreach (var filter in filtersArray.EnumerateArray()) + { + if (!filter.TryGetProperty("content_filter_results", out var results) + || results.ValueKind != JsonValueKind.Object) + { + continue; + } + + // Collect category data for aligned output. + var categories = new List<(string Name, bool Filtered, string? Severity)>(); + foreach (var category in results.EnumerateObject()) + { + if (category.Value.ValueKind != JsonValueKind.Object) + { + continue; + } + + bool filtered = category.Value.TryGetProperty("filtered", out var f) && f.GetBoolean(); + string? severity = category.Value.TryGetProperty("severity", out var s) ? s.GetString() : null; + categories.Add((category.Name, filtered, severity)); + } + + // Build all category lines into a single string. + int maxNameLen = categories.Count > 0 ? categories.Max(c => c.Name.Length) : 0; + var lines = new List(); + + foreach (var (name, filtered, severity) in categories) + { + string paddedName = name.PadRight(maxNameLen); + string icon = filtered ? "❌" : "✅"; + string statusText = filtered ? "Filtered " : "Not Filtered"; + string severityText = severity is not null ? $" Severity: {severity}" : ""; + + lines.Add($" {icon} {paddedName} {statusText}{severityText}"); + } + + if (lines.Count > 0) + { + return string.Join("\n", lines); + } + } + + return string.Empty; + } + catch + { + // Parsing not critical — skip silently if it fails. + return string.Empty; + } + } } From f7c5b8d108d0b8bc1cd2cfa3c4bea237ba688efa Mon Sep 17 00:00:00 2001 From: semenshi-m Date: Thu, 28 May 2026 16:54:20 +0100 Subject: [PATCH 03/61] Python: [Breaking] Refactor Skill API to async resource and script lookup (#6135) Port of .NET commit 08541ee5a9912b2d9b6b95b63ae3ea4ffea05cc1. Replace property-based Skill.content/resources/scripts with async by-name lookup methods: - content property -> async get_content() -> str - resources property -> async get_resource(name) -> SkillResource | None - scripts property -> async get_script(name) -> SkillScript | None SkillsProvider now always includes all three tools (load_skill, read_skill_resource, run_skill_script) and both instruction blocks regardless of whether any skills have resources or scripts. ClassSkill retains resources/scripts properties as overridable hooks for subclass reflection-based discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_skills.py | 429 +++++++++-------- .../packages/core/tests/core/test_skills.py | 438 ++++++++++-------- 2 files changed, 469 insertions(+), 398 deletions(-) diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 6268c00879..683302b13a 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -507,38 +507,45 @@ class Skill(ABC): """ ... - @property @abstractmethod - def content(self) -> str: - """The full skill content. + async def get_content(self) -> str: + """Get the full skill content. For file-based skills this is the raw SKILL.md file content, optionally augmented with a synthesized scripts block when scripts are present. For code-defined skills this is a synthesized XML document containing name, description, and body (instructions, resources, scripts). + + Returns: + The full skill content string. """ ... - @property - def resources(self) -> list[SkillResource]: - """Resources associated with this skill. + async def get_resource(self, name: str) -> SkillResource | None: + """Get a resource owned by this skill by name. - The default implementation returns an empty list. - Override this property in derived classes to provide skill-specific - resources. + Args: + name: The resource name (e.g. an identifier or a relative path + referenced inside the skill content). + + Returns: + The :class:`SkillResource`, or ``None`` when no resource with the + given name exists. """ - return [] + return None - @property - def scripts(self) -> list[SkillScript]: - """Scripts associated with this skill. + async def get_script(self, name: str) -> SkillScript | None: + """Get a script owned by this skill by name. - The default implementation returns an empty list. - Override this property in derived classes to provide skill-specific - scripts. + Args: + name: The script name. + + Returns: + The :class:`SkillScript`, or ``None`` when no script with the + given name exists. """ - return [] + return None @experimental(feature_id=ExperimentalFeature.SKILLS) @@ -767,12 +774,14 @@ class InlineSkill(Skill): """The L1 discovery metadata for this skill.""" return self._frontmatter - @property - def content(self) -> str: + async def get_content(self) -> str: """Synthesized XML content with name, description, instructions, resources, and scripts. The result is cached after the first access. Adding resources or scripts after the first access will not be reflected. + + Returns: + The synthesized XML content string. """ if self._cached_content is not None: return self._cached_content @@ -786,15 +795,31 @@ class InlineSkill(Skill): ) return self._cached_content - @property - def resources(self) -> list[SkillResource]: - """Mutable list of :class:`SkillResource` instances.""" - return self._resources + async def get_resource(self, name: str) -> SkillResource | None: + """Get a resource by name. - @property - def scripts(self) -> list[SkillScript]: - """Mutable list of :class:`SkillScript` instances.""" - return self._scripts + Args: + name: The resource name to look up (case-insensitive). + + Returns: + The :class:`SkillResource`, or ``None`` when no resource with the + given name exists. + """ + name_lower = name.lower() + return next((r for r in self._resources if r.name.lower() == name_lower), None) + + async def get_script(self, name: str) -> SkillScript | None: + """Get a script by name. + + Args: + name: The script name to look up (case-insensitive). + + Returns: + The :class:`SkillScript`, or ``None`` when no script with the + given name exists. + """ + name_lower = name.lower() + return next((s for s in self._scripts if s.name.lower() == name_lower), None) def resource( self, @@ -1318,11 +1343,13 @@ class ClassSkill(Skill, ABC): self._cached_scripts = scripts return list(self._cached_scripts) - @property - def content(self) -> str: + async def get_content(self) -> str: """Synthesized XML content containing name, description, instructions, resources, and scripts. The result is cached after the first access. + + Returns: + The synthesized XML content string. """ if self._cached_content is not None: return self._cached_content @@ -1336,6 +1363,32 @@ class ClassSkill(Skill, ABC): ) return self._cached_content + async def get_resource(self, name: str) -> SkillResource | None: + """Get a resource by name from the :attr:`resources` list. + + Args: + name: The resource name to look up (case-insensitive). + + Returns: + The :class:`SkillResource`, or ``None`` when no resource with the + given name exists. + """ + name_lower = name.lower() + return next((r for r in self.resources if r.name.lower() == name_lower), None) + + async def get_script(self, name: str) -> SkillScript | None: + """Get a script by name from the :attr:`scripts` list. + + Args: + name: The script name to look up (case-insensitive). + + Returns: + The :class:`SkillScript`, or ``None`` when no script with the + given name exists. + """ + name_lower = name.lower() + return next((s for s in self.scripts if s.name.lower() == name_lower), None) + @experimental(feature_id=ExperimentalFeature.SKILLS) class FileSkill(Skill): @@ -1378,8 +1431,7 @@ class FileSkill(Skill): """The L1 discovery metadata for this skill.""" return self._frontmatter - @property - def content(self) -> str: + async def get_content(self) -> str: """The skill content with appended scripts block. When scripts are present, a ```` XML block is appended @@ -1388,6 +1440,9 @@ class FileSkill(Skill): The result is cached after the first access. Adding scripts after the first access will not be reflected. + + Returns: + The skill content string. """ if self._cached_content is not None: return self._cached_content @@ -1398,15 +1453,31 @@ class FileSkill(Skill): self._cached_content = f"{self._content}\n\n\n{script_lines}\n" return self._cached_content - @property - def resources(self) -> list[SkillResource]: - """Resources discovered for this skill.""" - return self._resources + async def get_resource(self, name: str) -> SkillResource | None: + """Get a resource by name. - @property - def scripts(self) -> list[SkillScript]: - """Scripts discovered for this skill.""" - return self._scripts + Args: + name: The resource name to look up (case-insensitive). + + Returns: + The :class:`SkillResource`, or ``None`` when no resource with the + given name exists. + """ + name_lower = name.lower() + return next((r for r in self._resources if r.name.lower() == name_lower), None) + + async def get_script(self, name: str) -> SkillScript | None: + """Get a script by name. + + Args: + name: The script name to look up (case-insensitive). + + Returns: + The :class:`SkillScript`, or ``None`` when no script with the + given name exists. + """ + name_lower = name.lower() + return next((s for s in self._scripts if s.name.lower() == name_lower), None) # endregion @@ -1734,13 +1805,13 @@ class SkillsProvider(ContextProvider): Keyword Args: instruction_template: Custom system-prompt template for advertising skills. Must contain a ``{skills}`` placeholder for the - generated skills list. If the provider includes file-based script - execution instructions, the template must also contain - ``{runner_instructions}``. If the provider includes resource-reading - instructions, the template must also contain - ``{resource_instructions}``. Omitting any placeholder required by - the resolved skills configuration can raise :class:`ValueError` at - runtime. Uses a built-in template when ``None``. + generated skills list. May optionally contain + ``{runner_instructions}`` and/or ``{resource_instructions}`` + placeholders; when present, they are filled with built-in + guidance for script execution and resource reading respectively. + When omitted, those instructions are simply not included in the + rendered prompt (the corresponding tools are still registered). + Uses a built-in template when ``None``. require_script_approval: When ``True``, skill script execution requires explicit user approval before running. Instead of executing immediately, the agent pauses and returns a @@ -1867,29 +1938,20 @@ class SkillsProvider(ContextProvider): def _create_instructions( prompt_template: str | None, skills: Sequence[Skill], - include_script_runner_instructions: bool = False, - include_resource_instructions: bool = False, ) -> str | None: """Create the system-prompt text that advertises available skills. Generates an XML list of ```` elements (sorted by name) and inserts it into *prompt_template* at the ``{skills}`` placeholder. - When *include_script_runner_instructions* is ``True``, executor-provided - instructions are inserted at the ``{runner_instructions}`` placeholder. - When *include_resource_instructions* is ``True``, resource-reading - instructions are inserted at the ``{resource_instructions}`` placeholder. + Script-runner instructions are inserted at the + ``{runner_instructions}`` placeholder and resource-reading + instructions at the ``{resource_instructions}`` placeholder. Args: prompt_template: Custom template string with ``{skills}`` and optional ``{runner_instructions}`` and ``{resource_instructions}`` placeholders, or ``None`` to use the built-in default. skills: Registered skills. - include_script_runner_instructions: When ``True``, include - script-runner instructions in the generated prompt. - Defaults to ``False``. - include_resource_instructions: When ``True``, include - resource-reading instructions in the generated prompt. - Defaults to ``False``. Returns: The formatted instruction string, or ``None`` when *skills* is empty. @@ -1898,8 +1960,8 @@ class SkillsProvider(ContextProvider): ValueError: If *prompt_template* is not a valid format string (e.g. missing ``{skills}`` placeholder). """ - runner_instructions = SCRIPT_RUNNER_INSTRUCTIONS if include_script_runner_instructions else None - resource_instructions = RESOURCE_INSTRUCTIONS if include_resource_instructions else None + runner_instructions = SCRIPT_RUNNER_INSTRUCTIONS + resource_instructions = RESOURCE_INSTRUCTIONS template = DEFAULT_SKILLS_INSTRUCTION_PROMPT if prompt_template is not None: @@ -1921,16 +1983,6 @@ class SkillsProvider(ContextProvider): raise ValueError( "The provided instruction_template must contain a '{skills}' placeholder." # noqa: RUF027 ) - if runner_instructions and "__EXEC_PROBE__" not in result: - raise ValueError( - "The provided instruction_template must contain an '{runner_instructions}' placeholder " # noqa: RUF027 - "when a script runner is configured." - ) - if resource_instructions and "__RES_PROBE__" not in result: - raise ValueError( - "The provided instruction_template must contain a '{resource_instructions}' placeholder " # noqa: RUF027 - "when skills have resources." - ) template = prompt_template if not skills: @@ -1964,20 +2016,13 @@ class SkillsProvider(ContextProvider): if not skills: return skills, None, [] - has_scripts = any(s.scripts for s in skills) - has_resources = any(s.resources for s in skills) - instructions = self._create_instructions( prompt_template=self._instruction_template, skills=skills, - include_script_runner_instructions=has_scripts, - include_resource_instructions=has_resources, ) tools = self._create_tools( skills=skills, - include_script_runner_tool=has_scripts, - include_resource_tool=has_resources, require_script_approval=self._require_script_approval, ) @@ -2046,23 +2091,15 @@ class SkillsProvider(ContextProvider): def _create_tools( self, skills: Sequence[Skill], - include_script_runner_tool: bool, - include_resource_tool: bool, require_script_approval: bool = False, ) -> list[FunctionTool]: """Create the tool definitions for skill interaction. - Always includes ``load_skill``. Conditionally includes - ``read_skill_resource`` (when *include_resource_tool* is ``True``) - and ``run_skill_script`` (when *include_script_runner_tool* is - ``True``). + Always includes ``load_skill``, ``read_skill_resource``, and + ``run_skill_script``. Args: skills: The skills to bind to tool handlers. - include_script_runner_tool: Whether to include the - ``run_skill_script`` tool in the returned list. - include_resource_tool: Whether to include the - ``read_skill_resource`` tool in the returned list. require_script_approval: When ``True``, the ``run_skill_script`` tool pauses for user approval before each invocation. @@ -2070,11 +2107,23 @@ class SkillsProvider(ContextProvider): Returns: A list of :class:`FunctionTool` instances. """ - tools = [ + + async def _load(skill_name: str) -> str: + return await self._load_skill(skills, skill_name) + + async def _read_resource(skill_name: str, resource_name: str, **kwargs: Any) -> Any: + return await self._read_skill_resource(skills, skill_name, resource_name, **kwargs) + + async def _run_script( + skill_name: str, script_name: str, args: dict[str, Any] | list[str] | None = None, **kwargs: Any + ) -> Any: + return await self._run_skill_script(skills, skill_name, script_name, args, **kwargs) + + return [ FunctionTool( name="load_skill", description="Loads the full instructions for a specific skill.", - func=lambda skill_name: self._load_skill(skills, skill_name), # pyright: ignore[reportUnknownArgumentType, reportUnknownLambdaType] + func=_load, input_model={ "type": "object", "properties": { @@ -2083,108 +2132,88 @@ class SkillsProvider(ContextProvider): "required": ["skill_name"], }, ), + FunctionTool( + name="read_skill_resource", + description=( + "Reads a resource associated with a skill, such as references, assets, or dynamic data." + ), + func=_read_resource, + input_model={ + "type": "object", + "properties": { + "skill_name": {"type": "string", "description": "The name of the skill."}, + "resource_name": { + "type": "string", + "description": "The name of the resource.", + }, + }, + "required": ["skill_name", "resource_name"], + }, + ), + FunctionTool( + name="run_skill_script", + description="Runs a script associated with a skill.", + func=_run_script, + approval_mode="always_require" if require_script_approval else "never_require", + input_model={ + "type": "object", + "properties": { + "skill_name": {"type": "string", "description": "The name of the skill."}, + "script_name": { + "type": "string", + "description": ( + "The name of the script to run as listed in the skill, " + "preserving any directory prefix exactly as shown. " + "Do not add or remove path prefixes." + ), + }, + "args": { + "oneOf": [ + { + "type": "object", + "additionalProperties": True, + "description": ( + "Named arguments as key-value pairs " + '(e.g. {"length": 24, "uppercase": true}).' + ), + }, + { + "type": "array", + "items": {"type": "string"}, + "description": ( + "Positional CLI arguments as a string array " + '(e.g. ["input.docx", "--output", "result.idx"]).' + ), + }, + {"type": "null"}, + ], + "default": None, + "description": ( + "Arguments to pass to the script. " + "Use an array of strings for CLI-style positional arguments " + '(e.g. ["input.docx", "--output", "result.idx"]), ' + "or an object for named parameters " + '(e.g. {"length": 24, "uppercase": true}). ' + "How these values are mapped to the underlying script " + "is determined by the script implementation or configured runner." + ), + }, + }, + "required": ["skill_name", "script_name"], + }, + ), ] - if include_resource_tool: - - async def _read_resource(skill_name: str, resource_name: str, **kwargs: Any) -> Any: - return await self._read_skill_resource(skills, skill_name, resource_name, **kwargs) - - tools.append( - FunctionTool( - name="read_skill_resource", - description=( - "Reads a resource associated with a skill, such as references, assets, or dynamic data." - ), - func=_read_resource, - input_model={ - "type": "object", - "properties": { - "skill_name": {"type": "string", "description": "The name of the skill."}, - "resource_name": { - "type": "string", - "description": "The name of the resource.", - }, - }, - "required": ["skill_name", "resource_name"], - }, - ) - ) - - if include_script_runner_tool: - - async def _run_script( - skill_name: str, script_name: str, args: dict[str, Any] | list[str] | None = None, **kwargs: Any - ) -> Any: - return await self._run_skill_script(skills, skill_name, script_name, args, **kwargs) - - tools.append( - FunctionTool( - name="run_skill_script", - description="Runs a script associated with a skill.", - func=_run_script, - approval_mode="always_require" if require_script_approval else "never_require", - input_model={ - "type": "object", - "properties": { - "skill_name": {"type": "string", "description": "The name of the skill."}, - "script_name": { - "type": "string", - "description": ( - "The name of the script to run as listed in the skill, " - "preserving any directory prefix exactly as shown. " - "Do not add or remove path prefixes." - ), - }, - "args": { - "oneOf": [ - { - "type": "object", - "additionalProperties": True, - "description": ( - "Named arguments as key-value pairs " - '(e.g. {"length": 24, "uppercase": true}).' - ), - }, - { - "type": "array", - "items": {"type": "string"}, - "description": ( - "Positional CLI arguments as a string array " - '(e.g. ["input.docx", "--output", "result.idx"]).' - ), - }, - {"type": "null"}, - ], - "default": None, - "description": ( - "Arguments to pass to the script. " - "Use an array of strings for CLI-style positional arguments " - '(e.g. ["input.docx", "--output", "result.idx"]), ' - "or an object for named parameters " - '(e.g. {"length": 24, "uppercase": true}). ' - "How these values are mapped to the underlying script " - "is determined by the script implementation or configured runner." - ), - }, - }, - "required": ["skill_name", "script_name"], - }, - ) - ) - - return tools - @staticmethod def _find_skill(skills: Sequence[Skill], name: str) -> Skill | None: """Find a skill by name (case-insensitive linear scan).""" name_lower = name.lower() return next((s for s in skills if s.frontmatter.name.lower() == name_lower), None) - def _load_skill(self, skills: Sequence[Skill], skill_name: str) -> str: + async def _load_skill(self, skills: Sequence[Skill], skill_name: str) -> str: """Return the full content for the named skill. - Delegates to the skill's :attr:`~Skill.content` property, which + Delegates to the skill's :meth:`~Skill.get_content` method, which handles format differences between file-based and code-defined skills. Args: @@ -2204,7 +2233,7 @@ class SkillsProvider(ContextProvider): logger.info("Loading skill: %s", skill_name) - return skill.content + return await skill.get_content() async def _run_skill_script( self, @@ -2243,7 +2272,7 @@ class SkillsProvider(ContextProvider): if not skill: return f"Error: Skill '{skill_name}' not found." - script = next((s for s in skill.scripts if s.name.lower() == script_name.lower()), None) + script = await skill.get_script(script_name) if not script: return f"Error: Script '{script_name}' not found in skill '{skill_name}'." @@ -2284,12 +2313,8 @@ class SkillsProvider(ContextProvider): if skill is None: return f"Error: Skill '{skill_name}' not found." - # Find resource by name (case-insensitive) - resource_name_lower = resource_name.lower() - for resource in skill.resources: - if resource.name.lower() == resource_name_lower: - break - else: + resource = await skill.get_resource(resource_name) + if resource is None: return f"Error: Resource '{resource_name}' not found in skill '{skill_name}'." try: @@ -2481,27 +2506,29 @@ class FileSkillsSource(SkillsSource): ) continue - file_skill = FileSkill( - frontmatter=frontmatter, - content=content, - path=skill_path, - ) - - # Discover and attach file-based resources + # Discover file-based resources + resources: list[SkillResource] = [] for rn in FileSkillsSource._discover_resource_files( skill_path, self._resource_extensions, self._resource_directories ): resource_full_path = FileSkillsSource._get_validated_resource_path(skill_path, rn) - file_skill.resources.append(_FileSkillResource(name=rn, full_path=resource_full_path)) + resources.append(_FileSkillResource(name=rn, full_path=resource_full_path)) - # Discover and attach file-based scripts as SkillScript instances + # Discover file-based scripts + scripts: list[SkillScript] = [] for sn in FileSkillsSource._discover_script_files( skill_path, self._script_extensions, self._script_directories ): script_full_path = os.path.normpath(os.path.join(skill_path, sn)) # noqa: ASYNC240 - file_skill.scripts.append( - FileSkillScript(name=sn, full_path=script_full_path, runner=self._script_runner) - ) + scripts.append(FileSkillScript(name=sn, full_path=script_full_path, runner=self._script_runner)) + + file_skill = FileSkill( + frontmatter=frontmatter, + content=content, + path=skill_path, + resources=resources, + scripts=scripts, + ) skills[file_skill.frontmatter.name] = file_skill logger.info("Loaded skill: %s", file_skill.frontmatter.name) diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index 415d6ea857..17fb2cf5ce 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -451,7 +451,7 @@ class TestDiscoverAndLoadSkills: _write_skill(dir2, "my-skill", body="Second") skills = await _discover_file_skills_for_test([str(dir1), str(dir2)]) assert len(skills) == 1 - assert "First" in skills["my-skill"].content + assert "First" in (await skills["my-skill"].get_content()) async def test_empty_directory(self, tmp_path: Path) -> None: skills = await _discover_file_skills_for_test([str(tmp_path)]) @@ -489,7 +489,7 @@ class TestDiscoverAndLoadSkills: ) skills = await _discover_file_skills_for_test([str(tmp_path)]) assert "my-skill" in skills - assert [r.name for r in skills["my-skill"].resources] == ["references/FAQ.md"] + assert [r.name for r in skills["my-skill"]._resources] == ["references/FAQ.md"] async def test_skill_discovers_all_resource_files(self, tmp_path: Path) -> None: """Resources are discovered by filesystem scan, not by markdown links.""" @@ -501,7 +501,7 @@ class TestDiscoverAndLoadSkills: ) skills = await _discover_file_skills_for_test([str(tmp_path)]) assert "my-skill" in skills - resource_names = sorted(r.name for r in skills["my-skill"].resources) + resource_names = sorted(r.name for r in skills["my-skill"]._resources) assert "assets/doc.md" in resource_names assert "references/data.json" in resource_names @@ -676,9 +676,9 @@ class TestSkillsProvider: assert len(context.instructions) == 1 assert "my-skill" in context.instructions[0] - assert len(context.tools) == 1 tool_names = {t.name for t in context.tools} - assert tool_names == {"load_skill"} + assert len(context.tools) == 3 + assert tool_names == {"load_skill", "read_skill_resource", "run_skill_script"} async def test_before_run_without_skills(self, tmp_path: Path) -> None: provider = SkillsProvider.from_paths(str(tmp_path)) @@ -698,7 +698,7 @@ class TestSkillsProvider: _write_skill(tmp_path, "my-skill", body="Skill body content.") provider = SkillsProvider.from_paths(str(tmp_path)) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "Skill body content." in result async def test_load_skill_preserves_file_skill_content(self, tmp_path: Path) -> None: @@ -710,19 +710,19 @@ class TestSkillsProvider: ) provider = SkillsProvider.from_paths(str(tmp_path)) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "See [doc](references/FAQ.md)." in result async def test_load_skill_unknown_returns_error(self, tmp_path: Path) -> None: provider = SkillsProvider.from_paths(str(tmp_path)) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "nonexistent") + result = await provider._load_skill(_raw_skills(provider), "nonexistent") assert result.startswith("Error:") async def test_load_skill_empty_name_returns_error(self, tmp_path: Path) -> None: provider = SkillsProvider.from_paths(str(tmp_path)) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "") + result = await provider._load_skill(_raw_skills(provider), "") assert result.startswith("Error:") async def test_read_skill_resource_returns_content(self, tmp_path: Path) -> None: @@ -871,7 +871,7 @@ class TestSymlinkDetection: skills = await _discover_file_skills_for_test([str(tmp_path)]) assert "my-skill" in skills - resource_names = [r.name for r in skills["my-skill"].resources] + resource_names = [r.name for r in skills["my-skill"]._resources] assert "references/leak.md" not in resource_names assert "references/safe.md" in resource_names @@ -1032,7 +1032,7 @@ class TestInlineSkill: assert skill.frontmatter.name == "my-skill" assert skill.frontmatter.description == "A test skill." assert skill.instructions == "Instructions." - assert skill.resources == [] + assert skill._resources == [] def test_construction_with_static_resources(self) -> None: skill = InlineSkill( @@ -1042,8 +1042,8 @@ class TestInlineSkill: InlineSkillResource(name="ref", content="Reference content"), ], ) - assert len(skill.resources) == 1 - assert skill.resources[0].name == "ref" + assert len(skill._resources) == 1 + assert skill._resources[0].name == "ref" def test_empty_name_raises(self) -> None: with pytest.raises(ValueError, match="cannot be empty"): @@ -1083,11 +1083,11 @@ class TestInlineSkill: """Get the database schema.""" return "CREATE TABLE users (id INT)" - assert len(skill.resources) == 1 - assert skill.resources[0].name == "get_schema" - assert skill.resources[0].description is None - assert isinstance(skill.resources[0], InlineSkillResource) - assert skill.resources[0].function is get_schema + assert len(skill._resources) == 1 + assert skill._resources[0].name == "get_schema" + assert skill._resources[0].description is None + assert isinstance(skill._resources[0], InlineSkillResource) + assert skill._resources[0].function is get_schema def test_resource_decorator_with_args(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body") @@ -1096,9 +1096,9 @@ class TestInlineSkill: def my_resource() -> Any: return "data" - assert len(skill.resources) == 1 - assert skill.resources[0].name == "custom-name" - assert skill.resources[0].description == "Custom description" + assert len(skill._resources) == 1 + assert skill._resources[0].name == "custom-name" + assert skill._resources[0].description == "Custom description" def test_resource_decorator_returns_function(self) -> None: """Decorator should return the original function unchanged.""" @@ -1122,8 +1122,8 @@ class TestInlineSkill: def resource_b() -> Any: return "B" - assert len(skill.resources) == 2 - names = [r.name for r in skill.resources] + assert len(skill._resources) == 2 + names = [r.name for r in skill._resources] assert "resource_a" in names assert "resource_b" in names @@ -1134,9 +1134,9 @@ class TestInlineSkill: async def get_async_data() -> Any: return "async data" - assert len(skill.resources) == 1 - assert isinstance(skill.resources[0], InlineSkillResource) - assert skill.resources[0].function is get_async_data + assert len(skill._resources) == 1 + assert isinstance(skill._resources[0], InlineSkillResource) + assert skill._resources[0].function is get_async_data # --------------------------------------------------------------------------- @@ -1163,7 +1163,7 @@ class TestSkillsProviderCodeSkill: ) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "prog-skill") + result = await provider._load_skill(_raw_skills(provider), "prog-skill") assert "prog-skill" in result assert "A skill." in result assert "\nCode-defined instructions.\n" in result @@ -1180,7 +1180,7 @@ class TestSkillsProviderCodeSkill: ) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "prog-skill") + result = await provider._load_skill(_raw_skills(provider), "prog-skill") assert "prog-skill" in result assert "A skill." in result assert "Do things." in result @@ -1194,7 +1194,7 @@ class TestSkillsProviderCodeSkill: ) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "prog-skill") + result = await provider._load_skill(_raw_skills(provider), "prog-skill") assert "Body only." in result assert "" not in result @@ -1364,7 +1364,8 @@ class TestSkillsProviderCodeSkill: assert len(context.instructions) == 1 assert "prog-skill" in context.instructions[0] - assert len(context.tools) == 1 + assert len(context.tools) == 3 + assert {t.name for t in context.tools} == {"load_skill", "read_skill_resource", "run_skill_script"} async def test_before_run_empty_provider(self) -> None: provider = SkillsProvider([]) @@ -1407,7 +1408,7 @@ class TestSkillsProviderCodeSkill: ) await _init_provider(provider) # File-based is loaded first, so it wins - assert "File version" in _ctx(provider)[0]["my-skill"].content + assert "File version" in (await _ctx(provider)[0]["my-skill"].get_content()) async def test_combined_prompt_includes_both(self, tmp_path: Path) -> None: _write_skill(tmp_path, "file-skill") @@ -1446,7 +1447,7 @@ class TestSkillsProviderCodeSkill: provider = SkillsProvider.from_paths(str(tmp_path), resource_extensions=(".json",)) await _init_provider(provider) skill = _ctx(provider)[0]["my-skill"] - resource_names = [r.name for r in skill.resources] + resource_names = [r.name for r in skill._resources] assert "references/data.json" in resource_names assert "references/notes.txt" not in resource_names @@ -1459,14 +1460,14 @@ class TestSkillsProviderCodeSkill: class TestFileBasedSkillParsing: """Tests for file-based skills parsed from SKILL.md.""" - def test_content_contains_full_raw_file(self, tmp_path: Path) -> None: + async def test_content_contains_full_raw_file(self, tmp_path: Path) -> None: """content stores the entire SKILL.md file including frontmatter.""" _write_skill(tmp_path, "my-skill", description="A test skill.", body="Instructions here.") skill = _read_and_parse_skill_file_for_test(tmp_path / "my-skill") - assert "---" in skill.content - assert "name: my-skill" in skill.content - assert "description: A test skill." in skill.content - assert "Instructions here." in skill.content + assert "---" in (await skill.get_content()) + assert "name: my-skill" in (await skill.get_content()) + assert "description: A test skill." in (await skill.get_content()) + assert "Instructions here." in (await skill.get_content()) def test_name_and_description_from_frontmatter(self, tmp_path: Path) -> None: _write_skill(tmp_path, "my-skill", description="Skill desc.") @@ -1483,7 +1484,7 @@ class TestFileBasedSkillParsing: _write_skill(tmp_path, "my-skill", resources={"references/doc.md": "content"}) skills = await _discover_file_skills_for_test([str(tmp_path)]) assert "my-skill" in skills - resource_names = [r.name for r in skills["my-skill"].resources] + resource_names = [r.name for r in skills["my-skill"]._resources] assert "references/doc.md" in resource_names @@ -1500,7 +1501,7 @@ class TestLoadSkillFormatting: _write_skill(tmp_path, "my-skill", body="Do the thing.") provider = SkillsProvider.from_paths(str(tmp_path)) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "Do the thing." in result assert "" not in result assert "" not in result @@ -1512,7 +1513,7 @@ class TestLoadSkillFormatting: ) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "prog-skill") + result = await provider._load_skill(_raw_skills(provider), "prog-skill") assert "prog-skill" in result assert "A skill." in result assert "\nDo stuff.\n" in result @@ -1526,7 +1527,7 @@ class TestLoadSkillFormatting: ) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "prog-skill") + result = await provider._load_skill(_raw_skills(provider), "prog-skill") assert '' in result assert "description=" not in result @@ -1708,7 +1709,7 @@ class TestFileSkillsSourceDirectories: source = FileSkillsSource(str(tmp_path), resource_directories=["docs"]) skills = await source.get_skills() - resource_names = [r.name for r in skills[0].resources] + resource_names = [r.name for r in skills[0]._resources] assert "docs/guide.md" in resource_names assert "references/ref.md" not in resource_names @@ -1727,7 +1728,7 @@ class TestFileSkillsSourceDirectories: source = FileSkillsSource(str(tmp_path), script_directories=["tools"]) skills = await source.get_skills() - script_names = [s.name for s in skills[0].scripts] + script_names = [s.name for s in skills[0]._scripts] assert "tools/run.py" in script_names async def test_root_indicator_discovers_root_files(self, tmp_path: Path) -> None: @@ -1742,7 +1743,7 @@ class TestFileSkillsSourceDirectories: source = FileSkillsSource(str(tmp_path), resource_directories=[".", "references"]) skills = await source.get_skills() - resource_names = [r.name for r in skills[0].resources] + resource_names = [r.name for r in skills[0]._resources] assert "data.json" in resource_names async def test_from_paths_passes_directories(self, tmp_path: Path) -> None: @@ -1762,7 +1763,7 @@ class TestFileSkillsSourceDirectories: ) await _init_provider(provider) skill = _ctx(provider)[0]["my-skill"] - resource_names = [r.name for r in skill.resources] + resource_names = [r.name for r in skill._resources] assert "docs/guide.md" in resource_names @@ -2602,40 +2603,46 @@ class TestCreateInstructionsEdgeCases: assert alpha_pos < bravo_pos < charlie_pos def test_custom_template_missing_runner_instructions_raises(self) -> None: - """Custom template without {runner_instructions} raises when scripts are enabled.""" + """Custom templates may omit {runner_instructions}.""" skills = [ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), ] template = "Skills: {skills}" - with pytest.raises(ValueError, match="runner_instructions"): - SkillsProvider._create_instructions(template, skills, include_script_runner_instructions=True) + result = SkillsProvider._create_instructions(template, skills) + assert result is not None + assert result.startswith("Skills: ") + assert "my-skill" in result + assert "Skill." in result def test_custom_template_missing_resource_instructions_raises(self) -> None: - """Custom template without {resource_instructions} raises when resources exist.""" + """Custom templates may omit {resource_instructions}.""" skills = [ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), ] template = "Skills: {skills}" - with pytest.raises(ValueError, match="resource_instructions"): - SkillsProvider._create_instructions(template, skills, include_resource_instructions=True) + result = SkillsProvider._create_instructions(template, skills) + assert result is not None + assert result.startswith("Skills: ") + assert "my-skill" in result + assert "Skill." in result def test_include_resource_instructions_true_adds_resource_text(self) -> None: - """When include_resource_instructions is True, resource instructions appear in the prompt.""" + """Resource instructions always appear in the prompt.""" skills = [ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), ] - result = SkillsProvider._create_instructions(None, skills, include_resource_instructions=True) + result = SkillsProvider._create_instructions(None, skills) assert result is not None assert "read_skill_resource" in result def test_include_resource_instructions_false_omits_resource_text(self) -> None: - """When include_resource_instructions is False, resource instructions do not appear.""" + """Resource instructions are still included by default.""" skills = [ InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), ] - result = SkillsProvider._create_instructions(None, skills, include_resource_instructions=False) + result = SkillsProvider._create_instructions(None, skills) assert result is not None - assert "read_skill_resource" not in result + assert "read_skill_resource" in result def test_custom_template_with_unknown_placeholder_raises(self) -> None: """Template with an unknown placeholder raises ValueError.""" @@ -2646,6 +2653,39 @@ class TestCreateInstructionsEdgeCases: with pytest.raises(ValueError, match="valid format string"): SkillsProvider._create_instructions(template, skills) + def test_custom_template_with_all_placeholders_fills_them(self) -> None: + """Custom template with all three placeholders fills each one.""" + skills = [ + InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), + ] + template = "SKILLS:{skills}\nRUNNER:{runner_instructions}\nRESOURCE:{resource_instructions}" + result = SkillsProvider._create_instructions(template, skills) + assert result is not None + assert "my-skill" in result + assert "run_skill_script" in result + assert "read_skill_resource" in result + + def test_custom_template_omitting_runner_excludes_runner_text(self) -> None: + """Omitting {runner_instructions} from a custom template excludes script guidance.""" + skills = [ + InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), + ] + template = "Skills: {skills}" + result = SkillsProvider._create_instructions(template, skills) + assert result is not None + assert "run_skill_script" not in result + + def test_custom_template_omitting_resource_excludes_resource_text(self) -> None: + """Omitting {resource_instructions} from a custom template excludes resource guidance.""" + skills = [ + InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="Skill."), instructions="Body"), + ] + template = "Skills: {skills} {runner_instructions}" + result = SkillsProvider._create_instructions(template, skills) + assert result is not None + assert "run_skill_script" in result + assert "read_skill_resource" not in result + # --------------------------------------------------------------------------- # Tests: SkillsProvider edge cases @@ -2665,7 +2705,7 @@ class TestSkillsProviderEdgeCases: _write_skill(tmp_path, "my-skill") provider = SkillsProvider.from_paths(str(tmp_path)) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), " ") + result = await provider._load_skill(_raw_skills(provider), " ") assert result.startswith("Error:") assert "empty" in result @@ -2716,7 +2756,7 @@ class TestSkillsProviderEdgeCases: ) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "<tags>" in result assert "&" in result @@ -2738,9 +2778,9 @@ class TestSkillsProviderEdgeCases: await provider.before_run(agent=AsyncMock(), session=AsyncMock(), context=context, state={}) - assert len(context.tools) == 1 tool_names = {t.name for t in context.tools} - assert "load_skill" in tool_names + assert len(context.tools) == 3 + assert tool_names == {"load_skill", "read_skill_resource", "run_skill_script"} # --------------------------------------------------------------------------- @@ -2850,7 +2890,7 @@ class TestSkillResourceDecoratorEdgeCases: def no_docs() -> Any: return "data" - assert skill.resources[0].description is None + assert skill._resources[0].description is None def test_decorator_with_name_only(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body") @@ -2860,9 +2900,9 @@ class TestSkillResourceDecoratorEdgeCases: """Some docs.""" return "data" - assert skill.resources[0].name == "custom-name" + assert skill._resources[0].name == "custom-name" # description is None when not explicitly provided - assert skill.resources[0].description is None + assert skill._resources[0].description is None def test_decorator_with_description_only(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body") @@ -2871,8 +2911,8 @@ class TestSkillResourceDecoratorEdgeCases: def get_data() -> Any: return "data" - assert skill.resources[0].name == "get_data" - assert skill.resources[0].description == "Custom desc" + assert skill._resources[0].name == "get_data" + assert skill._resources[0].description == "Custom desc" def test_decorator_preserves_original_function_identity(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="A skill."), instructions="Body") @@ -3045,11 +3085,11 @@ class TestSkillScriptDecorator: """Run analysis.""" return "result" - assert len(skill.scripts) == 1 - assert skill.scripts[0].name == "analyze" - assert skill.scripts[0].description is None - assert isinstance(skill.scripts[0], InlineSkillScript) - assert skill.scripts[0].function is analyze + assert len(skill._scripts) == 1 + assert skill._scripts[0].name == "analyze" + assert skill._scripts[0].description is None + assert isinstance(skill._scripts[0], InlineSkillScript) + assert skill._scripts[0].function is analyze def test_parameterized_decorator(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") @@ -3058,11 +3098,11 @@ class TestSkillScriptDecorator: def my_func() -> str: return "data" - assert len(skill.scripts) == 1 - assert skill.scripts[0].name == "custom-name" - assert skill.scripts[0].description == "Custom desc" - assert isinstance(skill.scripts[0], InlineSkillScript) - assert skill.scripts[0].function is my_func + assert len(skill._scripts) == 1 + assert skill._scripts[0].name == "custom-name" + assert skill._scripts[0].description == "Custom desc" + assert isinstance(skill._scripts[0], InlineSkillScript) + assert skill._scripts[0].function is my_func def test_multiple_scripts(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") @@ -3075,9 +3115,9 @@ class TestSkillScriptDecorator: def script_b() -> str: return "b" - assert len(skill.scripts) == 2 - assert skill.scripts[0].name == "script_a" - assert skill.scripts[1].name == "script_b" + assert len(skill._scripts) == 2 + assert skill._scripts[0].name == "script_a" + assert skill._scripts[1].name == "script_b" def test_async_script(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") @@ -3087,10 +3127,10 @@ class TestSkillScriptDecorator: """Fetch remote data.""" return "data" - assert len(skill.scripts) == 1 - assert skill.scripts[0].name == "fetch_data" - assert isinstance(skill.scripts[0], InlineSkillScript) - assert skill.scripts[0].function is fetch_data + assert len(skill._scripts) == 1 + assert skill._scripts[0].name == "fetch_data" + assert isinstance(skill._scripts[0], InlineSkillScript) + assert skill._scripts[0].function is fetch_data def test_decorator_returns_original_function(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") @@ -3117,15 +3157,15 @@ class TestSkillWithScripts: def test_default_empty_scripts(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - assert skill.scripts == [] + assert skill._scripts == [] def test_scripts_at_construction(self) -> None: scripts = [InlineSkillScript(name="s1", function=lambda: None)] skill = InlineSkill( frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body", scripts=scripts ) - assert len(skill.scripts) == 1 - assert skill.scripts[0].name == "s1" + assert len(skill._scripts) == 1 + assert skill._scripts[0].name == "s1" # --------------------------------------------------------------------------- @@ -3147,7 +3187,7 @@ class TestSkillScriptRunnerProtocol: skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body") script = FileSkillScript(name="my-script", full_path=f"{_ABS}/test/scripts/run.py") - skill.scripts.append(script) + skill._scripts.append(script) result = await my_runner(skill, script, args={"key": "val"}) @@ -3165,7 +3205,7 @@ class TestSkillScriptRunnerProtocol: skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body") script = InlineSkillScript(name="my-script", function=lambda: None) - skill.scripts.append(script) + skill._scripts.append(script) result = await runner(skill, script, args={"key": "val"}) assert result == "custom result" @@ -3201,7 +3241,7 @@ class TestSkillScriptRunnerProtocol: skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body") script = FileSkillScript(name="my-script", full_path=f"{_ABS}/test/scripts/run.py") - skill.scripts.append(script) + skill._scripts.append(script) result = my_runner(skill, script, args={"key": "val"}) @@ -3219,7 +3259,7 @@ class TestSkillScriptRunnerProtocol: skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="test"), instructions="body") script = InlineSkillScript(name="my-script", function=lambda: None) - skill.scripts.append(script) + skill._scripts.append(script) result = runner(skill, script, args={"key": "val"}) assert result == "sync result" @@ -3255,7 +3295,7 @@ class TestSkillsProviderFactories: async def test_code_skills_with_scripts_creates_provider(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3267,16 +3307,18 @@ class TestSkillsProviderFactories: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") provider = SkillsProvider([skill]) await _init_provider(provider) - # No scripts with functions, no runner, no resources — only load_skill - assert len(_ctx(provider)[2]) == 1 - assert not any(hasattr(t, "name") and t.name == "run_skill_script" for t in _ctx(provider)[2]) + assert {t.name for t in _ctx(provider)[2] if hasattr(t, "name")} == { + "load_skill", + "read_skill_resource", + "run_skill_script", + } async def test_code_script_runs_directly(self) -> None: def my_function(key: str = "") -> str: return f"executed: {key}" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=my_function)) + skill._scripts.append(InlineSkillScript(name="s1", function=my_function)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3287,22 +3329,21 @@ class TestSkillsProviderFactories: async def test_no_scripts_no_tool(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - # No scripts at all — no run_skill_script tool provider = SkillsProvider([skill]) await _init_provider(provider) - assert not any(hasattr(t, "name") and t.name == "run_skill_script" for t in _ctx(provider)[2]) + assert any(hasattr(t, "name") and t.name == "run_skill_script" for t in _ctx(provider)[2]) async def test_no_resources_no_read_skill_resource_tool(self) -> None: - """When no skill has resources, read_skill_resource tool is not advertised.""" + """read_skill_resource is advertised even when no skill has resources.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") provider = SkillsProvider([skill]) await _init_provider(provider) - assert not any(hasattr(t, "name") and t.name == "read_skill_resource" for t in _ctx(provider)[2]) + assert any(hasattr(t, "name") and t.name == "read_skill_resource" for t in _ctx(provider)[2]) async def test_resources_present_includes_read_skill_resource_tool(self) -> None: """When a skill has resources, read_skill_resource tool is advertised.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.resources.append(InlineSkillResource(name="ref", content="reference data")) + skill._resources.append(InlineSkillResource(name="ref", content="reference data")) provider = SkillsProvider([skill]) await _init_provider(provider) assert any(hasattr(t, "name") and t.name == "read_skill_resource" for t in _ctx(provider)[2]) @@ -3310,22 +3351,22 @@ class TestSkillsProviderFactories: async def test_resources_present_includes_resource_instructions(self) -> None: """When a skill has resources, instructions mention read_skill_resource.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.resources.append(InlineSkillResource(name="ref", content="reference data")) + skill._resources.append(InlineSkillResource(name="ref", content="reference data")) provider = SkillsProvider([skill]) await _init_provider(provider) assert "read_skill_resource" in (_ctx(provider)[1] or "") async def test_no_resources_excludes_resource_instructions(self) -> None: - """When no skill has resources, instructions do not mention read_skill_resource.""" + """read_skill_resource instructions are included even without resources.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") provider = SkillsProvider([skill]) await _init_provider(provider) - assert "read_skill_resource" not in (_ctx(provider)[1] or "") + assert "read_skill_resource" in (_ctx(provider)[1] or "") async def test_read_skill_resource_tool_returns_content(self) -> None: """The read_skill_resource tool returns resource content when invoked.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.resources.append(InlineSkillResource(name="ref", content="reference data")) + skill._resources.append(InlineSkillResource(name="ref", content="reference data")) provider = SkillsProvider([skill]) await _init_provider(provider) read_tool = next(t for t in _ctx(provider)[2] if hasattr(t, "name") and t.name == "read_skill_resource") @@ -3428,7 +3469,7 @@ class TestSkillsProviderFactories: code_skill = InlineSkill( frontmatter=SkillFrontmatter(name="code-skill", description="test"), instructions="body" ) - code_skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + code_skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider( DeduplicatingSkillsSource( @@ -3459,8 +3500,8 @@ class TestSkillsProviderFactories: async def test_file_script_error_without_runner(self) -> None: # A skill with both a code script and a file-based script skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="code-s", function=lambda: "ok")) - skill.scripts.append(FileSkillScript(name="file-s", full_path=f"{_ABS}/test/scripts/s1.py")) + skill._scripts.append(InlineSkillScript(name="code-s", function=lambda: "ok")) + skill._scripts.append(FileSkillScript(name="file-s", full_path=f"{_ABS}/test/scripts/s1.py")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3480,7 +3521,7 @@ class TestSkillsProviderFactories: return f"async: {x}" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=async_func)) + skill._scripts.append(InlineSkillScript(name="s1", function=async_func)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3495,7 +3536,7 @@ class TestSkillsProviderFactories: return {"status": "ok", "value": 42} skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=returns_dict)) + skill._scripts.append(InlineSkillScript(name="s1", function=returns_dict)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3506,7 +3547,7 @@ class TestSkillsProviderFactories: async def test_code_script_returns_none(self) -> None: """Code-defined scripts returning None pass through as None.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3517,8 +3558,8 @@ class TestSkillsProviderFactories: async def test_script_with_path_errors_without_runner(self) -> None: """A file-based script without a runner should return an error.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="code-s", function=lambda: "ok")) - skill.scripts.append(FileSkillScript(name="path-s", full_path=f"{_ABS}/test/scripts/s1.py")) + skill._scripts.append(InlineSkillScript(name="code-s", function=lambda: "ok")) + skill._scripts.append(FileSkillScript(name="path-s", full_path=f"{_ABS}/test/scripts/s1.py")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3535,7 +3576,7 @@ class TestSkillsProviderFactories: async def test_run_skill_script_error_on_missing_skill(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3606,7 +3647,7 @@ class TestSkillsProviderFactories: async def test_run_skill_script_error_on_missing_script(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3617,7 +3658,7 @@ class TestSkillsProviderFactories: async def test_run_skill_script_error_on_empty_names(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3631,7 +3672,7 @@ class TestSkillsProviderFactories: async def test_instructions_include_script_runner_hints(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3642,12 +3683,11 @@ class TestSkillsProviderFactories: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") provider = SkillsProvider([skill]) await _init_provider(provider) - # No scripts and no runner — instructions should not mention run_skill_script - assert "run_skill_script" not in (_ctx(provider)[1] or "") + assert "run_skill_script" in (_ctx(provider)[1] or "") async def test_tool_schema_args_description_mentions_key_format(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3658,7 +3698,7 @@ class TestSkillsProviderFactories: async def test_require_script_approval_sets_approval_mode(self) -> None: """When require_script_approval=True, the run_skill_script tool has approval_mode='always_require'.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill], require_script_approval=True) await _init_provider(provider) @@ -3668,7 +3708,7 @@ class TestSkillsProviderFactories: async def test_require_script_approval_false_by_default(self) -> None: """By default, the run_skill_script tool has approval_mode='never_require'.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3676,14 +3716,14 @@ class TestSkillsProviderFactories: assert run_tool.approval_mode == "never_require" async def test_require_script_approval_does_not_affect_other_tools(self) -> None: - """The load_skill tool should never require approval.""" + """Non-script tools should never require approval.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill], require_script_approval=True) await _init_provider(provider) other_tools = [t for t in _ctx(provider)[2] if hasattr(t, "name") and t.name != "run_skill_script"] - assert len(other_tools) == 1 + assert len(other_tools) == 2 for t in other_tools: assert t.approval_mode == "never_require" @@ -3694,7 +3734,7 @@ class TestSkillsProviderFactories: raise RuntimeError("Something went wrong") skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="boom", function=failing_script)) + skill._scripts.append(InlineSkillScript(name="boom", function=failing_script)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -3705,16 +3745,20 @@ class TestSkillsProviderFactories: assert "Something went wrong" not in result async def test_custom_template_without_runner_placeholder_raises(self) -> None: - """Provider with code scripts and custom template missing {runner_instructions} raises.""" + """Providers accept custom templates without {runner_instructions}.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider( [skill], instruction_template="Skills: {skills}", ) - with pytest.raises(ValueError, match="runner_instructions"): - await _init_provider(provider) + await _init_provider(provider) + instructions = _ctx(provider)[1] + assert instructions is not None + assert instructions.startswith("Skills: ") + assert "my-skill" in instructions + assert "test" in instructions # --------------------------------------------------------------------------- @@ -3737,8 +3781,8 @@ class TestFileScriptDiscovery: skills = await _discover_file_skills_for_test(str(tmp_path)) assert "my-skill" in skills - assert len(skills["my-skill"].scripts) == 1 - assert skills["my-skill"].scripts[0].name == "scripts/analyze.py" + assert len(skills["my-skill"]._scripts) == 1 + assert skills["my-skill"]._scripts[0].name == "scripts/analyze.py" async def test_root_py_files_not_discovered_by_default(self, tmp_path: Path) -> None: """Scripts at the skill root are NOT discovered with default directories.""" @@ -3752,7 +3796,7 @@ class TestFileScriptDiscovery: skills = await _discover_file_skills_for_test(str(tmp_path)) assert "my-skill" in skills - assert len(skills["my-skill"].scripts) == 0 + assert len(skills["my-skill"]._scripts) == 0 async def test_discovered_script_has_absolute_full_path(self, tmp_path: Path) -> None: skill_dir = tmp_path / "my-skill" @@ -3765,7 +3809,7 @@ class TestFileScriptDiscovery: (scripts_dir / "generate.py").write_text("print('gen')", encoding="utf-8") skills = await _discover_file_skills_for_test(str(tmp_path)) - script = skills["my-skill"].scripts[0] + script = skills["my-skill"]._scripts[0] assert script.full_path is not None assert os.path.isabs(script.full_path) expected = str(Path(str(skill_dir), "scripts", "generate.py")) @@ -3788,8 +3832,8 @@ class TestFileScriptDiscovery: (sub_dir / "nested.py").write_text("print('nested')", encoding="utf-8") skills = await _discover_file_skills_for_test(str(tmp_path)) - assert len(skills["my-skill"].scripts) == 1 - assert skills["my-skill"].scripts[0].name == "scripts/top.py" + assert len(skills["my-skill"]._scripts) == 1 + assert skills["my-skill"]._scripts[0].name == "scripts/top.py" async def test_no_scripts_when_no_py_files(self, tmp_path: Path) -> None: skill_dir = tmp_path / "my-skill" @@ -3801,7 +3845,7 @@ class TestFileScriptDiscovery: (skill_dir / "readme.md").write_text("# Docs", encoding="utf-8") skills = await _discover_file_skills_for_test(str(tmp_path)) - assert len(skills["my-skill"].scripts) == 0 + assert len(skills["my-skill"]._scripts) == 0 class TestCustomScriptExtensions: @@ -3821,13 +3865,13 @@ class TestCustomScriptExtensions: # Default: only .py discovered skills_default = await _discover_file_skills_for_test(str(tmp_path)) - script_names_default = [s.name for s in skills_default["my-skill"].scripts] + script_names_default = [s.name for s in skills_default["my-skill"]._scripts] assert "scripts/analyze.py" in script_names_default assert "scripts/run.sh" not in script_names_default # Custom: only .sh discovered skills_custom = await _discover_file_skills_for_test(str(tmp_path), script_extensions=(".sh",)) - script_names_custom = [s.name for s in skills_custom["my-skill"].scripts] + script_names_custom = [s.name for s in skills_custom["my-skill"]._scripts] assert "scripts/run.sh" in script_names_custom assert "scripts/analyze.py" not in script_names_custom @@ -3851,7 +3895,7 @@ class TestCustomScriptExtensions: ) await _init_provider(provider) skill = _ctx(provider)[0]["my-skill"] - script_names = [s.name for s in skill.scripts] + script_names = [s.name for s in skill._scripts] assert "scripts/run.sh" in script_names assert "scripts/analyze.py" not in script_names @@ -3875,7 +3919,7 @@ class TestCustomScriptExtensions: ) await _init_provider(provider) skill = _ctx(provider)[0]["my-skill"] - script_names = [s.name for s in skill.scripts] + script_names = [s.name for s in skill._scripts] assert "scripts/analyze.py" in script_names assert "scripts/run.sh" in script_names assert "scripts/notes.txt" not in script_names @@ -3895,7 +3939,7 @@ class TestCreateInstructionsWithScripts: def test_excludes_script_count(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) result = SkillsProvider._create_instructions(None, [skill]) assert result is not None @@ -3919,11 +3963,11 @@ class TestLoadSkillWithScripts: async def test_code_skill_includes_scripts_element(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="analyze", description="Run analysis", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="analyze", description="Run analysis", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "" in result assert 'name="analyze"' in result @@ -3933,7 +3977,7 @@ class TestLoadSkillWithScripts: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "" not in result @@ -4000,25 +4044,25 @@ class TestClassSkill: skill = _MinimalClassSkill() assert skill.scripts == [] - def test_minimal_skill_content_contains_name(self) -> None: + async def test_minimal_skill_content_contains_name(self) -> None: skill = _MinimalClassSkill() - assert "minimal-skill" in skill.content + assert "minimal-skill" in (await skill.get_content()) - def test_minimal_skill_content_contains_description(self) -> None: + async def test_minimal_skill_content_contains_description(self) -> None: skill = _MinimalClassSkill() - assert "A minimal skill." in skill.content + assert "A minimal skill." in (await skill.get_content()) - def test_minimal_skill_content_contains_instructions(self) -> None: + async def test_minimal_skill_content_contains_instructions(self) -> None: skill = _MinimalClassSkill() - assert "Do minimal things." in skill.content + assert "Do minimal things." in (await skill.get_content()) - def test_minimal_skill_content_no_resources_element(self) -> None: + async def test_minimal_skill_content_no_resources_element(self) -> None: skill = _MinimalClassSkill() - assert "" not in skill.content + assert "" not in (await skill.get_content()) - def test_minimal_skill_content_no_scripts_element(self) -> None: + async def test_minimal_skill_content_no_scripts_element(self) -> None: skill = _MinimalClassSkill() - assert "" not in skill.content + assert "" not in (await skill.get_content()) def test_full_skill_has_resources(self) -> None: skill = _FullClassSkill() @@ -4030,20 +4074,20 @@ class TestClassSkill: assert len(skill.scripts) == 1 assert skill.scripts[0].name == "test-script" - def test_full_skill_content_contains_resources(self) -> None: + async def test_full_skill_content_contains_resources(self) -> None: skill = _FullClassSkill() - assert "" in skill.content - assert 'name="test-resource"' in skill.content + assert "" in (await skill.get_content()) + assert 'name="test-resource"' in (await skill.get_content()) - def test_full_skill_content_contains_scripts(self) -> None: + async def test_full_skill_content_contains_scripts(self) -> None: skill = _FullClassSkill() - assert "" in skill.content - assert 'name="test-script"' in skill.content + assert "" in (await skill.get_content()) + assert 'name="test-script"' in (await skill.get_content()) - def test_content_is_cached(self) -> None: + async def test_content_is_cached(self) -> None: skill = _MinimalClassSkill() - content1 = skill.content - content2 = skill.content + content1 = (await skill.get_content()) + content2 = (await skill.get_content()) assert content1 is content2 def test_resources_are_lazy_cached(self) -> None: @@ -4081,7 +4125,7 @@ class TestClassSkill: provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "full-skill") + result = await provider._load_skill(_raw_skills(provider), "full-skill") assert "Use this skill for full tasks." in result assert "" in result assert "" in result @@ -4299,15 +4343,15 @@ class TestClassSkillDecoratorDiscovery: assert s1 == s2 assert s1 is not s2 # defensive copy - def test_content_includes_discovered_resources(self) -> None: + async def test_content_includes_discovered_resources(self) -> None: skill = _DecoratorClassSkill() - assert "" in skill.content - assert 'name="lookup-table"' in skill.content + assert "" in (await skill.get_content()) + assert 'name="lookup-table"' in (await skill.get_content()) - def test_content_includes_discovered_scripts(self) -> None: + async def test_content_includes_discovered_scripts(self) -> None: skill = _DecoratorClassSkill() - assert "" in skill.content - assert 'name="convert"' in skill.content + assert "" in (await skill.get_content()) + assert 'name="convert"' in (await skill.get_content()) def test_duplicate_resource_name_raises(self) -> None: skill = _DuplicateResourceSkill() @@ -4361,9 +4405,9 @@ class TestClassSkillDecoratorDiscovery: skill = _PropertyResourceSkill() assert skill.resources[0].description is None - def test_property_resource_in_content(self) -> None: + async def test_property_resource_in_content(self) -> None: skill = _PropertyResourceSkill() - assert 'name="static-table"' in skill.content + assert 'name="static-table"' in (await skill.get_content()) async def test_mixed_property_and_method_resources(self) -> None: """Property and method resources can coexist.""" @@ -4386,11 +4430,11 @@ class TestClassSkillDecoratorDiscovery: scr = next(s for s in skill.scripts if s.name == "described-scr") assert scr.description == "A described script." - def test_explicit_description_in_content_xml(self) -> None: + async def test_explicit_description_in_content_xml(self) -> None: """Explicit descriptions appear in the skill content XML.""" skill = _ExplicitDescriptionSkill() - assert 'description="A described resource."' in skill.content - assert 'description="A described script."' in skill.content + assert 'description="A described resource."' in (await skill.get_content()) + assert 'description="A described script."' in (await skill.get_content()) def test_property_getter_not_called_during_discovery(self) -> None: """Property getter must NOT be evaluated when resources are discovered.""" @@ -4698,11 +4742,11 @@ class _MixedPropertyMethodSkill(ClassSkill): return "result" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="analyze", description="Run analysis", function=analyze)) + skill._scripts.append(InlineSkillScript(name="analyze", description="Run analysis", function=analyze)) provider = SkillsProvider([skill]) await _init_provider(provider) - result = provider._load_skill(_raw_skills(provider), "my-skill") + result = await provider._load_skill(_raw_skills(provider), "my-skill") assert "" in result assert 'name="analyze"' in result @@ -4715,7 +4759,7 @@ class TestReadSkillResourceWithScripts: async def test_reads_script_with_static_content(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="generate.py", function=lambda: "print('hello')")) + skill._scripts.append(InlineSkillScript(name="generate.py", function=lambda: "print('hello')")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -4725,7 +4769,7 @@ class TestReadSkillResourceWithScripts: async def test_script_not_accessible_via_read_resource(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="run.py", function=lambda: "script output")) + skill._scripts.append(InlineSkillScript(name="run.py", function=lambda: "script output")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -4738,7 +4782,7 @@ class TestReadSkillResourceWithScripts: return "async output" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="run.py", function=async_script)) + skill._scripts.append(InlineSkillScript(name="run.py", function=async_script)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -4747,7 +4791,7 @@ class TestReadSkillResourceWithScripts: async def test_script_case_insensitive_not_in_resources(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="Generate.py", function=lambda: "code")) + skill._scripts.append(InlineSkillScript(name="Generate.py", function=lambda: "code")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -4756,8 +4800,8 @@ class TestReadSkillResourceWithScripts: async def test_resource_takes_priority_over_script(self) -> None: skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.resources.append(InlineSkillResource(name="data.py", content="resource content")) - skill.scripts.append(InlineSkillScript(name="data.py", function=lambda: "script content")) + skill._resources.append(InlineSkillResource(name="data.py", content="resource content")) + skill._scripts.append(InlineSkillScript(name="data.py", function=lambda: "script content")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -4769,7 +4813,7 @@ class TestReadSkillResourceWithScripts: raise RuntimeError("boom") skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="bad.py", function=failing_script)) + skill._scripts.append(InlineSkillScript(name="bad.py", function=failing_script)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -5056,7 +5100,7 @@ class TestSkillsSource: source = FileSkillsSource(str(tmp_path), resource_extensions=(".json",)) skills = await source.get_skills() assert len(skills) == 1 - resource_names = [r.name for r in skills[0].resources] + resource_names = [r.name for r in skills[0]._resources] assert "references/data.json" in resource_names assert "references/data.csv" not in resource_names @@ -5304,7 +5348,7 @@ class TestSourceComposition: async def test_script_approval_on_provider(self) -> None: """SkillsProvider with require_script_approval sets the approval mode.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider( DeduplicatingSkillsSource(InMemorySkillsSource([skill])), @@ -5540,11 +5584,11 @@ class TestSkillsProviderConstructorEdgeCases: class TestInlineSkillContentCaching: """Tests for InlineSkill.content caching.""" - def test_content_cached_after_first_access(self) -> None: + async def test_content_cached_after_first_access(self) -> None: """InlineSkill.content returns the same object on subsequent accesses.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body") - first = skill.content - second = skill.content + first = (await skill.get_content()) + second = (await skill.get_content()) assert first is second # Same object (cached) assert "test-skill" in first @@ -5642,7 +5686,7 @@ class TestArrayStyleScriptArgs: async def test_tool_schema_accepts_array_args(self) -> None: """The run_skill_script tool schema accepts array-style args via oneOf.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: None)) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: None)) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -5680,7 +5724,7 @@ class TestArrayStyleScriptArgs: async def test_run_skill_script_inline_with_list_args_returns_error(self) -> None: """Inline script called with list args through provider returns error (TypeError caught).""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="my-skill", description="test"), instructions="body") - skill.scripts.append(InlineSkillScript(name="s1", function=lambda: "ok")) + skill._scripts.append(InlineSkillScript(name="s1", function=lambda: "ok")) provider = SkillsProvider([skill]) await _init_provider(provider) @@ -5689,7 +5733,7 @@ class TestArrayStyleScriptArgs: assert "Error" in result assert "Failed to run" in result - def test_file_skill_content_includes_scripts_block(self) -> None: + async def test_file_skill_content_includes_scripts_block(self) -> None: """FileSkill.content appends a block when scripts are present.""" script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py") skill = FileSkill( @@ -5698,16 +5742,16 @@ class TestArrayStyleScriptArgs: path=f"{_ABS}/test", scripts=[script], ) - assert "" in skill.content - assert 'name="run.py"' in skill.content - assert "" in skill.content - assert '"type": "array"' in skill.content + assert "" in (await skill.get_content()) + assert 'name="run.py"' in (await skill.get_content()) + assert "" in (await skill.get_content()) + assert '"type": "array"' in (await skill.get_content()) - def test_file_skill_content_no_scripts_no_block(self) -> None: + async def test_file_skill_content_no_scripts_no_block(self) -> None: """FileSkill.content does not append a block when no scripts.""" skill = FileSkill( frontmatter=SkillFrontmatter(name="my-skill", description="test"), content="---\nname: my-skill\n---\nBody", path=f"{_ABS}/test", ) - assert "" not in skill.content + assert "" not in (await skill.get_content()) From 718a1f14fd63d6ffeac84bdcfa56665621f1847a Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Thu, 28 May 2026 09:47:31 -0700 Subject: [PATCH 04/61] Add missing projects to solution for release (#6157) --- dotnet/agent-framework-release.slnf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/agent-framework-release.slnf b/dotnet/agent-framework-release.slnf index cc84fe6c5a..1b22e14e5a 100644 --- a/dotnet/agent-framework-release.slnf +++ b/dotnet/agent-framework-release.slnf @@ -23,11 +23,13 @@ "src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj", "src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj", "src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj", + "src\\Microsoft.Agents.AI.Mcp\\Microsoft.Agents.AI.Mcp.csproj", "src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj", "src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj", "src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj", "src\\Microsoft.Agents.AI.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj", "src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj", + "src\\Microsoft.Agents.AI.Workflows.Declarative.Mcp\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj", "src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj", "src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj", "src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj", From 401a552735a9c03727a35f1f98e22188851710ea Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 28 May 2026 13:43:18 -0400 Subject: [PATCH 05/61] .NET: Support ClaimsIdentity-based scoping of agent sessions (#5696) * feat: Add DelegatingAgentSessionStore Add helper for decorator pattern for AgentSessionStore * feat: Add UserIdentityScopedSessionStore Add support for using the ASP.Net Core ambient `ClaimsIdentity` User, along with a user-specified claim type to scope the session store based on authenticated identity. * fix: Harden scope mapping * fix: Add UserIdentityScopeSessionStoreOptions to avoid future breaking changes * Split UserIdentityScopedSessionStore into a separate IsolationKeyProvider and IsolationKeyScopedSessionStore * Add GetService<>() capabilities to interrogate AgentSessionStore delegation chain * Harden default for A2A hosting by using an IsolationKeyScopedAgentSessionStore when no store is available. * Pipe isolation through Hosting helper extension methods * Add comment to samples about adding SessionIsolationKeyProvider * Fix isolation key provider nullability semantics * fix A2A defaults * fixup * remove unneeded keyProvider requirement test * Add trust-model XML docs to AgentSessionStore, InMemoryAgentSessionStore, MapAGUI, A2A entry points Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/e466c53a-faad-40a8-8b5f-83cf0dce0b1d Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * fix: Switch ClaimsBasedIsolationKeyProvider to be Singleton * matches HttpContextAccessor and related MAF services * release: Ensure new project is in the release filter * fixup: Integraitaon tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 1 + dotnet/agent-framework-release.slnf | 3 +- .../A2AClientServer/A2AServer/Program.cs | 4 + .../AGUIClientServer/AGUIServer/Program.cs | 4 + .../AgentWebChat.AgentHost/Program.cs | 8 + ...ft.Agents.AI.Hosting.A2A.AspNetCore.csproj | 1 + .../A2AServerServiceCollectionExtensions.cs | 55 ++- .../AGUIEndpointRouteBuilderExtensions.cs | 20 + ...aimsIdentitySessionIsolationKeyProvider.cs | 78 ++++ ...ntitySessionIsolationKeyProviderOptions.cs | 30 ++ ...rosoft.Agents.AI.Hosting.AspNetCore.csproj | 30 ++ .../ServiceCollectionExtensions.cs | 42 ++ .../AgentSessionStore.cs | 63 +++ .../DelegatingAgentSessionStore.cs | 81 ++++ .../HostedAgentBuilderExtensions.cs | 43 +- .../IsolationKeyScopedAgentSessionStore.cs | 109 +++++ ...lationKeyScopedAgentSessionStoreOptions.cs | 25 + .../Local/InMemoryAgentSessionStore.cs | 14 + .../SessionIsolationKeyProvider.cs | 39 ++ .../SessionPersistenceTests.cs | 2 +- ...dentitySessionIsolationKeyProviderTests.cs | 251 ++++++++++ .../DelegatingAgentSessionStoreTests.cs | 400 ++++++++++++++++ ...solationKeyScopedAgentSessionStoreTests.cs | 430 ++++++++++++++++++ ...crosoft.Agents.AI.Hosting.UnitTests.csproj | 1 + .../SessionIsolationKeyProviderTests.cs | 95 ++++ 25 files changed, 1814 insertions(+), 15 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index c6480cf02a..fbf09d442a 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -605,6 +605,7 @@ + diff --git a/dotnet/agent-framework-release.slnf b/dotnet/agent-framework-release.slnf index 1b22e14e5a..f4bf930e45 100644 --- a/dotnet/agent-framework-release.slnf +++ b/dotnet/agent-framework-release.slnf @@ -20,7 +20,8 @@ "src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj", "src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj", "src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj", - "src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj", + "src\\Microsoft.Agents.AI.Hosting.AspNetCore\\Microsoft.Agents.AI.Hosting.AspNetCore.csproj", + "src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj", "src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj", "src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj", "src\\Microsoft.Agents.AI.Mcp\\Microsoft.Agents.AI.Mcp.csproj", diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs index c12a1c9431..c694351599 100644 --- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs @@ -101,6 +101,10 @@ else throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided"); } +// When running in production, make sure to use an SessionIsolationKeyProvider, e.g. ClaimsIdentity-based +// if using Claims-based Identity for Authentication/Authorization +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + builder.AddA2AServer(hostA2AAgent); var app = builder.Build(); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index a12ca1c5ad..e3b97d34e1 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -49,6 +49,10 @@ var agent = new AzureOpenAIClient( AGUIServerSerializerContext.Default.Options) ]); +// When running in production, make sure to use an SessionIsolationKeyProvider, e.g. ClaimsIdentity-based +// if using Claims-based Identity for Authentication/Authorization +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + // Register the agent with the host and configure it to use an in-memory session store // so that conversation state is maintained across requests. In production, you may want to use a persistent session store. builder diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 61cfdcdb68..ca399272b4 100644 --- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -28,6 +28,10 @@ builder.AddDevUI(); builder.AddOpenAIChatCompletions(); builder.AddOpenAIResponses(); +// When running in production, make sure to use an SessionIsolationKeyProvider, e.g. ClaimsIdentity-based +// if using Claims-based Identity for Authentication/Authorization +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + var pirateAgentBuilder = builder.AddAIAgent( "pirate", instructions: "You are a pirate. Speak like a pirate", @@ -148,6 +152,10 @@ builder.Services.AddKeyedSingleton("my-di-matchingname-agent", (sp, nam pirateAgentBuilder.AddA2AServer(); knightsKnavesAgentBuilder.AddA2AServer(); +// When running in production, make sure to use an SessionIsolationKeyProvider, e.g. ClaimsIdentity-based +// if using Claims-based Identity for Authentication/Authorization +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + var app = builder.Build(); app.MapOpenApi(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj index 200aa29ccc..b91ea40baa 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj @@ -27,6 +27,7 @@ + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs index 29ab28c250..cb0e15dac8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/A2AServerServiceCollectionExtensions.cs @@ -28,6 +28,23 @@ public static class A2AServerServiceCollectionExtensions /// The agent builder whose name identifies the agent. /// An optional callback to configure . /// The for chaining. + /// + /// + /// Trust model. The A2A contextId arrives from the wire + /// and is treated as a chain-resume identifier — not as an authorization + /// token. The contract carries no principal/owner + /// dimension, so when a persistent store is registered any caller who knows or + /// guesses another caller's contextId can resume that other caller's + /// persisted thread. Hosts that serve more than one user must compose a principal + /// dimension into the lookup key — typically by calling + /// UseClaimsBasedSessionIsolation(...) from + /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom + /// ). When no isolation provider is + /// registered, behavior is unchanged — the bare contextId is used as the + /// conversation identifier, which is appropriate for first-run / single-user / + /// prototyping scenarios but unsafe for multi-user hosts. + /// + /// public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBuilder, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(agentBuilder); @@ -46,6 +63,13 @@ public static class A2AServerServiceCollectionExtensions /// The name of the agent to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(builder); @@ -65,6 +89,13 @@ public static class A2AServerServiceCollectionExtensions /// The agent instance to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(builder); @@ -83,6 +114,13 @@ public static class A2AServerServiceCollectionExtensions /// The name of the agent to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(services); @@ -114,6 +152,13 @@ public static class A2AServerServiceCollectionExtensions /// The agent instance to create an A2A server for. /// An optional callback to configure . /// The for chaining. + /// + /// See the trust-model remarks on + /// for guidance on multi-user hosts (the wire contextId is a chain-resume + /// identifier, not an authorization token; multi-user hosts must compose a + /// principal dimension via UseClaimsBasedSessionIsolation(...) or a custom + /// ). + /// public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action? configureOptions = null) { ArgumentNullException.ThrowIfNull(services); @@ -140,9 +185,17 @@ public static class A2AServerServiceCollectionExtensions var agentSessionStore = serviceProvider.GetKeyedService(agent.Name); var runMode = options?.AgentRunMode ?? AgentRunMode.DisallowBackground; + // Ensure that we have an IsolationKeyScopedAgentSessionStore registered. + var isolationKeyProvider = serviceProvider.GetService(); + if (agentSessionStore?.GetService() is null) + { + agentSessionStore ??= new InMemoryAgentSessionStore(); + agentSessionStore = new IsolationKeyScopedAgentSessionStore(agentSessionStore, isolationKeyProvider, new() { Strict = isolationKeyProvider != null }); + } + var hostAgent = new AIHostAgent( innerAgent: agent, - sessionStore: agentSessionStore ?? new InMemoryAgentSessionStore()); + sessionStore: agentSessionStore); agentHandler = new A2AAgentHandler(hostAgent, runMode); } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs index 948ecdca42..85fd00fb8b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs @@ -73,6 +73,26 @@ public static class AGUIEndpointRouteBuilderExtensions /// it will be used to persist conversation sessions across requests using the AG-UI thread ID as the /// conversation identifier. If no session store is registered, sessions are ephemeral (not persisted). /// + /// + /// Trust model. The AG-UI RunAgentInput.ThreadId arrives + /// from the wire and is treated as a chain-resume identifier — not as an + /// authorization token. The contract carries no + /// principal/owner dimension, so when a persistent store is registered any caller + /// who knows or guesses another caller's ThreadId can resume that other + /// caller's persisted thread. Hosts that serve more than one user must compose a + /// principal dimension into the lookup key. The recommended way is to wrap the + /// keyed in + /// , typically by calling + /// UseClaimsBasedSessionIsolation(...) from + /// Microsoft.Agents.AI.Hosting.AspNetCore (or by registering a custom + /// ) and registering the store via the + /// WithSessionStore(...) / WithInMemorySessionStore(...) helpers on + /// so that the wrapper is applied. When no + /// isolation provider is registered, behavior is unchanged — the bare + /// ThreadId is used as the conversation identifier, which is appropriate + /// for first-run / single-user / prototyping scenarios but unsafe for + /// multi-user hosts. + /// /// public static IEndpointConventionBuilder MapAGUI( this IEndpointRouteBuilder endpoints, diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs new file mode 100644 index 0000000000..3d61b9bea1 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProvider.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// A that extracts the session isolation key from a claim +/// in the current user's identity, as provided by ASP.NET Core's . +/// +/// +/// +/// This provider is suitable for ASP.NET Core web applications where session isolation is based on +/// authenticated user identity. It reads a specified claim type (e.g., name, email, or a custom identifier) +/// from the ambient . +/// +/// +/// If the is unavailable, the user is not authenticated, or the specified claim +/// is missing, the provider returns . The consuming +/// will then enforce strict or pass-through behavior based on its configuration. +/// +/// +/// This class relies on , which uses +/// to provide access to the current . +/// +/// +public class ClaimsIdentitySessionIsolationKeyProvider : SessionIsolationKeyProvider +{ + private readonly IHttpContextAccessor? _httpContextAccessor; + private readonly string _claimType; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The used to retrieve the current HTTP context and user claims. + /// + /// The options for configuring the provider. If null, defaults are used. + /// + /// is null, empty, or whitespace. + /// + public ClaimsIdentitySessionIsolationKeyProvider( + IHttpContextAccessor? httpContextAccessor, + ClaimsIdentitySessionIsolationKeyProviderOptions? options = null) + { + options ??= new ClaimsIdentitySessionIsolationKeyProviderOptions(); + this._httpContextAccessor = httpContextAccessor; + this._claimType = Throw.IfNullOrWhitespace(options.ClaimType); + } + + /// + /// Extracts the session isolation key from the current user's claims. + /// + /// The to monitor for cancellation requests. + /// + /// A task that represents the asynchronous operation. The task result contains the value of the + /// configured claim type from the current user's identity, or if the claim + /// is not present or the HTTP context is unavailable. + /// + /// + /// This method retrieves the claim value from HttpContext.User.Claims. If multiple claims + /// of the specified type exist, the first match is returned. + /// + public override ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + { + Claim? claim = this._httpContextAccessor? + .HttpContext? + .User?.Claims.FirstOrDefault(c => c.Type == this._claimType); + + return new ValueTask(claim?.Value); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs new file mode 100644 index 0000000000..13845bc680 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ClaimsIdentitySessionIsolationKeyProviderOptions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Security.Claims; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Options for configuring . +/// +public class ClaimsIdentitySessionIsolationKeyProviderOptions +{ + /// + /// Gets or sets the claim type to extract from the user's identity for session isolation. + /// + /// + /// + /// Defaults to , which typically corresponds to + /// the user's name or unique identifier claim. + /// + /// + /// Common alternatives include: + /// + /// ClaimTypes.NameIdentifier — Stable user identifier + /// ClaimTypes.Email — Email address + /// Custom claim types specific to your authentication provider + /// + /// + /// + public string ClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj new file mode 100644 index 0000000000..2dd1834008 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj @@ -0,0 +1,30 @@ + + + + $(TargetFrameworksCore) + Microsoft.Agents.AI.Hosting.AspNetCore + preview + $(NoWarn) + + + + + + true + true + true + + + + + + + + + + + + Microsoft Agent Framework Hosting ASP.NET Core + Provides Microsoft Agent Framework support for hosting agents in an ASP.NET Core context. + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..0ff8d37371 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Extension methods for configuring AI hosting services in an . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers a that uses claims from the current user's identity + /// to generate session isolation keys. + /// + /// The to add services to. + /// Optional configuration for the claims-based session isolation key provider. + /// The so that additional calls can be chained. + /// + /// This method requires to be registered in the service collection. + /// Ensure that services.AddHttpContextAccessor() has been called before using this method. + /// + public static IServiceCollection UseClaimsBasedSessionIsolation( + this IServiceCollection services, + ClaimsIdentitySessionIsolationKeyProviderOptions? options = null) + { + options ??= new(); + ServiceDescriptor descriptor = new(typeof(SessionIsolationKeyProvider), CreateIsolationKeyProvider, ServiceLifetime.Singleton); + services.Add(descriptor); + + return services; + + object CreateIsolationKeyProvider(IServiceProvider serviceProvider) + { + IHttpContextAccessor contextAccessor = serviceProvider.GetRequiredService(); + + return new ClaimsIdentitySessionIsolationKeyProvider(contextAccessor, options); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs index 2f57e26409..7c0539fe51 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Hosting; @@ -9,9 +11,39 @@ namespace Microsoft.Agents.AI.Hosting; /// Defines the contract for storing and retrieving agent conversation threads. /// /// +/// /// Implementations of this interface enable persistent storage of conversation threads, /// allowing conversations to be resumed across HTTP requests, application restarts, /// or different service instances in hosted scenarios. +/// +/// +/// Trust model. The conversationId passed to +/// and typically originates +/// from the wire (for example, an AG-UI RunAgentInput.ThreadId or an A2A +/// contextId). It is a chain-resume identifier, not an authorization +/// token, and the (agent, conversationId) tuple carries no principal/owner +/// dimension. Hosts that serve more than one user from the same registered store must +/// therefore compose a principal dimension into the lookup key, otherwise any caller +/// who knows or guesses another caller's conversationId can resume +/// that other caller's persisted thread. The framework provides +/// as a decorator that rewrites +/// conversationId to include an isolation key resolved from a +/// (for example, the ASP.NET Core +/// ClaimsIdentitySessionIsolationKeyProvider wired up via +/// UseClaimsBasedSessionIsolation(...)). When no provider is registered, the +/// store behaves as a single-namespace persistence layer — appropriate for +/// single-user / first-run / prototyping scenarios but unsafe for multi-user hosts. +/// +/// +/// Implementer guidance. Implementations should treat +/// conversationId as opaque: do not parse it, do not impose length +/// or character-set constraints on it, and do not assume it round-trips to the value +/// the caller originally supplied (decorators such as +/// may rewrite it before forwarding). +/// Be aware that any logging, telemetry, or audit sink that surfaces +/// conversationId will also surface the isolation prefix when a +/// scoping decorator is in the chain. +/// /// public abstract class AgentSessionStore { @@ -43,4 +75,35 @@ public abstract class AgentSessionStore AIAgent agent, string conversationId, CancellationToken cancellationToken = default); + + /// Asks the for an object of the specified type . + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , + /// including itself or any services it might be wrapping. This is particularly useful for inspecting delegation chains + /// to verify that specific store implementations are present. + /// + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return serviceKey is null && serviceType.IsInstanceOfType(this) + ? this + : null; + } + + /// Asks the for an object of type . + /// The type of the object to be retrieved. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , + /// including itself or any services it might be wrapping. This is particularly useful for inspecting delegation chains + /// to verify that specific store implementations are present. + /// + public TService? GetService(object? serviceKey = null) + => this.GetService(typeof(TService), serviceKey) is TService service ? service : default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs new file mode 100644 index 0000000000..e80d3b907a --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Provides an abstract base class for agent session stores that delegate operations to an inner store +/// instance while allowing for extensibility and customization. +/// +/// +/// +/// implements the decorator pattern for s, +/// enabling the creation of pipelines where each layer can add functionality while delegating core operations to an +/// underlying store. +/// +/// +/// The default implementation provides transparent pass-through behavior, forwarding all operations to the inner store. +/// Derived classes can override specific methods to add custom behavior while maintaining compatibility with the store +/// interface. +/// +/// +public abstract class DelegatingAgentSessionStore : AgentSessionStore +{ + /// + /// Initializes a new instance of the class with the specified inner + /// store. + /// + /// The underlying session store instance that will handle the core operations. + /// is . + /// + /// The inner session store serves as the foundation of the delegation chain. All operations not overridden by + /// derived classes will be forwarded to this store. + /// + protected DelegatingAgentSessionStore(AgentSessionStore innerStore) + { + this.InnerStore = Throw.IfNull(innerStore); + } + + /// + /// Gets the inner session store instance that receives delegated operations. + /// + /// + /// The underlying instance that handles core storage operations. + /// + /// + /// Derived classes can use this property to access the inner session store for custom delegation scenarios + /// or to forward operations with additional processing. + /// + protected AgentSessionStore InnerStore { get; } + + /// + public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + => this.InnerStore.GetSessionAsync(agent, conversationId, cancellationToken); + + /// + public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + => this.InnerStore.SaveSessionAsync(agent, conversationId, session, cancellationToken); + + /// + /// + /// This implementation first checks if this instance satisfies the service request. + /// If not, it chains the request to the inner store, allowing services to be retrieved + /// from any store in the delegation chain. + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + // First, check if this instance satisfies the request + object? service = base.GetService(serviceType, serviceKey); + if (service is not null) + { + return service; + } + + // Chain to the inner store + return this.InnerStore.GetService(serviceType, serviceKey); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs index d1397fcda4..ed11840f5e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Hosting; @@ -16,12 +17,11 @@ public static class HostedAgentBuilderExtensions /// Configures the host agent builder to use an in-memory session store for agent session management. /// /// The host agent builder to configure with the in-memory session store. + /// When , wraps the session store with an + /// to provide isolation-key-based scoping for sessions. Defaults to . /// The same instance, configured to use an in-memory session store. - public static IHostedAgentBuilder WithInMemorySessionStore(this IHostedAgentBuilder builder) - { - builder.ServiceCollection.AddKeyedSingleton(builder.Name, new InMemoryAgentSessionStore()); - return builder; - } + public static IHostedAgentBuilder WithInMemorySessionStore(this IHostedAgentBuilder builder, bool withIsolation = true) + => builder.WithSessionStore(new InMemoryAgentSessionStore(), withIsolation); /// /// Registers the specified agent session store with the host agent builder, enabling session-specific storage for @@ -29,12 +29,11 @@ public static class HostedAgentBuilderExtensions /// /// The host agent builder to configure with the session store. Cannot be null. /// The agent session store instance to register. Cannot be null. + /// When , wraps the session store with an + /// to provide isolation-key-based scoping for sessions. Defaults to . /// The same host agent builder instance, allowing for method chaining. - public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, AgentSessionStore store) - { - builder.ServiceCollection.AddKeyedSingleton(builder.Name, store); - return builder; - } + public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, AgentSessionStore store, bool withIsolation = true) + => builder.WithSessionStore((sp, key) => store, ServiceLifetime.Singleton, withIsolation); /// /// Configures the host agent builder to use a custom session store implementation for agent sessions. @@ -44,16 +43,36 @@ public static class HostedAgentBuilderExtensions /// name. /// The DI service lifetime for the session store registration. Defaults to /// because session stores persist conversation state across requests and are consumed independently of the agent's lifetime. + /// When , wraps the session store with an + /// to provide isolation-key-based scoping for sessions. Defaults to . /// The same host agent builder instance, enabling further configuration. - public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func createAgentSessionStore, ServiceLifetime lifetime = ServiceLifetime.Singleton) + public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func createAgentSessionStore, ServiceLifetime lifetime = ServiceLifetime.Singleton, bool withIsolation = true) { builder.ServiceCollection.AddKeyedService(builder.Name, (sp, key) => { Throw.IfNull(key); var keyString = key as string; Throw.IfNullOrEmpty(keyString); - return createAgentSessionStore(sp, keyString) ?? + + AgentSessionStore store = createAgentSessionStore(sp, keyString) ?? throw new InvalidOperationException($"The agent session store factory did not return a valid {nameof(AgentSessionStore)} instance for key '{keyString}'."); + + if (withIsolation && store.GetService() is null) + { + var isolationKeyProvider = sp.GetService(); + + // Best efforts options getting + IsolationKeyScopedAgentSessionStoreOptions? options = sp.GetService(); + if (options is null) + { + var optionsProvider = sp.GetService>(); + options = optionsProvider?.Value; + } + + store = new IsolationKeyScopedAgentSessionStore(store, isolationKeyProvider, options ?? new()); + } + + return store; }, lifetime); return builder; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs new file mode 100644 index 0000000000..f379d625fd --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// A delegating that scopes session keys by an isolation key +/// provided by a , ensuring that sessions are isolated +/// per logical partition (e.g., user, tenant, or composite key). +/// +public class IsolationKeyScopedAgentSessionStore : DelegatingAgentSessionStore +{ + private readonly SessionIsolationKeyProvider? _keyProvider; + private readonly bool _strict; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying to delegate to. + /// + /// The used to retrieve the isolation key for the current context. + /// + /// The options for configuring the session store. If null, defaults are used. + /// + /// is . + /// + public IsolationKeyScopedAgentSessionStore( + AgentSessionStore innerStore, + SessionIsolationKeyProvider? keyProvider, + IsolationKeyScopedAgentSessionStoreOptions? options = null) + : base(innerStore) + { + this._keyProvider = keyProvider; + options ??= new IsolationKeyScopedAgentSessionStoreOptions(); + this._strict = options.Strict; + } + + /// + /// Asynchronously retrieves the isolation key from the provider and validates it if in strict mode. + /// + /// The cancellation token. + /// + /// The isolation key string, or if no key is available and non-strict mode is enabled. + /// + /// + /// The provider returned and strict mode is enabled. + /// + private async ValueTask GetIsolationKeyAsync(CancellationToken cancellationToken) + { + string? key = this._keyProvider != null + ? await this._keyProvider.GetSessionIsolationKeyAsync(cancellationToken).ConfigureAwait(false) + : null; + + if (this._strict && key == null) + { + throw new InvalidOperationException("Session isolation key is required but was not provided by the configured SessionIsolationKeyProvider."); + } + + return key; + } + + /// + /// Escapes special characters in the isolation key to ensure unambiguous scoped conversation IDs. + /// + /// The raw isolation key. + /// The escaped isolation key. + /// + /// Backslashes are escaped first (\ becomes \\), then colons (: becomes \:). + /// This ensures the scoped conversation ID format {key}::{conversationId} can be parsed correctly. + /// + private static string EscapeIsolationKey(string key) => key.Replace("\\", "\\\\").Replace(":", "\\:"); + + /// + /// Constructs a scoped conversation ID by prefixing the bare conversation ID with the escaped isolation key. + /// + /// The original conversation ID. + /// The cancellation token. + /// + /// The scoped conversation ID in the format {escapedKey}::{conversationId}, or the bare conversation ID + /// if no isolation key is available and non-strict mode is enabled. + /// + private async ValueTask GetScopedConversationIdAsync(string bareConversationId, CancellationToken cancellationToken) + { + string? key = await this.GetIsolationKeyAsync(cancellationToken).ConfigureAwait(false); + if (key == null) + { + return bareConversationId; + } + + return $"{EscapeIsolationKey(key)}::{bareConversationId}"; + } + + /// + public override async ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + { + string scopedConversationId = await this.GetScopedConversationIdAsync(conversationId, cancellationToken).ConfigureAwait(false); + return await this.InnerStore.GetSessionAsync(agent, scopedConversationId, cancellationToken).ConfigureAwait(false); + } + + /// + public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + { + string scopedConversationId = await this.GetScopedConversationIdAsync(conversationId, cancellationToken).ConfigureAwait(false); + await this.InnerStore.SaveSessionAsync(agent, scopedConversationId, session, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs new file mode 100644 index 0000000000..94f00f01bb --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStoreOptions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Options for configuring . +/// +public class IsolationKeyScopedAgentSessionStoreOptions +{ + /// + /// Gets or sets a value indicating whether an exception should be thrown when the isolation key cannot be determined. + /// + /// + /// + /// If (default), the store will throw an + /// when returns . + /// + /// + /// If , the conversation ID is passed through unmodified when the isolation key is absent, + /// allowing unscoped access to the underlying session store. This mode is suitable for development scenarios + /// or mixed environments where not all requests have isolation keys. + /// + /// + public bool Strict { get; set; } = true; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs index 9999527505..832c411977 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs @@ -24,6 +24,20 @@ namespace Microsoft.Agents.AI.Hosting; /// For production use with multiple instances or persistence across restarts, use a durable storage implementation /// such as Redis, SQL Server, or Azure Cosmos DB. /// +/// +/// Multi-user warning. This store keys threads by +/// (agent.Id, conversationId) only — it has no principal/owner dimension. When +/// the conversation identifier originates from the wire (for example, an AG-UI +/// RunAgentInput.ThreadId or an A2A contextId), any caller who knows +/// or guesses another caller's identifier can resume that other caller's persisted +/// thread. Multi-user hosts must wrap this store in +/// (typically by calling +/// UseClaimsBasedSessionIsolation(...) from +/// Microsoft.Agents.AI.Hosting.AspNetCore or by registering a custom +/// ) so that the conversation namespace is +/// scoped per principal. See the trust-model remarks on +/// for the full background. +/// /// public sealed class InMemoryAgentSessionStore : AgentSessionStore { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs new file mode 100644 index 0000000000..61ea82bd34 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/SessionIsolationKeyProvider.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Provides an abstract base class for resolving session isolation keys used to scope agent sessions. +/// +/// +/// +/// Session isolation keys enable multi-tenant or multi-user scenarios by scoping agent session storage +/// to a specific logical partition (e.g., user ID, tenant ID, or composite key). Derived classes +/// implement the key resolution logic appropriate to their hosting environment. +/// +/// +/// When a key is unavailable or cannot be determined, implementations should return . +/// The consuming session store can then enforce strict behavior (throwing an exception) or fall back +/// to unscoped storage based on its configuration. +/// +/// +public abstract class SessionIsolationKeyProvider +{ + /// + /// Asynchronously retrieves the session isolation key for the current request or execution context. + /// + /// The to monitor for cancellation requests. + /// + /// A task that represents the asynchronous operation. The task result contains the isolation key string, + /// or if no key is available in the current context. + /// + /// + /// Implementations should extract the key from ambient context (e.g., HTTP request headers, claims, + /// or environment variables). If the key cannot be determined, return to allow + /// the caller to decide on strict vs. pass-through behavior. + /// + public abstract ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SessionPersistenceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SessionPersistenceTests.cs index 785a3b2e00..33b842bf34 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SessionPersistenceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/SessionPersistenceTests.cs @@ -102,7 +102,7 @@ public sealed class SessionPersistenceTests : IAsyncDisposable // Register agent using hosting DI pattern with InMemorySessionStore builder.Services.AddAIAgent("session-test-agent", (_, name) => new FakeSessionAgent(name)) - .WithInMemorySessionStore(); + .WithInMemorySessionStore(withIsolation: false); this._app = builder.Build(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs new file mode 100644 index 0000000000..e22feec1a9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ClaimsIdentitySessionIsolationKeyProviderTests.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; + +namespace Microsoft.Agents.AI.Hosting.UnitTests; + +/// +/// Unit tests for . +/// +public class ClaimsIdentitySessionIsolationKeyProviderTests +{ + private const string TestUserId = "test-user-id"; + private const string CustomClaimType = "custom-claim-type"; + private const string CustomClaimValue = "custom-claim-value"; + + private readonly Mock _httpContextAccessorMock; + + /// + /// Initializes a new instance of the class. + /// + public ClaimsIdentitySessionIsolationKeyProviderTests() + { + this._httpContextAccessorMock = new Mock(); + } + + #region Constructor Tests + + /// + /// Verify that constructor uses default options when options is null. + /// + [Fact] + public void UsesDefaultOptionsWhenNull() + { + // Act & Assert - should not throw + var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object, options: null); + Assert.NotNull(provider); + } + + /// + /// Verify that constructor accepts null IHttpContextAccessor. + /// + [Fact] + public void Constructor_WithNullHttpContextAccessor_DoesNotThrow() + { + // Act & Assert - should not throw + var provider = new ClaimsIdentitySessionIsolationKeyProvider(httpContextAccessor: null); + Assert.NotNull(provider); + } + + /// + /// Verify that constructor throws ArgumentException when claimType is null. + /// + [Fact] + public void RequiresClaimType_NotNull() + { + // Act & Assert + Assert.Throws("options.ClaimType", () => + new ClaimsIdentitySessionIsolationKeyProvider( + this._httpContextAccessorMock.Object, + new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = null! })); + } + + /// + /// Verify that constructor throws ArgumentException when claimType is empty. + /// + [Fact] + public void RequiresClaimType_NotEmpty() + { + // Act & Assert + Assert.Throws("options.ClaimType", () => + new ClaimsIdentitySessionIsolationKeyProvider( + this._httpContextAccessorMock.Object, + new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = string.Empty })); + } + + /// + /// Verify that constructor throws ArgumentException when claimType is whitespace. + /// + [Fact] + public void RequiresClaimType_NotWhitespace() + { + // Act & Assert + Assert.Throws("options.ClaimType", () => + new ClaimsIdentitySessionIsolationKeyProvider( + this._httpContextAccessorMock.Object, + new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = " " })); + } + + #endregion + + #region GetSessionIsolationKeyAsync Tests + + /// + /// Verify that GetSessionIsolationKeyAsync extracts the claim value from the default claim type. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncExtractsDefaultClaimTypeAsync() + { + // Arrange + this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId); + var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Equal(TestUserId, result); + } + + /// + /// Verify that GetSessionIsolationKeyAsync uses custom claim type when specified. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncUsesCustomClaimTypeAsync() + { + // Arrange + this.SetupHttpContextWithClaim(CustomClaimType, CustomClaimValue); + var provider = new ClaimsIdentitySessionIsolationKeyProvider( + this._httpContextAccessorMock.Object, + new ClaimsIdentitySessionIsolationKeyProviderOptions { ClaimType = CustomClaimType }); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Equal(CustomClaimValue, result); + } + + /// + /// Verify that GetSessionIsolationKeyAsync returns null when the specified claim is missing. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncReturnsNullWhenClaimMissingAsync() + { + // Arrange + this.SetupHttpContextWithClaim("other-claim", "value"); + var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Null(result); + } + + /// + /// Verify behavior when HttpContextAccessor returns null HttpContext. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextNullAsync() + { + // Arrange + this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns((HttpContext?)null); + var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Null(result); + } + + /// + /// Verify behavior when HttpContextAccessor itself is null. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncReturnsNullWhenHttpContextAccessorNullAsync() + { + // Arrange + var provider = new ClaimsIdentitySessionIsolationKeyProvider(httpContextAccessor: null); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Null(result); + } + + /// + /// Verify that GetSessionIsolationKeyAsync returns the first matching claim when multiple exist. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncReturnsFirstMatchingClaimAsync() + { + // Arrange + const string FirstValue = "first-value"; + const string SecondValue = "second-value"; + var claims = new[] + { + new Claim(ClaimsIdentity.DefaultNameClaimType, FirstValue), + new Claim(ClaimsIdentity.DefaultNameClaimType, SecondValue), + }; + var identity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(identity); + + var httpContext = new DefaultHttpContext + { + User = principal + }; + + this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext); + var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Equal(FirstValue, result); + } + + /// + /// Verify that GetSessionIsolationKeyAsync handles empty claim values. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncHandlesEmptyClaimValueAsync() + { + // Arrange + this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, string.Empty); + var provider = new ClaimsIdentitySessionIsolationKeyProvider(this._httpContextAccessorMock.Object); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Equal(string.Empty, result); + } + + #endregion + + #region Helper Methods + + private void SetupHttpContextWithClaim(string claimType, string claimValue) + { + var claims = new[] { new Claim(claimType, claimValue) }; + var identity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(identity); + + var httpContext = new DefaultHttpContext + { + User = principal + }; + + this._httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext); + } + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs new file mode 100644 index 0000000000..f04ad7c20d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Moq; + +namespace Microsoft.Agents.AI.Hosting.UnitTests; + +/// +/// Unit tests for the class. +/// +public class DelegatingAgentSessionStoreTests +{ + private readonly Mock _innerStoreMock; + private readonly Mock _agentMock; + private readonly TestDelegatingAgentSessionStore _delegatingStore; + private readonly AgentSession _testSession; + + /// + /// Initializes a new instance of the class. + /// + public DelegatingAgentSessionStoreTests() + { + this._innerStoreMock = new Mock(); + this._agentMock = new Mock(); + this._testSession = new TestAgentSession(); + + // Setup inner store mock + this._innerStoreMock + .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(this._testSession); + + this._innerStoreMock + .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ValueTask.CompletedTask); + + this._delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object); + } + + #region Constructor Tests + + /// + /// Verify that constructor throws ArgumentNullException when innerStore is null. + /// + [Fact] + public void RequiresInnerStore() => + // Act & Assert + Assert.Throws("innerStore", () => new TestDelegatingAgentSessionStore(null!)); + + /// + /// Verify that constructor sets the inner store correctly. + /// + [Fact] + public void Constructor_WithValidInnerStore_SetsInnerStore() + { + // Act + var delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object); + + // Assert + Assert.Same(this._innerStoreMock.Object, delegatingStore.InnerStore); + } + + #endregion + + #region Method Delegation Tests + + /// + /// Verify that GetSessionAsync delegates to inner store with correct parameters. + /// + [Fact] + public async Task GetSessionAsyncDelegatesToInnerStoreAsync() + { + // Arrange + const string ExpectedConversationId = "test-conversation-id"; + var expectedCancellationToken = new CancellationToken(); + + this._innerStoreMock + .Setup(x => x.GetSessionAsync( + It.Is(a => a == this._agentMock.Object), + It.Is(c => c == ExpectedConversationId), + It.Is(ct => ct == expectedCancellationToken))) + .ReturnsAsync(this._testSession); + + // Act + var session = await this._delegatingStore.GetSessionAsync( + this._agentMock.Object, + ExpectedConversationId, + expectedCancellationToken); + + // Assert + Assert.Same(this._testSession, session); + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + ExpectedConversationId, + expectedCancellationToken), + Times.Once); + } + + /// + /// Verify that SaveSessionAsync delegates to inner store with correct parameters. + /// + [Fact] + public async Task SaveSessionAsyncDelegatesToInnerStoreAsync() + { + // Arrange + const string ExpectedConversationId = "test-conversation-id"; + var expectedCancellationToken = new CancellationToken(); + var expectedSession = new TestAgentSession(); + + this._innerStoreMock + .Setup(x => x.SaveSessionAsync( + It.Is(a => a == this._agentMock.Object), + It.Is(c => c == ExpectedConversationId), + It.Is(s => s == expectedSession), + It.Is(ct => ct == expectedCancellationToken))) + .Returns(ValueTask.CompletedTask); + + // Act + await this._delegatingStore.SaveSessionAsync( + this._agentMock.Object, + ExpectedConversationId, + expectedSession, + expectedCancellationToken); + + // Assert + this._innerStoreMock.Verify( + x => x.SaveSessionAsync( + this._agentMock.Object, + ExpectedConversationId, + expectedSession, + expectedCancellationToken), + Times.Once); + } + + /// + /// Verify that GetSessionAsync awaits the inner store's result before returning. + /// + [Fact] + public async Task GetSessionAsyncAwaitsInnerStoreResultAsync() + { + // Arrange + const string ExpectedConversationId = "test-conversation-id"; + var taskCompletionSource = new TaskCompletionSource(); + + var innerStoreMock = new Mock(); + innerStoreMock + .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new ValueTask(taskCompletionSource.Task)); + + var delegatingStore = new TestDelegatingAgentSessionStore(innerStoreMock.Object); + + // Act + var resultTask = delegatingStore.GetSessionAsync(this._agentMock.Object, ExpectedConversationId); + + // Assert + Assert.False(resultTask.IsCompleted); + taskCompletionSource.SetResult(this._testSession); + Assert.True(resultTask.IsCompleted); + Assert.Same(this._testSession, await resultTask); + } + + /// + /// Verify that SaveSessionAsync awaits the inner store's completion before returning. + /// + [Fact] + public async Task SaveSessionAsyncAwaitsInnerStoreCompletionAsync() + { + // Arrange + const string ExpectedConversationId = "test-conversation-id"; + var expectedSession = new TestAgentSession(); + var taskCompletionSource = new TaskCompletionSource(); + + var innerStoreMock = new Mock(); + innerStoreMock + .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new ValueTask(taskCompletionSource.Task)); + + var delegatingStore = new TestDelegatingAgentSessionStore(innerStoreMock.Object); + + // Act + var resultTask = delegatingStore.SaveSessionAsync(this._agentMock.Object, ExpectedConversationId, expectedSession); + + // Assert + Assert.False(resultTask.IsCompleted); + taskCompletionSource.SetResult(); + Assert.True(resultTask.IsCompleted); + await resultTask; + } + + #endregion + + #region GetService Tests + + /// + /// Verify that GetService returns itself when requesting the exact type. + /// + [Fact] + public void GetServiceReturnsItselfForExactType() + { + // Act + var result = this._delegatingStore.GetService(typeof(TestDelegatingAgentSessionStore)); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService returns itself when requesting a base type. + /// + [Fact] + public void GetServiceReturnsItselfForBaseType() + { + // Act + var result = this._delegatingStore.GetService(typeof(DelegatingAgentSessionStore)); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService returns itself when requesting AgentSessionStore. + /// + [Fact] + public void GetServiceReturnsItselfForAgentSessionStoreType() + { + // Act + var result = this._delegatingStore.GetService(typeof(AgentSessionStore)); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService chains to inner store when type is not satisfied by outer store. + /// + [Fact] + public void GetServiceChainsToInnerStore() + { + // Arrange + var innerStore = new ConcreteAgentSessionStore(); + var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); + + // Act + var result = delegatingStore.GetService(typeof(ConcreteAgentSessionStore)); + + // Assert + Assert.Same(innerStore, result); + } + + /// + /// Verify that GetService chains through multiple delegation layers. + /// + [Fact] + public void GetServiceChainsThoughMultipleDelegationLayers() + { + // Arrange - create a three-layer chain: outer -> middle -> inner + var innerStore = new ConcreteAgentSessionStore(); + var middleStore = new AnotherDelegatingAgentSessionStore(innerStore); + var outerStore = new TestDelegatingAgentSessionStore(middleStore); + + // Act - request the innermost store type + var result = outerStore.GetService(typeof(ConcreteAgentSessionStore)); + + // Assert + Assert.Same(innerStore, result); + } + + /// + /// Verify that GetService can find a store in the middle of the delegation chain. + /// + [Fact] + public void GetServiceFindsMiddleStoreInChain() + { + // Arrange - create a three-layer chain: outer -> middle -> inner + var innerStore = new ConcreteAgentSessionStore(); + var middleStore = new AnotherDelegatingAgentSessionStore(innerStore); + var outerStore = new TestDelegatingAgentSessionStore(middleStore); + + // Act - request the middle store type + var result = outerStore.GetService(typeof(AnotherDelegatingAgentSessionStore)); + + // Assert + Assert.Same(middleStore, result); + } + + /// + /// Verify that GetService returns null when the requested type is not found in the chain. + /// + [Fact] + public void GetServiceReturnsNullWhenTypeNotFound() + { + // Arrange + var innerStore = new ConcreteAgentSessionStore(); + var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); + + // Act + var result = delegatingStore.GetService(typeof(string)); + + // Assert + Assert.Null(result); + } + + /// + /// Verify that GetService returns null when a service key is provided but not matched. + /// + [Fact] + public void GetServiceReturnsNullWhenServiceKeyProvided() + { + // Act + var result = this._delegatingStore.GetService(typeof(TestDelegatingAgentSessionStore), "some-key"); + + // Assert + Assert.Null(result); + } + + /// + /// Verify that GetService throws ArgumentNullException when serviceType is null. + /// + [Fact] + public void GetServiceThrowsWhenServiceTypeIsNull() => + Assert.Throws("serviceType", () => this._delegatingStore.GetService(null!)); + + /// + /// Verify that GetService generic method works correctly. + /// + [Fact] + public void GetServiceGenericReturnsItself() + { + // Act + var result = this._delegatingStore.GetService(); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService generic method chains to inner store. + /// + [Fact] + public void GetServiceGenericChainsToInnerStore() + { + // Arrange + var innerStore = new ConcreteAgentSessionStore(); + var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); + + // Act + var result = delegatingStore.GetService(); + + // Assert + Assert.Same(innerStore, result); + } + + /// + /// Verify that GetService generic method returns null when type not found. + /// + [Fact] + public void GetServiceGenericReturnsNullWhenTypeNotFound() + { + // Act + var result = this._delegatingStore.GetService(); + + // Assert + Assert.Null(result); + } + + #endregion + + #region Test Implementation + + /// + /// Test implementation of DelegatingAgentSessionStore for testing purposes. + /// + private sealed class TestDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore) + { + public new AgentSessionStore InnerStore => base.InnerStore; + } + + /// + /// Another delegating store implementation for testing multi-layer chains. + /// + private sealed class AnotherDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore); + + /// + /// Concrete (non-delegating) session store for testing GetService chaining. + /// + private sealed class ConcreteAgentSessionStore : AgentSessionStore + { + public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + => new(new TestAgentSession()); + + public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + => ValueTask.CompletedTask; + } + + private sealed class TestAgentSession : AgentSession; + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs new file mode 100644 index 0000000000..d410543608 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs @@ -0,0 +1,430 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Moq; + +namespace Microsoft.Agents.AI.Hosting.UnitTests; + +/// +/// Unit tests for . +/// +public class IsolationKeyScopedAgentSessionStoreTests +{ + private const string TestIsolationKey = "test-key"; + private const string TestConversationId = "test-conversation-id"; + + private readonly Mock _innerStoreMock; + private readonly Mock _agentMock; + private readonly AgentSession _testSession; + + /// + /// Initializes a new instance of the class. + /// + public IsolationKeyScopedAgentSessionStoreTests() + { + this._innerStoreMock = new Mock(); + this._agentMock = new Mock(); + this._testSession = new TestAgentSession(); + + this._innerStoreMock + .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(this._testSession); + + this._innerStoreMock + .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ValueTask.CompletedTask); + } + + #region Constructor Tests + + /// + /// Verify that constructor throws ArgumentNullException when innerStore is null. + /// + [Fact] + public void RequiresInnerStore() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + + // Act & Assert + Assert.Throws("innerStore", () => + new IsolationKeyScopedAgentSessionStore(null!, provider)); + } + + /// + /// Verify that constructor uses default options when options is null. + /// + [Fact] + public void UsesDefaultOptionsWhenNull() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + + // Act & Assert - should not throw + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider, options: null); + Assert.NotNull(store); + } + + #endregion + + #region GetSessionAsync Tests + + /// + /// Verify that GetSessionAsync scopes the conversation ID with the isolation key. + /// + [Fact] + public async Task GetSessionAsyncScopesConversationIdWithKeyAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + await store.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + $"{TestIsolationKey}::{TestConversationId}", + It.IsAny()), + Times.Once); + } + + /// + /// Verify that GetSessionAsync throws InvalidOperationException when key is null in strict mode. + /// + [Fact] + public async Task GetSessionAsyncThrowsWhenKeyNullInStrictModeAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(null); + var store = new IsolationKeyScopedAgentSessionStore( + this._innerStoreMock.Object, + provider, + new IsolationKeyScopedAgentSessionStoreOptions { Strict = true }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await store.GetSessionAsync(this._agentMock.Object, TestConversationId)); + + Assert.Contains("Session isolation key is required", exception.Message); + } + + /// + /// Verify that GetSessionAsync does not throw when key is null in non-strict mode. + /// + [Fact] + public async Task GetSessionAsyncDoesNotThrowWhenKeyNullInNonStrictModeAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(null); + var store = new IsolationKeyScopedAgentSessionStore( + this._innerStoreMock.Object, + provider, + new IsolationKeyScopedAgentSessionStoreOptions { Strict = false }); + + // Act - should not throw + await store.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert - conversation ID should be passed through unmodified + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + TestConversationId, + It.IsAny()), + Times.Once); + } + + /// + /// Verify that GetSessionAsync returns the session from the inner store. + /// + [Fact] + public async Task GetSessionAsyncReturnsSessionFromInnerStoreAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + var result = await store.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert + Assert.Same(this._testSession, result); + } + + #endregion + + #region SaveSessionAsync Tests + + /// + /// Verify that SaveSessionAsync scopes the conversation ID with the isolation key. + /// + [Fact] + public async Task SaveSessionAsyncScopesConversationIdWithKeyAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + var sessionToSave = new TestAgentSession(); + + // Act + await store.SaveSessionAsync(this._agentMock.Object, TestConversationId, sessionToSave); + + // Assert + this._innerStoreMock.Verify( + x => x.SaveSessionAsync( + this._agentMock.Object, + $"{TestIsolationKey}::{TestConversationId}", + sessionToSave, + It.IsAny()), + Times.Once); + } + + /// + /// Verify that SaveSessionAsync throws InvalidOperationException when key is null in strict mode. + /// + [Fact] + public async Task SaveSessionAsyncThrowsWhenKeyNullInStrictModeAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(null); + var store = new IsolationKeyScopedAgentSessionStore( + this._innerStoreMock.Object, + provider, + new IsolationKeyScopedAgentSessionStoreOptions { Strict = true }); + var sessionToSave = new TestAgentSession(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + async () => await store.SaveSessionAsync(this._agentMock.Object, TestConversationId, sessionToSave)); + + Assert.Contains("Session isolation key is required", exception.Message); + } + + /// + /// Verify that SaveSessionAsync does not throw when key is null in non-strict mode. + /// + [Fact] + public async Task SaveSessionAsyncDoesNotThrowWhenKeyNullInNonStrictModeAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(null); + var store = new IsolationKeyScopedAgentSessionStore( + this._innerStoreMock.Object, + provider, + new IsolationKeyScopedAgentSessionStoreOptions { Strict = false }); + var sessionToSave = new TestAgentSession(); + + // Act - should not throw + await store.SaveSessionAsync(this._agentMock.Object, TestConversationId, sessionToSave); + + // Assert - conversation ID should be passed through unmodified + this._innerStoreMock.Verify( + x => x.SaveSessionAsync( + this._agentMock.Object, + TestConversationId, + sessionToSave, + It.IsAny()), + Times.Once); + } + + #endregion + + #region Escaping Tests + + /// + /// Verify that colons in the isolation key are escaped. + /// + [Fact] + public async Task EscapesColonsInIsolationKeyAsync() + { + // Arrange + const string KeyWithColon = "key:with:colons"; + var provider = new TestSessionIsolationKeyProvider(KeyWithColon); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + await store.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert - colons should be escaped as \: + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + $"key\\:with\\:colons::{TestConversationId}", + It.IsAny()), + Times.Once); + } + + /// + /// Verify that backslashes in the isolation key are escaped. + /// + [Fact] + public async Task EscapesBackslashesInIsolationKeyAsync() + { + // Arrange + const string KeyWithBackslash = @"domain\key"; + var provider = new TestSessionIsolationKeyProvider(KeyWithBackslash); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + await store.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert - backslashes should be escaped as \\ + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + $"domain\\\\key::{TestConversationId}", + It.IsAny()), + Times.Once); + } + + /// + /// Verify that both backslashes and colons in the isolation key are escaped correctly. + /// + [Fact] + public async Task EscapesBothBackslashesAndColonsInIsolationKeyAsync() + { + // Arrange + const string KeyWithBoth = @"domain\key:role"; + var provider = new TestSessionIsolationKeyProvider(KeyWithBoth); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + await store.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert - backslashes escaped first, then colons + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + $"domain\\\\key\\:role::{TestConversationId}", + It.IsAny()), + Times.Once); + } + + #endregion + + #region Isolation Tests + + /// + /// Verify that different isolation keys result in different scoped conversation IDs. + /// + [Fact] + public async Task DifferentKeysResultInDifferentScopedConversationIdsAsync() + { + // Arrange + const string Key1 = "key-1"; + const string Key2 = "key-2"; + string? capturedConversationId1 = null; + string? capturedConversationId2 = null; + + this._innerStoreMock + .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, conversationId, _) => + { + if (capturedConversationId1 == null) + { + capturedConversationId1 = conversationId; + } + else + { + capturedConversationId2 = conversationId; + } + }) + .ReturnsAsync(this._testSession); + + // Act - Key 1 + var provider1 = new TestSessionIsolationKeyProvider(Key1); + var store1 = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider1); + await store1.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Act - Key 2 + var provider2 = new TestSessionIsolationKeyProvider(Key2); + var store2 = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider2); + await store2.GetSessionAsync(this._agentMock.Object, TestConversationId); + + // Assert + Assert.Equal($"{Key1}::{TestConversationId}", capturedConversationId1); + Assert.Equal($"{Key2}::{TestConversationId}", capturedConversationId2); + Assert.NotEqual(capturedConversationId1, capturedConversationId2); + } + + #endregion + + #region GetService Tests + + /// + /// Verify that GetService can retrieve IsolationKeyScopedAgentSessionStore from a delegation chain. + /// + [Fact] + public void GetServiceReturnsIsolationKeyScopedAgentSessionStore() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + var result = store.GetService(); + + // Assert + Assert.Same(store, result); + } + + /// + /// Verify that GetService chains through to find inner store types. + /// + [Fact] + public void GetServiceChainsToInnerStore() + { + // Arrange + var concreteInnerStore = new ConcreteAgentSessionStore(); + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(concreteInnerStore, provider); + + // Act + var result = store.GetService(); + + // Assert + Assert.Same(concreteInnerStore, result); + } + + #endregion + + #region Helper Classes + + /// + /// Test implementation of for testing purposes. + /// + private sealed class TestSessionIsolationKeyProvider : SessionIsolationKeyProvider + { + private readonly string? _key; + + public TestSessionIsolationKeyProvider(string? key) + { + this._key = key; + } + + public override ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + { + return new ValueTask(this._key); + } + } + + private sealed class TestAgentSession : AgentSession; + + /// + /// Concrete (non-delegating) session store for testing GetService chaining. + /// + private sealed class ConcreteAgentSessionStore : AgentSessionStore + { + public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + => new(new TestAgentSession()); + + public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + => ValueTask.CompletedTask; + } + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj index 1279b20397..a6e2ccdb38 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj @@ -6,6 +6,7 @@ + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs new file mode 100644 index 0000000000..00bf2cd373 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/SessionIsolationKeyProviderTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Hosting.UnitTests; + +/// +/// Unit tests for and its contract. +/// +public class SessionIsolationKeyProviderTests +{ + /// + /// Verify that a concrete provider can return a non-null isolation key. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncReturnsNonNullKeyAsync() + { + // Arrange + const string ExpectedKey = "test-key"; + var provider = new TestSessionIsolationKeyProvider(ExpectedKey); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Equal(ExpectedKey, result); + } + + /// + /// Verify that a concrete provider can return null when no key is available. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncReturnsNullWhenNoKeyAvailableAsync() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(null); + + // Act + string? result = await provider.GetSessionIsolationKeyAsync(); + + // Assert + Assert.Null(result); + } + + /// + /// Verify that cancellation token is passed through to the provider implementation. + /// + [Fact] + public async Task GetSessionIsolationKeyAsyncPassesCancellationTokenAsync() + { + // Arrange + var provider = new TestCancellableSessionIsolationKeyProvider(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await provider.GetSessionIsolationKeyAsync(cts.Token)); + } + + #region Test Implementations + + /// + /// Test implementation of for testing purposes. + /// + private sealed class TestSessionIsolationKeyProvider : SessionIsolationKeyProvider + { + private readonly string? _key; + + public TestSessionIsolationKeyProvider(string? key) + { + this._key = key; + } + + public override ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + { + return new ValueTask(this._key); + } + } + + /// + /// Test implementation that respects cancellation tokens. + /// + private sealed class TestCancellableSessionIsolationKeyProvider : SessionIsolationKeyProvider + { + public override async ValueTask GetSessionIsolationKeyAsync(CancellationToken cancellationToken = default) + { + await Task.Delay(1000, cancellationToken); + return "key"; + } + } + + #endregion +} From 945647a065383b8487ff97a2c180a61af6534be7 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 28 May 2026 14:04:15 -0400 Subject: [PATCH 06/61] .NET: feat: Bring Handoff Orchestration to parity with Python (#6138) * feat: implement autonomous mode and termination conditions in handoff workflow * fixup: format * feat: enhance autonomous mode with per-agent configurations and add unit tests * fixup: remove empty file --------- Co-authored-by: Jacob Alber --- .../HandoffWorkflowBuilder.cs | 297 +++++++++++++++++- .../Specialized/HandoffAgentExecutor.cs | 52 ++- .../Specialized/HandoffEndExecutor.cs | 121 ++++++- .../Specialized/HandoffStartExecutor.cs | 17 +- .../Specialized/HandoffState.cs | 3 +- .../HandoffOrchestrationTests.cs | 293 +++++++++++++++++ 6 files changed, 759 insertions(+), 24 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs index 7142faad0b..e5b08ce928 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Specialized; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; @@ -57,6 +58,22 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl private string? _name; private string? _description; + // Autonomous mode configuration. When enabled, an agent's response that doesn't include a + // handoff triggers another invocation of that same agent with the continuation prompt, up to + // the configured turn limit per workflow turn. Optional per-agent overrides may further restrict + // which agents have autonomous mode enabled, or override the turn limit / continuation prompt + // on a per-agent basis. + private bool _autonomousMode; + private int _autonomousTurnLimit = HandoffWorkflowBuilderDefaults.DefaultAutonomousTurnLimit; + private string _autonomousContinuationPrompt = HandoffWorkflowBuilderDefaults.DefaultAutonomousContinuationPrompt; + private HashSet? _autonomousEnabledAgentIds; + private readonly Dictionary _autonomousTurnLimitsByAgentId = []; + private readonly Dictionary _autonomousContinuationPromptsByAgentId = []; + + // Termination condition. Evaluated after an agent response that does not request a handoff; + // if true, the workflow ends (and the autonomous loop, if any, terminates). + private Func, ValueTask>? _terminationCondition; + /// /// Initializes a new instance of the class with no handoff relationships. /// @@ -258,12 +275,204 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl return (TBuilder)this; } - private Dictionary CreateExecutorBindings(WorkflowBuilder builder) + /// + /// Adds the specified as participants in the handoff workflow without + /// defining handoff relationships for them. + /// + /// The agents to add as participants. + /// The updated builder instance. + /// + /// Use this method when you want a participant to be part of the workflow but you have not + /// explicitly defined handoff edges via . + /// When no handoffs are explicitly defined (default handoffs), all registered participants are + /// automatically wired so that every agent can hand off to every other agent. + /// + public TBuilder AddParticipants(params IEnumerable agents) + { + Throw.IfNull(agents); + + foreach (AIAgent agent in agents) + { + if (agent is null) + { + Throw.ArgumentNullException(nameof(agents), "One or more agents are null."); + } + + this._allAgents.Add(agent); + } + + return (TBuilder)this; + } + + /// + /// Enables autonomous mode for the handoff workflow. + /// + /// + /// + /// In autonomous mode, an agent whose response does not include a handoff is invoked again with + /// a continuation prompt, up to a configured turn limit. The autonomous loop for a given agent + /// ends when the agent invokes a handoff tool, the configured termination condition fires, or + /// the per-agent turn limit is reached — at which point the workflow yields control back to the + /// caller. + /// + /// + /// Per-agent turn counting. Autonomous-turn counters are tracked independently per agent + /// in the shared handoff state. A counter is incremented each time the End executor loops + /// control back to its source agent, and reset to zero in three cases: (1) when that agent + /// requests a handoff, (2) when its autonomous loop terminates (limit reached, termination + /// fires, or autonomous mode disabled for that agent), and (3) at the start of every fresh user + /// turn. As a consequence, if agent A loops twice and then hands off to B, A's counter resets + /// to zero; should control later return to A within the same user turn, A starts a new + /// autonomous run from zero. + /// + /// + /// + /// The default maximum number of autonomous continuation iterations per agent per workflow + /// turn. Applies to agents not listed in . If + /// , defaults to + /// (50). + /// + /// + /// The default user-role prompt fed to an agent on each autonomous continuation. Applies to + /// agents not listed in . If , + /// defaults to . + /// + /// + /// Optional allow-list restricting autonomous mode to a specific subset of agents. If + /// or empty, autonomous mode is enabled for every participant. + /// Agents not in the allow-list always yield control back to the caller after a single + /// invocation (when they do not request a handoff). + /// + /// + /// Optional per-agent turn-limit overrides. Each entry's key is the agent and its value the + /// turn limit that overrides for that agent. Agents not present + /// fall back to the default. + /// + /// + /// Optional per-agent continuation-prompt overrides. Each entry's key is the agent and its + /// value the continuation prompt used for that agent. Agents not present fall back to the + /// default. + /// + /// The updated builder instance. + public TBuilder WithAutonomousMode( + int? turnLimit = null, + string? continuationPrompt = null, + IEnumerable? agents = null, + IReadOnlyDictionary? agentTurnLimits = null, + IReadOnlyDictionary? agentContinuationPrompts = null) + { + if (turnLimit is { } limit && limit <= 0) + { + Throw.ArgumentOutOfRangeException(nameof(turnLimit), "Turn limit must be greater than zero."); + } + + this._autonomousMode = true; + this._autonomousTurnLimit = turnLimit ?? HandoffWorkflowBuilderDefaults.DefaultAutonomousTurnLimit; + this._autonomousContinuationPrompt = continuationPrompt ?? HandoffWorkflowBuilderDefaults.DefaultAutonomousContinuationPrompt; + + // Allow-list: null or empty means every participant has autonomous mode enabled. A non-empty + // list restricts autonomous mode to exactly those agents. + this._autonomousEnabledAgentIds = null; + if (agents is not null) + { + HashSet ids = []; + foreach (AIAgent agent in agents) + { + Throw.IfNull(agent, $"{nameof(agents)} element"); + ids.Add(agent.Id); + } + + if (ids.Count > 0) + { + this._autonomousEnabledAgentIds = ids; + } + } + + this._autonomousTurnLimitsByAgentId.Clear(); + if (agentTurnLimits is not null) + { + foreach (KeyValuePair kvp in agentTurnLimits) + { + Throw.IfNull(kvp.Key, $"{nameof(agentTurnLimits)} key"); + if (kvp.Value <= 0) + { + Throw.ArgumentOutOfRangeException( + nameof(agentTurnLimits), + $"Turn limit for agent '{kvp.Key.Name ?? kvp.Key.Id}' must be greater than zero."); + } + + this._autonomousTurnLimitsByAgentId[kvp.Key.Id] = kvp.Value; + } + } + + this._autonomousContinuationPromptsByAgentId.Clear(); + if (agentContinuationPrompts is not null) + { + foreach (KeyValuePair kvp in agentContinuationPrompts) + { + Throw.IfNull(kvp.Key, $"{nameof(agentContinuationPrompts)} key"); + Throw.IfNullOrEmpty(kvp.Value, $"{nameof(agentContinuationPrompts)} value"); + + this._autonomousContinuationPromptsByAgentId[kvp.Key.Id] = kvp.Value; + } + } + + return (TBuilder)this; + } + + /// + /// Sets a synchronous termination condition for the handoff workflow. + /// + /// + /// A predicate that receives the current conversation and returns if the + /// workflow should terminate (preventing further autonomous continuation). The synchronous + /// predicate is wrapped and forwarded to the async overload. + /// + /// The updated builder instance. + /// + /// The termination condition is evaluated after the agent produces a response that does not + /// request a handoff. When it returns , the workflow ends without invoking + /// another autonomous continuation. + /// + public TBuilder WithTerminationCondition(Func, bool> terminationCondition) + { + Throw.IfNull(terminationCondition); + + return this.WithTerminationCondition( + messages => new ValueTask(terminationCondition(messages))); + } + + /// + /// Sets an asynchronous termination condition for the handoff workflow. + /// + /// + /// A predicate that receives the current conversation and asynchronously returns + /// if the workflow should terminate (preventing further autonomous + /// continuation). + /// + /// The updated builder instance. + /// + /// The termination condition is evaluated after the agent produces a response that does not + /// request a handoff. When it returns , the workflow ends without invoking + /// another autonomous continuation. + /// + public TBuilder WithTerminationCondition(Func, ValueTask> terminationCondition) + { + Throw.IfNull(terminationCondition); + + this._terminationCondition = terminationCondition; + return (TBuilder)this; + } + + private Dictionary CreateExecutorBindings(WorkflowBuilder builder, Dictionary> effectiveTargets) { HandoffAgentExecutorOptions options = new(this.HandoffInstructions, this._emitAgentResponseEvents, this._emitAgentResponseUpdateEvents, - this._toolCallFilteringBehavior); + this._toolCallFilteringBehavior) + { + TerminationCondition = this._terminationCondition, + }; // There are two types of ids being used in this method, and it is critical that we are clear about // which one we are using, and where. @@ -277,7 +486,7 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl ExecutorBinding CreateFactoryBinding(AIAgent agent) { - if (!this._targets.TryGetValue(agent, out HashSet? handoffs)) + if (!effectiveTargets.TryGetValue(agent, out HashSet? handoffs)) { handoffs = new(); } @@ -287,10 +496,16 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl { foreach (HandoffTarget handoff in handoffs) { - sb.AddCase(state => state?.RequestedHandoffTargetAgentId == handoff.Target.Id, // Use AgentId for target matching + // Each handoff case also requires the turn to NOT be terminated; otherwise the + // turn falls through to the default branch, which routes to HandoffEndExecutor. + string targetAgentId = handoff.Target.Id; + sb.AddCase(state => state?.RequestedHandoffTargetAgentId == targetAgentId // Use AgentId for target matching + && state.IsTerminated != true, HandoffAgentExecutor.IdFor(handoff.Target)); // Use ExecutorId in for routing at the workflow level } + // Default branch catches: (a) turns with no handoff requested, and (b) terminated turns + // (whose handoff cases have been excluded above via the !IsTerminated guard). sb.WithDefault(HandoffEndExecutor.ExecutorId); }); @@ -309,6 +524,47 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl } } + private Dictionary> BuildDefaultHandoffTargets() + { + // Default handoffs: when the caller has not explicitly registered any handoffs via + // WithHandoff/WithHandoffs, every registered participant is wired to hand off to every other + // participant. + // The handoff "reason" is derived from the target agent's description/name/instructions, + // matching the resolution rules used in WithHandoff(). If no reason can be derived, we throw — + // same contract as the explicit handoff path. + Dictionary> defaultTargets = []; + + foreach (AIAgent source in this._allAgents) + { + HashSet targets = []; + foreach (AIAgent target in this._allAgents) + { + if (AIAgentIDEqualityComparer.Instance.Equals(source, target)) + { + continue; + } + + string? reason = (string.IsNullOrWhiteSpace(target.Description) ? null : target.Description) + ?? (string.IsNullOrWhiteSpace(target.Name) ? null : $"handoff to {target.Name}") + ?? target.GetService()?.Instructions; + + if (string.IsNullOrWhiteSpace(reason)) + { + Throw.InvalidOperationException( + $"Cannot build default handoffs: target agent '{(string.IsNullOrWhiteSpace(target.Name) ? target.Id : target.Name)}' " + + "has no description, name, or instructions from which to derive a handoff reason. Either provide one of these " + + "on the agent, or define handoffs explicitly via WithHandoff/WithHandoffs."); + } + + targets.Add(new HandoffTarget(target, reason)); + } + + defaultTargets[source] = targets; + } + + return defaultTargets; + } + /// /// Builds a composed of agents that operate via handoffs, with the next /// agent to process messages selected by the current agent. @@ -317,11 +573,25 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl public Workflow Build() { HandoffStartExecutor start = new(this._returnToPrevious); - HandoffEndExecutor end = new(this._returnToPrevious); + HandoffEndExecutor end = new( + returnToPrevious: this._returnToPrevious, + autonomousMode: this._autonomousMode, + autonomousTurnLimit: this._autonomousTurnLimit, + autonomousContinuationPrompt: this._autonomousContinuationPrompt, + autonomousEnabledAgentIds: this._autonomousEnabledAgentIds, + autonomousTurnLimitsByAgentId: this._autonomousTurnLimitsByAgentId, + autonomousContinuationPromptsByAgentId: this._autonomousContinuationPromptsByAgentId); WorkflowBuilder builder = new(start); + // Default handoffs: when the caller has not explicitly registered any handoffs via + // WithHandoff/WithHandoffs, every registered participant is wired to hand off to every other + // participant. + Dictionary> effectiveTargets = this._targets.Count == 0 + ? this.BuildDefaultHandoffTargets() + : this._targets; + // Create an factory-based ExecutorBinding for each agent. - Dictionary executors = this.CreateExecutorBindings(builder); + Dictionary executors = this.CreateExecutorBindings(builder, effectiveTargets); // Connect the start executor to the initial agent (or use dynamic routing when ReturnToPrevious is enabled). if (this._returnToPrevious) @@ -346,6 +616,21 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl builder.AddEdge(start, executors[this._initialAgent.Id]); } + // Autonomous-mode loop-back: when enabled, the End executor may emit a HandoffState targeting + // the source agent (carrying the synthesized continuation prompt in the shared conversation). + // A switch downstream of End routes that message back to the matching agent executor. + if (this._autonomousMode) + { + builder.AddSwitch(end, sb => + { + foreach (AIAgent agent in this._allAgents) + { + string agentId = agent.Id; + sb.AddCase(state => state?.RequestedHandoffTargetAgentId == agentId, executors[agentId]); + } + }); + } + if (!string.IsNullOrWhiteSpace(this._name)) { builder.WithName(this._name); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs index 87c67c81c2..02c48ea48d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs @@ -30,6 +30,17 @@ internal sealed class HandoffAgentExecutorOptions public bool? EmitAgentResponseUpdateEvents { get; set; } public HandoffToolCallFilteringBehavior ToolCallFilteringBehavior { get; set; } = HandoffToolCallFilteringBehavior.HandoffOnly; + + // Termination condition. When provided, evaluated after the agent responds and no handoff was + // requested. If it returns true, the outgoing HandoffState is stamped with IsTerminated = true + // so the per-agent routing switch routes the turn to HandoffEndExecutor instead of continuing. + public Func, ValueTask>? TerminationCondition { get; set; } +} + +internal static class HandoffWorkflowBuilderDefaults +{ + public const string DefaultAutonomousContinuationPrompt = "User did not respond. Continue assisting autonomously."; + public const int DefaultAutonomousTurnLimit = 50; } internal struct AgentInvocationResult(AgentResponse agentResponse, string? handoffTargetId) @@ -250,6 +261,7 @@ internal sealed class HandoffAgentExecutor : } int newConversationBookmark = state.ConversationBookmark; + List? conversationSnapshot = null; await this._sharedStateRef.InvokeWithStateAsync( (sharedState, ctx, ct) => { @@ -285,12 +297,25 @@ internal sealed class HandoffAgentExecutor : } _ = sharedState.Conversation.AddMessage(handoffCallResultMessage); + + // Reset this agent's autonomous-turn counter when it chooses to hand off, so that + // if control returns to this agent later in the turn (e.g. via another handoff), + // its autonomous loop starts fresh rather than carrying over prior iterations. + sharedState.AutonomousTurnsByAgent[this._agent.Id] = 0; } else { newConversationBookmark = sharedState.Conversation.AddMessages(result.Response.Messages); } + // Snapshot the conversation for termination evaluation while we still hold shared state access. + // Termination is only relevant when no handoff was requested — a requested handoff always + // routes to the target agent regardless of termination. + if (this._options.TerminationCondition is not null && !result.IsHandoffRequested) + { + conversationSnapshot = sharedState.Conversation.CloneHistory(); + } + return new ValueTask(); }, context, @@ -298,18 +323,27 @@ internal sealed class HandoffAgentExecutor : // We send on the HandoffState even if handoff is not requested because we might be terminating the processing, but this only // happens if we have no outstanding requests. - if (!this.HasOutstandingRequests) + if (this.HasOutstandingRequests) { - HandoffState outgoingState = new(state.IncomingState.TurnToken, result.HandoffTargetId, this._agent.Id); - - await context.SendMessageAsync(outgoingState, cancellationToken).ConfigureAwait(false); - - // reset the state for the next handoff, making sure to keep track of the conversation bookmark, and avoid resetting the - // agent session. (return-to-current is modeled as a new handoff turn, as opposed to "HITL", which can be a bit confusing.) - return state with { IncomingState = null, ConversationBookmark = newConversationBookmark }; + return state with { ConversationBookmark = newConversationBookmark }; } - return state; + // Evaluate the termination condition (when configured and no handoff was requested) and stamp + // the result onto the outgoing HandoffState so the per-agent routing switch can route the turn + // to HandoffEndExecutor instead of dispatching another handoff or autonomous continuation. + bool isTerminated = false; + if (conversationSnapshot is not null) + { + isTerminated = await this._options.TerminationCondition!(conversationSnapshot).ConfigureAwait(false); + } + + HandoffState outgoingState = new(state.IncomingState.TurnToken, result.HandoffTargetId, this._agent.Id, isTerminated); + + await context.SendMessageAsync(outgoingState, cancellationToken).ConfigureAwait(false); + + // Reset the turn-local state; keep the conversation bookmark and the agent session so the + // next invocation (handoff back, autonomous loop-back, or new user turn) resumes cleanly. + return state with { IncomingState = null, ConversationBookmark = newConversationBookmark }; } public override ValueTask HandleAsync(HandoffState message, IWorkflowContext context, CancellationToken cancellationToken = default) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffEndExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffEndExecutor.cs index edcc92d1c8..125a5a9475 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffEndExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffEndExecutor.cs @@ -8,18 +8,76 @@ using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.Specialized; -/// Executor used at the end of a handoff workflow to raise a final completed event. -internal sealed class HandoffEndExecutor(bool returnToPrevious) : Executor(ExecutorId, declareCrossRunShareable: true), IResettableExecutor +/// Executor used at the end of a handoff workflow to raise a final completed event, +/// and in autonomous mode to loop control back to the source agent. +/// +/// Autonomous-turn counters are tracked per source agent in . +/// On each invocation where the source agent did not request a handoff and termination has not fired, +/// the counter for that agent is incremented and control is sent back to that agent (via the +/// autonomous-return switch wired downstream of this executor). When the counter reaches the per-agent +/// turn limit — or when termination fires, or when autonomous mode is disabled for that agent — the +/// counter is reset to zero and the conversation is yielded as workflow output. +/// +internal sealed class HandoffEndExecutor : Executor, IResettableExecutor { public const string ExecutorId = "HandoffEnd"; + private readonly bool _returnToPrevious; + private readonly bool _autonomousMode; + private readonly int _autonomousTurnLimit; + private readonly string _autonomousContinuationPrompt; + private readonly HashSet? _autonomousEnabledAgentIds; + private readonly IReadOnlyDictionary _autonomousTurnLimitsByAgentId; + private readonly IReadOnlyDictionary _autonomousContinuationPromptsByAgentId; + private readonly StateRef _sharedStateRef = new(HandoffConstants.HandoffSharedStateKey, HandoffConstants.HandoffSharedStateScope); - protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) => - protocolBuilder.ConfigureRoutes(routeBuilder => routeBuilder.AddHandler( - (handoff, context, cancellationToken) => this.HandleAsync(handoff, context, cancellationToken))) - .YieldsOutput>(); + public HandoffEndExecutor( + bool returnToPrevious, + bool autonomousMode = false, + int autonomousTurnLimit = HandoffWorkflowBuilderDefaults.DefaultAutonomousTurnLimit, + string autonomousContinuationPrompt = HandoffWorkflowBuilderDefaults.DefaultAutonomousContinuationPrompt, + HashSet? autonomousEnabledAgentIds = null, + IReadOnlyDictionary? autonomousTurnLimitsByAgentId = null, + IReadOnlyDictionary? autonomousContinuationPromptsByAgentId = null) + : base(ExecutorId, declareCrossRunShareable: true) + { + this._returnToPrevious = returnToPrevious; + this._autonomousMode = autonomousMode; + this._autonomousTurnLimit = autonomousTurnLimit; + this._autonomousContinuationPrompt = autonomousContinuationPrompt; + this._autonomousEnabledAgentIds = autonomousEnabledAgentIds; + this._autonomousTurnLimitsByAgentId = autonomousTurnLimitsByAgentId ?? new Dictionary(); + this._autonomousContinuationPromptsByAgentId = autonomousContinuationPromptsByAgentId ?? new Dictionary(); + } + + private bool IsAutonomousEnabledFor(string agentId) => + // Null allow-list means every participant has autonomous mode enabled. + this._autonomousEnabledAgentIds?.Contains(agentId) ?? true; + + private int TurnLimitFor(string agentId) => + this._autonomousTurnLimitsByAgentId.TryGetValue(agentId, out int limit) ? limit : this._autonomousTurnLimit; + + private string ContinuationPromptFor(string agentId) => + this._autonomousContinuationPromptsByAgentId.TryGetValue(agentId, out string? prompt) ? prompt : this._autonomousContinuationPrompt; + + protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) + { + ProtocolBuilder pb = protocolBuilder + .ConfigureRoutes(routeBuilder => routeBuilder.AddHandler( + (handoff, context, cancellationToken) => this.HandleAsync(handoff, context, cancellationToken))) + .YieldsOutput>(); + + // Only advertise the outgoing-message capability when autonomous mode is enabled, since the + // downstream return switch (Builder.AddSwitch on End) is only wired in that case. + if (this._autonomousMode) + { + pb = pb.SendsMessage(); + } + + return pb; + } private async ValueTask HandleAsync(HandoffState handoff, IWorkflowContext context, CancellationToken cancellationToken) { @@ -31,7 +89,56 @@ internal sealed class HandoffEndExecutor(bool returnToPrevious) : Executor(Execu throw new InvalidOperationException("Handoff Orchestration shared state was not properly initialized."); } - if (returnToPrevious) + // Autonomous mode: when the agent did not request a handoff and termination has not fired, + // loop control back to the same agent (up to that agent's turn limit). Per-agent overrides + // (enabled-agents allow-list, turn limit, continuation prompt) are honored here. + bool canContinueAutonomously = this._autonomousMode + && !handoff.IsTerminated + && handoff.RequestedHandoffTargetAgentId is null + && handoff.PreviousAgentId is not null + && this.IsAutonomousEnabledFor(handoff.PreviousAgentId!); + + if (canContinueAutonomously) + { + string agentId = handoff.PreviousAgentId!; + int turns = sharedState.AutonomousTurnsByAgent.TryGetValue(agentId, out int existing) ? existing : 0; + int limit = this.TurnLimitFor(agentId); + + if (turns < limit) + { + sharedState.AutonomousTurnsByAgent[agentId] = turns + 1; + + // Append a synthetic user message containing the continuation prompt so the agent + // has fresh input to act on for the next autonomous iteration. + sharedState.Conversation.AddMessage(new ChatMessage(ChatRole.User, this.ContinuationPromptFor(agentId)) + { + CreatedAt = DateTimeOffset.UtcNow, + MessageId = Guid.NewGuid().ToString("N"), + }); + + // Send a HandoffState targeting the source agent. The downstream + // HandoffAutonomousReturnSwitch routes it to the matching agent executor. + HandoffState loopBack = new( + handoff.TurnToken, + RequestedHandoffTargetAgentId: agentId, + PreviousAgentId: agentId, + IsTerminated: false); + + await context.SendMessageAsync(loopBack, cancellationToken).ConfigureAwait(false); + + return sharedState; + } + } + + // Terminal path: either termination fired, autonomous mode is disabled, or the turn + // limit is reached. Reset this agent's autonomous counter so a subsequent user turn + // starts fresh, then yield the conversation as workflow output. + if (handoff.PreviousAgentId is not null) + { + sharedState.AutonomousTurnsByAgent[handoff.PreviousAgentId] = 0; + } + + if (this._returnToPrevious) { sharedState.PreviousAgentId = handoff.PreviousAgentId; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffStartExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffStartExecutor.cs index 8915b44aa0..47ef204d86 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffStartExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffStartExecutor.cs @@ -25,21 +25,32 @@ internal static class HandoffConstants internal sealed class HandoffSharedState { [JsonConstructor] - internal HandoffSharedState(MultiPartyConversation conversation, string? previousAgentId) + internal HandoffSharedState(MultiPartyConversation conversation, string? previousAgentId, Dictionary? autonomousTurnsByAgent) { this.Conversation = conversation; this.PreviousAgentId = previousAgentId; + this.AutonomousTurnsByAgent = autonomousTurnsByAgent ?? []; } public HandoffSharedState() { this.Conversation = new([]); + this.AutonomousTurnsByAgent = []; } [JsonInclude] public MultiPartyConversation Conversation { get; internal set; } public string? PreviousAgentId { get; set; } + + /// + /// Tracks the number of autonomous-mode continuation iterations consumed by each agent in the current + /// "active" autonomous run. The counter is incremented by each time + /// the End executor loops control back to the source agent in autonomous mode, and reset to 0 once + /// the autonomous loop terminates (limit reached or termination condition fired). + /// + [JsonInclude] + public Dictionary AutonomousTurnsByAgent { get; internal set; } } /// Executor used at the start of a handoffs workflow to accumulate messages and emit them as HandoffState upon receiving a turn token. @@ -64,6 +75,10 @@ internal sealed class HandoffStartExecutor(bool returnToPrevious) : ChatProtocol sharedState ??= new HandoffSharedState(); sharedState.Conversation.AddMessages(messages); + // Reset all autonomous-mode counters at the start of every fresh user turn so that a + // prior turn's counters cannot prematurely terminate the new turn's autonomous loop. + sharedState.AutonomousTurnsByAgent.Clear(); + string? previousAgentId = sharedState.PreviousAgentId; // If we are configured to return to the previous agent, include the previous agent id in the handoff state. diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffState.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffState.cs index 24cf788cb8..dad7848b47 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffState.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffState.cs @@ -5,4 +5,5 @@ namespace Microsoft.Agents.AI.Workflows.Specialized; internal sealed record class HandoffState( TurnToken TurnToken, string? RequestedHandoffTargetAgentId, - string? PreviousAgentId = null); + string? PreviousAgentId = null, + bool IsTerminated = false); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffOrchestrationTests.cs index c8abe4719c..0fe82db192 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffOrchestrationTests.cs @@ -1157,6 +1157,299 @@ public class HandoffOrchestrationTests } } + #region Default Handoffs Tests + + [Fact] + public async Task Handoffs_DefaultHandoffs_AllAgentsCanHandOffToAllOthersAsync() + { + // Verifies "default handoffs": when no explicit WithHandoff calls are made, + // every registered participant is wired to every other participant. + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + // Expect tools to include handoffs for B and C (every other agent). + var transferTools = options?.Tools?.Where(t => t.Name.StartsWith("handoff_to_", StringComparison.Ordinal)).ToList(); + Assert.NotNull(transferTools); + Assert.Equal(2, transferTools!.Count); + + // Pick the first one to hand off (it should route to either B or C). + return new(new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", transferTools[0].Name)])); + }), name: "agentA", description: "agent A"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => + new(new ChatMessage(ChatRole.Assistant, "B responded"))), + name: "agentB", description: "agent B"); + + var agentC = new ChatClientAgent(new MockChatClient((messages, options) => + new(new ChatMessage(ChatRole.Assistant, "C responded"))), + name: "agentC", description: "agent C"); + + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .AddParticipants(agentB, agentC) + .Build(); + + (string updateText, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "hi")]); + + // The first response handed off — verify the second agent responded. + Assert.NotNull(result); + Assert.True(updateText is "B responded" or "C responded", + $"Expected B or C to respond, got '{updateText}'"); + } + + [Fact] + public async Task Handoffs_DefaultHandoffs_OnlyAppliesWhenNoExplicitHandoffsAsync() + { + // When explicit handoffs are defined, default handoffs do NOT activate — only the explicit edges apply. + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + // Should only see one handoff tool (to B), not B and C. + var transferTools = options?.Tools?.Where(t => t.Name.StartsWith("handoff_to_", StringComparison.Ordinal)).ToList(); + Assert.NotNull(transferTools); + Assert.Single(transferTools!); + + return new(new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", transferTools![0].Name)])); + }), name: "agentA", description: "agent A"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => + new(new ChatMessage(ChatRole.Assistant, "B responded"))), + name: "agentB", description: "agent B"); + + // Only define an explicit A->B edge. Default mesh must not activate. + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .Build(); + + (string updateText, _, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "hi")]); + + Assert.Equal("B responded", updateText); + } + + #endregion Default Handoffs Tests + + #region Autonomous Mode Tests + + [Fact] + public async Task Handoffs_AutonomousMode_IteratesUntilHandoffAsync() + { + // With autonomous mode enabled, an agent that does not handoff is invoked again with the + // continuation prompt until it eventually invokes a handoff (or hits the turn limit). + + int agentACallCount = 0; + const int TargetIterations = 3; + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + agentACallCount++; + + if (agentACallCount < TargetIterations) + { + // Respond with text only (no handoff) — should trigger autonomous continuation. + return new(new ChatMessage(ChatRole.Assistant, $"iteration {agentACallCount}")); + } + + // After TargetIterations calls, hand off. + string? transferFuncName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("handoff_to_", StringComparison.Ordinal))?.Name; + Assert.NotNull(transferFuncName); + return new(new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", transferFuncName)])); + }), name: "agentA"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => + new(new ChatMessage(ChatRole.Assistant, "B final"))), + name: "agentB", description: "agent B"); + + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .WithAutonomousMode() + .Build(); + + (string updateText, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "go"),]); + + Assert.Equal(TargetIterations, agentACallCount); + Assert.NotNull(result); + Assert.Contains("B final", updateText); + + // Conversation should contain the continuation prompts injected between A's responses. + Assert.Contains(result, m => m.Role == ChatRole.User && m.Text == HandoffWorkflowBuilderDefaults.DefaultAutonomousContinuationPrompt); + } + + [Fact] + public async Task Handoffs_AutonomousMode_RespectsTurnLimitAsync() + { + // With a turn limit of N, the agent should be invoked initial+N times before the workflow ends + // (when the agent never invokes a handoff). + + int callCount = 0; + const int TurnLimit = 2; + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + callCount++; + return new(new ChatMessage(ChatRole.Assistant, $"call {callCount}")); + }), name: "agentA"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => + { + Assert.Fail("B should never be reached since A never hands off."); + return new(); + }), name: "agentB", description: "agent B"); + + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .WithAutonomousMode(turnLimit: TurnLimit) + .Build(); + + (_, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "go")]); + + // First call + TurnLimit continuation iterations = TurnLimit + 1 invocations. + Assert.Equal(TurnLimit + 1, callCount); + Assert.NotNull(result); + } + + [Fact] + public async Task Handoffs_AutonomousMode_UsesCustomContinuationPromptAsync() + { + const string CustomPrompt = "Keep going, please."; + int callCount = 0; + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + callCount++; + if (callCount > 1) + { + // After first call, verify the latest user message is the custom prompt. + var lastUserMessage = messages.LastOrDefault(m => m.Role == ChatRole.User); + Assert.NotNull(lastUserMessage); + Assert.Equal(CustomPrompt, lastUserMessage!.Text); + } + + return new(new ChatMessage(ChatRole.Assistant, $"call {callCount}")); + }), name: "agentA"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => new()), + name: "agentB", description: "agent B"); + + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .WithAutonomousMode(turnLimit: 2, continuationPrompt: CustomPrompt) + .Build(); + + (_, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "go")]); + + Assert.Equal(3, callCount); // 1 initial + 2 autonomous continuations + Assert.NotNull(result); + Assert.Contains(result, m => m.Role == ChatRole.User && m.Text == CustomPrompt); + } + + #endregion Autonomous Mode Tests + + #region Termination Condition Tests + + [Fact] + public async Task Handoffs_SyncTerminationCondition_EndsAutonomousLoopAsync() + { + int callCount = 0; + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + callCount++; + return new(new ChatMessage(ChatRole.Assistant, $"response {callCount}")); + }), name: "agentA"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => new()), + name: "agentB", description: "agent B"); + + // Sync termination: stop as soon as conversation contains a message with text "response 2". + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .WithAutonomousMode(turnLimit: 10) + .WithTerminationCondition(conversation => conversation.Any(m => m.Text == "response 2")) + .Build(); + + (_, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "go")]); + + // Agent should be invoked twice: once initially, once after the autonomous continuation, + // at which point the termination condition fires and the loop ends. + Assert.Equal(2, callCount); + Assert.NotNull(result); + } + + [Fact] + public async Task Handoffs_AsyncTerminationCondition_EndsAutonomousLoopAsync() + { + int callCount = 0; + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + callCount++; + return new(new ChatMessage(ChatRole.Assistant, $"response {callCount}")); + }), name: "agentA"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => new()), + name: "agentB", description: "agent B"); + + // Async termination: same effect, but exercises the async overload. + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .WithAutonomousMode(turnLimit: 10) + .WithTerminationCondition(async conversation => + { + await Task.Yield(); + return conversation.Any(m => m.Text == "response 3"); + }) + .Build(); + + (_, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "go")]); + + Assert.Equal(3, callCount); + Assert.NotNull(result); + } + + [Fact] + public async Task Handoffs_TerminationCondition_NotInvokedOnHandoffAsync() + { + // The termination condition is only evaluated when the agent did not request a handoff. + // Verify a handoff occurs without consulting the predicate. + + bool predicateInvoked = false; + + var agentA = new ChatClientAgent(new MockChatClient((messages, options) => + { + string? transferFuncName = options?.Tools?.FirstOrDefault(t => t.Name.StartsWith("handoff_to_", StringComparison.Ordinal))?.Name; + Assert.NotNull(transferFuncName); + return new(new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", transferFuncName)])); + }), name: "agentA"); + + var agentB = new ChatClientAgent(new MockChatClient((messages, options) => + new(new ChatMessage(ChatRole.Assistant, "B done"))), + name: "agentB", description: "agent B"); + + var workflow = + AgentWorkflowBuilder.CreateHandoffBuilderWith(agentA) + .WithHandoff(agentA, agentB) + .WithTerminationCondition(_ => + { + predicateInvoked = true; + return true; + }) + .Build(); + + (string updateText, _, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "go")]); + + // Only B's response should have ended the workflow; predicate evaluated on B (no further handoff). + Assert.Equal("B done", updateText); + Assert.True(predicateInvoked, "Predicate should have been invoked at least once (on the terminating agent)."); + } + + #endregion Termination Condition Tests + #region Helper Types and Methods private sealed record WorkflowRunResult(string UpdateText, List? Result, CheckpointInfo? LastCheckpoint, List PendingRequests); From 3ee1bb4f9fea2ad22e9eeb29ceed9447951e56c9 Mon Sep 17 00:00:00 2001 From: semenshi-m Date: Thu, 28 May 2026 19:14:57 +0100 Subject: [PATCH 07/61] .NET: [Breaking] Refactor AgentFileSkillsSource for depth-based discovery and predicate filters (#6109) * Refactor AgentFileSkillsSource to use filter predicates and add AgentFileSkillFilterContext - Replace hardcoded script/resource directory lists with configurable ScriptFilter and ResourceFilter predicates - Add AgentFileSkillFilterContext class to provide contextual file information to filter predicates - Replace MaxSearchDepth constant with configurable SearchDepth option - Update AgentFileSkillsSourceOptions with new filter and search depth properties - Update tests to reflect the new filtering approach Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Log '(none)' instead of empty string for missing file extensions in debug output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../File/AgentFileSkillFilterContext.cs | 46 ++ .../Skills/File/AgentFileSkillsSource.cs | 460 +++++++++--------- .../File/AgentFileSkillsSourceOptions.cs | 44 +- .../AgentFileSkillsSourceScriptTests.cs | 51 +- .../AgentSkills/FileAgentSkillLoaderTests.cs | 224 ++++----- 5 files changed, 417 insertions(+), 408 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillFilterContext.cs diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillFilterContext.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillFilterContext.cs new file mode 100644 index 0000000000..34937baed5 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillFilterContext.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides contextual information about a discovered file to the +/// and +/// predicates. +/// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] +public sealed class AgentFileSkillFilterContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the skill (from SKILL.md frontmatter). + /// + /// The path to the script or resource file relative to the skill directory (using forward slashes). + /// + internal AgentFileSkillFilterContext(string skillName, string relativeFilePath) + { + this.SkillName = Throw.IfNullOrWhitespace(skillName); + this.RelativeFilePath = Throw.IfNullOrWhitespace(relativeFilePath); + } + + /// + /// Gets the name of the skill as declared in the SKILL.md frontmatter. + /// + /// unit-converter + public string SkillName { get; } + + /// + /// Gets the path to the script or resource file relative to the skill directory (using forward slashes). + /// For root-level files this is just the filename; for nested files it includes the subdirectory. + /// + /// + /// run.py for a script at skill root, + /// scripts/convert.js for a nested script, or + /// references/guide.md for a nested resource. + /// + public string RelativeFilePath { get; } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs index d31501426e..54a5dec10c 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs @@ -30,18 +30,12 @@ namespace Microsoft.Agents.AI; internal sealed partial class AgentFileSkillsSource : AgentSkillsSource { private const string SkillFileName = "SKILL.md"; - private const int MaxSearchDepth = 2; - - // "." means the skill directory root itself (no subdirectory descent constraint) - private const string RootDirectoryIndicator = "."; + private const int DefaultSearchDepth = 2; + private const int MaxSkillDirectorySearchDepth = 2; private static readonly string[] s_defaultScriptExtensions = [".py", ".js", ".sh", ".ps1", ".cs", ".csx"]; private static readonly string[] s_defaultResourceExtensions = [".md", ".json", ".yaml", ".yml", ".csv", ".xml", ".txt"]; - // Standard subdirectory names per https://agentskills.io/specification#directory-structure - private static readonly string[] s_defaultScriptDirectories = ["scripts"]; - private static readonly string[] s_defaultResourceDirectories = ["references", "assets"]; - // Matches YAML frontmatter delimited by "---" lines. Group 1 = content between delimiters. // Multiline makes ^/$ match line boundaries; Singleline makes . match newlines across the block. // The \uFEFF? prefix allows an optional UTF-8 BOM that some editors prepend. @@ -63,8 +57,9 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource private readonly IEnumerable _skillPaths; private readonly HashSet _allowedResourceExtensions; private readonly HashSet _allowedScriptExtensions; - private readonly IReadOnlyList _scriptDirectories; - private readonly IReadOnlyList _resourceDirectories; + private readonly int _searchDepth; + private readonly Func? _scriptFilter; + private readonly Func? _resourceFilter; private readonly AgentFileSkillScriptRunner? _scriptRunner; private readonly ILogger _logger; @@ -111,13 +106,9 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource options?.AllowedScriptExtensions ?? s_defaultScriptExtensions, StringComparer.OrdinalIgnoreCase); - this._scriptDirectories = options?.ScriptDirectories is not null - ? [.. ValidateAndNormalizeDirectoryNames(options.ScriptDirectories, this._logger)] - : s_defaultScriptDirectories; - - this._resourceDirectories = options?.ResourceDirectories is not null - ? [.. ValidateAndNormalizeDirectoryNames(options.ResourceDirectories, this._logger)] - : s_defaultResourceDirectories; + this._searchDepth = Throw.IfLessThan(options?.SearchDepth ?? DefaultSearchDepth, 1); + this._scriptFilter = options?.ScriptFilter; + this._resourceFilter = options?.ResourceFilter; this._scriptRunner = scriptRunner; } @@ -174,7 +165,7 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource results.Add(Path.GetFullPath(directory)); } - if (currentDepth >= MaxSearchDepth) + if (currentDepth >= MaxSkillDirectorySearchDepth) { return; } @@ -305,216 +296,246 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource } /// - /// Scans configured resource directories within a skill directory for resource files matching the configured extensions. + /// Scans the skill directory recursively (up to the configured search depth) for resource files + /// matching the configured extensions. /// /// - /// By default, scans references/ and assets/ subdirectories as specified by the - /// Agent Skills specification. - /// Configure to scan different or - /// additional directories, including "." for the skill root itself. /// Each file is validated against path-traversal and symlink-escape checks; unsafe files are skipped. + /// If a predicate is configured, files + /// that do not satisfy it are excluded. /// private List DiscoverResourceFiles(string skillDirectoryFullPath, string skillName) { var resources = new List(); - foreach (string directory in this._resourceDirectories.Distinct(StringComparer.OrdinalIgnoreCase)) - { - bool isRootDirectory = string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal); - - // GetFullPath normalizes mixed separators (e.g. "C:\skill\scripts/f1" → "C:\skill\scripts\f1") - string targetDirectory = isRootDirectory - ? skillDirectoryFullPath - : Path.GetFullPath(Path.Combine(skillDirectoryFullPath, directory)) + Path.DirectorySeparatorChar; - - if (!Directory.Exists(targetDirectory)) - { - continue; - } - - // Directory-level symlink check: skip if targetDirectory (or any intermediate - // segment) is a reparse point. The root directory is excluded — it's a caller-supplied - // trusted path, and the security boundary guards files within it, not the path itself. - if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath)) - { - if (this._logger.IsEnabled(LogLevel.Warning)) - { - LogResourceSymlinkDirectory(this._logger, skillName, SanitizePathForLog(directory)); - } - - continue; - } - -#if NET - var enumerationOptions = new EnumerationOptions - { - RecurseSubdirectories = false, - IgnoreInaccessible = true, - AttributesToSkip = FileAttributes.ReparsePoint, - }; - - foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", enumerationOptions)) -#else - foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.TopDirectoryOnly)) -#endif - { - string fileName = Path.GetFileName(filePath); - - // Exclude SKILL.md itself - if (string.Equals(fileName, SkillFileName, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - // Filter by extension - string extension = Path.GetExtension(filePath); - if (string.IsNullOrEmpty(extension) || !this._allowedResourceExtensions.Contains(extension)) - { - if (this._logger.IsEnabled(LogLevel.Debug)) - { - LogResourceSkippedExtension(this._logger, skillName, SanitizePathForLog(filePath), extension); - } - - continue; - } - - // Normalize the enumerated path to guard against non-canonical forms. - // e.g. "references/../../../etc/shadow" → "/etc/shadow" - string resolvedFilePath = Path.GetFullPath(filePath); - - // Path containment: reject if the resolved path escapes the target directory. - // e.g. "/etc/shadow".StartsWith("/skills/myskill/references/") → false → skip - if (!resolvedFilePath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase)) - { - if (this._logger.IsEnabled(LogLevel.Warning)) - { - LogResourcePathTraversal(this._logger, skillName, SanitizePathForLog(filePath)); - } - - continue; - } - - // Per-file symlink check: detects if the file (or any intermediate segment) - // is a reparse point. e.g. "references/secret.md" → symlink to "/etc/shadow" - if (HasSymlinkInPath(resolvedFilePath, targetDirectory)) - { - if (this._logger.IsEnabled(LogLevel.Warning)) - { - LogResourceSymlinkEscape(this._logger, skillName, SanitizePathForLog(filePath)); - } - - continue; - } - - // Compute relative path and normalize separators. - // e.g. "/skills/myskill/references/guide.md" → "references/guide.md" - string relativePath = NormalizePath(resolvedFilePath.Substring(skillDirectoryFullPath.Length)); - - resources.Add(new AgentFileSkillResource(relativePath, resolvedFilePath)); - } - } + this.ScanDirectoryForResources(skillDirectoryFullPath, skillDirectoryFullPath, skillName, resources, currentDepth: 1); return resources; } + private void ScanDirectoryForResources(string targetDirectory, string skillDirectoryFullPath, string skillName, List resources, int currentDepth) + { + if (currentDepth > this._searchDepth) + { + return; + } + + bool isRootDirectory = string.Equals(targetDirectory, skillDirectoryFullPath, StringComparison.OrdinalIgnoreCase); + + // Directory-level symlink check: skip if targetDirectory (or any intermediate + // segment) is a reparse point. The root directory is excluded — it's a caller-supplied + // trusted path, and the security boundary guards files within it, not the path itself. + if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath)) + { + if (this._logger.IsEnabled(LogLevel.Warning)) + { + LogResourceSymlinkDirectory(this._logger, skillName, SanitizePathForLog(targetDirectory)); + } + + return; + } + +#if NET + var enumerationOptions = new EnumerationOptions + { + RecurseSubdirectories = false, + IgnoreInaccessible = true, + AttributesToSkip = FileAttributes.ReparsePoint, + }; + + foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", enumerationOptions)) +#else + foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.TopDirectoryOnly)) +#endif + { + string fileName = Path.GetFileName(filePath); + + // Exclude SKILL.md itself + if (string.Equals(fileName, SkillFileName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Filter by extension + string extension = Path.GetExtension(filePath); + if (string.IsNullOrEmpty(extension) || !this._allowedResourceExtensions.Contains(extension)) + { + if (this._logger.IsEnabled(LogLevel.Debug)) + { + LogResourceSkippedExtension(this._logger, skillName, SanitizePathForLog(filePath), string.IsNullOrEmpty(extension) ? "(none)" : extension); + } + + continue; + } + + // Normalize the enumerated path to guard against non-canonical forms. + // e.g. "references/../../../etc/shadow" → "/etc/shadow" + string resolvedFilePath = Path.GetFullPath(filePath); + + // Path containment: reject if the resolved path escapes the skill directory. + // e.g. "/etc/shadow".StartsWith("/skills/myskill/") → false → skip + if (!resolvedFilePath.StartsWith(skillDirectoryFullPath, StringComparison.OrdinalIgnoreCase)) + { + if (this._logger.IsEnabled(LogLevel.Warning)) + { + LogResourcePathTraversal(this._logger, skillName, SanitizePathForLog(filePath)); + } + + continue; + } + + // Per-file symlink check: detects if the file (or any intermediate segment) + // is a reparse point. e.g. "references/secret.md" → symlink to "/etc/shadow" + if (HasSymlinkInPath(resolvedFilePath, skillDirectoryFullPath)) + { + if (this._logger.IsEnabled(LogLevel.Warning)) + { + LogResourceSymlinkEscape(this._logger, skillName, SanitizePathForLog(filePath)); + } + + continue; + } + + // Compute relative path and normalize separators. + // e.g. "/skills/myskill/references/guide.md" → "references/guide.md" + string relativePath = NormalizePath(resolvedFilePath.Substring(skillDirectoryFullPath.Length)); + + // Apply user-provided filter predicate + if (this._resourceFilter is not null && !this._resourceFilter(new AgentFileSkillFilterContext(skillName, relativePath))) + { + continue; + } + + resources.Add(new AgentFileSkillResource(relativePath, resolvedFilePath)); + } + + // Recurse into subdirectories if within depth limit + if (currentDepth < this._searchDepth) + { +#if NET + foreach (string subdirectory in Directory.EnumerateDirectories(targetDirectory, "*", enumerationOptions)) +#else + foreach (string subdirectory in this.SafeEnumerateDirectories(targetDirectory)) +#endif + { + this.ScanDirectoryForResources(subdirectory, skillDirectoryFullPath, skillName, resources, currentDepth + 1); + } + } + } + /// - /// Scans configured script directories within a skill directory for script files matching the configured extensions. + /// Scans the skill directory recursively (up to the configured search depth) for script files + /// matching the configured extensions. /// /// - /// By default, scans the scripts/ subdirectory as specified by the - /// Agent Skills specification. - /// Configure to scan different or - /// additional directories, including "." for the skill root itself. /// Each file is validated against path-traversal and symlink-escape checks; unsafe files are skipped. + /// If a predicate is configured, files + /// that do not satisfy it are excluded. /// private List DiscoverScriptFiles(string skillDirectoryFullPath, string skillName) { var scripts = new List(); - foreach (string directory in this._scriptDirectories.Distinct(StringComparer.OrdinalIgnoreCase)) + this.ScanDirectoryForScripts(skillDirectoryFullPath, skillDirectoryFullPath, skillName, scripts, currentDepth: 1); + + return scripts; + } + + private void ScanDirectoryForScripts(string targetDirectory, string skillDirectoryFullPath, string skillName, List scripts, int currentDepth) + { + if (currentDepth > this._searchDepth) { - bool isRootDirectory = string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal); + return; + } - // GetFullPath normalizes mixed separators (e.g. "C:\skill\scripts/f1" → "C:\skill\scripts\f1") - string targetDirectory = isRootDirectory - ? skillDirectoryFullPath - : Path.GetFullPath(Path.Combine(skillDirectoryFullPath, directory)) + Path.DirectorySeparatorChar; + bool isRootDirectory = string.Equals(targetDirectory, skillDirectoryFullPath, StringComparison.OrdinalIgnoreCase); - if (!Directory.Exists(targetDirectory)) + // Directory-level symlink check: skip if targetDirectory (or any intermediate + // segment) is a reparse point. The root directory is excluded — it's a caller-supplied + // trusted path, and the security boundary guards files within it, not the path itself. + if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath)) + { + if (this._logger.IsEnabled(LogLevel.Warning)) + { + LogScriptSymlinkDirectory(this._logger, skillName, SanitizePathForLog(targetDirectory)); + } + + return; + } + +#if NET + var enumerationOptions = new EnumerationOptions + { + RecurseSubdirectories = false, + IgnoreInaccessible = true, + AttributesToSkip = FileAttributes.ReparsePoint, + }; + + foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", enumerationOptions)) +#else + foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.TopDirectoryOnly)) +#endif + { + // Filter by extension + string extension = Path.GetExtension(filePath); + if (string.IsNullOrEmpty(extension) || !this._allowedScriptExtensions.Contains(extension)) { continue; } - // Directory-level symlink check: skip if targetDirectory (or any intermediate - // segment) is a reparse point. The root directory is excluded — it's a caller-supplied - // trusted path, and the security boundary guards files within it, not the path itself. - if (!isRootDirectory && HasSymlinkInPath(targetDirectory, skillDirectoryFullPath)) + // Normalize the enumerated path to guard against non-canonical forms. + // e.g. "scripts/../../../etc/shadow" → "/etc/shadow" + string resolvedFilePath = Path.GetFullPath(filePath); + + // Path containment: reject if the resolved path escapes the skill directory. + // e.g. "/etc/shadow".StartsWith("/skills/myskill/") → false → skip + if (!resolvedFilePath.StartsWith(skillDirectoryFullPath, StringComparison.OrdinalIgnoreCase)) { if (this._logger.IsEnabled(LogLevel.Warning)) { - LogScriptSymlinkDirectory(this._logger, skillName, SanitizePathForLog(directory)); + LogScriptPathTraversal(this._logger, skillName, SanitizePathForLog(filePath)); } continue; } -#if NET - var enumerationOptions = new EnumerationOptions + // Per-file symlink check: detects if the file (or any intermediate segment) + // is a reparse point. e.g. "scripts/run.py" → symlink to "/etc/shadow" + if (HasSymlinkInPath(resolvedFilePath, skillDirectoryFullPath)) { - RecurseSubdirectories = false, - IgnoreInaccessible = true, - AttributesToSkip = FileAttributes.ReparsePoint, - }; - - foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", enumerationOptions)) -#else - foreach (string filePath in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.TopDirectoryOnly)) -#endif - { - // Filter by extension - string extension = Path.GetExtension(filePath); - if (string.IsNullOrEmpty(extension) || !this._allowedScriptExtensions.Contains(extension)) + if (this._logger.IsEnabled(LogLevel.Warning)) { - continue; + LogScriptSymlinkEscape(this._logger, skillName, SanitizePathForLog(filePath)); } - // Normalize the enumerated path to guard against non-canonical forms. - // e.g. "scripts/../../../etc/shadow" → "/etc/shadow" - string resolvedFilePath = Path.GetFullPath(filePath); - - // Path containment: reject if the resolved path escapes the target directory. - // e.g. "/etc/shadow".StartsWith("/skills/myskill/scripts/") → false → skip - if (!resolvedFilePath.StartsWith(targetDirectory, StringComparison.OrdinalIgnoreCase)) - { - if (this._logger.IsEnabled(LogLevel.Warning)) - { - LogScriptPathTraversal(this._logger, skillName, SanitizePathForLog(filePath)); - } - - continue; - } - - // Per-file symlink check: detects if the file (or any intermediate segment) - // is a reparse point. e.g. "scripts/run.py" → symlink to "/etc/shadow" - if (HasSymlinkInPath(resolvedFilePath, targetDirectory)) - { - if (this._logger.IsEnabled(LogLevel.Warning)) - { - LogScriptSymlinkEscape(this._logger, skillName, SanitizePathForLog(filePath)); - } - - continue; - } - - // Compute relative path and normalize separators. - // e.g. "/skills/myskill/scripts/parsepdf.py" → "scripts/parsepdf.py" - string relativePath = NormalizePath(resolvedFilePath.Substring(skillDirectoryFullPath.Length)); - - scripts.Add(new AgentFileSkillScript(relativePath, resolvedFilePath, this._scriptRunner)); + continue; } + + // Compute relative path and normalize separators. + // e.g. "/skills/myskill/scripts/parsepdf.py" → "scripts/parsepdf.py" + string relativePath = NormalizePath(resolvedFilePath.Substring(skillDirectoryFullPath.Length)); + + // Apply user-provided filter predicate + if (this._scriptFilter is not null && !this._scriptFilter(new AgentFileSkillFilterContext(skillName, relativePath))) + { + continue; + } + + scripts.Add(new AgentFileSkillScript(relativePath, resolvedFilePath, this._scriptRunner)); } - return scripts; + // Recurse into subdirectories if within depth limit + if (currentDepth < this._searchDepth) + { +#if NET + foreach (string subdirectory in Directory.EnumerateDirectories(targetDirectory, "*", enumerationOptions)) +#else + foreach (string subdirectory in this.SafeEnumerateDirectories(targetDirectory)) +#endif + { + this.ScanDirectoryForScripts(subdirectory, skillDirectoryFullPath, skillName, scripts, currentDepth + 1); + } + } } /// @@ -542,6 +563,31 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource return false; } +#if !NET + /// + /// Best-effort directory enumeration for target frameworks without + /// EnumerationOptions.IgnoreInaccessible support. Returns an empty + /// array when the caller lacks permission to read the directory contents, + /// so a single inaccessible child does not abort the entire skill scan. + /// + private string[] SafeEnumerateDirectories(string path) + { + try + { + return Directory.GetDirectories(path); + } + catch (UnauthorizedAccessException) + { + if (this._logger.IsEnabled(LogLevel.Warning)) + { + LogDirectoryAccessDenied(this._logger, SanitizePathForLog(path)); + } + + return Array.Empty(); + } + } +#endif + private static string ParseYamlScalarValue(string yamlContent, Match kvMatch) { string value = kvMatch.Groups[3].Value; @@ -664,46 +710,6 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource } } - private static IEnumerable ValidateAndNormalizeDirectoryNames(IEnumerable directories, ILogger logger) - { - foreach (string directory in directories) - { - if (string.IsNullOrWhiteSpace(directory)) - { - throw new ArgumentException("Directory names must not be null or whitespace.", nameof(directories)); - } - - // "." is valid — it means the skill root directory. - if (string.Equals(directory, RootDirectoryIndicator, StringComparison.Ordinal)) - { - yield return directory; - continue; - } - - // Reject absolute paths and any path segments that escape upward. - if (Path.IsPathRooted(directory) || ContainsParentTraversalSegment(directory)) - { - LogDirectoryNameSkippedInvalid(logger, directory); - continue; - } - - yield return NormalizePath(directory); - } - } - - private static bool ContainsParentTraversalSegment(string directory) - { - foreach (string segment in directory.Split('/', '\\')) - { - if (segment == "..") - { - return true; - } - } - - return false; - } - [LoggerMessage(LogLevel.Information, "Discovered {Count} potential skills")] private static partial void LogSkillsDiscovered(ILogger logger, int count); @@ -743,6 +749,6 @@ internal sealed partial class AgentFileSkillsSource : AgentSkillsSource [LoggerMessage(LogLevel.Warning, "Skipping script directory '{DirectoryName}' in skill '{SkillName}': directory path contains a symlink")] private static partial void LogScriptSymlinkDirectory(ILogger logger, string skillName, string directoryName); - [LoggerMessage(LogLevel.Warning, "Skipping invalid directory name '{DirectoryName}': must be a relative path with no '..' segments")] - private static partial void LogDirectoryNameSkippedInvalid(ILogger logger, string directoryName); + [LoggerMessage(LogLevel.Warning, "Skipping directory '{DirectoryPath}': access denied")] + private static partial void LogDirectoryAccessDenied(ILogger logger, string directoryPath); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSourceOptions.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSourceOptions.cs index b5c83c0220..c9604e166d 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSourceOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSourceOptions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Shared.DiagnosticIds; @@ -32,28 +33,31 @@ public sealed class AgentFileSkillsSourceOptions public IEnumerable? AllowedScriptExtensions { get; set; } /// - /// Gets or sets relative directory paths to scan for script files within each skill directory. - /// Values may be single-segment names (e.g., "scripts") or multi-segment relative - /// paths (e.g., "sub/scripts"). Use "." to include files directly at the - /// skill root. Leading "./" prefixes, trailing separators, and backslashes are - /// normalized automatically; paths containing ".." segments or absolute paths are - /// rejected. - /// When , defaults to scripts (per the - /// Agent Skills specification). - /// When set, replaces the defaults entirely. + /// Gets or sets the maximum depth to search for script and resource files within each skill directory. + /// A value of 1 searches only the skill root directory. A value of 2 searches the root + /// and one level of subdirectories. + /// When , the source uses the default depth of 2. /// - public IEnumerable? ScriptDirectories { get; set; } + /// + /// Must be greater than or equal to 1; lower values are rejected by the constructor. + /// + public int? SearchDepth { get; set; } /// - /// Gets or sets relative directory paths to scan for resource files within each skill directory. - /// Values may be single-segment names (e.g., "references") or multi-segment relative - /// paths (e.g., "sub/resources"). Use "." to include files directly at the - /// skill root. Leading "./" prefixes, trailing separators, and backslashes are - /// normalized automatically; paths containing ".." segments or absolute paths are - /// rejected. - /// When , defaults to references and assets (per the - /// Agent Skills specification). - /// When set, replaces the defaults entirely. + /// Gets or sets a predicate that filters discovered script files. + /// The predicate receives an containing the skill's name + /// and the file's path relative to the skill directory. + /// Return to include the file or to exclude it. + /// When , all scripts matching the allowed extensions are included. /// - public IEnumerable? ResourceDirectories { get; set; } + public Func? ScriptFilter { get; set; } + + /// + /// Gets or sets a predicate that filters discovered resource files. + /// The predicate receives an containing the skill's name + /// and the file's path relative to the skill directory. + /// Return to include the file or to exclude it. + /// When , all resources matching the allowed extensions are included. + /// + public Func? ResourceFilter { get; set; } } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs index 32e018ae90..b5d20c40dc 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs @@ -111,10 +111,9 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable } [Fact] - public async Task GetSkillsAsync_ScriptsOutsideScriptsDir_AreNotDiscoveredAsync() + public async Task GetSkillsAsync_ScriptsInRootAndSubdirectories_AreDiscoveredByDefaultAsync() { - // Arrange — scripts outside configured directories are not discovered; only files directly - // inside the configured directory are picked up (no subdirectory recursion) + // Arrange — with default depth=2, scripts in root and immediate subdirectories are discovered string skillDir = CreateSkillDir(this._testRoot, "root-scripts", "Root scripts skill", "Body."); CreateFile(skillDir, "convert.py", "print('root')"); CreateFile(skillDir, "tools/helper.sh", "echo 'helper'"); @@ -123,9 +122,10 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable // Act var skills = await source.GetSkillsAsync(CancellationToken.None); - // Assert — neither file is in the default scripts/ directory, so no scripts are discovered + // Assert — both root and subdirectory scripts are discovered Assert.Single(skills); - Assert.Null(await skills[0].GetScriptAsync("convert.py")); + Assert.NotNull(await skills[0].GetScriptAsync("convert.py")); + Assert.NotNull(await skills[0].GetScriptAsync("tools/helper.sh")); } [Fact] @@ -225,13 +225,13 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable } [Fact] - public async Task GetSkillsAsync_ScriptDirectoriesWithNestedPath_DiscoversScriptsAsync() + public async Task GetSkillsAsync_DeepScript_DiscoveredWithHigherDepthAsync() { - // Arrange — ScriptDirectories configured with a multi-segment relative path (f1/f2/f3) + // Arrange — script at depth 4 (f1/f2/f3/run.py) discovered with SearchDepth=5 string skillDir = CreateSkillDir(this._testRoot, "nested-script-skill", "Nested script directory", "Body."); CreateFile(skillDir, "f1/f2/f3/run.py", "print('nested')"); var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ScriptDirectories = ["f1/f2/f3"] }); + new AgentFileSkillsSourceOptions { SearchDepth = 5 }); // Act var skills = await source.GetSkillsAsync(CancellationToken.None); @@ -243,36 +243,25 @@ public sealed class AgentFileSkillsSourceScriptTests : IDisposable Assert.Equal("f1/f2/f3/run.py", nestedScript!.Name); } - [Theory] - [InlineData("./scripts")] - [InlineData("./scripts/f1")] - [InlineData("./scripts/f1", "./f2")] - public async Task GetSkillsAsync_ScriptDirectoryWithDotSlashPrefix_DiscoversScriptsAsync(params string[] directories) + [Fact] + public async Task GetSkillsAsync_ScriptFilter_ExcludesFilteredScriptsAsync() { - // Arrange — "./"-prefixed directories are equivalent to their counterparts without the prefix; - // the leading "./" is transparently normalized by Path.GetFullPath during file enumeration. - string skillDir = CreateSkillDir(this._testRoot, "dotslash-script-skill", "Dot-slash prefix", "Body."); - foreach (string directory in directories) - { - string directoryWithoutDotSlash = directory.Substring(2); // strip "./" - CreateFile(skillDir, $"{directoryWithoutDotSlash}/run.py", "print('dotslash')"); - } - + // Arrange — ScriptFilter excludes scripts in the "f2" subdirectory + string skillDir = CreateSkillDir(this._testRoot, "dotslash-script-skill", "Filter test", "Body."); + CreateFile(skillDir, "scripts/run.py", "print('scripts')"); + CreateFile(skillDir, "f2/run.py", "print('f2')"); var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ScriptDirectories = directories }); + new AgentFileSkillsSourceOptions { ScriptFilter = ctx => !ctx.RelativeFilePath.StartsWith("f2/", StringComparison.OrdinalIgnoreCase) }); // Act var skills = await source.GetSkillsAsync(CancellationToken.None); - // Assert — scripts are discovered with names identical to using directories without "./" + // Assert — only scripts/ script is included; f2/ is excluded by filter Assert.Single(skills); - foreach (string directory in directories) - { - string expectedName = $"{directory.Substring(2)}/run.py"; - var script = await skills[0].GetScriptAsync(expectedName); - Assert.NotNull(script); - Assert.Equal(expectedName, script!.Name); - } + var script = await skills[0].GetScriptAsync("scripts/run.py"); + Assert.NotNull(script); + Assert.Equal("scripts/run.py", script!.Name); + Assert.Null(await skills[0].GetScriptAsync("f2/run.py")); } private static string CreateSkillDir(string root, string name, string description, string body) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs index cd362ce64e..6eb561c5b6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs @@ -425,9 +425,9 @@ public sealed class FileAgentSkillLoaderTests : IDisposable } [Fact] - public async Task GetSkillsAsync_ResourceInSkillRoot_NotDiscoveredByDefaultAsync() + public async Task GetSkillsAsync_ResourceInSkillRoot_DiscoveredByDefaultAsync() { - // Arrange — resource files directly in the skill directory (not in a spec subdirectory) + // Arrange — resource files directly in the skill directory are discovered with default depth=2 string skillDir = Path.Combine(this._testRoot, "root-resource-skill"); Directory.CreateDirectory(skillDir); File.WriteAllText(Path.Combine(skillDir, "guide.md"), "guide content"); @@ -440,29 +440,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable // Act var skills = await source.GetSkillsAsync(); - // Assert — root-level files are NOT discovered unless "." is in ResourceDirectories - Assert.Single(skills); - Assert.Empty(skills[0].GetTestResources()!); - } - - [Fact] - public async Task GetSkillsAsync_ResourceInSkillRoot_DiscoveredWhenRootDirectoryConfiguredAsync() - { - // Arrange — "." in ResourceDirectories opts into root-level resource discovery - string skillDir = Path.Combine(this._testRoot, "root-opt-in-skill"); - Directory.CreateDirectory(skillDir); - File.WriteAllText(Path.Combine(skillDir, "guide.md"), "guide content"); - File.WriteAllText(Path.Combine(skillDir, "config.json"), "{}"); - File.WriteAllText( - Path.Combine(skillDir, "SKILL.md"), - "---\nname: root-opt-in-skill\ndescription: Root opt-in\n---\nBody."); - var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = ["references", "assets", "."] }); - - // Act - var skills = await source.GetSkillsAsync(); - - // Assert — both root-level resource files (and SKILL.md excluded) should be discovered + // Assert — root-level files are discovered by default (depth=2 includes root) Assert.Single(skills); var skill = skills[0]; Assert.Equal(2, skill.GetTestResources()!.Count); @@ -471,9 +449,22 @@ public sealed class FileAgentSkillLoaderTests : IDisposable } [Fact] - public async Task GetSkillsAsync_ResourceInNonSpecDirectory_NotDiscoveredByDefaultAsync() + public void Constructor_SearchDepthBelowOne_Throws() { - // Arrange — resource in a non-spec directory (neither references/ nor assets/) + // Arrange / Act / Assert — SearchDepth must be >= 1 + Assert.Throws(() => + new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, + new AgentFileSkillsSourceOptions { SearchDepth = 0 })); + + Assert.Throws(() => + new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, + new AgentFileSkillsSourceOptions { SearchDepth = -1 })); + } + + [Fact] + public async Task GetSkillsAsync_ResourceInSubdirectory_DiscoveredByDefaultAsync() + { + // Arrange — resource in any subdirectory is discovered with default depth=2 string skillDir = Path.Combine(this._testRoot, "non-spec-skill"); string customDir = Path.Combine(skillDir, "docs"); Directory.CreateDirectory(customDir); @@ -486,15 +477,16 @@ public sealed class FileAgentSkillLoaderTests : IDisposable // Act var skills = await source.GetSkillsAsync(); - // Assert — non-spec directories are not scanned by default + // Assert — subdirectory files are discovered by default Assert.Single(skills); - Assert.Empty(skills[0].GetTestResources()!); + Assert.Single(skills[0].GetTestResources()!); + Assert.Equal("docs/readme.md", skills[0].GetTestResources()![0].Name); } [Fact] - public async Task GetSkillsAsync_CustomResourceDirectories_ReplacesDefaultsAsync() + public async Task GetSkillsAsync_ResourceFilter_ExcludesFilteredFilesAsync() { - // Arrange — custom ResourceDirectories replaces the spec defaults + // Arrange — ResourceFilter excludes files in the "docs" subdirectory string skillDir = Path.Combine(this._testRoot, "custom-directory-skill"); string customDir = Path.Combine(skillDir, "docs"); string refsDir = Path.Combine(skillDir, "references"); @@ -506,16 +498,16 @@ public sealed class FileAgentSkillLoaderTests : IDisposable Path.Combine(skillDir, "SKILL.md"), "---\nname: custom-directory-skill\ndescription: Custom directory\n---\nBody."); var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = ["docs"] }); + new AgentFileSkillsSourceOptions { ResourceFilter = ctx => !ctx.RelativeFilePath.StartsWith("docs/", StringComparison.OrdinalIgnoreCase) }); // Act var skills = await source.GetSkillsAsync(); - // Assert — only docs/ is scanned; references/ is NOT scanned + // Assert — only references/ resource is included; docs/ is excluded by filter Assert.Single(skills); var skill = skills[0]; Assert.Single(skill.GetTestResources()!); - Assert.Equal("docs/readme.md", skill.GetTestResources()![0].Name); + Assert.Equal("references/ref.md", skill.GetTestResources()![0].Name); } [Fact] @@ -755,9 +747,9 @@ public sealed class FileAgentSkillLoaderTests : IDisposable } [Fact] - public async Task GetSkillsAsync_SymlinkedIntermediateSegment_SkipsCustomDirectoryAsync() + public async Task GetSkillsAsync_SymlinkedIntermediateSegment_SkipsSymlinkedDirectoryAsync() { - // Arrange — custom resource directory "sub/resources" where "sub" is a symlink. + // Arrange — "sub" directory is a symlink pointing outside the skill directory. // The directory-level HasSymlinkInPath check should detect the intermediate symlink. string skillDir = Path.Combine(this._testRoot, "symlink-intermediate"); Directory.CreateDirectory(skillDir); @@ -783,7 +775,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable var source = new AgentFileSkillsSource( this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = ["sub/resources"] }); + new AgentFileSkillsSourceOptions { SearchDepth = 4 }); // Act var skills = await source.GetSkillsAsync(); @@ -957,54 +949,32 @@ public sealed class FileAgentSkillLoaderTests : IDisposable Assert.Null(fm.Metadata); } - [Theory] - [InlineData("..")] - [InlineData("../escape")] - [InlineData("sub/../escape")] - [InlineData("/absolute")] - [InlineData("\\absolute")] - public void Constructor_InvalidDirectoryName_SkipsInvalidDirectories(string badDirectory) + [Fact] + public async Task GetSkillsAsync_SearchDepthOne_OnlyRootFilesDiscoveredAsync() { - // Arrange & Act — invalid directories are skipped with a warning rather than throwing - var source1 = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptDirectories = [badDirectory] }); - var source2 = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ResourceDirectories = [badDirectory] }); + // Arrange — with SearchDepth = 1, only root-level files are discovered + string skillDir = Path.Combine(this._testRoot, "depth-one-skill"); + string scriptsDir = Path.Combine(skillDir, "scripts"); + Directory.CreateDirectory(scriptsDir); + File.WriteAllText(Path.Combine(scriptsDir, "run.py"), "print('hello')"); + File.WriteAllText( + Path.Combine(skillDir, "SKILL.md"), + "---\nname: depth-one-skill\ndescription: Depth one\n---\nBody."); + var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, + new AgentFileSkillsSourceOptions { SearchDepth = 1 }); - // Assert - Assert.NotNull(source1); - Assert.NotNull(source2); - } + // Act + var skills = await source.GetSkillsAsync(); - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void Constructor_NullOrWhitespaceDirectoryName_ThrowsArgumentException(string? badDirectory) - { - // Arrange & Act & Assert — null/whitespace is a contract violation, not a config error - Assert.Throws(() => new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptDirectories = [badDirectory!] })); - Assert.Throws(() => new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ResourceDirectories = [badDirectory!] })); - } - - [Theory] - [InlineData("scripts")] - [InlineData("my-scripts")] - [InlineData("sub/directory")] - [InlineData(".")] - [InlineData("./scripts")] - [InlineData("./scripts/f1")] - [InlineData("my..scripts")] - public void Constructor_ValidDirectoryName_DoesNotThrow(string validDirectory) - { - // Arrange & Act & Assert - var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, new AgentFileSkillsSourceOptions { ScriptDirectories = [validDirectory] }); - Assert.NotNull(source); + // Assert — scripts in subdirectories are NOT discovered at depth 1 + Assert.Single(skills); + Assert.Null(await skills[0].GetScriptAsync("scripts/run.py")); } [Fact] - public async Task GetSkillsAsync_DuplicateDirectoriesAfterNormalization_NoDuplicateResourcesAsync() + public async Task GetSkillsAsync_ResourceInSubdirectory_DiscoveredWithDefaultDepthAsync() { - // Arrange — "references" and "./references" refer to the same directory; - // after normalization they should be deduplicated so resources appear only once. + // Arrange — resources in a subdirectory are discovered by default (depth=2) string skillDir = Path.Combine(this._testRoot, "dedup-directory-skill"); string refsDir = Path.Combine(skillDir, "references"); Directory.CreateDirectory(refsDir); @@ -1012,45 +982,21 @@ public sealed class FileAgentSkillLoaderTests : IDisposable File.WriteAllText( Path.Combine(skillDir, "SKILL.md"), "---\nname: dedup-directory-skill\ndescription: Dedup test\n---\nBody."); - var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = ["references", "./references"] }); + var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act var skills = await source.GetSkillsAsync(); - // Assert — only one copy of the resource despite two equivalent directory entries + // Assert — resource is discovered once Assert.Single(skills); Assert.Single(skills[0].GetTestResources()!); Assert.Equal("references/FAQ.md", skills[0].GetTestResources()![0].Name); } [Fact] - public async Task GetSkillsAsync_TrailingSlashDirectoryNormalized_NoDuplicateResourcesAsync() + public async Task GetSkillsAsync_ScriptInSubdirectory_DiscoveredWithDefaultDepthAsync() { - // Arrange — "references/" should be normalized to "references" - string skillDir = Path.Combine(this._testRoot, "trailing-slash-skill"); - string refsDir = Path.Combine(skillDir, "references"); - Directory.CreateDirectory(refsDir); - File.WriteAllText(Path.Combine(refsDir, "data.json"), "{}"); - File.WriteAllText( - Path.Combine(skillDir, "SKILL.md"), - "---\nname: trailing-slash-skill\ndescription: Trailing slash test\n---\nBody."); - var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = ["references", "references/"] }); - - // Act - var skills = await source.GetSkillsAsync(); - - // Assert — trailing slash variant deduplicated - Assert.Single(skills); - Assert.Single(skills[0].GetTestResources()!); - Assert.Equal("references/data.json", skills[0].GetTestResources()![0].Name); - } - - [Fact] - public async Task GetSkillsAsync_BackslashDirectoryNormalized_NoDuplicateScriptsAsync() - { - // Arrange — ".\\scripts" should be normalized to "scripts" + // Arrange — scripts in a subdirectory are discovered by default (depth=2) string skillDir = Path.Combine(this._testRoot, "backslash-skill"); string scriptsDir = Path.Combine(skillDir, "scripts"); Directory.CreateDirectory(scriptsDir); @@ -1058,50 +1004,48 @@ public sealed class FileAgentSkillLoaderTests : IDisposable File.WriteAllText( Path.Combine(skillDir, "SKILL.md"), "---\nname: backslash-skill\ndescription: Backslash test\n---\nBody."); - var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ScriptDirectories = ["scripts", ".\\scripts"] }); + var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act var skills = await source.GetSkillsAsync(); - // Assert — backslash variant deduplicated + // Assert — script is discovered Assert.Single(skills); var script = await skills[0].GetScriptAsync("scripts/run.py"); Assert.NotNull(script); Assert.Equal("scripts/run.py", script!.Name); } - [Theory] - [InlineData("./references")] - [InlineData("./assets/docs")] - public async Task GetSkillsAsync_ResourceDirectoryWithDotSlashPrefix_DiscoversResourcesAsync(string directory) + [Fact] + public async Task GetSkillsAsync_ResourceFilterWhitelist_OnlyMatchingFilesDiscoveredAsync() { - // Arrange — "./references" and "./assets/docs" are equivalent to "references" and "assets/docs"; - // the leading "./" is transparently normalized by Path.GetFullPath during file enumeration. - string directoryWithoutDotSlash = directory.Substring(2); // strip "./" + // Arrange — ResourceFilter acts as whitelist: only references/ paths included string skillDir = Path.Combine(this._testRoot, "dotslash-res-skill"); - string targetDir = Path.Combine(skillDir, directoryWithoutDotSlash.Replace('/', Path.DirectorySeparatorChar)); - Directory.CreateDirectory(targetDir); - File.WriteAllText(Path.Combine(targetDir, "data.json"), "{}"); + string refsDir = Path.Combine(skillDir, "references"); + string assetsDir = Path.Combine(skillDir, "assets"); + Directory.CreateDirectory(refsDir); + Directory.CreateDirectory(assetsDir); + File.WriteAllText(Path.Combine(refsDir, "data.json"), "{}"); + File.WriteAllText(Path.Combine(assetsDir, "image.txt"), "data"); File.WriteAllText( Path.Combine(skillDir, "SKILL.md"), "---\nname: dotslash-res-skill\ndescription: Dot-slash prefix\n---\nBody."); var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = [directory] }); + new AgentFileSkillsSourceOptions { ResourceFilter = ctx => ctx.RelativeFilePath.StartsWith("references/", StringComparison.OrdinalIgnoreCase) }); // Act var skills = await source.GetSkillsAsync(); - // Assert — the resource is discovered with a name identical to using the directory without "./" + // Assert — only the references/ resource is included Assert.Single(skills); Assert.Single(skills[0].GetTestResources()!); - Assert.Equal($"{directoryWithoutDotSlash}/data.json", skills[0].GetTestResources()![0].Name); + Assert.Equal("references/data.json", skills[0].GetTestResources()![0].Name); } [Fact] - public async Task GetSkillsAsync_ResourceDirectoriesWithNestedPath_DiscoversResourcesAsync() + public async Task GetSkillsAsync_DeepResource_NotDiscoveredWithDefaultDepthAsync() { - // Arrange — ResourceDirectories configured with a multi-segment relative path (f1/f2/f3) + // Arrange — resource at depth 3 (f1/f2/f3/data.json) exceeds default depth=2 string skillDir = Path.Combine(this._testRoot, "nested-directory-skill"); string nestedDir = Path.Combine(skillDir, "f1", "f2", "f3"); Directory.CreateDirectory(nestedDir); @@ -1109,8 +1053,29 @@ public sealed class FileAgentSkillLoaderTests : IDisposable File.WriteAllText( Path.Combine(skillDir, "SKILL.md"), "---\nname: nested-directory-skill\ndescription: Nested directory\n---\nBody."); + var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); + + // Act + var skills = await source.GetSkillsAsync(); + + // Assert — resource at depth 4 is NOT discovered with default depth=2 + Assert.Single(skills); + Assert.Empty(skills[0].GetTestResources()!); + } + + [Fact] + public async Task GetSkillsAsync_DeepResource_DiscoveredWithHigherDepthAsync() + { + // Arrange — resource at depth 4 (f1/f2/f3/data.json) discovered with SearchDepth=5 + string skillDir = Path.Combine(this._testRoot, "deep-res-skill"); + string nestedDir = Path.Combine(skillDir, "f1", "f2", "f3"); + Directory.CreateDirectory(nestedDir); + File.WriteAllText(Path.Combine(nestedDir, "data.json"), "{}"); + File.WriteAllText( + Path.Combine(skillDir, "SKILL.md"), + "---\nname: deep-res-skill\ndescription: Deep resource\n---\nBody."); var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ResourceDirectories = ["f1/f2/f3"] }); + new AgentFileSkillsSourceOptions { SearchDepth = 5 }); // Act var skills = await source.GetSkillsAsync(); @@ -1171,22 +1136,21 @@ public sealed class FileAgentSkillLoaderTests : IDisposable } [Fact] - public async Task GetSkillsAsync_ScriptInSkillRoot_DiscoveredWhenRootDirectoryConfiguredAsync() + public async Task GetSkillsAsync_ScriptInSkillRoot_DiscoveredByDefaultAsync() { - // Arrange — script file directly in the skill directory with ScriptDirectories = ["."] + // Arrange — script file directly in the skill directory is discovered with default depth=2 string skillDir = Path.Combine(this._testRoot, "root-script-skill"); Directory.CreateDirectory(skillDir); File.WriteAllText(Path.Combine(skillDir, "run.py"), "print('hello')"); File.WriteAllText( Path.Combine(skillDir, "SKILL.md"), "---\nname: root-script-skill\ndescription: Root script\n---\nBody."); - var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor, - new AgentFileSkillsSourceOptions { ScriptDirectories = ["."] }); + var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor); // Act var skills = await source.GetSkillsAsync(); - // Assert — script at the skill root should be discovered + // Assert — script at the skill root is discovered by default var skill = skills.FirstOrDefault(s => s.Frontmatter.Name == "root-script-skill"); Assert.NotNull(skill); var script = await skill.GetScriptAsync("run.py"); From b1e9efee7e6129bb67f66621309147b019f6b0b7 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Thu, 28 May 2026 11:37:19 -0700 Subject: [PATCH 08/61] Update package version (#6161) --- dotnet/nuget/nuget-package.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 38f0c94c7d..ed9a52f6c4 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -1,14 +1,14 @@ - 1.7.0 + 1.8.0 1 - 260526 + 260528 $(VersionPrefix)-rc$(RCNumber) $(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1 $(VersionPrefix)-preview.$(DateSuffix).1 $(VersionPrefix) - 1.7.0 + 1.8.0 Debug;Release;Publish true From d2f79930d5554e39add177f46f8bc61092c0ad34 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 28 May 2026 14:40:48 -0400 Subject: [PATCH 09/61] .NET: feat: Update GroupChatManager semantics to match other Orchestration patterns (#6140) * Refactor group chat workflow to prevent message echoing and enhance checkpointing - Updated GroupChatWorkflowBuilder to disable forwarding incoming messages to prevent duplicates. - Enhanced RoundRobinGroupChatManager with checkpointing support to preserve state across executions. - Modified GroupChatHost to maintain a history of messages and track the current speaker for message broadcasting. - Implemented broadcasting logic to ensure participants receive messages from others while excluding their own responses. - Added comprehensive unit tests for group chat orchestration, including scenarios for tool approval and function calls. - Introduced a new ApprovalHarness for testing tool invocation and approval workflows. * fixup: format * Add JSON serialization support for GroupChatManagerState and RoundRobinGroupChatManagerState --------- Co-authored-by: Jacob Alber --- .../GroupChatManager.cs | 149 +++++- .../GroupChatWorkflowBuilder.cs | 6 +- .../RoundRobinGroupChatManager.cs | 19 + .../Specialized/GroupChatHost.cs | 110 +++- .../WorkflowsJsonUtilities.cs | 2 + .../AgentWorkflowBuilderTests.cs | 280 +++++++++- .../GroupChatOrchestrationTests.cs | 479 ++++++++++++++++++ .../JsonSerializationTests.cs | 28 + .../RoundRobinGroupChatManagerTests.cs | 51 ++ 9 files changed, 1107 insertions(+), 17 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatOrchestrationTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatManager.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatManager.cs index d16a4b5b43..ab94a9fa8a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatManager.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -13,6 +15,16 @@ namespace Microsoft.Agents.AI.Workflows; /// public abstract class GroupChatManager { + // The state key under which GroupChatManager persists its own (non-subclass) state on the + // raw IWorkflowContext supplied by the hosting GroupChatHost executor. + internal const string BaseStateKey = "GroupChatManager"; + + // Prefix automatically applied to every key a subclass writes through the wrapped context + // supplied to OnCheckpointingAsync / OnCheckpointRestoredAsync. Keeps subclass-defined + // state in its own namespace so it cannot collide with the host's state keys nor with + // BaseStateKey itself. + internal const string SubclassStateKeyPrefix = "GroupChatManager_"; + /// /// Initializes a new instance of the class. /// @@ -48,12 +60,22 @@ public abstract class GroupChatManager CancellationToken cancellationToken = default); /// - /// Filters the chat history before it's passed to the next agent. + /// Filters the messages broadcast to participants for the current turn. /// - /// The chat history to filter. + /// + /// Under the broadcast model, each participant maintains its own per-agent session (history) + /// through its . The host distributes new messages + /// (initial user input on the first turn, the most recent speaker's response on subsequent turns) + /// to every participant — except the speaker that produced them — so every participant's session + /// stays synchronized. This method lets the manager shape that broadcast payload (for example, + /// to omit certain messages or to inject orchestrator-visible annotations). The full canonical + /// conversation is still available to and + /// . + /// + /// The new messages about to be broadcast to participants this turn. /// The to monitor for cancellation requests. /// The default is . - /// The filtered chat history. + /// The filtered message list to broadcast. protected internal virtual ValueTask> UpdateHistoryAsync( IReadOnlyList history, CancellationToken cancellationToken = default) => @@ -78,4 +100,125 @@ public abstract class GroupChatManager { this.IterationCount = 0; } + + /// + /// Invoked when the hosting group chat workflow is checkpointing, giving subclasses a chance to + /// persist any additional state they maintain (e.g., a round-robin cursor or an LLM session). + /// + /// + /// + /// The default implementation is a no-op. Base-class state (currently + /// ) is persisted automatically by the hosting + /// before this method is invoked; subclasses do not + /// need to call base.OnCheckpointingAsync. + /// + /// + /// The supplied is a wrapper that transparently prefixes every + /// state key with "GroupChatManager_", isolating subclass state from the host's own + /// state keys (and from the reserved base-state key). Implementations therefore may use any + /// human-readable key (e.g., "next_index") without worrying about collisions. + /// + /// + /// A wrapped workflow context that scopes state keys to the + /// subclass namespace. + /// The to monitor for cancellation requests. + /// The default is . + protected virtual ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + => default; + + /// + /// Invoked when the hosting group chat workflow is being restored from a checkpoint, giving + /// subclasses a chance to hydrate any additional state they persisted in + /// . + /// + /// + /// The default implementation is a no-op. Base-class state (currently + /// ) is restored automatically by the hosting + /// before this method is invoked; subclasses do not + /// need to call base.OnCheckpointRestoredAsync. The supplied + /// uses the same key-prefixing wrapper as . + /// + /// A wrapped workflow context that scopes state keys to the + /// subclass namespace. + /// The to monitor for cancellation requests. + /// The default is . + protected virtual ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + => default; + + // Root checkpoint entry point invoked by the hosting GroupChatHost. Persists the manager's + // own base state under the reserved BaseStateKey on the raw context, then delegates to the + // subclass-facing OnCheckpointingAsync hook with a wrapped context that prefixes every key + // with SubclassStateKeyPrefix. + internal async ValueTask CheckpointAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + await context.QueueStateUpdateAsync(BaseStateKey, new GroupChatManagerState(this.IterationCount), cancellationToken: cancellationToken).ConfigureAwait(false); + await this.OnCheckpointingAsync(new PrefixingWorkflowContext(context, SubclassStateKeyPrefix), cancellationToken).ConfigureAwait(false); + } + + // Root restore entry point invoked by the hosting GroupChatHost. Symmetric to CheckpointAsync. + internal async ValueTask RestoreCheckpointAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + GroupChatManagerState? state = await context.ReadStateAsync(BaseStateKey, cancellationToken: cancellationToken).ConfigureAwait(false); + this.IterationCount = state?.IterationCount ?? 0; + await this.OnCheckpointRestoredAsync(new PrefixingWorkflowContext(context, SubclassStateKeyPrefix), cancellationToken).ConfigureAwait(false); + } +} + +internal sealed record GroupChatManagerState(int IterationCount); + +// IWorkflowContext decorator that prepends a fixed prefix to every state key passed through it. +// All non-state members (events, message sending, output yielding, halt requests, trace context, +// and runtime characteristics) delegate directly to the wrapped context. +internal sealed class PrefixingWorkflowContext(IWorkflowContext inner, string prefix) : IWorkflowContext +{ + private readonly IWorkflowContext _inner = Throw.IfNull(inner); + private readonly string _prefix = Throw.IfNullOrEmpty(prefix); + + public IReadOnlyDictionary? TraceContext => this._inner.TraceContext; + + public bool ConcurrentRunsEnabled => this._inner.ConcurrentRunsEnabled; + + public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default) + => this._inner.AddEventAsync(workflowEvent, cancellationToken); + + public ValueTask SendMessageAsync(object message, string? targetId, CancellationToken cancellationToken = default) + => this._inner.SendMessageAsync(message, targetId, cancellationToken); + + public ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default) + => this._inner.YieldOutputAsync(output, cancellationToken); + + public ValueTask RequestHaltAsync() => this._inner.RequestHaltAsync(); + + public ValueTask ReadStateAsync(string key, string? scopeName = null, CancellationToken cancellationToken = default) + => this._inner.ReadStateAsync(this.Wrap(key), scopeName, cancellationToken); + + public ValueTask ReadOrInitStateAsync(string key, Func initialStateFactory, string? scopeName = null, CancellationToken cancellationToken = default) + => this._inner.ReadOrInitStateAsync(this.Wrap(key), initialStateFactory, scopeName, cancellationToken); + + public async ValueTask> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default) + { + HashSet rawKeys = await this._inner.ReadStateKeysAsync(scopeName, cancellationToken).ConfigureAwait(false); + return [.. rawKeys.Where(k => k.StartsWith(this._prefix, StringComparison.Ordinal)) + .Select(k => k.Substring(this._prefix.Length))]; + } + + public ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default) + => this._inner.QueueStateUpdateAsync(this.Wrap(key), value, scopeName, cancellationToken); + + public async ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default) + { + // Clearing the entire underlying scope would also remove keys owned by the host and other + // subsystems sharing the executor's default scope. Restrict the clear to keys carrying + // this wrapper's prefix. + HashSet rawKeys = await this._inner.ReadStateKeysAsync(scopeName, cancellationToken).ConfigureAwait(false); + foreach (string rawKey in rawKeys) + { + if (rawKey.StartsWith(this._prefix, StringComparison.Ordinal)) + { + await this._inner.QueueStateUpdateAsync(rawKey, null, scopeName, cancellationToken).ConfigureAwait(false); + } + } + } + + private string Wrap(string key) => this._prefix + Throw.IfNullOrEmpty(key); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs index 66e4429e35..cb922616bb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs @@ -75,10 +75,14 @@ public sealed class GroupChatWorkflowBuilder { AIAgent[] agents = this._participants.ToArray(); + // GroupChatHost owns the canonical conversation and broadcasts messages directly to every + // participant. Participants therefore must not echo their incoming messages back to the host + // (which would cause duplicates), but must still reframe other agents' assistant messages as + // user messages so each agent's own session reads coherently. AIAgentHostOptions options = new() { ReassignOtherAgentsAsUsers = true, - ForwardIncomingMessages = true + ForwardIncomingMessages = false }; Dictionary agentMap = agents.ToDictionary(a => a, a => a.BindAsExecutor(options)); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/RoundRobinGroupChatManager.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/RoundRobinGroupChatManager.cs index 8f11fe7ed6..cd849cbf15 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/RoundRobinGroupChatManager.cs @@ -69,4 +69,23 @@ public class RoundRobinGroupChatManager : GroupChatManager base.Reset(); this._nextIndex = 0; } + + /// + protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + => context.QueueStateUpdateAsync(StateKey, new RoundRobinGroupChatManagerState(this._nextIndex), cancellationToken: cancellationToken); + + /// + protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + RoundRobinGroupChatManagerState? state = await context.ReadStateAsync(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false); + this._nextIndex = state?.NextIndex ?? 0; + if (this._nextIndex < 0 || this._nextIndex >= this._agents.Count) + { + this._nextIndex = 0; + } + } + + private const string StateKey = "next_index"; } + +internal sealed record RoundRobinGroupChatManagerState(int NextIndex); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs index b902bf8ef1..a3f206fc1e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs @@ -20,12 +20,25 @@ internal sealed class GroupChatHost( AutoSendTurnToken = false }; + private const string HistoryStateKey = nameof(_history); + private const string CurrentSpeakerStateKey = nameof(_currentSpeakerExecutorId); + private readonly AIAgent[] _agents = agents; private readonly Dictionary _agentMap = agentMap; private readonly Func, GroupChatManager> _managerFactory = managerFactory; private GroupChatManager? _manager; + // Canonical conversation accumulated across turns. Each participant maintains its own per-agent + // session/thread; the host keeps this only as the source of truth for the manager hooks + // (SelectNextAgentAsync / ShouldTerminateAsync) and for the workflow's final output. + private List _history = []; + + // Executor id of the participant we most recently dispatched a TurnToken to – i.e., the current + // speaker whose response is about to arrive. Used to exclude that participant from the next + // broadcast (its own session already contains the message it produced). + private string? _currentSpeakerExecutorId; + protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) => base.ConfigureProtocol(protocolBuilder).YieldsOutput>(); @@ -33,30 +46,105 @@ internal sealed class GroupChatHost( { this._manager ??= this._managerFactory(this._agents); - if (!await this._manager.ShouldTerminateAsync(messages, cancellationToken).ConfigureAwait(false)) + // The delta arriving here is either the initial user input (turn 0) or the most recent speaker's + // response (subsequent turns) – participants no longer echo incoming messages back to the host. + if (messages.Count > 0) { - var filtered = await this._manager.UpdateHistoryAsync(messages, cancellationToken).ConfigureAwait(false); - messages = filtered is null || ReferenceEquals(filtered, messages) ? messages : [.. filtered]; + this._history.AddRange(messages); + } - if (await this._manager.SelectNextAgentAsync(messages, cancellationToken).ConfigureAwait(false) is AIAgent nextAgent && - this._agentMap.TryGetValue(nextAgent, out var executor)) + if (await this._manager.ShouldTerminateAsync(this._history, cancellationToken).ConfigureAwait(false)) + { + await this.CompleteAsync(context, cancellationToken).ConfigureAwait(false); + return; + } + + if (messages.Count > 0) + { + IEnumerable filteredDelta = await this._manager.UpdateHistoryAsync(messages, cancellationToken).ConfigureAwait(false); + List broadcastMessages = filteredDelta is null + ? messages + : (ReferenceEquals(filteredDelta, messages) ? messages : [.. filteredDelta]); + + if (broadcastMessages.Count > 0) { - this._manager.IterationCount++; - await context.SendMessageAsync(messages, executor.Id, cancellationToken).ConfigureAwait(false); - await context.SendMessageAsync(new TurnToken(emitEvents), executor.Id, cancellationToken).ConfigureAwait(false); - return; + await this.BroadcastAsync(broadcastMessages, context, cancellationToken).ConfigureAwait(false); } } - this._manager = null; - await context.YieldOutputAsync(messages, cancellationToken).ConfigureAwait(false); + if (await this._manager.SelectNextAgentAsync(this._history, cancellationToken).ConfigureAwait(false) is AIAgent nextAgent && + this._agentMap.TryGetValue(nextAgent, out ExecutorBinding? executor)) + { + this._manager.IterationCount++; + this._currentSpeakerExecutorId = executor.Id; + await context.SendMessageAsync(new TurnToken(emitEvents), executor.Id, cancellationToken).ConfigureAwait(false); + return; + } + + await this.CompleteAsync(context, cancellationToken).ConfigureAwait(false); } + + private ValueTask BroadcastAsync(List messages, IWorkflowContext context, CancellationToken cancellationToken) + { + List? sendTasks = null; + foreach (ExecutorBinding participant in this._agentMap.Values) + { + if (string.Equals(participant.Id, this._currentSpeakerExecutorId, StringComparison.Ordinal)) + { + continue; + } + + (sendTasks ??= []).Add(context.SendMessageAsync(messages, participant.Id, cancellationToken).AsTask()); + } + + return sendTasks is null ? default : new ValueTask(Task.WhenAll(sendTasks)); + } + + private async ValueTask CompleteAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + List output = this._history; + this._history = []; + this._currentSpeakerExecutorId = null; + this._manager = null; + + await context.YieldOutputAsync(output, cancellationToken).ConfigureAwait(false); + } + protected override ValueTask ResetAsync() { this._manager = null; + this._history = []; + this._currentSpeakerExecutorId = null; return base.ResetAsync(); } ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync(); + + protected internal override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + Task historyTask = context.QueueStateUpdateAsync(HistoryStateKey, this._history, cancellationToken: cancellationToken).AsTask(); + Task currentSpeakerTask = context.QueueStateUpdateAsync(CurrentSpeakerStateKey, this._currentSpeakerExecutorId, cancellationToken: cancellationToken).AsTask(); + Task baseTask = base.OnCheckpointingAsync(context, cancellationToken).AsTask(); + + // Eagerly materialize the manager so subclass state (e.g., the round-robin cursor) gets + // persisted on every checkpoint, even if no turn has been taken yet since the host was constructed. + this._manager ??= this._managerFactory(this._agents); + Task managerTask = this._manager.CheckpointAsync(context, cancellationToken).AsTask(); + + await Task.WhenAll(historyTask, currentSpeakerTask, baseTask, managerTask).ConfigureAwait(false); + } + + protected internal override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + this._history = await context.ReadStateAsync>(HistoryStateKey, cancellationToken: cancellationToken).ConfigureAwait(false) ?? []; + this._currentSpeakerExecutorId = await context.ReadStateAsync(CurrentSpeakerStateKey, cancellationToken: cancellationToken).ConfigureAwait(false); + + // Instantiate the manager eagerly so its restore hook can rehydrate IterationCount and any + // subclass-defined state (e.g., RoundRobinGroupChatManager._nextIndex). + this._manager = this._managerFactory(this._agents); + await this._manager.RestoreCheckpointAsync(context, cancellationToken).ConfigureAwait(false); + + await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs index 8b3d3e4ce8..3bf63b09be 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs @@ -101,6 +101,8 @@ internal static partial class WorkflowsJsonUtilities [JsonSerializable(typeof(MagenticPlanReviewRequest))] [JsonSerializable(typeof(MagenticPlanReviewResponse))] [JsonSerializable(typeof(MagenticTaskState))] + [JsonSerializable(typeof(GroupChatManagerState))] + [JsonSerializable(typeof(RoundRobinGroupChatManagerState))] [JsonSerializable(typeof(ResetChatSignal))] // Event Types diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs index 9dcd928314..d477140373 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs @@ -243,13 +243,36 @@ public class AgentWorkflowBuilderTests Assert.Null(result[0].AuthorName); Assert.Equal(UserInput, result[0].Text); + // The group-chat host broadcasts each new message (initial user input + each speaker's + // response) to every participant except the speaker that produced it. The selected + // speaker therefore sees only what's been broadcast to it since its previous turn. + string[] agentIds = ["agent1", "agent2", "agent3"]; + List[] buffers = new List[NumAgents]; + for (int a = 0; a < NumAgents; a++) + { + buffers[a] = [UserInput]; + } + string[] texts = new string[maxIterations + 1]; texts[0] = UserInput; string expectedTotal = string.Empty; for (int i = 1; i < maxIterations + 1; i++) { - string id = $"agent{((i - 1) % NumAgents) + 1}"; - texts[i] = $"{id}{Double(string.Concat(texts.Take(i)))}"; + int speakerIdx = (i - 1) % NumAgents; + string id = agentIds[speakerIdx]; + string concatReceived = string.Concat(buffers[speakerIdx]); + texts[i] = $"{id}{Double(concatReceived)}"; + buffers[speakerIdx].Clear(); + for (int a = 0; a < NumAgents; a++) + { + if (a == speakerIdx) + { + continue; + } + + buffers[a].Add(texts[i]); + } + Assert.Equal(ChatRole.Assistant, result[i].Role); Assert.Equal(id, result[i].AuthorName); Assert.Equal(texts[i], result[i].Text); @@ -338,4 +361,257 @@ public class AgentWorkflowBuilderTests } } } + + private sealed class RecordingAgent(string name) : AIAgent + { + public List> Invocations { get; } = []; + + public override string Name => name; + + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => new(new RecordingAgentSession()); + + protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => new(new RecordingAgentSession()); + + protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => default; + + protected override Task RunCoreAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Yield(); + + this.Invocations.Add(messages.Select(m => m.Text).ToList()); + + string id = Guid.NewGuid().ToString("N"); + yield return new AgentResponseUpdate(ChatRole.Assistant, name) { AuthorName = name, MessageId = id }; + } + } + + private sealed class RecordingAgentSession() : AgentSession(); + + [Fact] + public async Task BuildGroupChat_BroadcastsDeltaAndTargetsTurnTokenToSpeakerOnlyAsync() + { + var agentA = new RecordingAgent("agentA"); + var agentB = new RecordingAgent("agentB"); + var agentC = new RecordingAgent("agentC"); + + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 4 }) + .AddParticipants(agentA, agentB, agentC) + .Build(); + + const string UserInput = "hello"; + (_, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + + Assert.NotNull(result); + Assert.Equal(5, result.Count); // initial user input + 4 agent turns + Assert.Collection( + result, + m => Assert.Equal(UserInput, m.Text), + m => Assert.Equal("agentA", m.Text), + m => Assert.Equal("agentB", m.Text), + m => Assert.Equal("agentC", m.Text), + m => Assert.Equal("agentA", m.Text)); + + // Each agent's TurnToken fires exactly when it is the selected speaker — invocation counts + // confirm only the chosen participant receives a TurnToken on each round. + Assert.Equal(2, agentA.Invocations.Count); + Assert.Single(agentB.Invocations); + Assert.Single(agentC.Invocations); + + // Turn 1: agentA is the first speaker. Initial broadcast went to every participant, so + // agentA's only buffered message is the user input. + Assert.Equal([UserInput], agentA.Invocations[0]); + + // Turn 2: agentB. It received the initial broadcast (user input) plus turn-1 broadcast of + // agentA's response (agentA itself is excluded as the last speaker). + Assert.Equal([UserInput, "agentA"], agentB.Invocations[0]); + + // Turn 3: agentC. It also received every broadcast so far (it has never been excluded). + Assert.Equal([UserInput, "agentA", "agentB"], agentC.Invocations[0]); + + // Turn 4: agentA again. It was excluded on turn 2's broadcast (its own response), but + // received turn-3 (agentB's response) and turn-4 (agentC's response) deltas. + Assert.Equal(["agentB", "agentC"], agentA.Invocations[1]); + } + + [Fact] + public async Task BuildGroupChat_UpdateHistoryAsync_FiltersBroadcastPayloadAsync() + { + var agentA = new RecordingAgent("agentA"); + var agentB = new RecordingAgent("agentB"); + + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new PrefixingGroupChatManager(agents, "[broadcast] ") { MaximumIterationCount = 2 }) + .AddParticipants(agentA, agentB) + .Build(); + + const string UserInput = "hello"; + await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + + // Turn 1: agentA's buffer contains only the initial broadcast, which UpdateHistoryAsync + // prefixed. + Assert.Equal(["[broadcast] hello"], agentA.Invocations[0]); + + // Turn 2: agentB received both the initial broadcast and agentA's response — both passed + // through UpdateHistoryAsync before being broadcast. + Assert.Equal(["[broadcast] hello", "[broadcast] agentA"], agentB.Invocations[0]); + } + + [Fact] + public async Task BuildGroupChat_CheckpointResumeMidConversation_PreservesIterationCursorAndBroadcastExclusionAsync() + { + const string UserInput = "hello"; + const int MaxIterations = 6; + + // --- Baseline: run the full conversation under checkpointing and capture every checkpoint + // plus the final transcript. The same workflow + agents are reused for the resume, + // because the runner enforces workflow-shape compatibility on ResumeStreamingAsync. --- + BaselineRunResult baseline = await RunGroupChatBaselineAsync(UserInput, MaxIterations); + + // We need at least one mid-conversation checkpoint to resume from. The baseline produces a + // checkpoint per superstep, which for MaxIterations=6 yields many; we pick a checkpoint + // captured roughly midway so the resumed run still has work to do. + Assert.True(baseline.Checkpoints.Count >= 5, + $"expected at least 5 checkpoints in the baseline, got {baseline.Checkpoints.Count}"); + + int midIndex = baseline.Checkpoints.Count / 2; + CheckpointInfo midCheckpoint = baseline.Checkpoints[midIndex]; + + // Snapshot per-agent invocation counts before the resume so we can isolate the invocations + // produced after the checkpoint is restored. + int aPreCount = baseline.AgentA.Invocations.Count; + int bPreCount = baseline.AgentB.Invocations.Count; + int cPreCount = baseline.AgentC.Invocations.Count; + + // --- Resume the same workflow from the mid-conversation checkpoint. --- + List? resumedResult = null; + await using (StreamingRun resumed = await baseline.Environment + .ResumeStreamingAsync(baseline.Workflow, midCheckpoint)) + { + await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false)) + { + if (evt is WorkflowOutputEvent o) + { + resumedResult = o.As>(); + } + else if (evt is WorkflowErrorEvent err) + { + Assert.Fail($"Resumed workflow failed: {err.Exception}"); + } + } + } + + // (1) Iteration-count continuity: the resumed run terminates with exactly the same number + // of turns the baseline produced — proves IterationCount was rehydrated and the manager + // honored MaximumIterationCount across the boundary. + Assert.NotNull(resumedResult); + Assert.Equal(baseline.Result.Count, resumedResult!.Count); + + // (2) Next-speaker consistency: the full transcript (initial input + every speaker's turn, + // in order) matches the baseline — proves the round-robin cursor was restored. + List baselineTranscript = [.. baseline.Result.Select(m => m.Text)]; + List resumedTranscript = [.. resumedResult.Select(m => m.Text)]; + Assert.Equal(baselineTranscript, resumedTranscript); + + // (3) Broadcast exclusion holds across resume: a RecordingAgent's response text is just its + // own Name. Examine only the invocations recorded after the resume. If the host failed + // to exclude the current speaker from its post-resume broadcasts, an agent's next + // invocation buffer would contain its own previously produced response. Asserting that + // no post-resume invocation input contains the invoking agent's own name proves the + // exclusion was preserved through checkpoint+restore. + AssertPostResumeBroadcastExclusion(baseline.AgentA, aPreCount); + AssertPostResumeBroadcastExclusion(baseline.AgentB, bPreCount); + AssertPostResumeBroadcastExclusion(baseline.AgentC, cPreCount); + + // Sanity: at least one agent was actually invoked after the resume; otherwise the test + // would trivially pass even if the host stopped scheduling turns after restore. + int totalPost = baseline.AgentA.Invocations.Count - aPreCount + + (baseline.AgentB.Invocations.Count - bPreCount) + + (baseline.AgentC.Invocations.Count - cPreCount); + Assert.True(totalPost > 0, "at least one agent should be invoked after resuming from the mid-conversation checkpoint"); + + static void AssertPostResumeBroadcastExclusion(RecordingAgent agent, int preCount) + { + for (int i = preCount; i < agent.Invocations.Count; i++) + { + Assert.DoesNotContain(agent.Name, agent.Invocations[i]); + } + } + } + + private sealed record BaselineRunResult( + Workflow Workflow, + InProcessExecutionEnvironment Environment, + RecordingAgent AgentA, + RecordingAgent AgentB, + RecordingAgent AgentC, + List Result, + List Checkpoints, + CheckpointManager CheckpointManager); + + private static async Task RunGroupChatBaselineAsync(string userInput, int maxIterations) + { + var agentA = new RecordingAgent("agentA"); + var agentB = new RecordingAgent("agentB"); + var agentC = new RecordingAgent("agentC"); + + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxIterations }) + .AddParticipants(agentA, agentB, agentC) + .Build(); + + CheckpointManager checkpointMgr = CheckpointManager.CreateInMemory(); + InProcessExecutionEnvironment env = ExecutionEnvironment.InProcess_Lockstep + .ToWorkflowExecutionEnvironment() + .WithCheckpointing(checkpointMgr); + + List checkpoints = []; + List? finalResult = null; + + await using (StreamingRun run = await env.OpenStreamingAsync(workflow)) + { + await run.TrySendMessageAsync(new List { new(ChatRole.User, userInput) }); + await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false)) + { + switch (evt) + { + case SuperStepCompletedEvent step when step.CompletionInfo?.Checkpoint is { } cp: + checkpoints.Add(cp); + break; + case WorkflowOutputEvent o: + finalResult = o.As>(); + break; + case WorkflowErrorEvent err: + Assert.Fail($"Baseline workflow failed: {err.Exception}"); + break; + } + } + } + + Assert.NotNull(finalResult); + return new BaselineRunResult(workflow, env, agentA, agentB, agentC, finalResult!, checkpoints, checkpointMgr); + } + + private sealed class PrefixingGroupChatManager(IReadOnlyList agents, string prefix) : RoundRobinGroupChatManager(agents) + { + protected internal override ValueTask> UpdateHistoryAsync( + IReadOnlyList history, + CancellationToken cancellationToken = default) + { + IEnumerable prefixed = + history.Select(m => new ChatMessage(m.Role, $"{prefix}{m.Text}") { AuthorName = m.AuthorName }); + + return new(prefixed); + } + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatOrchestrationTests.cs new file mode 100644 index 0000000000..dbbcc1e44d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatOrchestrationTests.cs @@ -0,0 +1,479 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Agents.AI.Workflows.InProc; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +/// +/// Orchestration-level tests for covering +/// and behavior across +/// real participants. These tests parallel the equivalents in +/// to ensure that the broadcast-based group chat host +/// (each participant maintains its own per-agent session via ; +/// only the speaker receives a ; messages are broadcast to every other +/// participant) preserves the same HITL semantics as the handoff path. +/// +public class GroupChatOrchestrationTests +{ + /// + /// End-to-end tool-approval checkpoint/resume scenario through a + /// with a single participant. Mirrors the maximal repro added in PR #5952 (Track A2 in + /// docs/working/issue-5350-root-cause-validation-plan.md): a + /// over a mock chat client emits a for an + /// , the runtime surfaces a + /// as an external , the test + /// checkpoints while the request is pending, resumes from a fresh handle, asserts that the + /// resumed TARC.ToolCall is still a , sends an + /// approval response, and verifies that the wrapped is invoked + /// exactly once and the workflow completes without errors. + /// + [Fact] + public async Task GroupChat_ToolApproval_JsonCheckpointResume_PreservesFunctionCallContentAndInvokesToolAsync() + { + ApprovalHarness harness = new(); + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 4 }) + .AddParticipants(harness.Agent) + .Build(); + + await RunCheckpointedApprovalRoundTripAsync( + workflow, + harness, + CheckpointManager.CreateJson(new InMemoryJsonStore()), + scenarioName: "GroupChat (round-robin, single participant)"); + } + + /// + /// Round-robin group chat with two participants. The first participant exposes an + /// and emits a for it on + /// its first turn. The test denies the approval and asserts that the conversation continues: + /// the first agent runs once more (the FICC denial branch produces a final assistant message), + /// then the host broadcasts that message and selects the second agent, which produces its own + /// reply. This mirrors Handoffs_TwoTransfers_SecondAgentUserApproval_ResponseServedByThirdAgentAsync + /// but on the group-chat path. + /// + [Fact] + public async Task GroupChat_ToolApproval_DeniedResponse_ConversationContinuesAsync() + { + int approvalToolCallCount = 0; + + const string ApprovalCallId = "approve_call_1"; + const string ApprovalToolName = "DoSomethingPrivileged"; + + AIFunction approvalTool = new ApprovalRequiredAIFunction(AIFunctionFactory.Create( + () => + { + Interlocked.Increment(ref approvalToolCallCount); + return "tool result"; + }, + name: ApprovalToolName, + description: "Performs a privileged action")); + + int agent1CallCount = 0; + var agent1 = new ChatClientAgent( + new MockChatClient((messages, options) => + { + int call = Interlocked.Increment(ref agent1CallCount); + return call switch + { + 1 => new ChatResponse(new ChatMessage(ChatRole.Assistant, + [new FunctionCallContent(ApprovalCallId, ApprovalToolName)])), + _ => new ChatResponse(new ChatMessage(ChatRole.Assistant, "agent1 final response")), + }; + }), + instructions: "You are agent1.", + name: "agent1", + tools: [approvalTool]); + + int agent2CallCount = 0; + var agent2 = new ChatClientAgent( + new MockChatClient((messages, options) => + { + Interlocked.Increment(ref agent2CallCount); + return new ChatResponse(new ChatMessage(ChatRole.Assistant, "agent2 reply")); + }), + instructions: "You are agent2.", + name: "agent2"); + + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) + .AddParticipants(agent1, agent2) + .Build(); + + CheckpointManager checkpointManager = CheckpointManager.CreateInMemory(); + InProcessExecutionEnvironment env = InProcessExecution.OffThread.WithCheckpointing(checkpointManager); + + ExternalRequest? pendingRequest = null; + CheckpointInfo? lastCheckpoint = null; + List firstRunEvents = []; + + await using (StreamingRun firstRun = await env.RunStreamingAsync(workflow, new List { new(ChatRole.User, "hello") })) + { + (await firstRun.TrySendMessageAsync(new TurnToken(emitEvents: false))) + .Should().BeTrue(); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in firstRun.WatchStreamAsync(blockOnPendingRequest: false, cts.Token)) + { + firstRunEvents.Add(evt); + if (evt is RequestInfoEvent requestInfo) + { + pendingRequest ??= requestInfo.Request; + } + if (evt is SuperStepCompletedEvent step && step.CompletionInfo?.Checkpoint is { } cp) + { + lastCheckpoint = cp; + } + } + } + + pendingRequest.Should().NotBeNull("agent1 should have surfaced an approval request for the privileged tool"); + firstRunEvents.OfType().Should().BeEmpty(); + firstRunEvents.OfType().Should().BeEmpty(); + approvalToolCallCount.Should().Be(0, "the tool must not be invoked before approval is granted"); + + ToolApprovalRequestContent approvalRequest = + pendingRequest!.Data.As().Should().NotBeNull() + .And.Subject.As(); + approvalRequest.ToolCall.Should().BeOfType(); + ((FunctionCallContent)approvalRequest.ToolCall).Name.Should().Be(ApprovalToolName); + + // Deny the request and continue the conversation. + ExternalResponse denial = pendingRequest.CreateResponse(approvalRequest.CreateResponse(approved: false, reason: "Denied")); + + List secondRunEvents = []; + List? finalOutput = null; + await using (StreamingRun resumed = await env.ResumeStreamingAsync(workflow, lastCheckpoint!)) + { + await resumed.SendResponseAsync(denial); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts.Token)) + { + secondRunEvents.Add(evt); + if (evt is WorkflowOutputEvent outputEvt) + { + finalOutput = outputEvt.As>(); + } + } + } + + secondRunEvents.OfType().Should().BeEmpty( + "denying the approval should not surface any workflow errors"); + secondRunEvents.OfType().Should().BeEmpty( + "denying the approval should not raise executor failures (regression guard for the GroupChat duplicate-key bug pinned in PR #5952's A2 test before the broadcast refactor)"); + + approvalToolCallCount.Should().Be(0, "the tool must not be invoked after denial"); + agent1CallCount.Should().BeGreaterThanOrEqualTo(2, "agent1 should be re-invoked by FICC after the denial to produce a final assistant message"); + agent2CallCount.Should().Be(1, "agent2 should be the next round-robin speaker and produce its own reply"); + + finalOutput.Should().NotBeNull(); + finalOutput!.Should().Contain(m => m.AuthorName == "agent1"); + finalOutput.Should().Contain(m => m.AuthorName == "agent2" && m.Text == "agent2 reply"); + } + + /// + /// Round-robin group chat with two participants. The first participant declares a + /// non-invokable function via AIFunctionFactory.CreateDeclaration, + /// causing the function call to be surfaced as an external + /// (). The test responds with a + /// and asserts that the conversation continues: the first agent's second invocation produces a + /// final assistant message, then the group chat advances to the second agent which produces + /// its own reply. This mirrors Handoffs_TwoTransfers_SecondAgentToolCall_ResponseServedByThirdAgentAsync + /// but on the group-chat path. + /// + [Fact] + public async Task GroupChat_FunctionCall_ExternallyResolved_ConversationContinuesAsync() + { + const string FunctionCallId = "fcc_call_1"; + const string FunctionName = "FetchExternalData"; + + JsonElement schema = AIFunctionFactory.Create(() => true).JsonSchema; + AIFunctionDeclaration declaration = AIFunctionFactory.CreateDeclaration(FunctionName, "Fetches external data", schema); + + int agent1CallCount = 0; + var agent1 = new ChatClientAgent( + new MockChatClient((messages, options) => + { + int call = Interlocked.Increment(ref agent1CallCount); + return call switch + { + 1 => new ChatResponse(new ChatMessage(ChatRole.Assistant, + [new FunctionCallContent(FunctionCallId, FunctionName)])), + _ => new ChatResponse(new ChatMessage(ChatRole.Assistant, "agent1 final response")), + }; + }), + instructions: "You are agent1.", + name: "agent1", + tools: [declaration]); + + int agent2CallCount = 0; + var agent2 = new ChatClientAgent( + new MockChatClient((messages, options) => + { + Interlocked.Increment(ref agent2CallCount); + return new ChatResponse(new ChatMessage(ChatRole.Assistant, "agent2 reply")); + }), + instructions: "You are agent2.", + name: "agent2"); + + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) + .AddParticipants(agent1, agent2) + .Build(); + + CheckpointManager checkpointManager = CheckpointManager.CreateInMemory(); + InProcessExecutionEnvironment env = InProcessExecution.OffThread.WithCheckpointing(checkpointManager); + + ExternalRequest? pendingRequest = null; + CheckpointInfo? lastCheckpoint = null; + + await using (StreamingRun firstRun = await env.RunStreamingAsync(workflow, new List { new(ChatRole.User, "hello") })) + { + (await firstRun.TrySendMessageAsync(new TurnToken(emitEvents: false))) + .Should().BeTrue(); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in firstRun.WatchStreamAsync(blockOnPendingRequest: false, cts.Token)) + { + if (evt is RequestInfoEvent requestInfo) + { + pendingRequest ??= requestInfo.Request; + } + if (evt is SuperStepCompletedEvent step && step.CompletionInfo?.Checkpoint is { } cp) + { + lastCheckpoint = cp; + } + } + } + + pendingRequest.Should().NotBeNull("agent1 should have surfaced a FunctionCallContent for the declaration-only tool"); + + FunctionCallContent functionCall = + pendingRequest!.Data.As().Should().NotBeNull() + .And.Subject.As(); + functionCall.Name.Should().Be(FunctionName); + functionCall.CallId.Should().EndWith(FunctionCallId, + "the workflow rewrites the CallId with an executor-scoped prefix, but should preserve the original tail"); + + // Respond with a function result and let the conversation continue. + ExternalResponse response = pendingRequest.CreateResponse(new FunctionResultContent(functionCall.CallId, "external-data-payload")); + + List resumeEvents = []; + List? finalOutput = null; + await using (StreamingRun resumed = await env.ResumeStreamingAsync(workflow, lastCheckpoint!)) + { + await resumed.SendResponseAsync(response); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts.Token)) + { + resumeEvents.Add(evt); + if (evt is WorkflowOutputEvent outputEvt) + { + finalOutput = outputEvt.As>(); + } + } + } + + resumeEvents.OfType().Should().BeEmpty(); + resumeEvents.OfType().Should().BeEmpty(); + + agent1CallCount.Should().BeGreaterThanOrEqualTo(2, "agent1 should be re-invoked once the externally-resolved function result is delivered"); + agent2CallCount.Should().Be(1, "agent2 should be the next round-robin speaker after agent1 finishes"); + + finalOutput.Should().NotBeNull(); + finalOutput!.Should().Contain(m => m.AuthorName == "agent1"); + finalOutput.Should().Contain(m => m.AuthorName == "agent2" && m.Text == "agent2 reply"); + } + + /// + /// Shared end-to-end driver for the approval checkpoint/resume scenario; modelled on the + /// RunReproAsync helper from PR #5952. Runs the workflow until an approval request is + /// pending, captures the latest checkpoint, disposes the run, resumes from a fresh handle, + /// asserts the resumed payload still carries a , sends an + /// approval response, and asserts the wrapped tool is invoked exactly once and the workflow + /// finishes without errors. + /// + private static async Task RunCheckpointedApprovalRoundTripAsync( + Workflow workflow, + ApprovalHarness harness, + CheckpointManager checkpointManager, + string scenarioName) + { + InProcessExecutionEnvironment env = InProcessExecution.OffThread; + List inputMessages = [new(ChatRole.User, "What's the weather in Amsterdam?")]; + + ExternalRequest? firstRunRequest = null; + CheckpointInfo? checkpoint = null; + + await using (StreamingRun firstRun = await env.WithCheckpointing(checkpointManager) + .RunStreamingAsync(workflow, inputMessages)) + { + (await firstRun.TrySendMessageAsync(new TurnToken(emitEvents: false))) + .Should().BeTrue($"[{scenarioName}] the workflow should accept a TurnToken"); + + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in firstRun.WatchStreamAsync(blockOnPendingRequest: false, cts.Token)) + { + if (evt is RequestInfoEvent requestInfo) + { + firstRunRequest ??= requestInfo.Request; + } + if (evt is SuperStepCompletedEvent step && step.CompletionInfo?.Checkpoint is { } cp) + { + checkpoint = cp; + } + } + } + + firstRunRequest.Should().NotBeNull( + $"[{scenarioName}] the ChatClientAgent + FICC pipeline should surface the approval request as a workflow RequestInfoEvent"); + checkpoint.Should().NotBeNull( + $"[{scenarioName}] a checkpoint should have been produced while the approval request was pending"); + harness.ChatCallCount.Should().Be(1, $"[{scenarioName}] the mock chat client should have been called exactly once before approval was requested"); + harness.InvocationCount.Should().Be(0, $"[{scenarioName}] the underlying tool must NOT have been invoked before approval was granted"); + + ToolApprovalRequestContent? preCheckpoint = firstRunRequest!.Data.As(); + preCheckpoint.Should().NotBeNull($"[{scenarioName}] the pending external request should carry a ToolApprovalRequestContent payload"); + preCheckpoint!.ToolCall.Should().BeOfType( + $"[{scenarioName}] the pre-checkpoint pending request payload must already be a FunctionCallContent"); + + // Resume on a fresh handle and capture the re-emitted approval request. + ExternalRequest? resumedRequest = null; + List postResumeEvents = []; + + await using (StreamingRun resumed = await env.WithCheckpointing(checkpointManager) + .ResumeStreamingAsync(workflow, checkpoint!)) + { + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts.Token)) + { + if (evt is RequestInfoEvent requestInfo) + { + resumedRequest ??= requestInfo.Request; + } + } + + resumedRequest.Should().NotBeNull($"[{scenarioName}] the resumed workflow should re-emit the pending approval RequestInfoEvent"); + + ToolApprovalRequestContent? postResume = resumedRequest!.Data.As(); + postResume.Should().NotBeNull( + $"[{scenarioName}] ExternalRequest.Data.As() should materialize the payload after JSON-checkpoint resume"); + postResume!.ToolCall.Should().NotBeNull($"[{scenarioName}] the resumed TARC must carry its ToolCall"); + postResume.ToolCall.Should().BeOfType( + $"[{scenarioName}] after CheckpointManager.CreateJson round-trip via ResumeStreamingAsync, " + + "ToolApprovalRequestContent.ToolCall must still be a FunctionCallContent so that " + + "FunctionInvokingChatClient's pattern match (`tarc.ToolCall is FunctionCallContent`) continues to fire."); + + ToolApprovalResponseContent approvalResponse = postResume.CreateResponse(approved: true); + await resumed.SendResponseAsync(resumedRequest.CreateResponse(approvalResponse)); + + using CancellationTokenSource cts2 = new(TimeSpan.FromSeconds(30)); + await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts2.Token)) + { + postResumeEvents.Add(evt); + } + } + + harness.InvocationCount.Should().Be(1, + $"[{scenarioName}] approving the request should cause FunctionInvokingChatClient to invoke the wrapped AIFunction exactly once"); + postResumeEvents.OfType().Should().BeEmpty( + $"[{scenarioName}] no workflow errors should be raised when responding to the resumed approval request"); + postResumeEvents.OfType().Should().BeEmpty( + $"[{scenarioName}] no executor failures should be raised when responding to the resumed approval request " + + "(regression guard: pre-broadcast-refactor this test was the `Track A2` repro in PR #5952 which surfaced a " + + "duplicate-key ArgumentException out of FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses)."); + } + + /// + /// Bundles a with a counting + /// tool and a that emits a function call on the first chat turn + /// and a final assistant text on subsequent turns (after FICC has processed the approval + /// and appended a ). + /// + private sealed class ApprovalHarness + { + public const string ToolName = "GetWeather"; + public const string ToolResultText = "Sunny, 22°C"; + public const string ToolCallId = "call-1"; + public const string FinalAssistantText = "The weather in Amsterdam is sunny and 22°C."; + + private int _invocationCount; + private int _chatCallIndex; + + public int InvocationCount => Volatile.Read(ref this._invocationCount); + public int ChatCallCount => Volatile.Read(ref this._chatCallIndex); + + public ChatClientAgent Agent { get; } + + public ApprovalHarness() + { + AIFunction underlyingTool = AIFunctionFactory.Create( + ([Description("City to look up")] string city) => + { + Interlocked.Increment(ref this._invocationCount); + return ToolResultText; + }, + name: ToolName, + description: "Gets the weather for the given city"); + + ApprovalRequiredAIFunction approvalTool = new(underlyingTool); + + MockChatClient mockChatClient = new((messages, options) => + { + int index = Interlocked.Increment(ref this._chatCallIndex) - 1; + return index switch + { + 0 => new ChatResponse(new ChatMessage(ChatRole.Assistant, + [new FunctionCallContent( + callId: ToolCallId, + name: ToolName, + arguments: new Dictionary { ["city"] = "Amsterdam" })])), + _ => new ChatResponse(new ChatMessage(ChatRole.Assistant, FinalAssistantText)), + }; + }); + + this.Agent = new ChatClientAgent( + mockChatClient, + instructions: "You are a weather agent.", + name: "WeatherAgent", + tools: [approvalTool]); + } + } + + /// + /// Minimal stub for orchestration tests; delegates each call to a + /// caller-supplied factory. + /// + private sealed class MockChatClient(Func, ChatOptions?, ChatResponse> responseFactory) : IChatClient + { + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => Task.FromResult(responseFactory(messages, options)); + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ChatResponse response = await this.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + foreach (ChatResponseUpdate update in response.ToChatResponseUpdates()) + { + yield return update; + } + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + public void Dispose() { } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs index 8fed6fca5b..744c9264a2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs @@ -788,6 +788,34 @@ public class JsonSerializationTests result.IsTakingTurn.Should().Be(prototype.IsTakingTurn); } + [Fact] + public void Test_GroupChatManagerState_JsonRoundtrip() + { + // Arrange + GroupChatManagerState prototype = new(IterationCount: 7); + + // Act + GroupChatManagerState result = RunJsonRoundtrip(prototype); + + // Assert + result.Should().Be(prototype); + result.IterationCount.Should().Be(prototype.IterationCount); + } + + [Fact] + public void Test_RoundRobinGroupChatManagerState_JsonRoundtrip() + { + // Arrange + RoundRobinGroupChatManagerState prototype = new(NextIndex: 3); + + // Act + RoundRobinGroupChatManagerState result = RunJsonRoundtrip(prototype); + + // Assert + result.Should().Be(prototype); + result.NextIndex.Should().Be(prototype.NextIndex); + } + /// /// Verifies that the default behavior (without AllowOutOfOrderMetadataProperties) fails /// when $type metadata is not the first property, demonstrating the PostgreSQL jsonb issue. diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoundRobinGroupChatManagerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoundRobinGroupChatManagerTests.cs index 3c87507ec6..125d4f1c55 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoundRobinGroupChatManagerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RoundRobinGroupChatManagerTests.cs @@ -136,4 +136,55 @@ public class RoundRobinGroupChatManagerTests FluentActions.Invoking(() => new RoundRobinGroupChatManager([])) .Should().Throw(); } + + [Fact] + public async Task RoundRobinGroupChat_CheckpointRoundTrip_PreservesIterationCountAndCursorAsync() + { + TestEchoAgent agent1 = new(id: "agent1"); + TestEchoAgent agent2 = new(id: "agent2"); + TestEchoAgent agent3 = new(id: "agent3"); + List agents = [agent1, agent2, agent3]; + List history = []; + + TestRunState sharedState = new(); + TestWorkflowContext sourceContext = new("gcm-host", sharedState); + TestWorkflowContext sinkContext = new("gcm-host", sharedState); + + RoundRobinGroupChatManager source = new(agents); + await source.SelectNextAgentAsync(history); // cursor -> agent2 + source.IterationCount = 7; + + await source.CheckpointAsync(sourceContext); + + RoundRobinGroupChatManager restored = new(agents); + restored.IterationCount.Should().Be(0, "freshly constructed manager has no iteration count"); + + await restored.RestoreCheckpointAsync(sinkContext); + + restored.IterationCount.Should().Be(7, "the base hook must rehydrate IterationCount"); + + AIAgent next = await restored.SelectNextAgentAsync(history); + next.Should().BeSameAs(agent2, "the round-robin cursor should resume where the source left off"); + } + + [Fact] + public async Task RoundRobinGroupChat_RestoreWithoutCheckpoint_DefaultsToZeroStateAsync() + { + TestEchoAgent agent1 = new(id: "agent1"); + TestEchoAgent agent2 = new(id: "agent2"); + List agents = [agent1, agent2]; + List history = []; + + TestWorkflowContext emptyContext = new("gcm-host"); + + RoundRobinGroupChatManager manager = new(agents); + manager.IterationCount = 3; + await manager.SelectNextAgentAsync(history); // cursor advanced + + await manager.RestoreCheckpointAsync(emptyContext); + + manager.IterationCount.Should().Be(0, "restore from an empty checkpoint should clear IterationCount"); + AIAgent next = await manager.SelectNextAgentAsync(history); + next.Should().BeSameAs(agent1, "restore from an empty checkpoint should reset the cursor to the first agent"); + } } From e9a606344adbe5e41d9376b1f8508da593ea6c3b Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Thu, 28 May 2026 12:05:13 -0700 Subject: [PATCH 10/61] Python A2A: Expose `supported_protocol_bindings` as configurable parameter (#6098) * Expose supported_protocol_bindings as configurable parameter on A2AAgent Add supported_protocol_bindings parameter to A2AAgent.__init__() allowing users to configure which A2A protocol bindings (JSONRPC, GRPC, HTTP+JSON) the client prefers when connecting to remote agents. - Defaults to ["JSONRPC"] matching current behavior - Passes through to ClientConfig for transport negotiation - Replaces 4 hardcoded references with the configurable value Closes #6057 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix empty list falsy trap and add fallback path test coverage - Use 'is not None' check instead of 'or' to preserve explicit empty list - Add test verifying empty list is not silently replaced with defaults - Add test verifying fallback path uses custom bindings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document known protocol binding values in docstring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use Literal union for protocol binding type hint Provides IDE autocomplete for known values while keeping the type open for custom bindings (Literal is str at runtime). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../a2a/agent_framework_a2a/_agent.py | 13 ++- python/packages/a2a/tests/test_a2a_agent.py | 89 ++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index 8d6a0496c4..c620176779 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -176,6 +176,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): http_client: httpx.AsyncClient | None = None, auth_interceptor: AuthInterceptor | None = None, timeout: float | httpx.Timeout | None = None, + supported_protocol_bindings: list[Literal["JSONRPC", "GRPC", "HTTP+JSON"] | str] | None = None, **kwargs: Any, ) -> None: """Initialize the A2AAgent. @@ -193,6 +194,9 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): timeout: Request timeout configuration. Can be a float (applied to all timeout components), httpx.Timeout object (for full control), or None (uses 10.0s connect, 60.0s read, 10.0s write, 5.0s pool - optimized for A2A operations). + supported_protocol_bindings: List of protocol bindings to use for transport negotiation. + Known values: "JSONRPC", "GRPC", "HTTP+JSON". Defaults to ["JSONRPC"]. + The A2A spec treats this as an open-form string, so custom bindings are also accepted. kwargs: any additional properties, passed to BaseAgent. """ # Default name/description from agent_card when not explicitly provided @@ -205,6 +209,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): super().__init__(id=id, name=name, description=description, **kwargs) self._http_client: httpx.AsyncClient | None = http_client self._timeout_config = self._create_timeout_config(timeout) + bindings = supported_protocol_bindings if supported_protocol_bindings is not None else ["JSONRPC"] if client is not None: self.client = client self._non_streaming_client: Client | None = None @@ -214,7 +219,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): if url is None: raise ValueError("Either agent_card or url must be provided") # Create minimal agent card from URL - agent_card = minimal_agent_card(url, ["JSONRPC"]) + agent_card = minimal_agent_card(url, bindings) # Create or use provided httpx client if http_client is None: @@ -229,13 +234,13 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): streaming_config = ClientConfig( httpx_client=http_client, streaming=True, - supported_protocol_bindings=["JSONRPC"], + supported_protocol_bindings=bindings, ) # Create non-streaming client (single request/response for stream=False) non_streaming_config = ClientConfig( httpx_client=http_client, streaming=False, - supported_protocol_bindings=["JSONRPC"], + supported_protocol_bindings=bindings, ) streaming_factory = ClientFactory(streaming_config) non_streaming_factory = ClientFactory(non_streaming_config) @@ -256,7 +261,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): "Provide a 'url' argument or ensure 'agent_card.supported_interfaces' " "contains at least one interface with a URL." ) from transport_error - fallback_card = minimal_agent_card(fallback_url, ["JSONRPC"]) + fallback_card = minimal_agent_card(fallback_url, bindings) try: self.client = streaming_factory.create(fallback_card, interceptors=interceptors) # type: ignore self._non_streaming_client = non_streaming_factory.create( diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py index 37c1efedee..0ff5978c87 100644 --- a/python/packages/a2a/tests/test_a2a_agent.py +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -703,7 +703,94 @@ def test_a2a_agent_initialization_with_timeout_parameter() -> None: assert isinstance(timeout_arg, httpx.Timeout) -# region Continuation Token Tests +def test_a2a_agent_initialization_with_supported_protocol_bindings() -> None: + """Test A2AAgent initialization with custom supported_protocol_bindings.""" + with ( + patch("agent_framework_a2a._agent.httpx.AsyncClient") as mock_async_client, + patch("agent_framework_a2a._agent.ClientConfig") as mock_config, + patch("agent_framework_a2a._agent.ClientFactory") as mock_factory, + ): + mock_async_client.return_value = MagicMock() + mock_client_instance = MagicMock() + mock_factory.return_value.create.return_value = mock_client_instance + + A2AAgent( + name="Test Agent", + url="https://test-agent.example.com", + supported_protocol_bindings=["GRPC", "JSONRPC"], + ) + + # Verify ClientConfig was called with our custom bindings for both streaming and non-streaming + assert mock_config.call_count == 2 + for call in mock_config.call_args_list: + assert call.kwargs["supported_protocol_bindings"] == ["GRPC", "JSONRPC"] + + +def test_a2a_agent_initialization_defaults_to_jsonrpc() -> None: + """Test A2AAgent defaults to JSONRPC when supported_protocol_bindings is not provided.""" + with ( + patch("agent_framework_a2a._agent.httpx.AsyncClient") as mock_async_client, + patch("agent_framework_a2a._agent.ClientConfig") as mock_config, + patch("agent_framework_a2a._agent.ClientFactory") as mock_factory, + ): + mock_async_client.return_value = MagicMock() + mock_client_instance = MagicMock() + mock_factory.return_value.create.return_value = mock_client_instance + + A2AAgent(name="Test Agent", url="https://test-agent.example.com") + + # Verify ClientConfig was called with default JSONRPC bindings + assert mock_config.call_count == 2 + for call in mock_config.call_args_list: + assert call.kwargs["supported_protocol_bindings"] == ["JSONRPC"] + + +def test_a2a_agent_initialization_empty_list_preserved() -> None: + """Test that an explicit empty list is preserved and not replaced with defaults.""" + with ( + patch("agent_framework_a2a._agent.httpx.AsyncClient") as mock_async_client, + patch("agent_framework_a2a._agent.ClientConfig") as mock_config, + patch("agent_framework_a2a._agent.ClientFactory") as mock_factory, + ): + mock_async_client.return_value = MagicMock() + mock_client_instance = MagicMock() + mock_factory.return_value.create.return_value = mock_client_instance + + A2AAgent( + name="Test Agent", + url="https://test-agent.example.com", + supported_protocol_bindings=[], + ) + + # Verify ClientConfig was called with the explicit empty list, not the default + assert mock_config.call_count == 2 + for call in mock_config.call_args_list: + assert call.kwargs["supported_protocol_bindings"] == [] + + +def test_a2a_agent_fallback_uses_custom_bindings() -> None: + """Test that transport fallback path uses custom bindings.""" + mock_agent_card = MagicMock() + mock_agent_card.supported_interfaces = [MagicMock(url="https://fallback.example.com")] + + mock_factory = MagicMock() + # First create() call fails (primary streaming), then fallback calls succeed + primary_error = Exception("no compatible transports found") + mock_factory.create.side_effect = [primary_error, MagicMock(), MagicMock()] + + with ( + patch("agent_framework_a2a._agent.ClientFactory", return_value=mock_factory), + patch("agent_framework_a2a._agent.minimal_agent_card") as mock_minimal_card, + patch("agent_framework_a2a._agent.httpx.AsyncClient"), + ): + A2AAgent( + name="test-agent", + agent_card=mock_agent_card, + supported_protocol_bindings=["GRPC", "HTTP+JSON"], + ) + + # Verify minimal_agent_card was called with the custom bindings + mock_minimal_card.assert_called_once_with("https://fallback.example.com", ["GRPC", "HTTP+JSON"]) async def test_working_task_emits_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: From 0578f4c9107159346756d80d0650fa0a21c315a0 Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Thu, 28 May 2026 15:03:46 -0500 Subject: [PATCH 11/61] Backfill chat span request model if it's unknown and response model is avaliable (#6160) --- .../core/agent_framework/observability.py | 18 +++ .../core/tests/core/test_observability.py | 134 ++++++++++++++++++ .../foundry/agent_framework_foundry/_agent.py | 4 +- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 362be2146e..d7734f2457 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -1332,6 +1332,22 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): self.duration_histogram = _get_duration_histogram() self.otel_provider_name = otel_provider_name or getattr(self, "OTEL_PROVIDER_NAME", "unknown") + @staticmethod + def _backfill_request_model(span: trace.Span, attributes: dict[str, Any]) -> None: + """Backfill REQUEST_MODEL and the span name from RESPONSE_MODEL when unknown. + + Chat-completion spans use REQUEST_MODEL as part of the span name. If the + request model was not known at span creation time (e.g. the client could + not resolve it before sending the request), update both the attribute and + the span name to the actual model returned in the response. Mutates + ``attributes`` in place. + """ + response_model = attributes.get(OtelAttr.RESPONSE_MODEL) + if response_model and attributes.get(OtelAttr.REQUEST_MODEL, "unknown") == "unknown": + attributes[OtelAttr.REQUEST_MODEL] = response_model + operation = attributes.get(OtelAttr.OPERATION, "operation") + span.update_name(f"{operation} {response_model}") + @overload def get_response( self, @@ -1480,6 +1496,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): response: ChatResponse[Any] = await result_stream.get_final_response() duration = duration_state.get("duration") response_attributes = _get_response_attributes(attributes, response) + self._backfill_request_model(span, response_attributes) _capture_response( span=span, attributes=response_attributes, @@ -1549,6 +1566,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): raise duration = perf_counter() - start_time_stamp response_attributes = _get_response_attributes(attributes, response) + self._backfill_request_model(span, response_attributes) _capture_response( span=span, attributes=response_attributes, diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index d4403043af..4b48226e63 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -3449,6 +3449,140 @@ def test_capture_response_with_error_type(span_exporter: InMemorySpanExporter): assert spans[0].attributes.get(OtelAttr.ERROR_TYPE) == "ValueError" +def test_backfill_request_model_when_unknown(span_exporter: InMemorySpanExporter): + """_backfill_request_model updates the span name and REQUEST_MODEL attribute when unknown.""" + from agent_framework.observability import OtelAttr, get_tracer + + span_exporter.clear() + tracer = get_tracer() + + attrs: dict[str, Any] = { + OtelAttr.OPERATION: "chat", + OtelAttr.REQUEST_MODEL: "unknown", + OtelAttr.RESPONSE_MODEL: "gpt-4o-mini", + } + + with tracer.start_as_current_span("chat unknown") as span: + ChatTelemetryLayer._backfill_request_model(span, attrs) + + assert attrs[OtelAttr.REQUEST_MODEL] == "gpt-4o-mini" + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == "chat gpt-4o-mini" + + +def test_backfill_request_model_noop_when_request_model_known(span_exporter: InMemorySpanExporter): + """_backfill_request_model leaves a known REQUEST_MODEL and span name untouched.""" + from agent_framework.observability import OtelAttr, get_tracer + + span_exporter.clear() + tracer = get_tracer() + + attrs: dict[str, Any] = { + OtelAttr.OPERATION: "chat", + OtelAttr.REQUEST_MODEL: "gpt-4o", + OtelAttr.RESPONSE_MODEL: "gpt-4o-mini", + } + + with tracer.start_as_current_span("chat gpt-4o") as span: + ChatTelemetryLayer._backfill_request_model(span, attrs) + + assert attrs[OtelAttr.REQUEST_MODEL] == "gpt-4o" + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == "chat gpt-4o" + + +def test_backfill_request_model_noop_when_response_model_missing(span_exporter: InMemorySpanExporter): + """_backfill_request_model is a no-op when no RESPONSE_MODEL is available.""" + from agent_framework.observability import OtelAttr, get_tracer + + span_exporter.clear() + tracer = get_tracer() + + attrs: dict[str, Any] = { + OtelAttr.OPERATION: "chat", + OtelAttr.REQUEST_MODEL: "unknown", + } + + with tracer.start_as_current_span("chat unknown") as span: + ChatTelemetryLayer._backfill_request_model(span, attrs) + + assert attrs[OtelAttr.REQUEST_MODEL] == "unknown" + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == "chat unknown" + + +async def test_chat_client_backfills_request_model_from_response(span_exporter: InMemorySpanExporter): + """Non-streaming chat: when REQUEST_MODEL is unknown, the response model backfills it.""" + + class BackfillingChatClient(ChatTelemetryLayer, BaseChatClient[Any]): + def service_url(self): + return "https://test.example.com" + + def _inner_get_response( + self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any + ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: + async def _get() -> ChatResponse: + return ChatResponse( + messages=[Message("assistant", ["Test response"])], + model="resolved-model", + ) + + return _get() + + client = BackfillingChatClient() + span_exporter.clear() + # Note: no "model" in options, so REQUEST_MODEL starts as "unknown". + await client.get_response(messages=[Message(role="user", contents=["Hi"])], options={}) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "chat resolved-model" + assert span.attributes[OtelAttr.REQUEST_MODEL] == "resolved-model" + assert span.attributes[OtelAttr.RESPONSE_MODEL] == "resolved-model" + + +async def test_chat_client_streaming_backfills_request_model_from_response( + span_exporter: InMemorySpanExporter, +): + """Streaming chat: when REQUEST_MODEL is unknown, the response model backfills it.""" + + class BackfillingStreamingChatClient(ChatTelemetryLayer, BaseChatClient[Any]): + def service_url(self): + return "https://test.example.com" + + def _inner_get_response( + self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any + ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: + async def _stream() -> AsyncIterable[ChatResponseUpdate]: + yield ChatResponseUpdate(contents=[Content.from_text("Hello")], role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(" world")], role="assistant", finish_reason="stop") + + def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse: + response = ChatResponse.from_updates(updates) + response.model = "resolved-stream-model" + return response + + return ResponseStream(_stream(), finalizer=_finalize) + + client = BackfillingStreamingChatClient() + span_exporter.clear() + stream = client.get_response(stream=True, messages=[Message(role="user", contents=["Hi"])], options={}) + async for _ in stream: + pass + await stream.get_final_response() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "chat resolved-stream-model" + assert span.attributes[OtelAttr.REQUEST_MODEL] == "resolved-stream-model" + assert span.attributes[OtelAttr.RESPONSE_MODEL] == "resolved-stream-model" + + def test_configure_otel_providers_with_env_file_path(monkeypatch, tmp_path): """Test configure_otel_providers with env_file_path creates new settings.""" import importlib diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 25b3b388d9..c7f2d03d60 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -507,10 +507,10 @@ class _FoundryAgentChatClient( # type: ignore[misc] .. code-block:: python from agent_framework import Agent - from agent_framework.foundry import FoundryAgentClient + from agent_framework.foundry import FoundryAgent from azure.identity import AzureCliCredential - client = FoundryAgentClient( + client = FoundryAgent( project_endpoint="https://your-project.services.ai.azure.com", agent_name="my-prompt-agent", agent_version="1", From b000a2cf514a025b3cb7873c05c9a282a2c287c5 Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Thu, 28 May 2026 13:09:50 -0700 Subject: [PATCH 12/61] Python: Adding AgentFileStore and FileAccessProvider to support file access operations. (#6099) * Adding AgentFileStore and FileAccessProvider to support file ased operations for agents. * Address PR review feedback on FileAccessProvider - Probe symlinks on the unresolved candidate path so in-root symlinks cannot silently pass and out-of-root symlinks surface the correct error message. - Validate matching_lines elements in FileSearchResult.from_dict and raise a clean ValueError for non-mapping entries. - Cap search regex pattern length (256 chars) via a new _compile_search_regex helper to mitigate ReDoS, and surface the cap in the file_access_search_files tool description. - Skip non-UTF-8 files during filesystem search instead of aborting the entire directory walk. - Replace the module-scope trailing string in the data-processing sample with comments to avoid Ruff B018. - Remove the checked-in working/region_totals.md sample artifact so the save flow works from a clean checkout. - Expand the Windows stdout reconfiguration comment in task_runner.py for clarity. - Add tests for invalid/oversize regex, non-UTF-8 file search, and in-root symlink rejection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix mypy redundant-cast in FileSearchResult.from_dict Use cast(list[object], ...) instead of cast(list[Any], ...) so the cast represents a real type change (lists are invariant) and is no longer flagged by mypy as redundant, while still satisfying pyright's reportUnknownVariableType. Matches the existing pattern in _memory.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten path normalization and directory resolution in FileAccess - _normalize_relative_path now strips surrounding whitespace up front so leading/trailing spaces never leak into file segments, and rejects trailing path separators for file paths so 'foo/' is no longer silently coerced to 'foo'. - FileSystemAgentFileStore._resolve_safe_directory_path normalizes with is_directory=True and maps an empty normalized result to the root. This matches InMemoryAgentFileStore so whitespace-only directory inputs resolve to the root instead of raising. - Added tests for whitespace stripping, trailing-separator rejection, and whitespace-only directory listing on the filesystem store. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden FileAccess search and atomic save in store API - Add wall-clock timeout (10s) around regex scans so a pathological pattern (e.g. `(a+)+`) below the length cap cannot stall the event loop. - Offload the InMemoryAgentFileStore regex scan to a worker thread, matching the filesystem store. - Fail closed when `Path.is_symlink` raises during the safe-path probe so a permission error cannot silently bypass the symlink/reparse-point rejection. - Add `overwrite: bool = True` to `AgentFileStore.write_file`; the in-memory store performs the check under the existing lock and the filesystem store uses `open(mode='x')` so concurrent callers cannot race past `overwrite=False`. - `file_access_save_file` now relies on the atomic store call instead of a separate `file_exists` round-trip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python 3.10 timeout handling and add directory arg to list/search tools - Catch asyncio.TimeoutError in _run_search_with_timeout. In Python 3.10 asyncio.wait_for raises asyncio.exceptions.TimeoutError, which is distinct from the builtin TimeoutError (the two were unified in 3.11). Catching the asyncio alias works on every supported version. - Add an optional directory parameter to file_access_list_files and file_access_search_files so agents can enumerate / scope searches to nested folders, not just the store root. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address FileAccess review feedback: case, errors, signal, TOCTOU - InMemoryAgentFileStore now stores (display_name, content) so list_files and search_files return the original-case names callers wrote, matching the behaviour of FileSystemAgentFileStore on case-preserving filesystems and removing the silent in-memory vs. on-disk contract divergence. - FileSystemAgentFileStore.read_file raises ValueError instead of letting UnicodeDecodeError bubble for binary / non-UTF-8 input, restoring symmetry with search_files (which still skips) and giving the tool layer a recoverable type to translate. - Tool wrappers now catch ValueError and OSError around every operation and surface them as readable strings, so 'you used ..' and 'the file already exists' are both reported to the model the same way instead of the former crashing out as an unhandled exception. - _search_files_sync logs per skipped non-UTF-8 file at WARNING and an aggregate INFO summary so operators can distinguish 'no matches' from 'half the corpus was unreadable'. - FileSystemAgentFileStore softens its docstrings to acknowledge the inherent probe-then-open TOCTOU window. On POSIX both read and write now pass O_NOFOLLOW so the kernel refuses if the leaf segment becomes a symlink between the probe and the open. Windows has no equivalent flag; the limitation is documented. - Tests cover: case preservation on list/search, ValueError on non-UTF-8 read at the store and tool layer, tool-layer string responses for path-traversal and oversized-regex inputs, search-skip log output, symlink rejection on delete/search/list, and symlinked intermediate directory rejection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address FileAccess nit comments: docstrings, enumerate, opt-in delete approval - Expand FileSearchMatch/FileSearchResult.to_dict docstrings to explain why the override is needed (__slots__ defeats the mixin's __dict__ iteration) and why exclude/exclude_none are accepted-but-ignored (mixin signature compatibility for callers like to_json). - Use enumerate(lines, start=1) in _search_file_content so the +1 below is no longer needed; rename loop variable to line_number for clarity. - Add opt-in require_delete_approval: bool = False on FileAccessProvider. When True, file_access_delete_file is registered with approval_mode 'always_require' so the host must approve every delete. Default False preserves current behaviour and matches the .NET reference, but deployments that want a safer-by-default posture can enable it. - Add tests covering both delete approval modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * FileAccess: require delete approval by default Flip the default for FileAccessProvider(require_delete_approval=...) from False to True so destructive deletes are gated by host approval out of the box. Callers that want the previous autonomous behaviour (which matches the .NET reference) can pass require_delete_approval=False. Tests updated accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixing linkinspector by installing Chrome for puppeteer first. --------- Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/markdown-link-check.yml | 8 + python/packages/core/AGENTS.md | 8 + .../packages/core/agent_framework/__init__.py | 18 + .../agent_framework/_harness/_file_access.py | 1018 +++++++++++++++++ .../tests/core/test_harness_file_access.py | 713 ++++++++++++ .../02-agents/context_providers/README.md | 6 + .../file_access_data_processing/README.md | 62 + .../data_processing.py | 145 +++ .../working/sales.csv | 50 + python/scripts/task_runner.py | 14 +- 10 files changed, 2040 insertions(+), 2 deletions(-) create mode 100644 python/packages/core/agent_framework/_harness/_file_access.py create mode 100644 python/packages/core/tests/core/test_harness_file_access.py create mode 100644 python/samples/02-agents/context_providers/file_access_data_processing/README.md create mode 100644 python/samples/02-agents/context_providers/file_access_data_processing/data_processing.py create mode 100644 python/samples/02-agents/context_providers/file_access_data_processing/working/sales.csv diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml index 0e59e4254f..3f6a66fd4d 100644 --- a/.github/workflows/markdown-link-check.yml +++ b/.github/workflows/markdown-link-check.yml @@ -23,6 +23,14 @@ jobs: with: persist-credentials: false + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Chrome for Puppeteer + run: npx puppeteer browsers install chrome + # Checks the status of hyperlinks in all files - name: Run linkspector uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1 diff --git a/python/packages/core/AGENTS.md b/python/packages/core/AGENTS.md index ed47f363a2..ffb6b3e2c5 100644 --- a/python/packages/core/AGENTS.md +++ b/python/packages/core/AGENTS.md @@ -76,6 +76,14 @@ agent_framework/ - **`SkillScriptRunner`** - Protocol for file-based script execution. Any callable matching `(skill, script, args) -> Any` satisfies it. Code-defined scripts do not use a runner. - **`SkillsProvider`** - Context provider (extends `ContextProvider`) that discovers file-based skills from `SKILL.md` files and/or accepts code-defined `Skill` instances. Follows progressive disclosure: advertise → load → read resources / run scripts. +### File Access Harness (`_harness/_file_access.py`) + +- **`AgentFileStore`** - Abstract async store backing the file-access harness. Implementations expose `write_file`, `read_file`, `delete_file`, `list_files`, `file_exists`, `search_files`, and `create_directory` over forward-slash relative paths. +- **`InMemoryAgentFileStore`** - Dict-backed store suitable for tests and lightweight scenarios. +- **`FileSystemAgentFileStore`** - Disk-backed store rooted under a configurable directory. Enforces relative-path normalization, root containment, and rejects symlink/reparse-point segments to prevent escape. +- **`FileSearchResult`** / **`FileSearchMatch`** - `SerializationMixin` DTOs returned by `search_files`, carrying the matching file name, a context snippet, and the matching lines with 1-based line numbers. +- **`FileAccessProvider`** - `ContextProvider` that adds shared file-access tools (`file_access_save_file`, `file_access_read_file`, `file_access_delete_file`, `file_access_list_files`, `file_access_search_files`) plus default usage instructions to each invocation. Unlike `MemoryContextProvider`, the store is intentionally shared across sessions and agents. + ### Workflows (`_workflows/`) - **`Workflow`** - Graph-based workflow definition diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index d199388b17..cc517d993e 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -90,6 +90,16 @@ from ._harness._background_agents import ( BackgroundTaskInfo, BackgroundTaskStatus, ) +from ._harness._file_access import ( + DEFAULT_FILE_ACCESS_INSTRUCTIONS, + DEFAULT_FILE_ACCESS_SOURCE_ID, + AgentFileStore, + FileAccessProvider, + FileSearchMatch, + FileSearchResult, + FileSystemAgentFileStore, + InMemoryAgentFileStore, +) from ._harness._memory import ( DEFAULT_MEMORY_SOURCE_ID, MemoryContextProvider, @@ -309,6 +319,8 @@ __all__ = [ "APP_INFO", "COMPACTION_STATE_KEY", "DEFAULT_BACKGROUND_AGENTS_SOURCE_ID", + "DEFAULT_FILE_ACCESS_INSTRUCTIONS", + "DEFAULT_FILE_ACCESS_SOURCE_ID", "DEFAULT_HARNESS_INSTRUCTIONS", "DEFAULT_MAX_ITERATIONS", "DEFAULT_MEMORY_SOURCE_ID", @@ -334,6 +346,7 @@ __all__ = [ "AgentExecutor", "AgentExecutorRequest", "AgentExecutorResponse", + "AgentFileStore", "AgentFrameworkException", "AgentMiddleware", "AgentMiddlewareLayer", @@ -393,11 +406,15 @@ __all__ = [ "ExperimentalFeature", "FanInEdgeGroup", "FanOutEdgeGroup", + "FileAccessProvider", "FileCheckpointStorage", "FileHistoryProvider", + "FileSearchMatch", + "FileSearchResult", "FileSkill", "FileSkillScript", "FileSkillsSource", + "FileSystemAgentFileStore", "FilteringSkillsSource", "FinalT", "FinishReason", @@ -414,6 +431,7 @@ __all__ = [ "GeneratedEmbeddings", "GraphConnectivityError", "HistoryProvider", + "InMemoryAgentFileStore", "InMemoryCheckpointStorage", "InMemoryHistoryProvider", "InMemorySkillsSource", diff --git a/python/packages/core/agent_framework/_harness/_file_access.py b/python/packages/core/agent_framework/_harness/_file_access.py new file mode 100644 index 0000000000..08632827c9 --- /dev/null +++ b/python/packages/core/agent_framework/_harness/_file_access.py @@ -0,0 +1,1018 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""File-access harness provider exposing CRUD/search tools backed by an ``AgentFileStore``. + +Unlike :class:`~agent_framework.MemoryContextProvider`, which provides +session-scoped memory that may be isolated per session, :class:`FileAccessProvider` +operates on a shared, persistent storage area whose contents are visible across +sessions and agents. The provider exposes five tools — ``file_access_save_file``, +``file_access_read_file``, ``file_access_delete_file``, ``file_access_list_files``, +and ``file_access_search_files`` — by registering them on the per-invocation +:class:`~agent_framework.SessionContext` in :meth:`FileAccessProvider.before_run`. + +The store abstraction is generic so callers can plug in in-memory, local-disk, or +remote-blob backends. Two backends are shipped here: + +* :class:`InMemoryAgentFileStore` — dict-backed, suitable for tests. +* :class:`FileSystemAgentFileStore` — disk-backed, with traversal and symlink + protections. +""" + +from __future__ import annotations + +import asyncio +import errno +import fnmatch +import logging +import os +import re +from abc import ABC, abstractmethod +from collections.abc import Callable, Mapping, MutableMapping +from pathlib import Path +from typing import Any, cast + +from .._feature_stage import ExperimentalFeature, experimental +from .._serialization import SerializationMixin +from .._sessions import AgentSession, ContextProvider, SessionContext +from .._tools import ApprovalMode, tool + +logger = logging.getLogger(__name__) + +DEFAULT_FILE_ACCESS_SOURCE_ID = "file_access" +DEFAULT_FILE_ACCESS_INSTRUCTIONS = ( + "## File Access\n" + "You have access to a shared file storage area via the `file_access_*` tools " + "for reading, writing, and managing files.\n" + "These files persist beyond the current session and may be shared across " + "sessions or agents.\n" + "Use these tools to read input data provided by the user, write output " + "artifacts, and manage any files the user has asked you to work with.\n\n" + "- Never delete or overwrite existing files unless the user has explicitly " + "asked you to do so." +) + +# Maximum number of characters of context to include on either side of the first +# regex match when building a result snippet. +_SEARCH_SNIPPET_RADIUS = 50 + +# Hard cap on the length of a user-supplied search regex. Python's ``re`` module +# has no built-in timeout, so a catastrophic-backtracking pattern (such as +# ``(a+)+$``) submitted by the model could spin the CPU indefinitely. The cap +# alone does not stop short pathological patterns, so :meth:`search_files` +# additionally executes the regex scan in a worker thread and bounds the wall +# clock with :data:`_SEARCH_TIMEOUT_SECONDS`. The thread itself cannot be +# safely interrupted from Python, so a runaway scan continues until the +# regex engine returns, but the caller and event loop stay responsive. +_MAX_SEARCH_PATTERN_LENGTH = 256 +_SEARCH_TIMEOUT_SECONDS = 10.0 + +# Errno raised by POSIX ``open`` when ``O_NOFOLLOW`` was requested and the +# leaf path component is a symbolic link. Used to translate the kernel-level +# refusal into the same :class:`ValueError` the static probe raises so the +# caller can treat the two cases uniformly. +_ELOOP = errno.ELOOP + + +def _compile_search_regex(pattern: str) -> re.Pattern[str]: + """Compile a case-insensitive search regex, enforcing the length cap. + + Raises: + ValueError: When ``pattern`` exceeds ``_MAX_SEARCH_PATTERN_LENGTH`` characters. + re.error: When ``pattern`` is not a valid regular expression. + """ + if len(pattern) > _MAX_SEARCH_PATTERN_LENGTH: + raise ValueError( + f"Regex pattern is too long ({len(pattern)} characters). " + f"Maximum supported length is {_MAX_SEARCH_PATTERN_LENGTH} characters." + ) + return re.compile(pattern, flags=re.IGNORECASE) + + +async def _run_search_with_timeout( + fn: Callable[[], list[FileSearchResult]], +) -> list[FileSearchResult]: + """Run ``fn`` in a worker thread with a bounded wall-clock timeout. + + Raises: + ValueError: When the search does not complete within + :data:`_SEARCH_TIMEOUT_SECONDS` seconds. + """ + try: + return await asyncio.wait_for(asyncio.to_thread(fn), timeout=_SEARCH_TIMEOUT_SECONDS) + except asyncio.TimeoutError as exc: + # On Python 3.10 ``asyncio.wait_for`` raises ``asyncio.TimeoutError`` + # which is distinct from the builtin ``TimeoutError`` (the two were + # unified in 3.11). Catching the asyncio alias works on every + # supported version. + raise ValueError( + f"Regex search did not complete within {_SEARCH_TIMEOUT_SECONDS:g} seconds. " + "Use a more specific pattern (avoid nested quantifiers such as '(a+)+')." + ) from exc + + +def _normalize_relative_path(path: str, *, is_directory: bool = False) -> str: + """Normalize and validate a relative store path. + + Trims surrounding whitespace, replaces backslashes with forward slashes, + collapses repeated separators, and rejects rooted paths, drive letters, and + ``.``/``..`` segments. When ``is_directory`` is True, an empty result is + allowed and represents the root; otherwise an empty result is rejected and + trailing separators are not accepted (so ``"foo/"`` does not silently + become the file path ``"foo"``). + + Args: + path: The relative path to normalize. + + Keyword Args: + is_directory: Whether the path represents a directory (allows empty + results and trailing separators) or a file (rejects empty results + and trailing separators). + + Returns: + The normalized forward-slash relative path. + + Raises: + ValueError: When the path is rooted, starts with a drive letter, contains + ``.``/``..`` segments, is empty for a file path, or ends with a + separator for a file path. + """ + if not path or not path.strip(): + if not is_directory: + raise ValueError("A file path must not be empty or whitespace-only.") + return "" + + # Trim surrounding whitespace so spaces never leak into file segments. + path = path.strip() + converted = path.replace("\\", "/") + + # For file paths reject trailing separators so a directory-shaped string + # such as ``"foo/"`` is never silently treated as the file ``"foo"``. + if not is_directory and converted.endswith("/"): + raise ValueError(f"Invalid path: {path!r}. A file path must not end with a path separator.") + + normalized = converted.strip("/") + + if ( + os.path.isabs(path) + or path.startswith(("/", "\\")) + or (len(normalized) >= 2 and normalized[0].isalpha() and normalized[1] == ":") + ): + raise ValueError( + f"Invalid path: {path!r}. Paths must be relative and must not start with '/', '\\', or a drive root." + ) + + clean_segments: list[str] = [] + for segment in normalized.split("/"): + if not segment: + continue + if segment in (".", ".."): + raise ValueError(f"Invalid path: {path!r}. Paths must not contain '.' or '..' segments.") + clean_segments.append(segment) + + result = "/".join(clean_segments) + if not is_directory and not result: + raise ValueError(f"Invalid path: {path!r}. A file path must not be empty.") + return result + + +def _matches_glob(file_name: str, pattern: str | None) -> bool: + """Return whether ``file_name`` matches the optional glob pattern (case-insensitive). + + When ``pattern`` is ``None`` or blank this returns True so callers can skip + filtering by passing nothing. Matching uses :func:`fnmatch.fnmatchcase` over a + lowercased pattern/name pair to give consistent results across operating + systems (``fnmatch.fnmatch`` is case-sensitive on POSIX but not on Windows). + """ + if pattern is None or not pattern.strip(): + return True + return fnmatch.fnmatchcase(file_name.lower(), pattern.lower()) + + +@experimental(feature_id=ExperimentalFeature.HARNESS) +class FileSearchMatch(SerializationMixin): + """Represent one line within a file that matched a search pattern.""" + + line_number: int + line: str + __slots__ = ("line", "line_number") + + def __init__(self, line_number: int, line: str) -> None: + r"""Initialize one search match. + + Args: + line_number: The 1-based line number where the match was found. + line: The content of the matching line (trailing ``\r`` removed). + """ + if line_number < 1: + raise ValueError("line_number must be a positive integer.") + self.line_number = line_number + self.line = line + + def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]: + """Serialize this match to a JSON-compatible dictionary. + + Overrides :meth:`SerializationMixin.to_dict` because this DTO is + declared with ``__slots__``: the base implementation iterates + ``self.__dict__`` which is empty for slotted classes and would emit + only the auto-injected ``type`` field. The ``exclude`` / + ``exclude_none`` arguments are accepted (and discarded) so the + signature remains drop-in compatible with the mixin — callers like + :meth:`SerializationMixin.to_json` always forward them. + """ + del exclude, exclude_none + return {"line_number": self.line_number, "line": self.line} + + @classmethod + def from_dict( + cls, raw_match: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None + ) -> FileSearchMatch: + """Parse one search match from its dict representation.""" + del dependencies + line_number = raw_match.get("line_number") + line = raw_match.get("line", "") + if not isinstance(line_number, int) or isinstance(line_number, bool): + raise ValueError("FileSearchMatch.line_number must be an integer.") + if not isinstance(line, str): + raise ValueError("FileSearchMatch.line must be a string.") + return cls(line_number=line_number, line=line) + + def __eq__(self, other: object) -> bool: + """Return whether two matches have the same values.""" + return isinstance(other, FileSearchMatch) and self.to_dict() == other.to_dict() + + def __repr__(self) -> str: + """Return a helpful debug representation.""" + return f"FileSearchMatch(line_number={self.line_number!r}, line={self.line!r})" + + +@experimental(feature_id=ExperimentalFeature.HARNESS) +class FileSearchResult(SerializationMixin): + """Represent the search result for one file: the file name, a snippet, and the matching lines.""" + + file_name: str + snippet: str + matching_lines: list[FileSearchMatch] + __slots__ = ("file_name", "matching_lines", "snippet") + + def __init__( + self, + file_name: str, + snippet: str = "", + matching_lines: list[FileSearchMatch] | None = None, + ) -> None: + """Initialize one search result. + + Args: + file_name: The name of the file that matched the search. + snippet: A short context snippet around the first match. + matching_lines: The list of matching lines within the file. + """ + self.file_name = file_name + self.snippet = snippet + self.matching_lines = list(matching_lines) if matching_lines is not None else [] + + def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]: + """Serialize this result to a JSON-compatible dictionary. + + Overrides :meth:`SerializationMixin.to_dict` for the same reason as + :meth:`FileSearchMatch.to_dict`: this DTO uses ``__slots__`` so the + base implementation cannot introspect the payload. The ``exclude`` / + ``exclude_none`` arguments are accepted and ignored to preserve + signature compatibility with the mixin. + """ + del exclude, exclude_none + return { + "file_name": self.file_name, + "snippet": self.snippet, + "matching_lines": [match.to_dict() for match in self.matching_lines], + } + + @classmethod + def from_dict( + cls, raw_result: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None + ) -> FileSearchResult: + """Parse one search result from its dict representation.""" + del dependencies + file_name = raw_result.get("file_name", "") + snippet = raw_result.get("snippet", "") + raw_matching_lines = raw_result.get("matching_lines", []) + if not isinstance(file_name, str): + raise ValueError("FileSearchResult.file_name must be a string.") + if not isinstance(snippet, str): + raise ValueError("FileSearchResult.snippet must be a string.") + if not isinstance(raw_matching_lines, list): + raise ValueError("FileSearchResult.matching_lines must be a list.") + matching_lines: list[FileSearchMatch] = [] + for item in cast(list[object], raw_matching_lines): + if not isinstance(item, Mapping): + raise ValueError("FileSearchResult.matching_lines elements must be mappings.") + matching_lines.append(FileSearchMatch.from_dict(cast(MutableMapping[str, Any], item))) + return cls(file_name=file_name, snippet=snippet, matching_lines=matching_lines) + + def __eq__(self, other: object) -> bool: + """Return whether two results have the same values.""" + return isinstance(other, FileSearchResult) and self.to_dict() == other.to_dict() + + def __repr__(self) -> str: + """Return a helpful debug representation.""" + return ( + "FileSearchResult(" + f"file_name={self.file_name!r}, snippet={self.snippet!r}, matching_lines={self.matching_lines!r})" + ) + + +def _search_file_content(file_name: str, content: str, regex: re.Pattern[str]) -> FileSearchResult | None: + r"""Search one file's content and return a :class:`FileSearchResult` if any lines match. + + Lines are split on ``\n`` (so ``\r`` at the end of each line is stripped on + the matching line itself). A snippet of up to ``±_SEARCH_SNIPPET_RADIUS`` + characters around the first match is included. Returns ``None`` when no + lines match. + """ + lines = content.split("\n") + matching_lines: list[FileSearchMatch] = [] + first_snippet: str | None = None + line_start_offset = 0 + + for line_number, line in enumerate(lines, start=1): + match = regex.search(line) + if match is not None: + matching_lines.append(FileSearchMatch(line_number=line_number, line=line.rstrip("\r"))) + if first_snippet is None: + char_index = line_start_offset + match.start() + snippet_start = max(0, char_index - _SEARCH_SNIPPET_RADIUS) + snippet_end = min(len(content), char_index + (match.end() - match.start()) + _SEARCH_SNIPPET_RADIUS) + first_snippet = content[snippet_start:snippet_end] + # Advance past this line and the implied '\n' separator. + line_start_offset += len(line) + 1 + + if not matching_lines: + return None + return FileSearchResult( + file_name=file_name, + snippet=first_snippet or "", + matching_lines=matching_lines, + ) + + +@experimental(feature_id=ExperimentalFeature.HARNESS) +class AgentFileStore(ABC): + """Abstract base class for file storage operations used by :class:`FileAccessProvider`. + + All paths are relative to an implementation-defined root. Implementations may + map these paths to a local file system, in-memory store, remote blob storage, + or other mechanisms. Paths use forward slashes as separators and must not + escape the root (e.g., via ``..`` segments). Implementations are responsible + for enforcing that invariant. + """ + + @abstractmethod + async def write_file(self, path: str, content: str, *, overwrite: bool = True) -> None: + """Write ``content`` to the file at ``path``. + + Args: + path: The relative path of the file to write. + content: The content to write to the file. + + Keyword Args: + overwrite: When ``True`` (default) any existing file is replaced. + When ``False`` the implementation must perform an atomic + exclusive create and raise :class:`FileExistsError` if a file + already exists at ``path``. + + Raises: + FileExistsError: When ``overwrite`` is ``False`` and a file already + exists at ``path``. + """ + + @abstractmethod + async def read_file(self, path: str) -> str | None: + """Read the content of the file at ``path``. + + Args: + path: The relative path of the file to read. + + Returns: + The file content, or ``None`` if the file does not exist. + """ + + @abstractmethod + async def delete_file(self, path: str) -> bool: + """Delete the file at ``path``. + + Args: + path: The relative path of the file to delete. + + Returns: + ``True`` if the file was deleted; ``False`` if it did not exist. + """ + + @abstractmethod + async def list_files(self, directory: str = "") -> list[str]: + """List the direct child files of ``directory``. + + Args: + directory: The relative directory path to list. Use ``""`` for the root. + + Returns: + The list of file names (not full paths) in the specified directory. + """ + + @abstractmethod + async def file_exists(self, path: str) -> bool: + """Return whether a file exists at ``path``. + + Args: + path: The relative path of the file to check. + """ + + @abstractmethod + async def search_files( + self, + directory: str, + regex_pattern: str, + file_pattern: str | None = None, + ) -> list[FileSearchResult]: + """Search files in ``directory`` for content matching ``regex_pattern``. + + Args: + directory: The relative directory to search. Use ``""`` for the root. + regex_pattern: A regular expression matched against file contents + (case-insensitive). For example, ``"error|warning"`` matches lines + containing ``"error"`` or ``"warning"``. + file_pattern: An optional glob pattern (case-insensitive) used to + filter which files are searched. When ``None`` or blank, every + file in the directory is searched. + + Returns: + The list of files whose content matched, with snippet and matching + line metadata. + """ + + @abstractmethod + async def create_directory(self, path: str) -> None: + """Ensure ``path`` exists as a directory, creating it if necessary.""" + + +@experimental(feature_id=ExperimentalFeature.HARNESS) +class InMemoryAgentFileStore(AgentFileStore): + """An in-memory :class:`AgentFileStore` backed by a dict. + + Suitable for tests and lightweight scenarios where persistence is not + required. Directory concepts are simulated using path prefixes — no explicit + directory structure is maintained. + """ + + def __init__(self) -> None: + """Initialize an empty in-memory file store.""" + # Keys are case-insensitive (normalized + lowercased) so the store + # behaves consistently on case-insensitive deployments. Each entry + # also records the *original* normalized path so ``list_files`` and + # ``search_files`` return display names that match what the caller + # wrote, mirroring how :class:`FileSystemAgentFileStore` preserves the + # on-disk casing. + self._files: dict[str, tuple[str, str]] = {} + self._lock = asyncio.Lock() + + @staticmethod + def _key(path: str) -> str: + return _normalize_relative_path(path).lower() + + async def write_file(self, path: str, content: str, *, overwrite: bool = True) -> None: + """Write ``content`` to the file at ``path``. + + When ``overwrite`` is ``False`` the check-and-write happens under the + store lock so concurrent callers cannot both observe a missing file + and race to create it. + """ + display = _normalize_relative_path(path) + key = display.lower() + async with self._lock: + if not overwrite and key in self._files: + raise FileExistsError(f"File already exists: {path!r}") + self._files[key] = (display, content) + + async def read_file(self, path: str) -> str | None: + """Return the file content, or ``None`` if the file does not exist.""" + key = self._key(path) + async with self._lock: + entry = self._files.get(key) + return entry[1] if entry is not None else None + + async def delete_file(self, path: str) -> bool: + """Delete the file and return whether anything was removed.""" + key = self._key(path) + async with self._lock: + return self._files.pop(key, None) is not None + + async def list_files(self, directory: str = "") -> list[str]: + """Return the direct child files of ``directory``. + + Returns the *original-case* file names that were written, so a caller + that does ``write_file("Plan.MD", ...)`` then ``list_files()`` gets + back ``["Plan.MD"]`` rather than ``["plan.md"]``. This matches the + behaviour of :class:`FileSystemAgentFileStore` on case-preserving + filesystems. + """ + prefix = _normalize_relative_path(directory, is_directory=True).lower() + if prefix and not prefix.endswith("/"): + prefix += "/" + async with self._lock: + entries = [(key, display) for key, (display, _) in self._files.items()] + results: list[str] = [] + for key, display in entries: + if not key.startswith(prefix): + continue + if "/" in key[len(prefix) :]: + continue + # ``display`` is the original-case normalized path; strip the + # directory prefix using the same length we matched on ``key``. + results.append(display[len(prefix) :]) + return results + + async def file_exists(self, path: str) -> bool: + """Return whether the file exists.""" + key = self._key(path) + async with self._lock: + return key in self._files + + async def search_files( + self, + directory: str, + regex_pattern: str, + file_pattern: str | None = None, + ) -> list[FileSearchResult]: + """Search file contents for ``regex_pattern`` matches. + + Snapshots the entries under the store lock and offloads the regex scan + to a worker thread with a bounded timeout so a pathological pattern + cannot stall the event loop. Returned :class:`FileSearchResult` + instances use the *original-case* file names so the result mirrors + what :class:`FileSystemAgentFileStore` would produce. + """ + prefix = _normalize_relative_path(directory, is_directory=True).lower() + if prefix and not prefix.endswith("/"): + prefix += "/" + regex = _compile_search_regex(regex_pattern) + + async with self._lock: + entries = [(key, display, content) for key, (display, content) in self._files.items()] + + def scan() -> list[FileSearchResult]: + results: list[FileSearchResult] = [] + for key, display, file_content in entries: + if not key.startswith(prefix): + continue + relative_key = key[len(prefix) :] + if "/" in relative_key: + continue + relative_display = display[len(prefix) :] + if not _matches_glob(relative_display, file_pattern): + continue + result = _search_file_content(relative_display, file_content, regex) + if result is not None: + results.append(result) + return results + + return await _run_search_with_timeout(scan) + + async def create_directory(self, path: str) -> None: + """No-op: directories are implicit from file paths in the in-memory store.""" + del path + + +@experimental(feature_id=ExperimentalFeature.HARNESS) +class FileSystemAgentFileStore(AgentFileStore): + """A disk-backed :class:`AgentFileStore` rooted under a configurable directory. + + All paths are resolved relative to the root directory provided at + construction time. Lexical path traversal attempts (for example, via ``..`` + segments or absolute paths) are rejected with :class:`ValueError`. The root + directory is created automatically if it does not already exist. + + Symbolic links and reparse points anywhere along the resolved path are + rejected on read, write, delete, list, and existence checks. The check is + a probe followed by an open: on POSIX the open also passes ``O_NOFOLLOW`` + so the kernel refuses if the leaf segment becomes a symlink between the + probe and the open. On Windows the protection is best-effort: it covers + the static case (a symlink already exists when the call is made) but + cannot cover an adversarial caller that swaps a parent directory for a + symlink between the probe and the file open. This store is designed for + single-tenant or co-operating-tenant use; it is not a sandbox against a + hostile process that shares the root directory. + """ + + def __init__(self, root_directory: str | os.PathLike[str]) -> None: + """Initialize the file-system store. + + Args: + root_directory: The directory under which all files are stored. + Created if it does not exist. + """ + raw_root = os.fspath(root_directory) + if not raw_root or not raw_root.strip(): + raise ValueError("root_directory must not be empty or whitespace-only.") + root_path = Path(raw_root).resolve() + root_path.mkdir(parents=True, exist_ok=True) + self._root_path = root_path + + @property + def root_path(self) -> Path: + """Return the resolved root directory.""" + return self._root_path + + def _resolve_safe_path(self, relative_path: str) -> Path: + """Resolve a relative file path safely under the root directory. + + Symbolic links and reparse points are detected on the *unresolved* path + before any call to :meth:`~pathlib.Path.resolve`. ``Path.resolve`` + collapses symbolic links, so probing for them on a resolved path would + either silently follow in-root symlinks or produce a misleading + "escapes the root" error for out-of-root targets. Checking the + unresolved candidate first keeps the rejection deterministic and gives + the caller the specific symlink error. + + The probe is followed by the actual open, so there is an unavoidable + race window in which a concurrent writer on the host can swap an + intermediate path segment for a symlink. The open path mitigates the + common case by passing ``O_NOFOLLOW`` on POSIX so the kernel refuses + if the *leaf* segment becomes a symlink between the probe and the + open. The Windows ``open`` API has no equivalent flag and intermediate + directory swaps cannot be closed from user space on either platform. + """ + normalized = _normalize_relative_path(relative_path) + candidate = self._root_path / normalized + self._throw_if_contains_symlink(candidate) + resolved = candidate.resolve() + try: + resolved.relative_to(self._root_path) + except ValueError as exc: + raise ValueError(f"Invalid path: {relative_path!r}. The resolved path escapes the root directory.") from exc + return resolved + + def _resolve_safe_directory_path(self, relative_directory: str) -> Path: + """Resolve a relative directory path safely under the root directory. + + Empty and whitespace-only inputs both resolve to the root directory, + matching the behavior of ``_normalize_relative_path(..., is_directory=True)`` + and the convention used by :class:`InMemoryAgentFileStore`. + """ + normalized = _normalize_relative_path(relative_directory, is_directory=True) + if not normalized: + return self._root_path + return self._resolve_safe_path(normalized) + + def _throw_if_contains_symlink(self, candidate: Path) -> None: + """Reject any segment between the root and ``candidate`` that is a symlink/reparse point. + + Walks each ancestor down from the root on the *unresolved* candidate so + ``Path.is_symlink`` observes the on-disk entries instead of their + canonical targets. Stops once a segment does not exist on disk so write + scenarios remain allowed. ``Path.is_symlink`` detects both POSIX + symlinks and Windows reparse points (junctions). + """ + try: + relative_parts = candidate.relative_to(self._root_path).parts + except ValueError: + # ``_resolve_safe_path`` already validates containment; an + # unrelated path here would mean we were called with a path that + # never belonged to the root in the first place. + raise ValueError("Invalid path: the resolved path is not under the root directory.") from None + + current = self._root_path + for segment in relative_parts: + current = current / segment + try: + is_link = current.is_symlink() + except OSError as exc: + # Fail closed: if we cannot verify whether a segment is a + # symlink/reparse point we refuse the operation rather than + # silently allow access that may escape the root. + raise ValueError( + f"Invalid path: unable to verify whether '{segment}' is a symbolic link or reparse point." + ) from exc + if is_link: + raise ValueError("Invalid path: the resolved path contains a symbolic link or reparse point.") + if not current.exists(): + break + + async def write_file(self, path: str, content: str, *, overwrite: bool = True) -> None: + """Write ``content`` to the file at ``path``. + + When ``overwrite`` is ``False`` the file is created using + ``O_CREAT | O_EXCL`` so the underlying ``open`` call performs an + atomic exclusive create and raises :class:`FileExistsError` if a file + already exists. On POSIX the open additionally passes ``O_NOFOLLOW`` + so the kernel refuses to overwrite or replace a leaf symlink, closing + the obvious probe-then-open race for the file itself. + """ + full_path = self._resolve_safe_path(path) + await asyncio.to_thread(self._write_file_sync, full_path, content, overwrite) + + @staticmethod + def _write_file_sync(full_path: Path, content: str, overwrite: bool) -> None: + full_path.parent.mkdir(parents=True, exist_ok=True) + encoded = content.encode("utf-8") + flags = os.O_WRONLY | os.O_CREAT + if overwrite: + flags |= os.O_TRUNC + else: + flags |= os.O_EXCL + # ``O_NOFOLLOW`` is POSIX-only; on Windows ``Path.is_symlink`` / + # reparse-point detection in :meth:`_throw_if_contains_symlink` is the + # only line of defence for the leaf segment. + nofollow = getattr(os, "O_NOFOLLOW", 0) + flags |= nofollow + try: + fd = os.open(full_path, flags, 0o644) + except OSError as exc: + if not overwrite and isinstance(exc, FileExistsError): + raise + # ``ELOOP`` (POSIX): the open refused because the leaf is a + # symlink. Surface the same message as the static symlink probe so + # the caller's exception-handling path is uniform. + if nofollow and getattr(exc, "errno", None) == _ELOOP: + raise ValueError("Invalid path: the resolved path contains a symbolic link or reparse point.") from exc + raise + with os.fdopen(fd, "wb") as handle: + handle.write(encoded) + + async def read_file(self, path: str) -> str | None: + """Return the file content, or ``None`` if the file does not exist. + + Raises :class:`ValueError` if the file exists but its bytes are not + valid UTF-8. Tooling that calls this on possibly-binary content should + catch :class:`ValueError` and present the failure to the agent as a + recoverable string response rather than a stack trace. + """ + full_path = self._resolve_safe_path(path) + return await asyncio.to_thread(self._read_file_sync, full_path) + + @staticmethod + def _read_file_sync(full_path: Path) -> str | None: + if not full_path.is_file(): + return None + nofollow = getattr(os, "O_NOFOLLOW", 0) + try: + fd = os.open(full_path, os.O_RDONLY | nofollow) + except OSError as exc: + if nofollow and getattr(exc, "errno", None) == _ELOOP: + raise ValueError("Invalid path: the resolved path contains a symbolic link or reparse point.") from exc + raise + with os.fdopen(fd, "rb") as handle: + raw = handle.read() + try: + return raw.decode("utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"File '{full_path.name}' is not UTF-8 text and cannot be read.") from exc + + async def delete_file(self, path: str) -> bool: + """Delete the file and return whether anything was removed.""" + full_path = self._resolve_safe_path(path) + return await asyncio.to_thread(self._delete_file_sync, full_path) + + @staticmethod + def _delete_file_sync(full_path: Path) -> bool: + if not full_path.is_file(): + return False + full_path.unlink() + return True + + async def list_files(self, directory: str = "") -> list[str]: + """Return the direct child files of ``directory``.""" + full_dir = self._resolve_safe_directory_path(directory) + return await asyncio.to_thread(self._list_files_sync, full_dir) + + @staticmethod + def _list_files_sync(full_dir: Path) -> list[str]: + if not full_dir.is_dir(): + return [] + names: list[str] = [] + for entry in full_dir.iterdir(): + if entry.is_symlink(): + continue + if entry.is_file(): + names.append(entry.name) + return names + + async def file_exists(self, path: str) -> bool: + """Return whether the file exists.""" + full_path = self._resolve_safe_path(path) + return await asyncio.to_thread(self._file_exists_sync, full_path) + + @staticmethod + def _file_exists_sync(full_path: Path) -> bool: + return full_path.is_file() + + async def search_files( + self, + directory: str, + regex_pattern: str, + file_pattern: str | None = None, + ) -> list[FileSearchResult]: + """Search file contents for ``regex_pattern`` matches. + + Files whose bytes are not valid UTF-8 are skipped (so a single binary + file does not abort the whole directory search). Each skip is logged at + ``WARNING`` level and a summary is logged at ``INFO`` so operators can + tell the difference between "no matches" and "the corpus was largely + not searchable". + """ + full_dir = self._resolve_safe_directory_path(directory) + regex = _compile_search_regex(regex_pattern) + return await _run_search_with_timeout(lambda: self._search_files_sync(full_dir, regex, file_pattern)) + + @staticmethod + def _search_files_sync(full_dir: Path, regex: re.Pattern[str], file_pattern: str | None) -> list[FileSearchResult]: + if not full_dir.is_dir(): + return [] + results: list[FileSearchResult] = [] + skipped: list[str] = [] + for entry in full_dir.iterdir(): + if entry.is_symlink() or not entry.is_file(): + continue + file_name = entry.name + if not _matches_glob(file_name, file_pattern): + continue + try: + file_content = entry.read_text(encoding="utf-8") + except UnicodeDecodeError: + # Skip binary or otherwise non-UTF-8 files so a single + # un-decodable entry doesn't abort the whole directory search. + # Log per file so operators can audit which files were skipped. + logger.warning("Skipping non-UTF-8 file during search: %s", entry) + skipped.append(file_name) + continue + result = _search_file_content(file_name, file_content, regex) + if result is not None: + results.append(result) + if skipped: + logger.info( + "Search under %s skipped %d non-UTF-8 file(s) (matched %d).", + full_dir, + len(skipped), + len(results), + ) + return results + + async def create_directory(self, path: str) -> None: + """Ensure the directory at ``path`` exists, creating it if necessary.""" + full_path = self._resolve_safe_directory_path(path) + await asyncio.to_thread(lambda: full_path.mkdir(parents=True, exist_ok=True)) + + +@experimental(feature_id=ExperimentalFeature.HARNESS) +class FileAccessProvider(ContextProvider): + """Context provider that gives an agent CRUD/search access to a shared file store. + + The provider exposes five tools to the agent via the per-invocation + :class:`~agent_framework.SessionContext`: + + - ``file_access_save_file`` — Save a file (refuses to overwrite by default). + - ``file_access_read_file`` — Read the content of a file by name. + - ``file_access_delete_file`` — Delete a file by name. + - ``file_access_list_files`` — List all file names at the store root. + - ``file_access_search_files`` — Search file contents using a case-insensitive + regex, optionally filtered by a glob pattern over file names. + + Unlike :class:`~agent_framework.MemoryContextProvider`, which provides + session-scoped memory that may be isolated per session, + :class:`FileAccessProvider` operates on a shared, persistent store whose + contents are visible across sessions and agents. The store is passed in by + the caller and should already be scoped to the desired folder or storage + location. + """ + + def __init__( + self, + store: AgentFileStore, + *, + source_id: str = DEFAULT_FILE_ACCESS_SOURCE_ID, + instructions: str | None = None, + require_delete_approval: bool = True, + ) -> None: + """Initialize the file access provider. + + Args: + store: The file store implementation used for storage operations. + The store should already be scoped to the desired folder or + storage location. + + Keyword Args: + source_id: Unique source ID for the provider. + instructions: Optional instruction override. When ``None`` the + default file-access instructions are used. + require_delete_approval: When ``True`` (the default) the + ``file_access_delete_file`` tool is registered with + ``approval_mode="always_require"`` so the host must approve every + delete the model proposes. Set to ``False`` to opt out and allow + the agent to delete files autonomously (matching the .NET + ``FileAccessProvider``, which has no approval mechanism). + """ + super().__init__(source_id) + self.store = store + self.instructions = instructions or DEFAULT_FILE_ACCESS_INSTRUCTIONS + self.require_delete_approval = require_delete_approval + + async def before_run( + self, + *, + agent: Any, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Inject file-access tools and instructions before the model runs.""" + del agent, session, state + + @tool(name="file_access_save_file", approval_mode="never_require") + async def file_access_save_file(file_name: str, content: str, overwrite: bool = False) -> str: + """Save a file with the given name and content. By default, does not overwrite an existing file unless overwrite is set to true.""" # noqa: E501 + try: + normalized = _normalize_relative_path(file_name) + await self.store.write_file(normalized, content, overwrite=overwrite) + except FileExistsError: + return f"File '{file_name}' already exists. To replace it, save again with overwrite set to true." + except ValueError as exc: + return f"Could not save file '{file_name}': {exc}" + except OSError as exc: + return f"Could not save file '{file_name}': {exc.strerror or exc}" + return f"File '{file_name}' saved." + + @tool(name="file_access_read_file", approval_mode="never_require") + async def file_access_read_file(file_name: str) -> str: + """Read the content of a file by name. Returns the file content or a message indicating the file could not be read.""" # noqa: E501 + try: + normalized = _normalize_relative_path(file_name) + content = await self.store.read_file(normalized) + except ValueError as exc: + return f"Could not read file '{file_name}': {exc}" + except OSError as exc: + return f"Could not read file '{file_name}': {exc.strerror or exc}" + return content if content is not None else f"File '{file_name}' not found." + + delete_approval_mode: ApprovalMode = "always_require" if self.require_delete_approval else "never_require" + + @tool(name="file_access_delete_file", approval_mode=delete_approval_mode) + async def file_access_delete_file(file_name: str) -> str: + """Delete a file by name.""" + try: + normalized = _normalize_relative_path(file_name) + deleted = await self.store.delete_file(normalized) + except ValueError as exc: + return f"Could not delete file '{file_name}': {exc}" + except OSError as exc: + return f"Could not delete file '{file_name}': {exc.strerror or exc}" + return f"File '{file_name}' deleted." if deleted else f"File '{file_name}' not found." + + @tool(name="file_access_list_files", approval_mode="never_require") + async def file_access_list_files(directory: str | None = None) -> list[str] | str: + """List the direct child file names of a directory. Omit ``directory`` (or pass an empty string) to list the root. To enumerate files in a subdirectory, pass its relative path, for example ``"reports"`` or ``"reports/2024"``.""" # noqa: E501 + target = directory if directory and directory.strip() else "" + try: + return await self.store.list_files(target) + except ValueError as exc: + return f"Could not list directory '{directory or ''}': {exc}" + except OSError as exc: + return f"Could not list directory '{directory or ''}': {exc.strerror or exc}" + + @tool(name="file_access_search_files", approval_mode="never_require") + async def file_access_search_files( + regex_pattern: str, + file_pattern: str | None = None, + directory: str | None = None, + ) -> list[dict[str, Any]] | str: + """Search file contents using a regular expression pattern (case-insensitive). Optionally filter which files to search using a glob pattern (e.g., "*.md", "research*"). Optionally scope the search to a subdirectory by passing its relative path; omit ``directory`` (or pass an empty string) to search the root. Returns matching file names, snippets, and matching lines with line numbers. The regex_pattern must be 256 characters or fewer.""" # noqa: E501 + pattern = file_pattern if file_pattern and file_pattern.strip() else None + target = directory if directory and directory.strip() else "" + try: + results = await self.store.search_files(target, regex_pattern, pattern) + except ValueError as exc: + return f"Could not search files: {exc}" + except OSError as exc: + return f"Could not search files: {exc.strerror or exc}" + return [result.to_dict() for result in results] + + context.extend_instructions(self.source_id, [self.instructions]) + context.extend_tools( + self.source_id, + [ + file_access_save_file, + file_access_read_file, + file_access_delete_file, + file_access_list_files, + file_access_search_files, + ], + ) + + +__all__ = [ + "DEFAULT_FILE_ACCESS_INSTRUCTIONS", + "DEFAULT_FILE_ACCESS_SOURCE_ID", + "AgentFileStore", + "FileAccessProvider", + "FileSearchMatch", + "FileSearchResult", + "FileSystemAgentFileStore", + "InMemoryAgentFileStore", +] diff --git a/python/packages/core/tests/core/test_harness_file_access.py b/python/packages/core/tests/core/test_harness_file_access.py new file mode 100644 index 0000000000..c9599ccbf3 --- /dev/null +++ b/python/packages/core/tests/core/test_harness_file_access.py @@ -0,0 +1,713 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import json +import re +import time +from pathlib import Path + +import pytest + +from agent_framework import ( + Agent, + AgentFileStore, + AgentSession, + ExperimentalFeature, + FileAccessProvider, + FileSearchMatch, + FileSearchResult, + FileSystemAgentFileStore, + InMemoryAgentFileStore, + Message, + SupportsChatGetResponse, +) +from agent_framework._harness import _file_access as _file_access_module +from agent_framework._harness._file_access import ( + DEFAULT_FILE_ACCESS_INSTRUCTIONS, + DEFAULT_FILE_ACCESS_SOURCE_ID, + _matches_glob, + _normalize_relative_path, + _run_search_with_timeout, +) + + +def _tool_by_name(tools: list[object], name: str) -> object: + """Return the tool with the requested name from a prepared tool list.""" + for tool in tools: + if getattr(tool, "name", None) == name: + return tool + raise AssertionError(f"Tool {name!r} was not found.") + + +def test_normalize_relative_path_collapses_and_validates() -> None: + """The path normalizer should accept relative forward/backslash paths and reject unsafe ones.""" + assert _normalize_relative_path("foo/bar.txt") == "foo/bar.txt" + assert _normalize_relative_path("foo\\bar.txt") == "foo/bar.txt" + assert _normalize_relative_path("foo//bar.txt") == "foo/bar.txt" + assert _normalize_relative_path(" foo/bar.txt ") == "foo/bar.txt" + assert _normalize_relative_path("", is_directory=True) == "" + assert _normalize_relative_path(" ", is_directory=True) == "" + assert _normalize_relative_path("sub/", is_directory=True) == "sub" + assert _normalize_relative_path("sub\\", is_directory=True) == "sub" + + with pytest.raises(ValueError, match="must not be empty"): + _normalize_relative_path("") + with pytest.raises(ValueError, match="must not be empty"): + _normalize_relative_path(" ") + with pytest.raises(ValueError, match="must not end with a path separator"): + _normalize_relative_path("foo/") + with pytest.raises(ValueError, match="must not end with a path separator"): + _normalize_relative_path("foo\\") + with pytest.raises(ValueError, match="'..' segments"): + _normalize_relative_path("foo/../bar.txt") + with pytest.raises(ValueError, match="'..' segments"): + _normalize_relative_path("./bar.txt") + with pytest.raises(ValueError, match="must be relative"): + _normalize_relative_path("C:/abs/path") + with pytest.raises(ValueError, match="must be relative"): + _normalize_relative_path("\\rooted") + with pytest.raises(ValueError, match="must be relative"): + _normalize_relative_path("/foo/bar.txt") + + +def test_matches_glob_is_case_insensitive_and_optional() -> None: + """The glob matcher should be case-insensitive and treat missing patterns as match-all.""" + assert _matches_glob("notes.MD", "*.md") + assert _matches_glob("research_one.txt", "research*") + assert not _matches_glob("plan.txt", "*.md") + assert _matches_glob("anything", None) + assert _matches_glob("anything", "") + assert _matches_glob("anything", " ") + + +def test_file_search_match_round_trips() -> None: + """File search match values should serialize and validate cleanly.""" + raw_match = {"line_number": 3, "line": "error: boom"} + + match = FileSearchMatch.from_dict(raw_match) + assert match == FileSearchMatch(line_number=3, line="error: boom") + assert match.to_dict() == raw_match + assert "FileSearchMatch(" in repr(match) + + with pytest.raises(ValueError, match="positive integer"): + FileSearchMatch(line_number=0, line="oops") + with pytest.raises(ValueError, match="must be an integer"): + FileSearchMatch.from_dict({"line_number": "1", "line": "oops"}) + with pytest.raises(ValueError, match="must be a string"): + FileSearchMatch.from_dict({"line_number": 1, "line": 42}) + + +def test_file_search_result_round_trips() -> None: + """File search result values should serialize the matching-line list correctly.""" + raw_result = { + "file_name": "notes.md", + "snippet": "hello error world", + "matching_lines": [{"line_number": 2, "line": "error one"}], + } + + result = FileSearchResult.from_dict(raw_result) + assert result.file_name == "notes.md" + assert result.snippet == "hello error world" + assert result.matching_lines == [FileSearchMatch(line_number=2, line="error one")] + assert result.to_dict() == raw_result + assert json.loads(result.to_json()) == raw_result + + with pytest.raises(ValueError, match="matching_lines must be a list"): + FileSearchResult.from_dict({"file_name": "x", "snippet": "", "matching_lines": {}}) + + with pytest.raises(ValueError, match="elements must be mappings"): + FileSearchResult.from_dict({"file_name": "x", "snippet": "", "matching_lines": ["not-a-dict"]}) + + +async def test_in_memory_store_round_trips_files() -> None: + """The in-memory store should support write/read/exists/delete/list operations.""" + store = InMemoryAgentFileStore() + + await store.write_file("a.txt", "alpha") + await store.write_file("sub/b.txt", "beta") + + assert await store.file_exists("a.txt") + assert not await store.file_exists("missing.txt") + assert await store.read_file("a.txt") == "alpha" + assert await store.read_file("missing.txt") is None + + assert sorted(await store.list_files()) == ["a.txt"] # subdirs are not direct children + assert sorted(await store.list_files("sub")) == ["b.txt"] + + assert await store.delete_file("a.txt") is True + assert await store.delete_file("a.txt") is False + assert sorted(await store.list_files()) == [] + + +async def test_in_memory_store_search_returns_matches_with_snippets() -> None: + """The in-memory store should search file content case-insensitively and respect glob filters.""" + store = InMemoryAgentFileStore() + await store.write_file("a.md", "line one\nThis line has ERROR inside\nline three\r") + await store.write_file("b.md", "no match here") + await store.write_file("notes.txt", "ERROR but wrong extension") + + results = await store.search_files("", "error", "*.md") + assert [result.file_name for result in results] == ["a.md"] + matching_lines = results[0].matching_lines + assert matching_lines == [FileSearchMatch(line_number=2, line="This line has ERROR inside")] + assert "ERROR" in results[0].snippet + + # No glob -> searches every file. + results_all = await store.search_files("", "error") + assert {result.file_name for result in results_all} == {"a.md", "notes.txt"} + + +async def test_in_memory_store_search_rejects_invalid_and_oversize_regex() -> None: + """``search_files`` should surface clean errors for bad regex input.""" + store = InMemoryAgentFileStore() + await store.write_file("a.md", "hello") + + with pytest.raises(re.error): + await store.search_files("", "[unclosed") + + with pytest.raises(ValueError, match="too long"): + await store.search_files("", "a" * 257) + + +async def test_in_memory_store_normalizes_paths() -> None: + """Path normalization should reject traversal in the in-memory store too.""" + store = InMemoryAgentFileStore() + for bad in ("../escape.txt", "/abs/path.txt", "."): + with pytest.raises(ValueError): + await store.write_file(bad, "boom") + + +async def test_filesystem_store_round_trips_files(tmp_path: Path) -> None: + """The filesystem store should round-trip files on disk and create parents on write.""" + store = FileSystemAgentFileStore(tmp_path) + + await store.write_file("nested/a.txt", "alpha") + assert (tmp_path / "nested" / "a.txt").read_text(encoding="utf-8") == "alpha" + + assert await store.read_file("nested/a.txt") == "alpha" + assert await store.read_file("missing.txt") is None + assert await store.file_exists("nested/a.txt") + assert not await store.file_exists("missing.txt") + assert sorted(await store.list_files("nested")) == ["a.txt"] + assert sorted(await store.list_files()) == [] # root only contains the directory + + assert await store.delete_file("nested/a.txt") is True + assert await store.delete_file("nested/a.txt") is False + + +async def test_filesystem_store_rejects_traversal_and_rooted_paths(tmp_path: Path) -> None: + """The filesystem store should refuse paths that escape the configured root.""" + store = FileSystemAgentFileStore(tmp_path) + + for bad in ("../escape.txt", "/etc/passwd", "C:/Windows/System32/notepad.exe", ".", ".."): + with pytest.raises(ValueError): + await store.write_file(bad, "boom") + + +async def test_filesystem_store_rejects_symlinks_into_root(tmp_path: Path) -> None: + """The filesystem store should refuse to read through a symlink target.""" + target = tmp_path / "outside.txt" + target.write_text("outside", encoding="utf-8") + root = tmp_path / "root" + root.mkdir() + link = root / "link.txt" + try: + link.symlink_to(target) + except (OSError, NotImplementedError) as exc: + pytest.skip(f"Symbolic links are not supported in this environment: {exc!r}") + + store = FileSystemAgentFileStore(root) + with pytest.raises(ValueError, match="symbolic link"): + await store.read_file("link.txt") + with pytest.raises(ValueError, match="symbolic link"): + await store.write_file("link.txt", "stomp") + + # List operations should silently skip the symlink entry rather than raise. + assert await store.list_files() == [] + + +async def test_filesystem_store_rejects_in_root_symlinks(tmp_path: Path) -> None: + """Symlinks whose target lives under the root must still be rejected. + + ``Path.resolve`` collapses the symlink, so a naive resolved-path check + would silently follow it. The symlink probe must operate on the + unresolved candidate for this case to fail closed. + """ + root = tmp_path / "root" + root.mkdir() + real = root / "real.txt" + real.write_text("payload", encoding="utf-8") + link = root / "alias.txt" + try: + link.symlink_to(real) + except (OSError, NotImplementedError) as exc: + pytest.skip(f"Symbolic links are not supported in this environment: {exc!r}") + + store = FileSystemAgentFileStore(root) + with pytest.raises(ValueError, match="symbolic link"): + await store.read_file("alias.txt") + # The non-symlinked sibling must still be readable. + assert await store.read_file("real.txt") == "payload" + + +async def test_filesystem_store_search_matches_lines_and_filters_globs(tmp_path: Path) -> None: + """The filesystem store should search files on disk and apply glob filters by file name.""" + store = FileSystemAgentFileStore(tmp_path) + await store.write_file("a.md", "hello\nERROR happens\nbye\r") + await store.write_file("b.txt", "ERROR happens too") + await store.write_file("c.md", "nothing here") + + results = await store.search_files("", "error", "*.md") + assert [result.file_name for result in results] == ["a.md"] + assert results[0].matching_lines == [FileSearchMatch(line_number=2, line="ERROR happens")] + assert "ERROR" in results[0].snippet + + results_all = await store.search_files("", "error") + assert {result.file_name for result in results_all} == {"a.md", "b.txt"} + + +async def test_filesystem_store_search_skips_non_utf8_files(tmp_path: Path) -> None: + """The filesystem store should silently skip non-UTF-8 files instead of aborting the search.""" + store = FileSystemAgentFileStore(tmp_path) + await store.write_file("notes.md", "ERROR happens here") + (tmp_path / "blob.bin").write_bytes(b"\x80\x81\x82\x83") + + results = await store.search_files("", "error") + assert [result.file_name for result in results] == ["notes.md"] + + +async def test_filesystem_store_create_directory(tmp_path: Path) -> None: + """The filesystem store should create directories under the configured root.""" + store = FileSystemAgentFileStore(tmp_path) + await store.create_directory("nested/inner") + assert (tmp_path / "nested" / "inner").is_dir() + + +async def test_filesystem_store_list_files_accepts_blank_directory(tmp_path: Path) -> None: + """Whitespace-only directory inputs should resolve to the root, matching the in-memory store.""" + store = FileSystemAgentFileStore(tmp_path) + await store.write_file("a.txt", "alpha") + assert sorted(await store.list_files("")) == ["a.txt"] + assert sorted(await store.list_files(" ")) == ["a.txt"] + + +def test_filesystem_store_requires_non_empty_root() -> None: + """The filesystem store constructor should refuse blank root paths.""" + with pytest.raises(ValueError, match="must not be empty"): + FileSystemAgentFileStore("") + with pytest.raises(ValueError, match="must not be empty"): + FileSystemAgentFileStore(" ") + + +async def test_file_access_provider_registers_tools_and_instructions( + chat_client_base: SupportsChatGetResponse, +) -> None: + """``FileAccessProvider.before_run`` should add the canonical instructions and five tools.""" + session = AgentSession(session_id="session-1") + store = InMemoryAgentFileStore() + provider = FileAccessProvider(store=store) + agent = Agent(client=chat_client_base, context_providers=[provider]) + + _, options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["work with files"])], + ) + + tools = options["tools"] + assert isinstance(tools, list) + expected_names = { + "file_access_save_file", + "file_access_read_file", + "file_access_delete_file", + "file_access_list_files", + "file_access_search_files", + } + assert {getattr(tool, "name", None) for tool in tools} >= expected_names + + instructions = options.get("instructions") + if isinstance(instructions, str): + assert DEFAULT_FILE_ACCESS_INSTRUCTIONS in instructions + else: + assert any(DEFAULT_FILE_ACCESS_INSTRUCTIONS in chunk for chunk in (instructions or [])) + + +async def test_file_access_provider_delete_approval_defaults_to_always_require( + chat_client_base: SupportsChatGetResponse, +) -> None: + """By default ``file_access_delete_file`` should require host approval.""" + session = AgentSession(session_id="session-1") + provider = FileAccessProvider(store=InMemoryAgentFileStore()) + agent = Agent(client=chat_client_base, context_providers=[provider]) + + _, options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["work with files"])], + ) + + tools = options["tools"] + assert isinstance(tools, list) + delete_file = _tool_by_name(tools, "file_access_delete_file") + assert delete_file.approval_mode == "always_require" + # The non-destructive tools should remain autonomous. + for name in ( + "file_access_save_file", + "file_access_read_file", + "file_access_list_files", + "file_access_search_files", + ): + assert _tool_by_name(tools, name).approval_mode == "never_require" + + +async def test_file_access_provider_delete_approval_opt_out( + chat_client_base: SupportsChatGetResponse, +) -> None: + """``require_delete_approval=False`` should drop delete to ``never_require``.""" + session = AgentSession(session_id="session-1") + provider = FileAccessProvider(store=InMemoryAgentFileStore(), require_delete_approval=False) + agent = Agent(client=chat_client_base, context_providers=[provider]) + + _, options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["work with files"])], + ) + + delete_file = _tool_by_name(options["tools"], "file_access_delete_file") # type: ignore[arg-type] + assert delete_file.approval_mode == "never_require" + + +async def test_file_access_provider_tools_round_trip_files( + chat_client_base: SupportsChatGetResponse, +) -> None: + """The provider's tools should drive save/read/list/search/delete flows on an ``InMemoryAgentFileStore``.""" + session = AgentSession(session_id="session-1") + store = InMemoryAgentFileStore() + provider = FileAccessProvider(store=store) + agent = Agent(client=chat_client_base, context_providers=[provider]) + + _, options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["work with files"])], + ) + tools = options["tools"] + assert isinstance(tools, list) + + save_file = _tool_by_name(tools, "file_access_save_file") + read_file = _tool_by_name(tools, "file_access_read_file") + delete_file = _tool_by_name(tools, "file_access_delete_file") + list_files = _tool_by_name(tools, "file_access_list_files") + search_files = _tool_by_name(tools, "file_access_search_files") + + saved = await save_file.invoke(arguments={"file_name": "plan.md", "content": "step 1\nERROR step 2"}) + assert "plan.md" in saved[0].text and "saved" in saved[0].text + + # Default overwrite=False should refuse the second save. + refused = await save_file.invoke(arguments={"file_name": "plan.md", "content": "stomp"}) + assert "already exists" in refused[0].text + + # overwrite=True should succeed. + overwritten = await save_file.invoke( + arguments={"file_name": "plan.md", "content": "stomp\nERROR replaced", "overwrite": True} + ) + assert "saved" in overwritten[0].text + + read_back = await read_file.invoke(arguments={"file_name": "plan.md"}) + assert read_back[0].text == "stomp\nERROR replaced" + + listed = await list_files.invoke() + assert json.loads(listed[0].text) == ["plan.md"] + + # The list tool should accept an optional directory argument so agents can + # enumerate nested folders (not only the root). + await save_file.invoke(arguments={"file_name": "reports/2024.md", "content": "annual"}) + listed_nested = await list_files.invoke(arguments={"directory": "reports"}) + assert json.loads(listed_nested[0].text) == ["2024.md"] + # Blank / whitespace directory should fall back to the root listing. + listed_blank = await list_files.invoke(arguments={"directory": " "}) + assert sorted(json.loads(listed_blank[0].text)) == ["plan.md"] + + missing = await read_file.invoke(arguments={"file_name": "missing.md"}) + assert "not found" in missing[0].text + + search_payload = await search_files.invoke(arguments={"regex_pattern": "error", "file_pattern": "*.md"}) + parsed = json.loads(search_payload[0].text) + assert parsed[0]["file_name"] == "plan.md" + assert parsed[0]["matching_lines"][0]["line"] == "ERROR replaced" + + # The search tool should likewise accept an optional directory argument so + # agents can scope a search to a subfolder. + await save_file.invoke(arguments={"file_name": "reports/issues.md", "content": "ERROR nested"}) + scoped = await search_files.invoke( + arguments={"regex_pattern": "error", "file_pattern": "*.md", "directory": "reports"} + ) + scoped_parsed = json.loads(scoped[0].text) + assert [entry["file_name"] for entry in scoped_parsed] == ["issues.md"] + + deleted = await delete_file.invoke(arguments={"file_name": "plan.md"}) + assert "deleted" in deleted[0].text + + missing_delete = await delete_file.invoke(arguments={"file_name": "plan.md"}) + assert "not found" in missing_delete[0].text + + +async def test_file_access_provider_accepts_custom_instructions() -> None: + """Custom instructions should override the default banner.""" + store = InMemoryAgentFileStore() + provider = FileAccessProvider(store=store, instructions="custom-banner") + assert provider.instructions == "custom-banner" + assert provider.source_id == DEFAULT_FILE_ACCESS_SOURCE_ID + + +async def test_in_memory_store_write_file_raises_when_exists_and_no_overwrite() -> None: + """The atomic exclusive-create path should raise ``FileExistsError`` under the lock.""" + store = InMemoryAgentFileStore() + await store.write_file("plan.md", "v1") + + with pytest.raises(FileExistsError): + await store.write_file("plan.md", "v2", overwrite=False) + + # The original content is preserved. + assert await store.read_file("plan.md") == "v1" + + # Default ``overwrite=True`` still replaces. + await store.write_file("plan.md", "v3") + assert await store.read_file("plan.md") == "v3" + + +async def test_filesystem_store_write_file_raises_when_exists_and_no_overwrite(tmp_path: Path) -> None: + """The filesystem store should use exclusive-create semantics when ``overwrite=False``.""" + store = FileSystemAgentFileStore(tmp_path) + await store.write_file("plan.md", "v1") + + with pytest.raises(FileExistsError): + await store.write_file("plan.md", "v2", overwrite=False) + + assert (tmp_path / "plan.md").read_text(encoding="utf-8") == "v1" + + await store.write_file("plan.md", "v3", overwrite=True) + assert (tmp_path / "plan.md").read_text(encoding="utf-8") == "v3" + + +async def test_run_search_with_timeout_raises_value_error(monkeypatch: pytest.MonkeyPatch) -> None: + """A scan that exceeds the timeout should surface a clean ``ValueError``.""" + monkeypatch.setattr(_file_access_module, "_SEARCH_TIMEOUT_SECONDS", 0.01) + + def slow() -> list[FileSearchResult]: + time.sleep(0.5) + return [] + + with pytest.raises(ValueError, match="did not complete"): + await _run_search_with_timeout(slow) + + +async def test_filesystem_store_symlink_probe_fails_closed_on_oserror( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """If ``Path.is_symlink`` raises during the probe, the operation must be refused.""" + store = FileSystemAgentFileStore(tmp_path) + await store.write_file("ok.txt", "content") + + def boom(self: Path) -> bool: + raise PermissionError("access denied") + + monkeypatch.setattr(Path, "is_symlink", boom) + + with pytest.raises(ValueError, match="symbolic link or reparse point"): + await store.read_file("ok.txt") + + +def test_file_access_harness_classes_are_marked_experimental() -> None: + """File-access harness public classes should expose HARNESS experimental metadata.""" + assert AgentFileStore.__feature_id__ == ExperimentalFeature.HARNESS.value + assert InMemoryAgentFileStore.__feature_id__ == ExperimentalFeature.HARNESS.value + assert FileSystemAgentFileStore.__feature_id__ == ExperimentalFeature.HARNESS.value + assert FileSearchMatch.__feature_id__ == ExperimentalFeature.HARNESS.value + assert FileSearchResult.__feature_id__ == ExperimentalFeature.HARNESS.value + assert FileAccessProvider.__feature_id__ == ExperimentalFeature.HARNESS.value + assert ".. warning:: Experimental" in (FileAccessProvider.__doc__ or "") + + +async def test_in_memory_store_preserves_original_case_on_list_and_search() -> None: + """``list_files`` / ``search_files`` should return original-case names, not lowercased keys. + + Matches :class:`FileSystemAgentFileStore` on case-preserving filesystems so + tests written against the in-memory backend cannot encode a contract that + will diverge in production. + """ + store = InMemoryAgentFileStore() + await store.write_file("Plan.MD", "ERROR happens here\n") + await store.write_file("Reports/Q1.MD", "alpha") + + # list_files keeps the original case + assert sorted(await store.list_files()) == ["Plan.MD"] + assert sorted(await store.list_files("Reports")) == ["Q1.MD"] + + # case-insensitive directory lookup still works + assert sorted(await store.list_files("reports")) == ["Q1.MD"] + + # search_files emits the original-case file name in FileSearchResult + results = await store.search_files("", "error", "*.MD") + assert [r.file_name for r in results] == ["Plan.MD"] + + # read_file remains case-insensitive + assert await store.read_file("plan.md") == "ERROR happens here\n" + + +async def test_filesystem_store_read_file_raises_value_error_on_non_utf8(tmp_path: Path) -> None: + """Binary / non-UTF-8 files should raise a clean ``ValueError`` rather than ``UnicodeDecodeError``. + + The tool-layer wrapper relies on this contract to convert the failure into + a recoverable string response for the agent. + """ + store = FileSystemAgentFileStore(tmp_path) + (tmp_path / "blob.bin").write_bytes(b"\x80\x81\x82\x83") + + with pytest.raises(ValueError, match="not UTF-8 text"): + await store.read_file("blob.bin") + + +async def test_filesystem_store_search_logs_skipped_non_utf8_files( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """``search_files`` skips non-UTF-8 files but logs per-file and a summary so operators have signal.""" + store = FileSystemAgentFileStore(tmp_path) + await store.write_file("notes.md", "ERROR happens here") + (tmp_path / "blob.bin").write_bytes(b"\x80\x81\x82\x83") + + with caplog.at_level("INFO", logger="agent_framework._harness._file_access"): + results = await store.search_files("", "error") + + assert [r.file_name for r in results] == ["notes.md"] + assert any("Skipping non-UTF-8 file during search" in rec.message for rec in caplog.records) + assert any("skipped 1 non-UTF-8 file" in rec.message for rec in caplog.records) + + +async def test_file_access_tool_wrappers_surface_value_error_as_message( + chat_client_base: SupportsChatGetResponse, +) -> None: + """Recoverable failures (bad path, oversized regex, non-UTF-8 read) should be returned as strings. + + Without these wrappers the model sees a raw stack trace for "you used ``..``" + but a polite message for "the file already exists", which is the opposite + of what is recoverable. + """ + session = AgentSession(session_id="session-1") + store = InMemoryAgentFileStore() + provider = FileAccessProvider(store=store) + agent = Agent(client=chat_client_base, context_providers=[provider]) + + _, options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["work with files"])], + ) + tools = options["tools"] + assert isinstance(tools, list) + + save_file = _tool_by_name(tools, "file_access_save_file") + read_file = _tool_by_name(tools, "file_access_read_file") + delete_file = _tool_by_name(tools, "file_access_delete_file") + list_files = _tool_by_name(tools, "file_access_list_files") + search_files = _tool_by_name(tools, "file_access_search_files") + + # Path-traversal attempts on each tool should return a clean string, not raise. + saved = await save_file.invoke(arguments={"file_name": "../escape.txt", "content": "x"}) + assert "Could not save" in saved[0].text and "escape" in saved[0].text.lower() + read = await read_file.invoke(arguments={"file_name": "../escape.txt"}) + assert "Could not read" in read[0].text + deleted = await delete_file.invoke(arguments={"file_name": "../escape.txt"}) + assert "Could not delete" in deleted[0].text + listed = await list_files.invoke(arguments={"directory": "../escape"}) + assert "Could not list" in listed[0].text + + # Regex length cap should also be returned to the model as text. + too_long = "a" * 1024 + searched = await search_files.invoke(arguments={"regex_pattern": too_long}) + assert "Could not search files" in searched[0].text + + +async def test_file_access_tool_read_file_wrapper_surfaces_non_utf8( + tmp_path: Path, chat_client_base: SupportsChatGetResponse +) -> None: + """The read-file tool wrapper should convert a non-UTF-8 ``ValueError`` into a readable string.""" + store = FileSystemAgentFileStore(tmp_path) + (tmp_path / "blob.bin").write_bytes(b"\x80\x81\x82\x83") + + session = AgentSession(session_id="session-1") + provider = FileAccessProvider(store=store) + agent = Agent(client=chat_client_base, context_providers=[provider]) + + _, options = await agent._prepare_session_and_messages( # type: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["read it"])], + ) + read_file = _tool_by_name(options["tools"], "file_access_read_file") + response = await read_file.invoke(arguments={"file_name": "blob.bin"}) + assert "Could not read" in response[0].text and "UTF-8" in response[0].text + + +_NEEDS_SYMLINK = "Symbolic links are not supported in this environment" + + +async def test_filesystem_store_rejects_symlink_on_delete_search_and_list(tmp_path: Path) -> None: + """The same symlink probe must front delete/search/list, not just read/write.""" + target = tmp_path / "outside.txt" + target.write_text("outside", encoding="utf-8") + root = tmp_path / "root" + root.mkdir() + link = root / "link.txt" + try: + link.symlink_to(target) + except (OSError, NotImplementedError) as exc: + pytest.skip(f"{_NEEDS_SYMLINK}: {exc!r}") + + store = FileSystemAgentFileStore(root) + + with pytest.raises(ValueError, match="symbolic link"): + await store.delete_file("link.txt") + + # search_files of the root never touches the symlink leaf directly, but + # search_files of a symlinked *directory* path must be rejected by the + # safe-directory resolver. + dir_link = root / "alias_dir" + other_dir = tmp_path / "outside_dir" + other_dir.mkdir() + try: + dir_link.symlink_to(other_dir) + except (OSError, NotImplementedError) as exc: + pytest.skip(f"{_NEEDS_SYMLINK}: {exc!r}") + + with pytest.raises(ValueError, match="symbolic link"): + await store.search_files("alias_dir", "anything") + with pytest.raises(ValueError, match="symbolic link"): + await store.list_files("alias_dir") + + +async def test_filesystem_store_rejects_symlinked_intermediate_directory(tmp_path: Path) -> None: + """A symlink used as a non-leaf path segment must still be rejected. + + The classic escape vector is ``root/aliased_dir/file.txt`` where + ``aliased_dir`` is a symlink to somewhere outside the root. The + ``_throw_if_contains_symlink`` walk must check every segment, not only + the leaf. + """ + outside = tmp_path / "outside_dir" + outside.mkdir() + (outside / "secret.txt").write_text("payload", encoding="utf-8") + root = tmp_path / "root" + root.mkdir() + link = root / "aliased_dir" + try: + link.symlink_to(outside) + except (OSError, NotImplementedError) as exc: + pytest.skip(f"{_NEEDS_SYMLINK}: {exc!r}") + + store = FileSystemAgentFileStore(root) + + for op in ("read", "write", "delete"): + with pytest.raises(ValueError, match="symbolic link"): + if op == "read": + await store.read_file("aliased_dir/secret.txt") + elif op == "write": + await store.write_file("aliased_dir/secret.txt", "stomp") + else: + await store.delete_file("aliased_dir/secret.txt") diff --git a/python/samples/02-agents/context_providers/README.md b/python/samples/02-agents/context_providers/README.md index 04f3a1395f..e49a472f39 100644 --- a/python/samples/02-agents/context_providers/README.md +++ b/python/samples/02-agents/context_providers/README.md @@ -8,6 +8,7 @@ These samples demonstrate how to use context providers to enrich agent conversat |---------------|-------------| | [`simple_context_provider.py`](simple_context_provider.py) | Implement a custom context provider by extending `ContextProvider` to extract and inject structured user information across turns. | | [`azure_ai_foundry_memory.py`](azure_ai_foundry_memory.py) | Use `FoundryMemoryProvider` to add semantic memory — automatically retrieves, searches, and stores memories via Azure AI Foundry. | +| [`file_access_data_processing/`](file_access_data_processing/) | Use `FileAccessProvider` with `FileSystemAgentFileStore` to give an agent read/write/search access to a folder of CSV data files. See its own [README](file_access_data_processing/README.md). | | [`azure_ai_search/`](azure_ai_search/) | Retrieval Augmented Generation (RAG) with Azure AI Search in semantic and agentic modes. See its own [README](azure_ai_search/README.md). | | [`mem0/`](mem0/) | Memory-powered context using the Mem0 integration (open-source and managed). See its own [README](mem0/README.md). | | [`redis/`](redis/) | Redis-backed context providers for conversation memory and sessions. See its own [README](redis/README.md). | @@ -25,4 +26,9 @@ These samples demonstrate how to use context providers to enrich agent conversat - `AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME`: Embedding model deployment name (e.g., `text-embedding-ada-002`) - Azure CLI authentication (`az login`) +**For `file_access_data_processing/`:** +- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint +- `FOUNDRY_MODEL`: Chat model deployment name +- Azure CLI authentication (`az login`) + See each subfolder's README for provider-specific prerequisites. diff --git a/python/samples/02-agents/context_providers/file_access_data_processing/README.md b/python/samples/02-agents/context_providers/file_access_data_processing/README.md new file mode 100644 index 0000000000..9984b1f13b --- /dev/null +++ b/python/samples/02-agents/context_providers/file_access_data_processing/README.md @@ -0,0 +1,62 @@ +# File Access Data Processing + +This sample demonstrates how to give an `Agent` access to a folder of data files +by attaching `FileAccessProvider` (backed by `FileSystemAgentFileStore`) as a +context provider. + +The agent is given a `working/` folder containing `sales.csv` — ~50 rows of +sales transaction data — and is driven through a short scripted conversation +that exercises every tool the provider exposes: + +| Step | Prompt | Tool(s) used | +|---|---|---| +| 1 | "What files do you have access to?" | `file_access_list_files` | +| 2 | "Read sales.csv and summarize…" | `file_access_read_file` | +| 3 | "Calculate the total revenue per region…" | (uses previously read data) | +| 4 | "Save a markdown report named `region_totals.md`…" | `file_access_save_file` | +| 5 | "List the files again so I can confirm…" | `file_access_list_files` | + +After the run, the sample prints the final contents of `working/` so the +written file is easy to spot. + +## Prerequisites + +| Variable | Description | +|---|---| +| `FOUNDRY_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint. | +| `FOUNDRY_MODEL` | Chat model deployment name (e.g. `gpt-4o`). | + +Run `az login` before executing the sample so `AzureCliCredential` can +authenticate. + +## Running the sample + +From `python/`: + +```bash +uv run --package agent-framework-core python samples/02-agents/context_providers/file_access_data_processing/data_processing.py +``` + +Or directly: + +```bash +python samples/02-agents/context_providers/file_access_data_processing/data_processing.py +``` + +## Sample data + +`working/sales.csv` contains January–March 2025 sales transactions with these +columns: + +| Column | Description | +|---|---| +| `date` | Transaction date (YYYY-MM-DD) | +| `product` | Product name | +| `category` | Product category (Electronics, Furniture, Stationery) | +| `quantity` | Units sold | +| `unit_price` | Price per unit | +| `region` | Sales region (North, South, West) | +| `salesperson` | Name of the salesperson | + +The sample writes `region_totals.md` into the same folder. Delete it between +runs if you want a clean state. diff --git a/python/samples/02-agents/context_providers/file_access_data_processing/data_processing.py b/python/samples/02-agents/context_providers/file_access_data_processing/data_processing.py new file mode 100644 index 0000000000..0d4e67ff38 --- /dev/null +++ b/python/samples/02-agents/context_providers/file_access_data_processing/data_processing.py @@ -0,0 +1,145 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Sample: use ``FileAccessProvider`` to give an agent access to a folder of CSV data files. + +This sample demonstrates how to attach :class:`FileAccessProvider` (backed by +:class:`FileSystemAgentFileStore`) to an ``Agent`` so the model can read input +data, perform analysis, and write summary output back to the same folder via +the ``file_access_*`` tools. + +The sibling ``working/`` folder contains ``sales.csv`` — ~50 rows of sales +transactions (date, product, category, quantity, unit_price, region, +salesperson). The agent is asked, in a single session, to: list available +files, inspect the data, compute regional totals, and save a markdown summary. + +Prerequisites: + - ``FOUNDRY_PROJECT_ENDPOINT``: Your Azure AI Foundry project endpoint. + - ``FOUNDRY_MODEL``: Chat model deployment name. + - Run ``az login`` before executing the sample. +""" + +import asyncio +import os +from pathlib import Path + +from agent_framework import Agent, FileAccessProvider, FileSystemAgentFileStore +from agent_framework.foundry import FoundryChatClient +from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +# Load python/.env (python-dotenv walks up from this file by default). Pass +# override=True so values from .env take precedence over any pre-existing OS +# environment variables — without this, OS-level values silently win. +load_dotenv(override=True) + +INSTRUCTIONS = """ +You are a data analyst assistant. You have access to a folder of data files via +the file_access_* tools. + +## Getting started +- Start by listing available files with file_access_list_files to see what data + is available. +- Read the files to understand their structure and contents. + +## Working with data +- When asked to analyze data, read the relevant files first, then perform the + analysis. +- Show your analysis clearly with tables, summaries, and key insights. +- When calculations are needed, work through them step by step and show your + reasoning. + +## Writing output +- When asked to produce output files (e.g., reports, summaries, filtered data), + use file_access_save_file to write them. +- Use appropriate file formats: CSV for tabular data, Markdown for reports. +- Confirm what you wrote and where. + +## Important +- Never modify or delete the original input data files unless explicitly asked + to do so. +- If asked about data you haven't read yet, read it first before answering. +- Always explain your reasoning between tool calls so the user can follow along. +""" + +PROMPTS = [ + "What files do you have access to?", + "Read sales.csv and summarize what columns it contains and how many rows it has.", + "Calculate the total revenue (quantity * unit_price) per region and show the result as a table.", + ( + "Save a markdown report named region_totals.md that contains the regional totals " + "and a one-paragraph summary of which region performed best." + ), + "List the files again so I can confirm region_totals.md was created.", +] + + +async def main() -> None: + # 1. Resolve the working directory bundled alongside this script. + working_dir = Path(__file__).parent / "working" + + # 2. Build the chat client. + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], + credential=AzureCliCredential(), + ) + + # 3. Wire up the file access provider against a file-system-backed store + # rooted at the sample's working/ folder. The provider injects its + # default instructions plus exposes five file_access_* tools to the + # agent for the duration of each run. + file_access = FileAccessProvider(store=FileSystemAgentFileStore(working_dir)) + + # 4. Create the agent and attach the provider. + async with Agent( + client=client, + name="DataAnalyst", + description="A data analyst assistant that reads, analyzes, and processes data files.", + instructions=INSTRUCTIONS, + context_providers=[file_access], + ) as agent: + # 5. Run all prompts inside one session so the conversation remains + # coherent across turns. + session = agent.create_session() + for prompt in PROMPTS: + print(f"\nUser: {prompt}") + response = await agent.run(prompt, session=session) + print(f"Assistant: {response}") + + # 6. Show the final folder contents so the side effects of the run are + # visible to the reader. + print("\nFinal contents of working/:") + for path in sorted(working_dir.iterdir()): + print(f" - {path.name} ({path.stat().st_size} bytes)") + + +if __name__ == "__main__": + asyncio.run(main()) + + +# Sample output (truncated): +# +# User: What files do you have access to? +# Assistant: I can see one file in the working directory: sales.csv. +# +# User: Read sales.csv and summarize what columns it contains and how many rows it has. +# Assistant: sales.csv has 50 data rows and 7 columns: date, product, category, +# quantity, unit_price, region, salesperson. +# +# User: Calculate the total revenue (quantity * unit_price) per region and show the result as a table. +# Assistant: +# | Region | Total Revenue | +# |--------|---------------| +# | North | $X,XXX.XX | +# | South | $X,XXX.XX | +# | West | $X,XXX.XX | +# +# User: Save a markdown report named region_totals.md ... +# Assistant: I wrote region_totals.md to the working folder. +# +# User: List the files again so I can confirm region_totals.md was created. +# Assistant: The working folder now contains: region_totals.md, sales.csv. +# +# Final contents of working/: +# - region_totals.md (NNN bytes) +# - sales.csv (3175 bytes) diff --git a/python/samples/02-agents/context_providers/file_access_data_processing/working/sales.csv b/python/samples/02-agents/context_providers/file_access_data_processing/working/sales.csv new file mode 100644 index 0000000000..50a2369942 --- /dev/null +++ b/python/samples/02-agents/context_providers/file_access_data_processing/working/sales.csv @@ -0,0 +1,50 @@ +date,product,category,quantity,unit_price,region,salesperson +2025-01-03,Laptop Pro 15,Electronics,2,1299.99,North,Alice +2025-01-05,Ergonomic Chair,Furniture,5,349.50,South,Bob +2025-01-07,Wireless Mouse,Electronics,12,24.99,North,Alice +2025-01-08,Standing Desk,Furniture,1,599.00,West,Carol +2025-01-10,USB-C Hub,Electronics,8,45.99,North,David +2025-01-12,Monitor 27in,Electronics,3,429.00,South,Bob +2025-01-14,Desk Lamp,Furniture,6,79.95,West,Carol +2025-01-15,Keyboard Mech,Electronics,4,149.99,North,Alice +2025-01-17,Filing Cabinet,Furniture,2,189.00,South,David +2025-01-20,Webcam HD,Electronics,10,89.99,West,Bob +2025-01-22,Laptop Pro 15,Electronics,1,1299.99,South,Carol +2025-01-24,Ergonomic Chair,Furniture,3,349.50,North,Alice +2025-01-25,Notebook Pack,Stationery,20,12.99,South,David +2025-01-27,Wireless Mouse,Electronics,15,24.99,West,Carol +2025-01-28,Whiteboard,Stationery,4,129.00,North,Bob +2025-01-30,Standing Desk,Furniture,2,599.00,South,Alice +2025-02-02,USB-C Hub,Electronics,6,45.99,West,David +2025-02-04,Monitor 27in,Electronics,2,429.00,North,Carol +2025-02-05,Desk Lamp,Furniture,8,79.95,South,Bob +2025-02-07,Keyboard Mech,Electronics,5,149.99,West,Alice +2025-02-09,Filing Cabinet,Furniture,1,189.00,North,David +2025-02-11,Webcam HD,Electronics,7,89.99,South,Carol +2025-02-13,Laptop Pro 15,Electronics,3,1299.99,West,Bob +2025-02-15,Notebook Pack,Stationery,30,12.99,North,Alice +2025-02-17,Ergonomic Chair,Furniture,4,349.50,South,David +2025-02-19,Wireless Mouse,Electronics,20,24.99,North,Carol +2025-02-20,Whiteboard,Stationery,2,129.00,West,Bob +2025-02-22,Standing Desk,Furniture,1,599.00,North,Alice +2025-02-24,USB-C Hub,Electronics,10,45.99,South,David +2025-02-26,Monitor 27in,Electronics,4,429.00,West,Carol +2025-02-28,Desk Lamp,Furniture,3,79.95,North,Bob +2025-03-02,Keyboard Mech,Electronics,6,149.99,South,Alice +2025-03-04,Filing Cabinet,Furniture,3,189.00,West,David +2025-03-06,Webcam HD,Electronics,9,89.99,North,Carol +2025-03-08,Laptop Pro 15,Electronics,2,1299.99,South,Bob +2025-03-10,Notebook Pack,Stationery,25,12.99,West,Alice +2025-03-12,Ergonomic Chair,Furniture,6,349.50,North,David +2025-03-14,Wireless Mouse,Electronics,18,24.99,South,Carol +2025-03-15,Whiteboard,Stationery,5,129.00,North,Bob +2025-03-17,Standing Desk,Furniture,3,599.00,West,Alice +2025-03-19,USB-C Hub,Electronics,7,45.99,North,David +2025-03-21,Monitor 27in,Electronics,5,429.00,South,Carol +2025-03-23,Desk Lamp,Furniture,4,79.95,West,Bob +2025-03-25,Keyboard Mech,Electronics,3,149.99,North,Alice +2025-03-27,Filing Cabinet,Furniture,2,189.00,South,David +2025-03-28,Webcam HD,Electronics,11,89.99,West,Carol +2025-03-29,Laptop Pro 15,Electronics,1,1299.99,North,Bob +2025-03-30,Notebook Pack,Stationery,15,12.99,South,Alice +2025-03-31,Ergonomic Chair,Furniture,2,349.50,West,David diff --git a/python/scripts/task_runner.py b/python/scripts/task_runner.py index 617b4d58b0..8bd038359a 100644 --- a/python/scripts/task_runner.py +++ b/python/scripts/task_runner.py @@ -8,6 +8,7 @@ filters the same way. """ import concurrent.futures +import contextlib import glob import os import subprocess @@ -17,6 +18,16 @@ from collections.abc import Sequence from fnmatch import fnmatch from pathlib import Path +# On Windows, stdout defaults to cp1252 under non-interactive callers (e.g. +# prek / pre-commit hooks). Reconfigure to UTF-8 before importing rich so +# unicode glyphs like ``\u2713`` don't raise ``UnicodeEncodeError``. +if sys.platform == "win32": + for _stream in (sys.stdout, sys.stderr): + reconfigure = getattr(_stream, "reconfigure", None) + if callable(reconfigure): + with contextlib.suppress(OSError, ValueError): + reconfigure(encoding="utf-8") + import tomli from rich import print @@ -122,8 +133,7 @@ def project_filter_matches(project: Path | str, pattern: str, aliases: Sequence[ """ normalized_pattern = normalize_project_filter(pattern).lower() return any( - fnmatch(candidate, normalized_pattern) - for candidate in build_project_filter_candidates(project, aliases) + fnmatch(candidate, normalized_pattern) for candidate in build_project_filter_candidates(project, aliases) ) From 8ed2159c4b0b6663dc92b3469c24648a37e8e289 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 28 May 2026 17:26:31 -0400 Subject: [PATCH 13/61] .NET: Workflow Outputs Overhaul: Support Tagging, Filtering Agent Outputs (#6045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: reshuffle .NET Workflow tests in preparation for Outputs overhaul Phase 1 of the .NET Workflows outputs overhaul (see working/implementation-plan.md). Pure moves/renames in dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests; no production code changes, no new test cases. The split keeps each orchestration mode in its own source file so the upcoming tag-aware and orchestration-default test additions land on clean diffs. Renames: * WorkflowBuilderSmokeTests.cs -> WorkflowBuilderTests.cs (with class rename to match). The scope is no longer "smoke"-only once subsequent phases add tag-aware builder tests. * InputWaiterAndOutputFilterTests.cs -> InputWaiterTests.cs + OutputFilterTests.cs. The file already declared the two test classes separately; this split simply gives each its own file so the output-filter cases have a dedicated home for tag-aware additions. Split of AgentWorkflowBuilderTests.cs: * AgentWorkflowBuilderTests.cs is now the outer `public static partial class AgentWorkflowBuilderTests` holding the shared test helpers (DoubleEchoAgent + session + WithBarrier variant, WorkflowRunResult, RunWorkflow* methods) bumped from `private` to `internal` so the new top-level GroupChatWorkflowBuilderTests in the same assembly can reach them. * AgentWorkflowBuilder.SequentialTests.cs (nested SequentialTests): BuildSequential_InvalidArguments_Throws, BuildSequential_AgentsRunInOrderAsync. * AgentWorkflowBuilder.ConcurrentTests.cs (nested ConcurrentTests): BuildConcurrent_InvalidArguments_Throws, BuildConcurrent_AgentsRunInParallelAsync. Sequential and Concurrent are kept as nested classes because they're modes of the same `AgentWorkflowBuilder` static factory and do not produce dedicated builder types. New file: * GroupChatWorkflowBuilderTests.cs (top-level): the existing BuildGroupChat_* and GroupChatManager_* cases moved out of the old AgentWorkflowBuilderTests file. They exercise the `GroupChatWorkflowBuilder` type (returned by `AgentWorkflowBuilder.CreateGroupChatBuilderWith`), so a dedicated top-level test class - matching the convention reserved by the plan for HandoffWorkflowBuilderTests / MagenticWorkflowBuilderTests - is the right home. Cross-class helper references qualify with `AgentWorkflowBuilderTests.DoubleEchoAgent` and `AgentWorkflowBuilderTests.RunWorkflowAsync`. The outer partial class is `static` (and nested classes carry the instance test methods) because the outer holds only static helpers; this satisfies CA1052 without suppressions and is invisible to xUnit discovery, which finds tests on the nested classes as `AgentWorkflowBuilderTests.SequentialTests.*` etc. Validation: `dotnet build` clean on both target frameworks; all 547 tests in Microsoft.Agents.AI.Workflows.UnitTests pass on net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: introduce OutputTag, Futures, and tag-aware WorkflowBuilder API Phase 2 of the .NET Workflows outputs overhaul. Additive code change only - no observable runtime behavior change. The runner still uses the legacy bypass for AgentResponse / AgentResponseUpdate payloads, and the new `Futures.EnableAgentResponseOutputTaggingAndFiltering` flag defaults to false. Phase 3 will wire the flag into the runner; this commit only introduces the types and the builder API. New public surface: * `OutputTag` (readonly struct): wraps a string Value with ordinal equality (IEquatable, GetHashCode, == / !=) so it can participate as a HashSet element. Internal ctor closes the set. One public singleton: `OutputTag.Intermediate`. Terminal / regular outputs carry no tag (empty Tags set). JSON-serialized as a bare string via [JsonConverter(typeof(OutputTagJsonConverter))], with the converter rehydrating to the well-known singleton on read. * `Futures` (static class): hosts opt-in pre-GA behavior switches. First flag is `EnableAgentResponseOutputTaggingAndFiltering`; XML doc captures the v2.0.0 obsoletion / v3.0.0 removal lifecycle. * `WorkflowOutputEvent.Tags`: `HashSet` exposed directly (concrete collection, matches the JSON-serialization convention used for `WorkflowInfo.OutputExecutorIds`). Never null; empty for legacy / terminal events. New ctors take a single `OutputTag` or `IEnumerable?`; the existing (data, executorId) ctor remains and produces an untagged event. `HasTag(OutputTag)` helper. `AgentResponseEvent` and `AgentResponseUpdateEvent` gain matching tag-accepting ctors forwarding to the base. * `WorkflowOutputEventExtensions.IsIntermediate(this WorkflowOutputEvent)`: extension method returning `evt.HasTag(OutputTag.Intermediate)`. The preferred way to ask "is this an intermediate output?" without reaching into the Tags set. * `WorkflowBuilder.WithOutputFrom(IEnumerable, OutputTag)` and `WorkflowBuilder.WithOutputFrom(ExecutorBinding, OutputTag)`: forward-looking tagged overloads. The IEnumerable form is the primary tagged surface; the single-executor form is a convenience for the common one-executor case. Currently usable for the `OutputTag.Intermediate` singleton; will become the primary surface once the `OutputTag` constructor is opened to user-defined tags in a future release. Callers in this release should prefer the intent-specific `WithIntermediateOutputFrom` extension for the intermediate case. Tags accumulate across repeated calls; same tag repeated dedupes via the HashSet. * `WorkflowBuilderExtensions.WithIntermediateOutputFrom(this WorkflowBuilder, IEnumerable)`: helper that forwards to `WithOutputFrom(executors, OutputTag.Intermediate)`. Takes an IEnumerable (matching the tagged WithOutputFrom shape) - callers pass collection literals: `builder.WithIntermediateOutputFrom([a, b])`. XML doc remarks call out the Futures-flag interaction and the AIAgent-payload forwarding contract. Internal shape changes: * `WorkflowBuilder._outputExecutors`: HashSet -> Dictionary< string, HashSet>. The value set is empty for executors designated only via the untagged WithOutputFrom; contains Intermediate (and possibly future tags) otherwise. * `Workflow.OutputExecutors`: HashSet -> Dictionary>. * `OutputFilter.CanOutput`: `Contains(id)` -> `ContainsKey(id)`. * `WorkflowInfo.OutputExecutorIds`: HashSet -> Dictionary< string, HashSet>, with a custom JsonConverter that reads both the new map shape (`{id: ["intermediate", ...]}`) and the legacy array shape (`[id1, id2]`, where each id is treated as an untagged output). Always writes the map shape. IsMatch updated to compare per-id tag sets. Tests landing in this commit (per the test-with-feature principle): * `OutputTagTests.cs` (6 tests): KnownValues, EqualityIsOrdinalOnValue, DefaultStructValueIsDistinct (default(OutputTag) does not collide with the Intermediate singleton in a HashSet), GetHashCodeMatchesEquals, JsonConverter_RoundtripsValueAsString, ConstructorIsInternal (reflection-based assertion that the (string) ctor is `internal`). * `WorkflowBuilderTests.cs` adds 7 new tests pinning the builder API contract: RegistersWithEmptyTagSet, AddsIntermediateTag, MultipleExecutorsAllUntagged, ThenIntermediate_AccumulatesTags, RepeatedDedupes, OnlyRegistersWithoutPriorWithOutputFrom, TracksExecutorBinding. * `BackwardsCompatibility/JsonCheckpointSerializationTests.cs` (new folder + file, 5 tests): event-level ctor contract tests (single-tag, no-tag, multi-tag — the last with a custom tag); IsIntermediate() asserted; load-bearing JSON BC tests for `WorkflowInfo.OutputExecutorIds` - `WorkflowOutputExecutorsReadsLegacyArrayShape` (legacy ids map to empty tag sets) and `WorkflowOutputExecutorsWritesMapShape`. The plan's three JSON round-trip tests for `WorkflowOutputEvent.Tags` were dropped: `WorkflowEvent` is not currently a serialized checkpoint shape (see the comment in WorkflowsJsonUtilities.cs about events not being persisted), so there is no real back-compat surface to pin through JSON. They are substituted with in-process ctor/property round-trip tests that exercise the `Tags` / `HasTag` / `IsIntermediate` contract. Validation: full `Microsoft.Agents.AI.Workflows.UnitTests` suite runs green on net10.0 (565 passing, 0 failing). Core library builds clean on net472, netstandard2.0, net8.0, net9.0, and net10.0. Test project builds clean on net472 + net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: route AgentResponse(Update) through the output filter under a Futures flag `InProcessRunnerContext.YieldOutputAsync` historically special-cased AgentResponse and AgentResponseUpdate payloads: it built the typed event subclass and emitted it directly, bypassing the output filter. Rewrites the method so that: - When `Futures.EnableAgentResponseOutputTaggingAndFiltering` is `false` (the current default), AgentResponse(Update) keep the legacy bypass — emitted as AgentResponseEvent / AgentResponseUpdateEvent with no tags. Existing callers see no behavior change. - When the flag is `true`, AIAgent payloads flow through the output filter just like every other payload type: undesignated sources are dropped, and the emitted event carries the source's tag set (empty for terminal `WithOutputFrom`, `{Intermediate}` for `WithIntermediateOutputFrom`, the set union when both designations apply). Non-AIAgent (POCO) outputs also now carry the source's tag set on the emitted WorkflowOutputEvent unconditionally — additive, since no existing assertion inspected Tags. Subclass events (`AgentResponseEvent` / `AgentResponseUpdateEvent`) continue to be emitted under both modes so `switch (evt) { case AgentResponseEvent: ... }` consumer code keeps matching. Adds `OutputFilter.TryGetTags` as the tag-aware lookup used by the runner. `OutputFilter.CanOutput` is kept (still used by the existing sync tests in `OutputFilterTests.cs`). Tests ----- - `Futures/Futures.AgentResponseOutputFilteringAndTaggingTests.cs` (new): the F1–F13 matrix from the plan, covering every combination of `(flag on/off) × (designation) × (payload shape)`. Uses a `FuturesScope` IDisposable + a `FuturesSerial` xUnit collection (DisableParallelization = true) to keep the process-global flag from leaking across parallel tests. - `OutputFilterTests.cs`: four new `Test_OutputFilter_…` cases for the `TryGetTags` surface (empty-tag-set for terminal designation, `{Intermediate}` for intermediate designation, union for accumulated designation, `false` for unregistered). 582/582 unit tests pass on net10.0 (565 baseline + 17 new). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: tag-aware defaults and designation API on orchestration builders Aligns the .NET orchestration builders with Python's output / intermediate-output distinction. Each builder either applies a Python-aligned default designation set or replays the user's explicit `WithOutputFrom` / `WithIntermediateOutputFrom` calls, never both. Static `AgentWorkflowBuilder.BuildSequential` / `BuildConcurrent` apply defaults unconditionally (no user-facing fluent surface to take control through): - Sequential: terminal `end` + every agent designated intermediate. - Concurrent: terminal `end` + every agent and per-agent accumulator designated intermediate. The three fluent instance builders memoize agent-typed designation calls in a `Dictionary>` (empty set = terminal-only, non-empty = intermediate tag(s)) so repeated calls dedupe naturally. They replay the entries at `Build()` time, suppressing defaults when any call has been made: - `HandoffWorkflowBuilder` / `HandoffWorkflowBuilderCore` (also picked up by the obsolete `HandoffsWorkflowBuilder` via inheritance). Default: terminal `HandoffEnd` + every handoff agent intermediate. (Bug fix: legacy code relied on `WithOutputFrom(end)` to bind `HandoffEnd`. The new explicit-designation path bypasses that, so `Build()` now calls `BindExecutor(end)` unconditionally to keep validation happy.) - `GroupChatWorkflowBuilder` — default: terminal host + every participant intermediate. - `MagenticWorkflowBuilder` — default: terminal orchestrator + every team member intermediate. Designating a non-participant agent throws `InvalidOperationException`. The bare `WorkflowBuilder` default is unchanged — only the orchestration-style builders gain implicit defaults, matching the plan's non-goal. Tests ----- - `AgentWorkflowBuilder.SequentialTests` / `.ConcurrentTests`: one default-spec assertion each. - `GroupChatWorkflowBuilderTests`: defaults-match-spec, explicit-replaces-defaults, non-participant throws. - `HandoffWorkflowBuilderTests` (new file): same three. - `MagenticWorkflowBuilderTests` (new file): same three. 593/593 unit tests pass on net10.0 (582 baseline + 11 new). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: WorkflowHostAgent forwards AgentResponseEvent unconditionally under Futures-on Aligns the .NET Workflow-as-Agent surface with Python `as_agent`. Under `Futures.EnableAgentResponseOutputTaggingAndFiltering = true`, `WorkflowSession.InvokeStageAsync` now forwards `AgentResponseEvent` unconditionally — joining `AgentResponseUpdateEvent` in ignoring the host's `includeWorkflowOutputsInResponse` switch. That switch keeps governing the generic `WorkflowOutputEvent` path for non-AIAgent payloads, where it is further short-circuited by an `IsIntermediate()` check (tagged intermediate outputs always surface). Under Futures-off the legacy asymmetry is preserved: `AgentResponseUpdateEvent` always forwarded, `AgentResponseEvent` gated by `includeWorkflowOutputsInResponse`. Back-compat: with `Futures.EnableAgentResponseOutputTaggingAndFiltering` left at its default `false`, observable behavior is identical to before. `Futures` documentation gains a remark explaining the `Workflow.AsAIAgent()` interaction in both flag states. Runner fix ---------- `InProcessRunnerContext.YieldOutputAsync` now skips `Executor.CanOutput` for AgentResponse-shaped payloads under both Futures branches. `AIAgentHostExecutor` doesn't declare AgentResponse(Update) in its `Yields` set, so the historical legacy bypass had silently skipped the check; Phase 3's Futures-on path was running it and would reject AIAgent payloads. AIAgent-shaped payloads are now always a valid output shape, matching the legacy bypass semantics. Phase 4 follow-on ----------------- Switched the three orchestration-builder designation-replay loops to iterate `Dictionary.Keys` with a value lookup instead of constructing/destructuring `KeyValuePair<,>`. Cleaner shape and avoids the netstandard2.0 / net472 `KeyValuePair<,>.Deconstruct` unavailability that surfaced when this branch multi-TFM-built. Tests ----- `WorkflowHostSmokeTests.IntermediateForwarding` (new nested class, 6 tests): - intermediate AgentResponse forwarded past the include-outputs gate (Futures on) - terminal AgentResponse forwarded unconditionally (Futures on) - terminal AgentResponse gated by include flag (Futures off, legacy) - undesignated AIAgent executor emits no AgentResponseEvent under Futures-on - legacy bypass still emits AgentResponseEvent under Futures-off - intermediate tag is observable via `update.RawRepresentation` The class joins the `FuturesSerial` xUnit collection so the process-global flag is serialized against other Futures-toggling tests. 599/599 unit tests pass on net10.0 (593 baseline + 6 new). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: SequentialWorkflowBuilder and ConcurrentWorkflowBuilder, OrchestrationBuilderBase Promotes the Sequential and Concurrent orchestration shapes to first-class fluent builder classes, matching Handoff / GroupChat / Magentic. Users can call `WithOutputFrom(agents)` / `WithIntermediateOutputFrom(agents)` to control which agents are designated output / intermediate sources; when no designation call is made, the Python-aligned defaults apply (terminal aggregator output + every agent intermediate; Concurrent also tags per-agent accumulators). `AgentWorkflowBuilder.BuildSequential(...)` and `BuildConcurrent(...)` are kept and now delegate to the new builders; observable behavior unchanged. Five static factories now mirror each other: - `AgentWorkflowBuilder.CreateSequentialBuilderWith(params IEnumerable)` - `AgentWorkflowBuilder.CreateConcurrentBuilderWith(params IEnumerable)` - `AgentWorkflowBuilder.CreateHandoffBuilderWith(AIAgent)` (already existed) - `AgentWorkflowBuilder.CreateGroupChatBuilderWith(Func<...>)` (already existed) - `AgentWorkflowBuilder.CreateMagenticBuilderWith(AIAgent)` (new) OrchestrationBuilderBase ------------------------ New abstract `OrchestrationBuilderBase` unifies the shared fluent surface across all five orchestration builders: `WithName`, `WithDescription`, `WithOutputFrom`, `WithIntermediateOutputFrom`, and the `ApplyOutputDesignations(builder, agentMap, kind, applyDefaults)` helper that either replays the user's designations or invokes the orchestration-specific defaults. Removes ~150 LOC of duplicated designation-management code from the four non-Handoff builders, plus the equivalent from `HandoffWorkflowBuilderCore`. Tests ----- - New `SequentialWorkflowBuilderTests.cs` / `ConcurrentWorkflowBuilderTests.cs` (replace the old `AgentWorkflowBuilder.{Sequential,Concurrent}Tests.cs` nested-class files). Method names normalized to `Test__[Async]`. - Shared helpers (`DoubleEchoAgent`, `DoubleEchoAgentWithBarrier`, `WorkflowRunResult`, `RunWorkflow*`) moved from the old `AgentWorkflowBuilderTests` partial class into a new `OrchestrationTestHelpers` static class in `OrchestrationTestHelpers.cs`. Downstream test files (Group Chat, Handoff, Sequential, Concurrent) updated to qualify with `OrchestrationTestHelpers.*`. - A new `AgentWorkflowBuilderTests.cs` covers the static surface directly: `BuildSequential` / `BuildConcurrent` invariants and aggregator wiring, plus null-rejection + round-trip checks for every `Create*BuilderWith` factory. - New AsAgent intermediate-suppression tests on a nested `AsAgentForwarding` class for each of Sequential and Concurrent: build with only the terminal agent designated via `WithOutputFrom`, run via `AsAIAgent(...)`, assert via `AgentResponseUpdate.AuthorName` that intermediate agents do not surface. Both join the `FuturesSerial` collection. - New `Test__WithDescriptionPropagatesToWorkflow` smoke tests on Sequential and Concurrent (newly available via the base class). 625/625 unit tests pass on net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: dotnet format * fixup: encoding * fixup: charset * fixup: Updates for PR feedback * fixup: format * fixup: merge issue * Fix intermediate filtering on .AsAgent() * fix filter logic * fix: Revert logic change and add comments --------- Co-authored-by: Jacob Alber Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AgentResponseEvent.cs | 23 + .../AgentResponseUpdateEvent.cs | 22 + .../AgentWorkflowBuilder.cs | 95 +-- .../Checkpointing/WorkflowInfo.cs | 22 +- .../WorkflowInfoOutputExecutorsConverter.cs | 122 ++++ .../ConcurrentWorkflowBuilder.cs | 104 +++ .../Execution/OutputFilter.cs | 8 +- .../Microsoft.Agents.AI.Workflows/Futures.cs | 40 ++ .../GroupChatWorkflowBuilder.cs | 47 +- .../HandoffWorkflowBuilder.cs | 46 +- .../InProc/InProcessRunnerContext.cs | 43 +- .../MagenticWorkflowBuilder.cs | 39 +- .../OrchestrationBuilderBase.cs | 154 ++++ .../OutputTag.cs | 52 ++ .../OutputTagJsonConverter.cs | 43 ++ .../SequentialWorkflowBuilder.cs | 84 +++ .../Microsoft.Agents.AI.Workflows/Workflow.cs | 4 +- .../WorkflowBuilder.cs | 81 ++- .../WorkflowBuilderExtensions.cs | 24 + .../WorkflowOutputEvent.cs | 43 +- .../WorkflowOutputEventExtensions.cs | 21 + .../WorkflowSession.cs | 17 +- .../WorkflowsJsonUtilities.cs | 5 +- .../AgentWorkflowBuilderTests.cs | 674 +++--------------- .../JsonCheckpointSerializationTests.cs | 121 ++++ .../ConcurrentWorkflowBuilderTests.cs | 164 +++++ ...tResponseOutputFilteringAndTaggingTests.cs | 289 ++++++++ .../Futures/FuturesScope.cs | 27 + .../Futures/FuturesSerialCollection.cs | 19 + .../GroupChatWorkflowBuilderTests.cs | 477 +++++++++++++ .../HandoffWorkflowBuilderTests.cs | 77 ++ ...tputFilterTests.cs => InputWaiterTests.cs} | 47 -- .../JsonSerializationTests.cs | 8 +- .../MagenticWorkflowBuilderTests.cs | 79 ++ .../OrchestrationTestHelpers.cs | 131 ++++ .../OutputFilterTests.cs | 108 +++ .../OutputTagTests.cs | 77 ++ .../SequentialWorkflowBuilderTests.cs | 183 +++++ ...rSmokeTests.cs => WorkflowBuilderTests.cs} | 110 ++- .../WorkflowHostSmokeTests.cs | 126 ++++ 40 files changed, 3070 insertions(+), 786 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfoOutputExecutorsConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ConcurrentWorkflowBuilder.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/Futures.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/OrchestrationBuilderBase.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/OutputTag.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/OutputTagJsonConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/SequentialWorkflowBuilder.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEventExtensions.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/BackwardsCompatibility/JsonCheckpointSerializationTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ConcurrentWorkflowBuilderTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/Futures.AgentResponseOutputFilteringAndTaggingTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesScope.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesSerialCollection.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatWorkflowBuilderTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffWorkflowBuilderTests.cs rename dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/{InputWaiterAndOutputFilterTests.cs => InputWaiterTests.cs} (74%) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticWorkflowBuilderTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OrchestrationTestHelpers.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputFilterTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputTagTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SequentialWorkflowBuilderTests.cs rename dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/{WorkflowBuilderSmokeTests.cs => WorkflowBuilderTests.cs} (83%) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs index e57204ea4e..5d59366a20 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows; @@ -19,6 +20,28 @@ public sealed class AgentResponseEvent : WorkflowOutputEvent this.Response = Throw.IfNull(response); } + /// + /// Initializes a new instance of the class with the given output tag. + /// + /// The identifier of the executor that generated this event. + /// The agent response. + /// The output tag to associate with this event. + public AgentResponseEvent(string executorId, AgentResponse response, OutputTag tag) : base(response, executorId, tag) + { + this.Response = Throw.IfNull(response); + } + + /// + /// Initializes a new instance of the class with the given output tags. + /// + /// The identifier of the executor that generated this event. + /// The agent response. + /// The output tags to associate with this event. May be or empty. + public AgentResponseEvent(string executorId, AgentResponse response, IEnumerable? tags) : base(response, executorId, tags) + { + this.Response = Throw.IfNull(response); + } + /// /// Gets the agent response. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs index 017dce1763..f3d5215ccd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs @@ -20,6 +20,28 @@ public sealed class AgentResponseUpdateEvent : WorkflowOutputEvent this.Update = Throw.IfNull(update); } + /// + /// Initializes a new instance of the class with the given output tag. + /// + /// The identifier of the executor that generated this event. + /// The agent run response update. + /// The output tag to associate with this event. + public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update, OutputTag tag) : base(update, executorId, tag) + { + this.Update = Throw.IfNull(update); + } + + /// + /// Initializes a new instance of the class with the given output tags. + /// + /// The identifier of the executor that generated this event. + /// The agent run response update. + /// The output tags to associate with this event. May be or empty. + public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update, IEnumerable? tags) : base(update, executorId, tags) + { + this.Update = Throw.IfNull(update); + } + /// /// Gets the agent run response update. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs index 9d7aa5b8c7..007920f6bc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Workflows.Specialized; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; @@ -37,31 +34,10 @@ public static partial class AgentWorkflowBuilder { Throw.IfNullOrEmpty(agents); - // Create a builder that chains the agents together in sequence. The workflow simply begins - // with the first agent in the sequence. - - AIAgentHostOptions options = new() - { - ReassignOtherAgentsAsUsers = true, - ForwardIncomingMessages = true, - }; - - List agentExecutors = agents.Select(agent => agent.BindAsExecutor(options)).ToList(); - - ExecutorBinding previous = agentExecutors[0]; - WorkflowBuilder builder = new(previous); - - foreach (ExecutorBinding next in agentExecutors.Skip(1)) - { - builder.AddEdge(previous, next); - previous = next; - } - - OutputMessagesExecutor end = new(); - builder = builder.AddEdge(previous, end).WithOutputFrom(end); + SequentialWorkflowBuilder builder = new(agents); if (workflowName is not null) { - builder = builder.WithName(workflowName); + builder.WithName(workflowName); } return builder.Build(); } @@ -107,41 +83,14 @@ public static partial class AgentWorkflowBuilder { Throw.IfNull(agents); - // A workflow needs a starting executor, so we create one that forwards everything to each agent. - ChatForwardingExecutor start = new("Start"); - WorkflowBuilder builder = new(start); - - // For each agent, we create an executor to host it and an accumulator to batch up its output messages, - // so that the final accumulator receives a single list of messages from each agent. Otherwise, the - // accumulator would not be able to determine what came from what agent, as there's currently no - // provenance tracking exposed in the workflow context passed to a handler. - - ExecutorBinding[] agentExecutors = (from agent in agents - select agent.BindAsExecutor(new AIAgentHostOptions() { ReassignOtherAgentsAsUsers = true })).ToArray(); - ExecutorBinding[] accumulators = [.. from agent in agentExecutors select (ExecutorBinding)new AggregateTurnMessagesExecutor($"Batcher/{agent.Id}")]; - builder.AddFanOutEdge(start, agentExecutors); - - for (int i = 0; i < agentExecutors.Length; i++) - { - builder.AddEdge(agentExecutors[i], accumulators[i]); - } - - // Create the accumulating executor that will gather the results from each agent, and connect - // each agent's accumulator to it. If no aggregation function was provided, we default to returning - // the last message from each agent - aggregator ??= static lists => (from list in lists where list.Count > 0 select list.Last()).ToList(); - - Func> endFactory = - (_, __) => new(new ConcurrentEndExecutor(agentExecutors.Length, aggregator)); - - ExecutorBinding end = endFactory.BindExecutor(ConcurrentEndExecutor.ExecutorId); - - builder.AddFanInBarrierEdge(accumulators, end); - - builder = builder.WithOutputFrom(end); + ConcurrentWorkflowBuilder builder = new(agents); if (workflowName is not null) { - builder = builder.WithName(workflowName); + builder.WithName(workflowName); + } + if (aggregator is not null) + { + builder.WithAggregator(aggregator); } return builder.Build(); } @@ -179,4 +128,32 @@ public static partial class AgentWorkflowBuilder Throw.IfNull(managerFactory); return new GroupChatWorkflowBuilder(managerFactory); } + + /// Creates a new with the given pipeline of . + /// The sequence of agents to compose into a sequential workflow. + /// The builder for creating a sequential workflow. + public static SequentialWorkflowBuilder CreateSequentialBuilderWith(params IEnumerable agents) + { + Throw.IfNull(agents); + return new SequentialWorkflowBuilder(agents); + } + + /// Creates a new with the given participating . + /// The set of agents to compose into a concurrent workflow. + /// The builder for creating a concurrent workflow. + public static ConcurrentWorkflowBuilder CreateConcurrentBuilderWith(params IEnumerable agents) + { + Throw.IfNull(agents); + return new ConcurrentWorkflowBuilder(agents); + } + + /// Creates a new with the given . + /// The LLM-powered manager agent that coordinates the team. + /// The builder for creating a Magentic workflow. + [Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] + public static MagenticWorkflowBuilder CreateMagenticBuilderWith(AIAgent managerAgent) + { + Throw.IfNull(managerAgent); + return new MagenticWorkflowBuilder(managerAgent); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs index f40882265a..aac14fee35 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; @@ -15,14 +16,14 @@ internal sealed class WorkflowInfo Dictionary> edges, HashSet requestPorts, string startExecutorId, - HashSet? outputExecutorIds) + Dictionary>? outputExecutorIds) { this.Executors = Throw.IfNull(executors); this.Edges = Throw.IfNull(edges); this.RequestPorts = Throw.IfNull(requestPorts); this.StartExecutorId = Throw.IfNullOrEmpty(startExecutorId); - this.OutputExecutorIds = outputExecutorIds ?? []; + this.OutputExecutorIds = outputExecutorIds ?? new Dictionary>(StringComparer.Ordinal); } public Dictionary Executors { get; } @@ -32,7 +33,15 @@ internal sealed class WorkflowInfo public TypeId? InputType { get; } public string StartExecutorId { get; } - public HashSet OutputExecutorIds { get; } + /// + /// Map of executor id to the set of s under which the executor is registered. + /// An empty set means the executor is registered as a regular (untagged) output source. + /// JSON shape: { "executorId": ["intermediate"], ... }. Legacy payloads using the + /// older string[] shape are read by and + /// each id is treated as registered with an empty tag set. + /// + [JsonConverter(typeof(WorkflowInfoOutputExecutorsConverter))] + public Dictionary> OutputExecutorIds { get; } public bool IsMatch(Workflow workflow) { @@ -80,9 +89,12 @@ internal sealed class WorkflowInfo return false; } - // Validate the outputs + // Validate the outputs (key set + tag set per id must match) if (workflow.OutputExecutors.Count != this.OutputExecutorIds.Count || - this.OutputExecutorIds.Any(id => !workflow.OutputExecutors.Contains(id))) + this.OutputExecutorIds.Any(kvp => + !workflow.OutputExecutors.TryGetValue(kvp.Key, out HashSet? tags) || + tags.Count != kvp.Value.Count || + !tags.SetEquals(kvp.Value))) { return false; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfoOutputExecutorsConverter.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfoOutputExecutorsConverter.cs new file mode 100644 index 0000000000..8ee8d39590 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfoOutputExecutorsConverter.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Workflows.Checkpointing; + +/// +/// JSON converter for that supports both the new +/// map shape ({ "id": ["intermediate"] }) and the legacy array shape +/// (["id1", "id2"]). Legacy-shaped payloads are read as if every id had been registered +/// as a regular (untagged) output source; output is always written in the new map shape. +/// +internal sealed class WorkflowInfoOutputExecutorsConverter : JsonConverter>> +{ + public override Dictionary> Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Dictionary> result = new(StringComparer.Ordinal); + + if (reader.TokenType == JsonTokenType.Null) + { + return result; + } + + if (reader.TokenType == JsonTokenType.StartArray) + { + // Legacy shape: a flat array of executor ids. Treat each as a registered + // (untagged) output executor. + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + return result; + } + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Expected a string in legacy outputExecutorIds array, got {reader.TokenType}."); + } + + string id = reader.GetString()!; + result[id] = []; + } + + throw new JsonException("Unexpected end of legacy outputExecutorIds array."); + } + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"Expected object or array for outputExecutorIds, got {reader.TokenType}."); + } + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return result; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException($"Expected property name in outputExecutorIds object, got {reader.TokenType}."); + } + + string id = reader.GetString()!; + reader.Read(); + + HashSet tags = []; + if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Expected a string tag, got {reader.TokenType}."); + } + + tags.Add(ReadTag(reader.GetString()!)); + } + } + else + { + throw new JsonException($"Expected array of tags for outputExecutorIds[{id}], got {reader.TokenType}."); + } + + result[id] = tags; + } + + throw new JsonException("Unexpected end of outputExecutorIds object."); + } + + private static OutputTag ReadTag(string value) + { + if (string.Equals(value, OutputTag.Intermediate.Value, StringComparison.Ordinal)) + { + return OutputTag.Intermediate; + } + return new OutputTag(value); + } + + public override void Write( + Utf8JsonWriter writer, + Dictionary> value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (KeyValuePair> kvp in value) + { + writer.WritePropertyName(kvp.Key); + writer.WriteStartArray(); + foreach (OutputTag tag in kvp.Value) + { + writer.WriteStringValue(tag.Value); + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ConcurrentWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ConcurrentWorkflowBuilder.cs new file mode 100644 index 0000000000..feb31ddd9f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ConcurrentWorkflowBuilder.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Specialized; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Fluent builder for concurrent agent workflows: a fan-out start that broadcasts the +/// incoming messages to every participating agent, a per-agent accumulator that batches +/// each agent's outgoing messages, and a fan-in aggregator that reduces them into a +/// single output list. +/// +/// +/// When no explicit output designations are made, the default is the Python-aligned +/// shape: the terminal aggregator is the workflow output, and every participating agent +/// (plus its per-agent accumulator) is designated as an intermediate output source. +/// Calling +/// or +/// at all suppresses these defaults. +/// +public sealed class ConcurrentWorkflowBuilder : OrchestrationBuilderBase +{ + private readonly List _agents = []; + private Func>, List>? _aggregator; + + /// + /// Initializes a new with the given participating + /// . + /// + public ConcurrentWorkflowBuilder(params IEnumerable agents) + { + Throw.IfNull(agents); + foreach (AIAgent agent in agents) + { + Throw.IfNull(agent, nameof(agents)); + this._agents.Add(agent); + } + } + + /// + /// Sets the aggregator function. If not called, defaults to returning the last message + /// from each agent that produced at least one message. + /// + public ConcurrentWorkflowBuilder WithAggregator(Func>, List> aggregator) + { + this._aggregator = Throw.IfNull(aggregator); + return this; + } + + /// Builds the configured concurrent workflow. + public Workflow Build() + { + if (this._agents.Count == 0) + { + throw new ArgumentException("At least one agent must be provided to the ConcurrentWorkflowBuilder.", "agents"); + } + + ChatForwardingExecutor start = new("Start"); + WorkflowBuilder builder = new(start); + + Dictionary agentMap = new(AIAgentIDEqualityComparer.Instance); + ExecutorBinding[] agentExecutors = new ExecutorBinding[this._agents.Count]; + ExecutorBinding[] accumulators = new ExecutorBinding[this._agents.Count]; + AIAgentHostOptions options = new() { ReassignOtherAgentsAsUsers = true }; + for (int i = 0; i < this._agents.Count; i++) + { + AIAgent agent = this._agents[i]; + ExecutorBinding binding = agent.BindAsExecutor(options); + agentExecutors[i] = binding; + agentMap[agent] = binding; + accumulators[i] = new AggregateTurnMessagesExecutor($"Batcher/{binding.Id}"); + } + + builder.AddFanOutEdge(start, agentExecutors); + for (int i = 0; i < agentExecutors.Length; i++) + { + builder.AddEdge(agentExecutors[i], accumulators[i]); + } + + Func>, List> aggregator = + this._aggregator ?? (static lists => (from list in lists where list.Count > 0 select list.Last()).ToList()); + + Func> endFactory = + (_, __) => new(new ConcurrentEndExecutor(agentExecutors.Length, aggregator)); + + ExecutorBinding end = endFactory.BindExecutor(ConcurrentEndExecutor.ExecutorId); + builder.AddFanInBarrierEdge(accumulators, end); + + this.ApplyMetadata(builder); + this.ApplyOutputDesignations(builder, agentMap, "concurrent", () => + { + builder.WithOutputFrom(end); + builder.WithIntermediateOutputFrom([.. agentExecutors, .. accumulators]); + }); + + return builder.Build(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/OutputFilter.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/OutputFilter.cs index cecf1da9f8..5ef5b8713b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/OutputFilter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/OutputFilter.cs @@ -1,11 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.Agents.AI.Workflows.Execution; internal sealed class OutputFilter(Workflow workflow) { public bool CanOutput(string sourceExecutorId, object output) { - return workflow.OutputExecutors.Contains(sourceExecutorId); + return workflow.OutputExecutors.ContainsKey(sourceExecutorId); } + + public bool TryGetTags(string sourceExecutorId, [NotNullWhen(true)] out HashSet? tags) + => workflow.OutputExecutors.TryGetValue(sourceExecutorId, out tags); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Futures.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Futures.cs new file mode 100644 index 0000000000..b2c83f112a --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Futures.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Process-wide opt-in switches for in-development behavior changes that will become +/// the default in a future major release. Each flag defaults to +/// and should be toggled once at application startup. +/// +public static class Futures +{ + /// + /// When , and + /// payloads yielded by an executor participate + /// in the normal output-filter pipeline (i.e. they must be designated via + /// or + /// + /// to surface), and the resulting s carry + /// reflecting that designation. + /// + /// + /// + /// When (the current default), the runner emits + /// and unconditionally, + /// bypassing the output filter (historical behavior). Lifecycle: opt-in today, marked + /// [Obsolete] in v2.0.0 when the new behavior becomes default, and removed in v3.0.0. + /// + /// + /// Interaction with . When this flag + /// is , joins + /// in being forwarded out of the agent surface + /// unconditionally — neither honors the host's includeWorkflowOutputsInResponse + /// switch. That switch only governs the generic path for + /// non-AIAgent payloads. When this flag is , the legacy asymmetry + /// is preserved: is always forwarded but + /// stays gated by includeWorkflowOutputsInResponse. + /// + /// + public static bool EnableAgentResponseOutputTaggingAndFiltering { get; set; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs index cb922616bb..14167a2e8f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs @@ -12,12 +12,10 @@ namespace Microsoft.Agents.AI.Workflows; /// /// Provides a builder for specifying group chat relationships between agents and building the resulting workflow. /// -public sealed class GroupChatWorkflowBuilder +public sealed class GroupChatWorkflowBuilder : OrchestrationBuilderBase { private readonly Func, GroupChatManager> _managerFactory; private readonly HashSet _participants = new(AIAgentIDEqualityComparer.Instance); - private string _name = string.Empty; - private string _description = string.Empty; internal GroupChatWorkflowBuilder(Func, GroupChatManager> managerFactory) => this._managerFactory = managerFactory; @@ -44,28 +42,6 @@ public sealed class GroupChatWorkflowBuilder return this; } - /// - /// Sets the human-readable name for the workflow. - /// - /// The name of the workflow. - /// This instance of the . - public GroupChatWorkflowBuilder WithName(string name) - { - this._name = name; - return this; - } - - /// - /// Sets the description for the workflow. - /// - /// The description of what the workflow does. - /// This instance of the . - public GroupChatWorkflowBuilder WithDescription(string description) - { - this._description = description; - return this; - } - /// /// Builds a composed of agents that operate via group chat, with the next /// agent to process messages selected by the group chat manager. @@ -93,15 +69,7 @@ public sealed class GroupChatWorkflowBuilder ExecutorBinding host = groupChatHostFactory.BindExecutor(nameof(GroupChatHost)); WorkflowBuilder builder = new(host); - if (!string.IsNullOrEmpty(this._name)) - { - builder = builder.WithName(this._name); - } - - if (!string.IsNullOrEmpty(this._description)) - { - builder = builder.WithDescription(this._description); - } + this.ApplyMetadata(builder); foreach (var participant in agentMap.Values) { @@ -110,6 +78,15 @@ public sealed class GroupChatWorkflowBuilder .AddEdge(participant, host); } - return builder.WithOutputFrom(host).Build(); + this.ApplyOutputDesignations(builder, agentMap, "group chat", () => + { + builder.WithOutputFrom(host); + if (agentMap.Count > 0) + { + builder.WithIntermediateOutputFrom([.. agentMap.Values]); + } + }); + + return builder.Build(); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs index e5b08ce928..30ec899edc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs @@ -39,7 +39,8 @@ public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkfl /// Provides a builder for specifying the handoff relationships between agents and building the resulting workflow. /// [Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] -public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkflowBuilderCore +public class HandoffWorkflowBuilderCore : OrchestrationBuilderBase + where TBuilder : HandoffWorkflowBuilderCore { /// /// The prefix for function calls that trigger handoffs to other agents; the full name is then `{FunctionPrefix}<agent_id>`, @@ -55,8 +56,6 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl private bool _emitAgentResponseUpdateEvents; private HandoffToolCallFilteringBehavior _toolCallFilteringBehavior = HandoffToolCallFilteringBehavior.HandoffOnly; private bool _returnToPrevious; - private string? _name; - private string? _description; // Autonomous mode configuration. When enabled, an agent's response that doesn't include a // handoff triggers another invocation of that same agent with the continuation prompt, up to @@ -116,20 +115,6 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl return (TBuilder)this; } - /// - public TBuilder WithName(string name) - { - this._name = name; - return (TBuilder)this; - } - - /// - public TBuilder WithDescription(string description) - { - this._description = description; - return (TBuilder)this; - } - /// /// Sets a value indicating whether agent streaming update events should be emitted during execution. /// If , the value will be taken from the @@ -631,16 +616,31 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl }); } - if (!string.IsNullOrWhiteSpace(this._name)) + // Ensure the end executor is bound regardless of whether it ends up as an output + // designation source — the user may take full control of output designations. + builder.BindExecutor(end); + + // Build the AIAgent -> ExecutorBinding map the base helper expects. + Dictionary agentMap = new(AIAgentIDEqualityComparer.Instance); + foreach (AIAgent agent in this._allAgents) { - builder.WithName(this._name); + agentMap[agent] = executors[agent.Id]; } - if (!string.IsNullOrWhiteSpace(this._description)) + this.ApplyMetadata(builder); + this.ApplyOutputDesignations(builder, agentMap, "handoff", () => { - builder.WithDescription(this._description); - } + // Defaults (matches Python's Handoff orchestration): + // end -> terminal output + // every handoff agent -> intermediate output + builder.WithOutputFrom(end); + List agentBindings = [.. executors.Values]; + if (agentBindings.Count > 0) + { + builder.WithIntermediateOutputFrom(agentBindings); + } + }); - return builder.WithOutputFrom(end).Build(); + return builder.Build(); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs index d6c7d301e3..8201f8d8fe 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs @@ -241,30 +241,47 @@ internal sealed class InProcessRunnerContext : IRunnerContext this.CheckEnded(); Throw.IfNull(output); - // Special-case AgentResponse and AgentResponseUpdate to create their specific event types - // and bypass the output filter (for backwards compatibility - these events were previously - // emitted directly via AddEventAsync without filtering) - if (output is AgentResponseUpdate update) + bool isAgentResponseShaped = output is AgentResponse or AgentResponseUpdate; + + if (isAgentResponseShaped && !Futures.EnableAgentResponseOutputTaggingAndFiltering) { - await this.AddEventAsync(new AgentResponseUpdateEvent(sourceId, update), cancellationToken).ConfigureAwait(false); - return; - } - else if (output is AgentResponse response) - { - await this.AddEventAsync(new AgentResponseEvent(sourceId, response), cancellationToken).ConfigureAwait(false); + // Legacy bypass: AgentResponse/AgentResponseUpdate skip the output filter and are + // emitted as their typed event subclasses with no tags. Preserved verbatim for + // back-compat; once Futures.EnableAgentResponseOutputTaggingAndFiltering becomes the + // default in v2.0.0, this branch goes away. + WorkflowEvent typedEvent = output switch + { + AgentResponseUpdate u => new AgentResponseUpdateEvent(sourceId, u), + AgentResponse r => new AgentResponseEvent(sourceId, r), + _ => throw new InvalidOperationException("Unexpected AIAgent-shaped payload type."), + }; + await this.AddEventAsync(typedEvent, cancellationToken).ConfigureAwait(false); return; } Executor sourceExecutor = await this.EnsureExecutorAsync(sourceId, tracer: null, cancellationToken).ConfigureAwait(false); - if (!sourceExecutor.CanOutput(output.GetType())) + if (!isAgentResponseShaped && !sourceExecutor.CanOutput(output.GetType())) { + // AIAgent-shaped payloads bypass the per-executor declared-yield check (matching the + // legacy bypass branch above). The AIAgent host executor relays the agent's output + // without declaring AgentResponse(Update) in its Yields set, so a CanOutput probe + // here would always reject — but those payloads are always a valid output shape. throw new InvalidOperationException($"Cannot output object of type {output.GetType().Name}. Expecting one of [{string.Join(", ", sourceExecutor.OutputTypes)}]."); } - if (this._outputFilter.CanOutput(sourceId, output)) + if (!this._outputFilter.TryGetTags(sourceId, out HashSet? tags)) { - await this.AddEventAsync(new WorkflowOutputEvent(output, sourceId), cancellationToken).ConfigureAwait(false); + // Not designated as an output source — drop silently. + return; } + + WorkflowOutputEvent evt = output switch + { + AgentResponseUpdate u => new AgentResponseUpdateEvent(sourceId, u, tags), + AgentResponse r => new AgentResponseEvent(sourceId, r, tags), + _ => new WorkflowOutputEvent(output, sourceId, tags), + }; + await this.AddEventAsync(evt, cancellationToken).ConfigureAwait(false); } public IExternalRequestContext BindExternalRequestContext(string executorId) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs index 4470c4ee9a..c75a3d045b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs @@ -28,11 +28,9 @@ namespace Microsoft.Agents.AI.Workflows; /// /// [Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] -public class MagenticWorkflowBuilder(AIAgent managerAgent) +public class MagenticWorkflowBuilder(AIAgent managerAgent) : OrchestrationBuilderBase { private readonly List _team = new(); - private string? _name; - private string? _description; private int _maxStalls = TaskLimits.DefaultMaxStallCount; private int? _maxRounds; private int? _maxResets; @@ -45,20 +43,6 @@ public class MagenticWorkflowBuilder(AIAgent managerAgent) return this; } - /// - public MagenticWorkflowBuilder WithName(string name) - { - this._name = name; - return this; - } - - /// - public MagenticWorkflowBuilder WithDescription(string description) - { - this._description = description; - return this; - } - /// /// Set the maximum number of coordination rounds. means unlimited. /// @@ -115,28 +99,29 @@ public class MagenticWorkflowBuilder(AIAgent managerAgent) ForwardIncomingMessages = false }; + Dictionary teamMap = new(AIAgentIDEqualityComparer.Instance); List teamBindings = []; foreach (AIAgent agent in team) { ExecutorBinding binding = agent.BindAsExecutor(options); teamBindings.Add(binding); + teamMap[agent] = binding; result.AddEdge(binding, orchestrator); } - result.AddFanOutEdge(orchestrator, teamBindings) - .WithOutputFrom(orchestrator); + result.AddFanOutEdge(orchestrator, teamBindings); - if (!string.IsNullOrWhiteSpace(this._name)) + this.ApplyOutputDesignations(result, teamMap, "Magentic", () => { - result.WithName(this._name); - } - - if (!string.IsNullOrWhiteSpace(this._description)) - { - result.WithDescription(this._description); - } + result.WithOutputFrom(orchestrator); + if (teamMap.Count > 0) + { + result.WithIntermediateOutputFrom([.. teamMap.Values]); + } + }); + this.ApplyMetadata(result); return result; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/OrchestrationBuilderBase.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/OrchestrationBuilderBase.cs new file mode 100644 index 0000000000..6d44c03509 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/OrchestrationBuilderBase.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Common fluent surface shared by every orchestration-style workflow builder: +/// human-readable name + description, and the +/// / output-designation +/// pair with memoized defaults-suppression semantics. +/// +/// The concrete builder type, for fluent self-return. +public abstract class OrchestrationBuilderBase + where TBuilder : OrchestrationBuilderBase +{ + /// Optional workflow name; applied to the inner at Build(). + protected string? Name { get; private set; } + + /// Optional workflow description; applied to the inner at Build(). + protected string? Description { get; private set; } + + /// + /// Memoized output designations. means the user has not made any + /// explicit designation, and the orchestration-specific defaults will be applied at + /// Build() time. A non- (possibly empty) map means the user took + /// control and only these designations will be replayed onto the inner + /// . An entry's value is the set of tags requested for the + /// agent — an empty set encodes a terminal-only designation. + /// + protected Dictionary>? OutputDesignations { get; private set; } + + /// Sets the human-readable name for the workflow. + public TBuilder WithName(string name) + { + this.Name = name; + return (TBuilder)this; + } + + /// Sets the description for the workflow. + public TBuilder WithDescription(string description) + { + this.Description = description; + return (TBuilder)this; + } + + /// + /// Designates the given as sources of terminal workflow output. + /// Calling any output-designation method (this or ) + /// suppresses the orchestration-specific defaults: only the user-specified designations + /// reach the inner . + /// + public TBuilder WithOutputFrom(params IEnumerable agents) + { + Throw.IfNull(agents); + this.OutputDesignations ??= new(AIAgentIDEqualityComparer.Instance); + foreach (AIAgent agent in agents) + { + Throw.IfNull(agent, nameof(agents)); + if (!this.OutputDesignations.ContainsKey(agent)) + { + this.OutputDesignations[agent] = []; + } + } + return (TBuilder)this; + } + + /// + /// Designates the given as sources of intermediate workflow + /// output. See for the defaults-suppression semantics. + /// + public TBuilder WithIntermediateOutputFrom(IEnumerable agents) + { + Throw.IfNull(agents); + this.OutputDesignations ??= new(AIAgentIDEqualityComparer.Instance); + foreach (AIAgent agent in agents) + { + Throw.IfNull(agent, nameof(agents)); + if (!this.OutputDesignations.TryGetValue(agent, out HashSet? tags)) + { + tags = []; + this.OutputDesignations[agent] = tags; + } + tags.Add(OutputTag.Intermediate); + } + return (TBuilder)this; + } + + /// + /// Applies the optional and to . + /// Subclasses should call this from their Build() implementation. + /// + protected void ApplyMetadata(WorkflowBuilder builder) + { + Throw.IfNull(builder); + if (!string.IsNullOrWhiteSpace(this.Name)) + { + builder.WithName(this.Name!); + } + if (!string.IsNullOrWhiteSpace(this.Description)) + { + builder.WithDescription(this.Description!); + } + } + + /// + /// Applies the user's memoized output designations to , or invokes + /// if the user made no explicit designation. + /// + /// The inner . + /// Map from participating to its bound executor. + /// Used in the not-a-participant error message (e.g. "sequential", "group chat"). + /// Action invoked when no explicit designation was made. + protected void ApplyOutputDesignations( + WorkflowBuilder builder, + IReadOnlyDictionary agentMap, + string orchestrationKind, + Action applyDefaults) + { + Throw.IfNull(builder); + Throw.IfNull(agentMap); + Throw.IfNull(applyDefaults); + + if (this.OutputDesignations is null) + { + applyDefaults(); + return; + } + + foreach (AIAgent agent in this.OutputDesignations.Keys) + { + if (!agentMap.TryGetValue(agent, out ExecutorBinding? binding)) + { + throw new InvalidOperationException( + $"Output designation references agent '{agent.Name ?? agent.Id}', which is not a participant in this {orchestrationKind} workflow."); + } + + HashSet tags = this.OutputDesignations[agent]; + if (tags.Count == 0) + { + builder.WithOutputFrom(binding); + } + else + { + foreach (OutputTag tag in tags) + { + builder.WithOutputFrom(binding, tag); + } + } + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/OutputTag.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/OutputTag.cs new file mode 100644 index 0000000000..073e2f995f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/OutputTag.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json.Serialization; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Identifies the kind of output that a represents. +/// A thin ChatRole-style wrapper around a normalized string , +/// with value equality and a closed set of well-known singletons (the constructor is +/// for now). +/// +[JsonConverter(typeof(OutputTagJsonConverter))] +public readonly struct OutputTag : IEquatable +{ + /// + /// The string identifier of the tag. Compared with ordinal equality. + /// + public string? Value { get; } + + internal OutputTag(string value) + { + this.Value = Throw.IfNullOrEmpty(value); + } + + /// + /// The tag denoting an intermediate workflow output — emitted by executors + /// registered via . + /// Terminal (non-intermediate) outputs carry no tag. + /// + public static OutputTag Intermediate { get; } = new("intermediate"); + + /// + public bool Equals(OutputTag other) => string.Equals(this.Value, other.Value, StringComparison.Ordinal); + + /// + public override bool Equals(object? obj) => obj is OutputTag other && this.Equals(other); + + /// + public override int GetHashCode() => this.Value is null ? 0 : StringComparer.Ordinal.GetHashCode(this.Value); + + /// Determines whether two values are equal. + public static bool operator ==(OutputTag left, OutputTag right) => left.Equals(right); + + /// Determines whether two values are not equal. + public static bool operator !=(OutputTag left, OutputTag right) => !left.Equals(right); + + /// + public override string ToString() => this.Value ?? string.Empty; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/OutputTagJsonConverter.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/OutputTagJsonConverter.cs new file mode 100644 index 0000000000..4b19e3b2cb --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/OutputTagJsonConverter.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// JSON converter for that round-trips the underlying +/// as a bare JSON string. +/// +internal sealed class OutputTagJsonConverter : JsonConverter +{ + public override OutputTag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? value = reader.GetString(); + if (string.IsNullOrEmpty(value)) + { + return default; + } + + // Reuse the well-known singleton where possible so callers can do reference + // comparisons on the common case without paying the extra allocation cost. + if (string.Equals(value, OutputTag.Intermediate.Value, StringComparison.Ordinal)) + { + return OutputTag.Intermediate; + } + + return new OutputTag(value!); + } + + public override void Write(Utf8JsonWriter writer, OutputTag value, JsonSerializerOptions options) + { + if (value.Value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStringValue(value.Value); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/SequentialWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/SequentialWorkflowBuilder.cs new file mode 100644 index 0000000000..cfe1d1e1d4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/SequentialWorkflowBuilder.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Fluent builder for sequential agent workflows: a pipeline where the output of one +/// agent is the input to the next, terminating in an aggregator that yields the +/// accumulated s as the workflow output. +/// +/// +/// When no explicit output designations are made, the default is the Python-aligned +/// shape: the terminal aggregator is the workflow output, and every participating agent +/// is designated as an intermediate output source. Calling +/// +/// or +/// at all suppresses these defaults. +/// +public sealed class SequentialWorkflowBuilder : OrchestrationBuilderBase +{ + private readonly List _agents = []; + + /// + /// Initializes a new with the given pipeline + /// of . + /// + public SequentialWorkflowBuilder(params IEnumerable agents) + { + Throw.IfNull(agents); + foreach (AIAgent agent in agents) + { + Throw.IfNull(agent, nameof(agents)); + this._agents.Add(agent); + } + } + + /// Builds the configured sequential workflow. + public Workflow Build() + { + if (this._agents.Count == 0) + { + throw new ArgumentException("At least one agent must be provided to the SequentialWorkflowBuilder.", "agents"); + } + + AIAgentHostOptions options = new() + { + ReassignOtherAgentsAsUsers = true, + ForwardIncomingMessages = true, + }; + + Dictionary agentMap = new(AIAgentIDEqualityComparer.Instance); + List agentExecutors = new(this._agents.Count); + foreach (AIAgent agent in this._agents) + { + ExecutorBinding binding = agent.BindAsExecutor(options); + agentExecutors.Add(binding); + agentMap[agent] = binding; + } + + ExecutorBinding previous = agentExecutors[0]; + WorkflowBuilder builder = new(previous); + foreach (ExecutorBinding next in agentExecutors.Skip(1)) + { + builder.AddEdge(previous, next); + previous = next; + } + + OutputMessagesExecutor end = new(); + builder.AddEdge(previous, end).BindExecutor(end); + + this.ApplyMetadata(builder); + this.ApplyOutputDesignations(builder, agentMap, "sequential", () => + { + builder.WithOutputFrom(end); + builder.WithIntermediateOutputFrom(agentExecutors); + }); + + return builder.Build(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs index eff1cfb9a3..03c8f6a920 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs @@ -24,7 +24,7 @@ public class Workflow internal Dictionary ExecutorBindings { get; init; } = []; internal Dictionary> Edges { get; init; } = []; - internal HashSet OutputExecutors { get; init; } = []; + internal Dictionary> OutputExecutors { get; init; } = new(StringComparer.Ordinal); /// /// Gets the collection of edges grouped by their source node identifier. @@ -221,7 +221,7 @@ public class Workflow startExecutor.AttachRequestContext(new NoOpExternalRequestContext()); ProtocolDescriptor inputProtocol = startExecutor.DescribeProtocol(); - IEnumerable> outputExecutorTasks = this.OutputExecutors.Select(executorId => this.ExecutorBindings[executorId].CreateInstanceAsync(string.Empty).AsTask()); + IEnumerable> outputExecutorTasks = this.OutputExecutors.Keys.Select(executorId => this.ExecutorBindings[executorId].CreateInstanceAsync(string.Empty).AsTask()); Executor[] outputExecutors = await Task.WhenAll(outputExecutorTasks).ConfigureAwait(false); IEnumerable yieldedTypes = outputExecutors.SelectMany(executor => executor.DescribeProtocol().Yields); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs index e29abca5ab..3acb0c3f61 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs @@ -33,7 +33,7 @@ public class WorkflowBuilder private readonly HashSet _unboundExecutors = []; private readonly HashSet _conditionlessConnections = []; private readonly Dictionary _requestPorts = []; - private readonly HashSet _outputExecutors = []; + private readonly Dictionary> _outputExecutors = new(StringComparer.Ordinal); private readonly string _startExecutorId; private string? _name; @@ -97,22 +97,89 @@ public class WorkflowBuilder } /// - /// Register executors as an output source. Executors can use to yield output values. - /// By default, message handlers with a non-void return type will also be yielded, unless - /// is set to . + /// Register executors as a source of terminal workflow outputs. Executors can use + /// to yield output values; yielded values from + /// registered executors are surfaced as (or one of its + /// subclasses) with an empty set. + /// By default, message handlers with a non-void return type will also be yielded, unless + /// is set to . /// - /// - /// + /// + /// AIAgent payloads ( / ) only + /// participate in this designation when + /// is + /// ; otherwise they are emitted unconditionally and untagged. + /// + /// The executors to register as output sources. + /// The current instance, enabling fluent configuration. public WorkflowBuilder WithOutputFrom(params ExecutorBinding[] executors) { foreach (ExecutorBinding executor in executors) { - this._outputExecutors.Add(this.Track(executor).Id); + this.EnsureOutputExecutor(this.Track(executor).Id); } return this; } + /// + /// Register executors as a source of workflow outputs carrying the given . + /// Tags accumulate across repeated calls; the registered id always exists with the union of all + /// tags applied across all calls (and an empty set if only the untagged + /// overload was used). + /// + /// + /// Forward-looking surface for when the constructor opens to + /// user-defined tags. Today, prefer + /// + /// for the case. + /// + /// The executors to register. + /// The tag to apply to events yielded by the listed executors. + /// The current instance, enabling fluent configuration. + public WorkflowBuilder WithOutputFrom(IEnumerable executors, OutputTag tag) + { + Throw.IfNull(executors); + + foreach (ExecutorBinding executor in executors) + { + this.EnsureOutputExecutor(this.Track(executor).Id).Add(tag); + } + + return this; + } + + /// + /// Register a single executor as a source of workflow outputs carrying the given . + /// Convenience overload for the single-executor case; equivalent to passing a one-element sequence + /// to . + /// + /// The executor to register. + /// The tag to apply to events yielded by the executor. + /// The current instance, enabling fluent configuration. + public WorkflowBuilder WithOutputFrom(ExecutorBinding executor, OutputTag tag) + { + Throw.IfNull(executor); + + this.EnsureOutputExecutor(this.Track(executor).Id).Add(tag); + + return this; + } + + /// + /// Ensures the executor id is present in ; if newly added, + /// initializes with an empty tag set. Returns the tag set for the id (mutable). + /// + private HashSet EnsureOutputExecutor(string executorId) + { + if (!this._outputExecutors.TryGetValue(executorId, out HashSet? tags)) + { + tags = []; + this._outputExecutors[executorId] = tags; + } + return tags; + } + /// /// Sets the human-readable name for the workflow. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilderExtensions.cs index a22aa8e722..6db047255d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilderExtensions.cs @@ -211,4 +211,28 @@ public static class WorkflowBuilderExtensions return switchBuilder.ReduceToFanOut(builder, source); } + + /// + /// Register executors as a source of intermediate workflow outputs. The resulting + /// s carry in their + /// set, and + /// returns + /// . Use this for progress updates, partial results, and other + /// non-terminal emissions that downstream consumers (DevUI, logging, Workflow-as-Agent + /// surfaces) should see distinctly from the workflow's final output. + /// + /// + /// AIAgent payloads ( / ) only + /// participate in this designation when + /// is + /// ; otherwise they bypass the filter and are emitted untagged. + /// + /// The workflow builder to register executors on. + /// The executors to register as intermediate output sources. + /// The , enabling fluent configuration. + public static WorkflowBuilder WithIntermediateOutputFrom(this WorkflowBuilder builder, IEnumerable executors) + { + Throw.IfNull(builder); + return builder.WithOutputFrom(executors, OutputTag.Intermediate); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs index f0fe884f6d..380baa22d2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; @@ -13,14 +14,39 @@ namespace Microsoft.Agents.AI.Workflows; [JsonDerivedType(typeof(AgentResponseUpdateEvent))] public class WorkflowOutputEvent : WorkflowEvent { + private readonly HashSet _tags; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with no tags. /// /// The output data. /// The identifier of the executor that yielded this output. - public WorkflowOutputEvent(object data, string executorId) : base(data) + public WorkflowOutputEvent(object data, string executorId) : this(data, executorId, tags: null) + { + } + + /// + /// Initializes a new instance of the class carrying the + /// given output tag. + /// + /// The output data. + /// The identifier of the executor that yielded this output. + /// The single output tag to associate with this event. + public WorkflowOutputEvent(object data, string executorId, OutputTag tag) : this(data, executorId, tags: new[] { tag }) + { + } + + /// + /// Initializes a new instance of the class carrying the + /// given output tags (deduplicated). + /// + /// The output data. + /// The identifier of the executor that yielded this output. + /// The output tags to associate with this event. May be or empty (the event is then untagged). + public WorkflowOutputEvent(object data, string executorId, IEnumerable? tags) : base(data) { this.ExecutorId = executorId; + this._tags = tags is null ? new HashSet() : new HashSet(tags); } /// @@ -32,8 +58,21 @@ public class WorkflowOutputEvent : WorkflowEvent /// The unique identifier of the executor that yielded this output. /// [Obsolete("Use ExecutorId instead.")] + [JsonIgnore] public string SourceId => this.ExecutorId; + /// + /// The set of output tags associated with this event. Never ; + /// empty for terminal/regular outputs. The presence of + /// marks this event as an intermediate output. + /// + public IEnumerable Tags => this._tags; + + /// + /// Returns if this event carries the given tag. + /// + public bool HasTag(OutputTag tag) => this._tags.Contains(tag); + /// /// Determines whether the underlying data is of the specified type or a derived type. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEventExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEventExtensions.cs new file mode 100644 index 0000000000..1bbad70f87 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEventExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Workflows; + +/// +/// Extension helpers for inspecting tag membership. +/// +public static class WorkflowOutputEventExtensions +{ + /// + /// Returns if the event carries + /// in its . + /// + public static bool IsIntermediate(this WorkflowOutputEvent evt) + { + Throw.IfNull(evt); + return evt.HasTag(OutputTag.Intermediate); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowSession.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowSession.cs index 719b72e112..812bda1150 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowSession.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowSession.cs @@ -520,11 +520,20 @@ internal sealed class WorkflowSession : AgentSession goto default; case AgentResponseEvent agentResponse: - if (!this._includeWorkflowOutputsInResponse) + // Under Futures.EnableAgentResponseOutputTaggingAndFiltering=true, mirror + // AgentResponseUpdateEvent's behavior: always forward, regardless of the + // _includeWorkflowOutputsInResponse host flag / "intermediate" tag. Under + // the legacy default, keep today's behavior — gated by the include flag. + if (!Futures.EnableAgentResponseOutputTaggingAndFiltering && !this._includeWorkflowOutputsInResponse) { goto default; } + // Either EnableAgentResponseOutputTaggingAndFiltering -- so yield the Response + // regardless of whether it is tagged "intermediate" or whether the + // _includeWorkflowOutputInResponse flag is set. Reason being: The user specifies + // exclusion of an event by enabling filtering and then _not_ marking an Executor + // as an output executor. foreach (ChatMessage message in agentResponse.Response.Messages) { yield return this.CreateUpdate(this.LastResponseId, evt, message); @@ -539,7 +548,11 @@ internal sealed class WorkflowSession : AgentSession _ => null }; - if (!this._includeWorkflowOutputsInResponse || updateMessages == null) + // Same assymetry as with AgentResponseEvent, but there is no EnableFiltering flag + // to consider. If this made it here (and since it is not an AgentResponse[Update]), + // it means it is already been selected as an Output() from the user. Intermediate + // is irrelevant here. + if (updateMessages == null || !this._includeWorkflowOutputsInResponse) { goto default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs index 3bf63b09be..0614aca36d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs @@ -80,9 +80,8 @@ internal static partial class WorkflowsJsonUtilities [JsonSerializable(typeof(ExecutorIdentity))] [JsonSerializable(typeof(RunnerStateData))] - // Workflow Representation Types - [JsonSerializable(typeof(WorkflowInfo))] - [JsonSerializable(typeof(EdgeConnection))] + // Workflow Output Types + [JsonSerializable(typeof(OutputTag))] // Workflow-as-Agent [JsonSerializable(typeof(WorkflowChatHistoryProvider.StoreState))] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs index d477140373..787fca810c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs @@ -4,12 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Workflows.InProc; +using FluentAssertions; using Microsoft.Extensions.AI; #pragma warning disable SYSLIB1045 // Use GeneratedRegex @@ -17,601 +14,164 @@ using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.UnitTests; +/// +/// Tests targeting the static helper surface — +/// , +/// , +/// and the various Create*BuilderWith factories. Per-builder unit tests live in their own +/// files (, , etc.). +/// public class AgentWorkflowBuilderTests { [Fact] - public void BuildSequential_InvalidArguments_Throws() + public void Test_AgentWorkflowBuilder_BuildSequential_InvalidArguments_Throws() { Assert.Throws("agents", () => AgentWorkflowBuilder.BuildSequential(workflowName: null!, null!)); Assert.Throws("agents", () => AgentWorkflowBuilder.BuildSequential()); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public async Task Test_AgentWorkflowBuilder_BuildSequential_DelegatesToBuilderAsync(int numAgents) + { + Workflow workflow = AgentWorkflowBuilder.BuildSequential( + from i in Enumerable.Range(1, numAgents) + select new OrchestrationTestHelpers.DoubleEchoAgent($"agent{i}")); + + // Smoke: end-to-end run produces a non-empty result. Detailed pipeline-ordering + // assertions live in SequentialWorkflowBuilderTests. + (string updateText, List? result, _, _) = + await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]); + + Assert.NotNull(result); + Assert.Equal(numAgents + 1, result.Count); + Assert.NotEmpty(updateText); + } + [Fact] - public void BuildConcurrent_InvalidArguments_Throws() + public void Test_AgentWorkflowBuilder_BuildSequential_WithWorkflowNameSetsNameOnWorkflow() + { + Workflow workflow = AgentWorkflowBuilder.BuildSequential( + "static-sequential", + new OrchestrationTestHelpers.DoubleEchoAgent("agent1")); + + workflow.Name.Should().Be("static-sequential"); + } + + [Fact] + public void Test_AgentWorkflowBuilder_BuildConcurrent_InvalidArguments_Throws() { Assert.Throws("agents", () => AgentWorkflowBuilder.BuildConcurrent(null!)); } [Fact] - public void BuildGroupChat_InvalidArguments_Throws() - { - Assert.Throws("managerFactory", () => AgentWorkflowBuilder.CreateGroupChatBuilderWith(null!)); - - var groupChat = AgentWorkflowBuilder.CreateGroupChatBuilderWith(_ => new RoundRobinGroupChatManager([new DoubleEchoAgent("a1")])); - Assert.NotNull(groupChat); - Assert.Throws("agents", () => groupChat.AddParticipants(null!)); - Assert.Throws("agents", () => groupChat.AddParticipants([null!])); - Assert.Throws("agents", () => groupChat.AddParticipants(new DoubleEchoAgent("a1"), null!)); - - Assert.Throws("agents", () => new RoundRobinGroupChatManager(null!)); - } - - [Fact] - public void GroupChatManager_MaximumIterationCount_Invalid_Throws() - { - var manager = new RoundRobinGroupChatManager([new DoubleEchoAgent("a1")]); - - const int DefaultMaxIterations = 40; - Assert.Equal(DefaultMaxIterations, manager.MaximumIterationCount); - Assert.Throws("value", void () => manager.MaximumIterationCount = 0); - Assert.Throws("value", void () => manager.MaximumIterationCount = -1); - Assert.Equal(DefaultMaxIterations, manager.MaximumIterationCount); - - manager.MaximumIterationCount = 30; - Assert.Equal(30, manager.MaximumIterationCount); - - manager.MaximumIterationCount = 1; - Assert.Equal(1, manager.MaximumIterationCount); - - manager.MaximumIterationCount = int.MaxValue; - Assert.Equal(int.MaxValue, manager.MaximumIterationCount); - } - - [Fact] - public void BuildGroupChat_WithNameAndDescription_SetsWorkflowNameAndDescription() - { - const string WorkflowName = "Test Group Chat"; - const string WorkflowDescription = "A test group chat workflow"; - - var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) - .AddParticipants(new DoubleEchoAgent("agent1"), new DoubleEchoAgent("agent2")) - .WithName(WorkflowName) - .WithDescription(WorkflowDescription) - .Build(); - - Assert.Equal(WorkflowName, workflow.Name); - Assert.Equal(WorkflowDescription, workflow.Description); - } - - [Fact] - public void BuildGroupChat_WithNameOnly_SetsWorkflowName() - { - const string WorkflowName = "Named Group Chat"; - - var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) - .AddParticipants(new DoubleEchoAgent("agent1")) - .WithName(WorkflowName) - .Build(); - - Assert.Equal(WorkflowName, workflow.Name); - Assert.Null(workflow.Description); - } - - [Fact] - public void BuildGroupChat_WithoutNameOrDescription_DefaultsToNull() - { - var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) - .AddParticipants(new DoubleEchoAgent("agent1")) - .Build(); - - Assert.Null(workflow.Name); - Assert.Null(workflow.Description); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - public async Task BuildSequential_AgentsRunInOrderAsync(int numAgents) - { - var workflow = AgentWorkflowBuilder.BuildSequential( - from i in Enumerable.Range(1, numAgents) - select new DoubleEchoAgent($"agent{i}")); - - for (int iter = 0; iter < 3; iter++) - { - const string UserInput = "abc"; - (string updateText, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); - - Assert.NotNull(result); - Assert.Equal(numAgents + 1, result.Count); - - Assert.Equal(ChatRole.User, result[0].Role); - Assert.Null(result[0].AuthorName); - Assert.Equal(UserInput, result[0].Text); - - string[] texts = new string[numAgents + 1]; - texts[0] = UserInput; - string expectedTotal = string.Empty; - for (int i = 1; i < numAgents + 1; i++) - { - string id = $"agent{((i - 1) % numAgents) + 1}"; - texts[i] = $"{id}{Double(string.Concat(texts.Take(i)))}"; - Assert.Equal(ChatRole.Assistant, result[i].Role); - Assert.Equal(id, result[i].AuthorName); - Assert.Equal(texts[i], result[i].Text); - expectedTotal += texts[i]; - } - - Assert.Equal(expectedTotal, updateText); - Assert.Equal(UserInput + expectedTotal, string.Concat(result)); - - static string Double(string s) => s + s; - } - } - - private class DoubleEchoAgent(string name) : AIAgent - { - public override string Name => name; - - protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) - => new(new DoubleEchoAgentSession()); - - protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) - => new(new DoubleEchoAgentSession()); - - protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) - => default; - - protected override Task RunCoreAsync( - IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await Task.Yield(); - - var contents = messages.SelectMany(m => m.Contents).ToList(); - string id = Guid.NewGuid().ToString("N"); - yield return new AgentResponseUpdate(ChatRole.Assistant, this.Name) { AuthorName = this.Name, MessageId = id }; - yield return new AgentResponseUpdate(ChatRole.Assistant, contents) { AuthorName = this.Name, MessageId = id }; - yield return new AgentResponseUpdate(ChatRole.Assistant, contents) { AuthorName = this.Name, MessageId = id }; - } - } - - private sealed class DoubleEchoAgentSession() : AgentSession(); - - [Fact] - public async Task BuildConcurrent_AgentsRunInParallelAsync() + public async Task Test_AgentWorkflowBuilder_BuildConcurrent_DelegatesToBuilderAsync() { StrongBox> barrier = new(); StrongBox remaining = new(); - var workflow = AgentWorkflowBuilder.BuildConcurrent( + Workflow workflow = AgentWorkflowBuilder.BuildConcurrent( [ - new DoubleEchoAgentWithBarrier("agent1", barrier, remaining), - new DoubleEchoAgentWithBarrier("agent2", barrier, remaining), + new OrchestrationTestHelpers.DoubleEchoAgentWithBarrier("agent1", barrier, remaining), + new OrchestrationTestHelpers.DoubleEchoAgentWithBarrier("agent2", barrier, remaining), ]); - for (int iter = 0; iter < 3; iter++) - { - barrier.Value = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - remaining.Value = 2; + barrier.Value = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + remaining.Value = 2; - (string updateText, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]); - Assert.NotEmpty(updateText); - Assert.NotNull(result); - - // TODO: https://github.com/microsoft/agent-framework/issues/784 - // These asserts are flaky until we guarantee message delivery order. - Assert.Single(Regex.Matches(updateText, "agent1")); - Assert.Single(Regex.Matches(updateText, "agent2")); - Assert.Equal(4, Regex.Matches(updateText, "abc").Count); - Assert.Equal(2, result.Count); - } - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - public async Task BuildGroupChat_AgentsRunInOrderAsync(int maxIterations) - { - const int NumAgents = 3; - var workflow = AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxIterations }) - .AddParticipants(new DoubleEchoAgent("agent1"), new DoubleEchoAgent("agent2")) - .AddParticipants(new DoubleEchoAgent("agent3")) - .Build(); - - for (int iter = 0; iter < 3; iter++) - { - const string UserInput = "abc"; - (string updateText, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); - - Assert.NotNull(result); - Assert.Equal(maxIterations + 1, result.Count); - - Assert.Equal(ChatRole.User, result[0].Role); - Assert.Null(result[0].AuthorName); - Assert.Equal(UserInput, result[0].Text); - - // The group-chat host broadcasts each new message (initial user input + each speaker's - // response) to every participant except the speaker that produced it. The selected - // speaker therefore sees only what's been broadcast to it since its previous turn. - string[] agentIds = ["agent1", "agent2", "agent3"]; - List[] buffers = new List[NumAgents]; - for (int a = 0; a < NumAgents; a++) - { - buffers[a] = [UserInput]; - } - - string[] texts = new string[maxIterations + 1]; - texts[0] = UserInput; - string expectedTotal = string.Empty; - for (int i = 1; i < maxIterations + 1; i++) - { - int speakerIdx = (i - 1) % NumAgents; - string id = agentIds[speakerIdx]; - string concatReceived = string.Concat(buffers[speakerIdx]); - texts[i] = $"{id}{Double(concatReceived)}"; - buffers[speakerIdx].Clear(); - for (int a = 0; a < NumAgents; a++) - { - if (a == speakerIdx) - { - continue; - } - - buffers[a].Add(texts[i]); - } - - Assert.Equal(ChatRole.Assistant, result[i].Role); - Assert.Equal(id, result[i].AuthorName); - Assert.Equal(texts[i], result[i].Text); - expectedTotal += texts[i]; - } - - Assert.Equal(expectedTotal, updateText); - Assert.Equal(UserInput + expectedTotal, string.Concat(result)); - - static string Double(string s) => s + s; - } - } - - private sealed record WorkflowRunResult(string UpdateText, List? Result, CheckpointInfo? LastCheckpoint, List PendingRequests); - - private static async Task RunWorkflowCheckpointedAsync( - Workflow workflow, List input, InProcessExecutionEnvironment environment, CheckpointInfo? fromCheckpoint = null) - { - await using StreamingRun run = - fromCheckpoint != null ? await environment.ResumeStreamingAsync(workflow, fromCheckpoint) - : await environment.OpenStreamingAsync(workflow); - - await run.TrySendMessageAsync(input); - await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); - - return await ProcessWorkflowRunAsync(run); - } - - private static async Task ProcessWorkflowRunAsync(StreamingRun run) - { - StringBuilder sb = new(); - WorkflowOutputEvent? output = null; - CheckpointInfo? lastCheckpoint = null; - - List pendingRequests = []; - - await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false).ConfigureAwait(false)) - { - switch (evt) - { - case AgentResponseUpdateEvent responseUpdate: - sb.Append(responseUpdate.Data); - break; - - case RequestInfoEvent requestInfo: - pendingRequests.Add(requestInfo); - break; - - case WorkflowOutputEvent e: - output = e; - break; - - case WorkflowErrorEvent errorEvent: - Assert.Fail($"Workflow execution failed with error: {errorEvent.Exception}"); - break; - - case SuperStepCompletedEvent stepCompleted: - lastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint; - break; - } - } - - return new(sb.ToString(), output?.As>(), lastCheckpoint, pendingRequests); - } - - private static Task RunWorkflowAsync( - Workflow workflow, List input, ExecutionEnvironment executionEnvironment = ExecutionEnvironment.InProcess_Lockstep) - => RunWorkflowCheckpointedAsync(workflow, input, executionEnvironment.ToWorkflowExecutionEnvironment()); - - private sealed class DoubleEchoAgentWithBarrier(string name, StrongBox> barrier, StrongBox remaining) : DoubleEchoAgent(name) - { - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - if (Interlocked.Decrement(ref remaining.Value) == 0) - { - barrier.Value!.SetResult(true); - } - - await barrier.Value!.Task.ConfigureAwait(false); - - await foreach (var update in base.RunCoreStreamingAsync(messages, session, options, cancellationToken)) - { - await Task.Yield(); - yield return update; - } - } - } - - private sealed class RecordingAgent(string name) : AIAgent - { - public List> Invocations { get; } = []; - - public override string Name => name; - - protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) - => new(new RecordingAgentSession()); - - protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) - => new(new RecordingAgentSession()); - - protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) - => default; - - protected override Task RunCoreAsync( - IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await Task.Yield(); - - this.Invocations.Add(messages.Select(m => m.Text).ToList()); - - string id = Guid.NewGuid().ToString("N"); - yield return new AgentResponseUpdate(ChatRole.Assistant, name) { AuthorName = name, MessageId = id }; - } - } - - private sealed class RecordingAgentSession() : AgentSession(); - - [Fact] - public async Task BuildGroupChat_BroadcastsDeltaAndTargetsTurnTokenToSpeakerOnlyAsync() - { - var agentA = new RecordingAgent("agentA"); - var agentB = new RecordingAgent("agentB"); - var agentC = new RecordingAgent("agentC"); - - var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 4 }) - .AddParticipants(agentA, agentB, agentC) - .Build(); - - const string UserInput = "hello"; - (_, List? result, _, _) = await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + (string updateText, List? result, _, _) = + await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]); + Assert.NotEmpty(updateText); Assert.NotNull(result); - Assert.Equal(5, result.Count); // initial user input + 4 agent turns - Assert.Collection( - result, - m => Assert.Equal(UserInput, m.Text), - m => Assert.Equal("agentA", m.Text), - m => Assert.Equal("agentB", m.Text), - m => Assert.Equal("agentC", m.Text), - m => Assert.Equal("agentA", m.Text)); - - // Each agent's TurnToken fires exactly when it is the selected speaker — invocation counts - // confirm only the chosen participant receives a TurnToken on each round. - Assert.Equal(2, agentA.Invocations.Count); - Assert.Single(agentB.Invocations); - Assert.Single(agentC.Invocations); - - // Turn 1: agentA is the first speaker. Initial broadcast went to every participant, so - // agentA's only buffered message is the user input. - Assert.Equal([UserInput], agentA.Invocations[0]); - - // Turn 2: agentB. It received the initial broadcast (user input) plus turn-1 broadcast of - // agentA's response (agentA itself is excluded as the last speaker). - Assert.Equal([UserInput, "agentA"], agentB.Invocations[0]); - - // Turn 3: agentC. It also received every broadcast so far (it has never been excluded). - Assert.Equal([UserInput, "agentA", "agentB"], agentC.Invocations[0]); - - // Turn 4: agentA again. It was excluded on turn 2's broadcast (its own response), but - // received turn-3 (agentB's response) and turn-4 (agentC's response) deltas. - Assert.Equal(["agentB", "agentC"], agentA.Invocations[1]); + Assert.Equal(2, result.Count); + Assert.Single(Regex.Matches(updateText, "agent1")); + Assert.Single(Regex.Matches(updateText, "agent2")); } [Fact] - public async Task BuildGroupChat_UpdateHistoryAsync_FiltersBroadcastPayloadAsync() + public void Test_AgentWorkflowBuilder_BuildConcurrent_WithWorkflowNameSetsNameOnWorkflow() { - var agentA = new RecordingAgent("agentA"); - var agentB = new RecordingAgent("agentB"); + Workflow workflow = AgentWorkflowBuilder.BuildConcurrent( + "static-concurrent", + [new OrchestrationTestHelpers.DoubleEchoAgent("agent1")]); - var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => new PrefixingGroupChatManager(agents, "[broadcast] ") { MaximumIterationCount = 2 }) - .AddParticipants(agentA, agentB) - .Build(); - - const string UserInput = "hello"; - await RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); - - // Turn 1: agentA's buffer contains only the initial broadcast, which UpdateHistoryAsync - // prefixed. - Assert.Equal(["[broadcast] hello"], agentA.Invocations[0]); - - // Turn 2: agentB received both the initial broadcast and agentA's response — both passed - // through UpdateHistoryAsync before being broadcast. - Assert.Equal(["[broadcast] hello", "[broadcast] agentA"], agentB.Invocations[0]); + workflow.Name.Should().Be("static-concurrent"); } [Fact] - public async Task BuildGroupChat_CheckpointResumeMidConversation_PreservesIterationCursorAndBroadcastExclusionAsync() + public async Task Test_AgentWorkflowBuilder_BuildConcurrent_AggregatorIsHonoredAsync() { - const string UserInput = "hello"; - const int MaxIterations = 6; + // Replace the default ("last message from each agent") with a custom aggregator, + // and confirm the workflow yields its result. + List sentinel = [new(ChatRole.Assistant, "custom-aggregator-result")]; - // --- Baseline: run the full conversation under checkpointing and capture every checkpoint - // plus the final transcript. The same workflow + agents are reused for the resume, - // because the runner enforces workflow-shape compatibility on ResumeStreamingAsync. --- - BaselineRunResult baseline = await RunGroupChatBaselineAsync(UserInput, MaxIterations); + Workflow workflow = AgentWorkflowBuilder.BuildConcurrent( + [new OrchestrationTestHelpers.DoubleEchoAgent("agent1")], + aggregator: _ => sentinel); - // We need at least one mid-conversation checkpoint to resume from. The baseline produces a - // checkpoint per superstep, which for MaxIterations=6 yields many; we pick a checkpoint - // captured roughly midway so the resumed run still has work to do. - Assert.True(baseline.Checkpoints.Count >= 5, - $"expected at least 5 checkpoints in the baseline, got {baseline.Checkpoints.Count}"); + (_, List? result, _, _) = + await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]); - int midIndex = baseline.Checkpoints.Count / 2; - CheckpointInfo midCheckpoint = baseline.Checkpoints[midIndex]; - - // Snapshot per-agent invocation counts before the resume so we can isolate the invocations - // produced after the checkpoint is restored. - int aPreCount = baseline.AgentA.Invocations.Count; - int bPreCount = baseline.AgentB.Invocations.Count; - int cPreCount = baseline.AgentC.Invocations.Count; - - // --- Resume the same workflow from the mid-conversation checkpoint. --- - List? resumedResult = null; - await using (StreamingRun resumed = await baseline.Environment - .ResumeStreamingAsync(baseline.Workflow, midCheckpoint)) - { - await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false)) - { - if (evt is WorkflowOutputEvent o) - { - resumedResult = o.As>(); - } - else if (evt is WorkflowErrorEvent err) - { - Assert.Fail($"Resumed workflow failed: {err.Exception}"); - } - } - } - - // (1) Iteration-count continuity: the resumed run terminates with exactly the same number - // of turns the baseline produced — proves IterationCount was rehydrated and the manager - // honored MaximumIterationCount across the boundary. - Assert.NotNull(resumedResult); - Assert.Equal(baseline.Result.Count, resumedResult!.Count); - - // (2) Next-speaker consistency: the full transcript (initial input + every speaker's turn, - // in order) matches the baseline — proves the round-robin cursor was restored. - List baselineTranscript = [.. baseline.Result.Select(m => m.Text)]; - List resumedTranscript = [.. resumedResult.Select(m => m.Text)]; - Assert.Equal(baselineTranscript, resumedTranscript); - - // (3) Broadcast exclusion holds across resume: a RecordingAgent's response text is just its - // own Name. Examine only the invocations recorded after the resume. If the host failed - // to exclude the current speaker from its post-resume broadcasts, an agent's next - // invocation buffer would contain its own previously produced response. Asserting that - // no post-resume invocation input contains the invoking agent's own name proves the - // exclusion was preserved through checkpoint+restore. - AssertPostResumeBroadcastExclusion(baseline.AgentA, aPreCount); - AssertPostResumeBroadcastExclusion(baseline.AgentB, bPreCount); - AssertPostResumeBroadcastExclusion(baseline.AgentC, cPreCount); - - // Sanity: at least one agent was actually invoked after the resume; otherwise the test - // would trivially pass even if the host stopped scheduling turns after restore. - int totalPost = baseline.AgentA.Invocations.Count - aPreCount - + (baseline.AgentB.Invocations.Count - bPreCount) - + (baseline.AgentC.Invocations.Count - cPreCount); - Assert.True(totalPost > 0, "at least one agent should be invoked after resuming from the mid-conversation checkpoint"); - - static void AssertPostResumeBroadcastExclusion(RecordingAgent agent, int preCount) - { - for (int i = preCount; i < agent.Invocations.Count; i++) - { - Assert.DoesNotContain(agent.Name, agent.Invocations[i]); - } - } + result.Should().NotBeNull().And.ContainSingle(); + result![0].Text.Should().Be("custom-aggregator-result"); } - private sealed record BaselineRunResult( - Workflow Workflow, - InProcessExecutionEnvironment Environment, - RecordingAgent AgentA, - RecordingAgent AgentB, - RecordingAgent AgentC, - List Result, - List Checkpoints, - CheckpointManager CheckpointManager); - - private static async Task RunGroupChatBaselineAsync(string userInput, int maxIterations) + [Fact] + public void Test_AgentWorkflowBuilder_CreateSequentialBuilderWith_RejectsNull() { - var agentA = new RecordingAgent("agentA"); - var agentB = new RecordingAgent("agentB"); - var agentC = new RecordingAgent("agentC"); - - Workflow workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxIterations }) - .AddParticipants(agentA, agentB, agentC) - .Build(); - - CheckpointManager checkpointMgr = CheckpointManager.CreateInMemory(); - InProcessExecutionEnvironment env = ExecutionEnvironment.InProcess_Lockstep - .ToWorkflowExecutionEnvironment() - .WithCheckpointing(checkpointMgr); - - List checkpoints = []; - List? finalResult = null; - - await using (StreamingRun run = await env.OpenStreamingAsync(workflow)) - { - await run.TrySendMessageAsync(new List { new(ChatRole.User, userInput) }); - await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); - - await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false)) - { - switch (evt) - { - case SuperStepCompletedEvent step when step.CompletionInfo?.Checkpoint is { } cp: - checkpoints.Add(cp); - break; - case WorkflowOutputEvent o: - finalResult = o.As>(); - break; - case WorkflowErrorEvent err: - Assert.Fail($"Baseline workflow failed: {err.Exception}"); - break; - } - } - } - - Assert.NotNull(finalResult); - return new BaselineRunResult(workflow, env, agentA, agentB, agentC, finalResult!, checkpoints, checkpointMgr); + Assert.Throws("agents", () => AgentWorkflowBuilder.CreateSequentialBuilderWith(null!)); } - private sealed class PrefixingGroupChatManager(IReadOnlyList agents, string prefix) : RoundRobinGroupChatManager(agents) + [Fact] + public void Test_AgentWorkflowBuilder_CreateSequentialBuilderWith_ReturnsConfigurableBuilder() { - protected internal override ValueTask> UpdateHistoryAsync( - IReadOnlyList history, - CancellationToken cancellationToken = default) - { - IEnumerable prefixed = - history.Select(m => new ChatMessage(m.Role, $"{prefix}{m.Text}") { AuthorName = m.AuthorName }); + OrchestrationTestHelpers.DoubleEchoAgent agent = new("agent1"); - return new(prefixed); - } + SequentialWorkflowBuilder builder = AgentWorkflowBuilder.CreateSequentialBuilderWith(agent); + Workflow workflow = builder.WithName("via-factory").Build(); + + workflow.Name.Should().Be("via-factory"); + } + + [Fact] + public void Test_AgentWorkflowBuilder_CreateConcurrentBuilderWith_RejectsNull() + { + Assert.Throws("agents", () => AgentWorkflowBuilder.CreateConcurrentBuilderWith(null!)); + } + + [Fact] + public void Test_AgentWorkflowBuilder_CreateConcurrentBuilderWith_ReturnsConfigurableBuilder() + { + OrchestrationTestHelpers.DoubleEchoAgent agent = new("agent1"); + + ConcurrentWorkflowBuilder builder = AgentWorkflowBuilder.CreateConcurrentBuilderWith(agent); + Workflow workflow = builder.WithName("via-factory").Build(); + + workflow.Name.Should().Be("via-factory"); + } + + [Fact] + public void Test_AgentWorkflowBuilder_CreateHandoffBuilderWith_RejectsNull() + { +#pragma warning disable MAAIW001 + Assert.Throws("initialAgent", () => AgentWorkflowBuilder.CreateHandoffBuilderWith(null!)); +#pragma warning restore MAAIW001 + } + + [Fact] + public void Test_AgentWorkflowBuilder_CreateGroupChatBuilderWith_RejectsNull() + { + Assert.Throws("managerFactory", () => AgentWorkflowBuilder.CreateGroupChatBuilderWith(null!)); + } + + [Fact] + public void Test_AgentWorkflowBuilder_CreateMagenticBuilderWith_RejectsNull() + { +#pragma warning disable MAAIW001 + Assert.Throws("managerAgent", () => AgentWorkflowBuilder.CreateMagenticBuilderWith(null!)); +#pragma warning restore MAAIW001 } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/BackwardsCompatibility/JsonCheckpointSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/BackwardsCompatibility/JsonCheckpointSerializationTests.cs new file mode 100644 index 0000000000..e0f2bfca7d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/BackwardsCompatibility/JsonCheckpointSerializationTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using FluentAssertions; +using Microsoft.Agents.AI.Workflows.Checkpointing; + +namespace Microsoft.Agents.AI.Workflows.UnitTests.BackwardsCompatibility; + +/// +/// Tests pinning the JSON shape of checkpoint-adjacent types so older payloads keep +/// deserializing correctly after the Outputs overhaul (see implementation-plan §5.7). +/// +public class JsonCheckpointSerializationTests +{ + private static readonly JsonSerializerOptions s_options = WorkflowsJsonUtilities.DefaultOptions; + + private static WorkflowInfo BuildInfoWithOutputExecutors(Dictionary> outputs) + => new( + executors: new Dictionary(), + edges: new Dictionary>(), + requestPorts: [], + startExecutorId: "start", + outputExecutorIds: outputs); + + // ---------- WorkflowOutputEvent.Tags in-process round-trip (no JSON) ---------- + + [Fact] + public void Test_WorkflowOutputEvent_SingleTagCtorPopulatesTags() + { + WorkflowOutputEvent evt = new(data: "hello", executorId: "e1", tag: OutputTag.Intermediate); + + evt.ExecutorId.Should().Be("e1"); + evt.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + evt.HasTag(OutputTag.Intermediate).Should().BeTrue(); + evt.IsIntermediate().Should().BeTrue(); + } + + [Fact] + public void Test_WorkflowOutputEvent_NoTagsCtorIsUntagged() + { + WorkflowOutputEvent evt = new(data: "hello", executorId: "e1"); + + evt.Tags.Should().BeEmpty(); + evt.IsIntermediate().Should().BeFalse("an event with no tags is a terminal/regular output"); + } + + [Fact] + public void Test_WorkflowOutputEvent_MultiTagCtorPreservesAllTags() + { + OutputTag customTag = JsonSerializer.Deserialize("\"custom\"", s_options); + + WorkflowOutputEvent evt = new(data: "hello", executorId: "e1", tags: new[] { OutputTag.Intermediate, customTag }); + + evt.Tags.Should().HaveCount(2); + evt.HasTag(OutputTag.Intermediate).Should().BeTrue(); + evt.HasTag(customTag).Should().BeTrue(); + evt.IsIntermediate().Should().BeTrue(); + } + + // ---------- WorkflowInfo.OutputExecutorIds shape ---------- + // + // Note: per the comment in WorkflowsJsonUtilities, WorkflowEvent / WorkflowOutputEvent + // is *not* currently a serialized checkpoint shape (events are not persisted into + // checkpoints today), so we do not pin a JSON round-trip for Tags on the event itself + // here. The tag JSON round-trip is exercised by OutputTagTests; the + // OutputExecutorIds map shape is the actually-load-bearing back-compat surface. + + [Fact] + public void Test_JsonCheckpoint_WorkflowOutputExecutorsReadsLegacyArrayShape() + { + const string LegacyJson = """ + { + "executors": {}, + "edges": {}, + "requestPorts": [], + "startExecutorId": "start", + "outputExecutorIds": ["a", "b"] + } + """; + + WorkflowInfo? info = JsonSerializer.Deserialize(LegacyJson, s_options); + + info.Should().NotBeNull(); + info!.OutputExecutorIds.Should().HaveCount(2); + info.OutputExecutorIds["a"].Should().BeEmpty("legacy ids are untagged regular outputs"); + info.OutputExecutorIds["b"].Should().BeEmpty(); + } + + [Fact] + public void Test_JsonCheckpoint_WorkflowOutputExecutorsWritesMapShape() + { + Dictionary> outputs = new() + { + ["a"] = [], + ["b"] = [OutputTag.Intermediate], + }; + + WorkflowInfo info = BuildInfoWithOutputExecutors(outputs); + + string json = JsonSerializer.Serialize(info, s_options); + + WorkflowInfo? back = JsonSerializer.Deserialize(json, s_options); + + back.Should().NotBeNull(); + back!.OutputExecutorIds.Should().HaveCount(2); + back.OutputExecutorIds["a"].Should().BeEmpty(); + back.OutputExecutorIds["b"].Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + + // The map shape is detectable in the serialized JSON: the property value starts with `{`, not `[`. + int idx = json.IndexOf("\"outputExecutorIds\"", System.StringComparison.Ordinal); + idx.Should().BeGreaterThan(-1); + int colon = json.IndexOf(':', idx); + int firstNonSpace = colon + 1; + while (firstNonSpace < json.Length && char.IsWhiteSpace(json[firstNonSpace])) + { + firstNonSpace++; + } + json[firstNonSpace].Should().Be('{', "OutputExecutorIds is written in the new map shape"); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ConcurrentWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ConcurrentWorkflowBuilderTests.cs new file mode 100644 index 0000000000..33405a61e9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ConcurrentWorkflowBuilderTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Agents.AI.Workflows.UnitTests.Futures; +using Microsoft.Extensions.AI; + +#pragma warning disable SYSLIB1045 // Use GeneratedRegex +#pragma warning disable RCS1186 // Use Regex instance instead of static method + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +public class ConcurrentWorkflowBuilderTests +{ + [Fact] + public void Test_ConcurrentWorkflowBuilder_InvalidArguments_Throws() + { + Assert.Throws("agents", () => new ConcurrentWorkflowBuilder(null!)); + Assert.Throws("agents", () => new ConcurrentWorkflowBuilder().Build()); + + Assert.Throws("agents", () => AgentWorkflowBuilder.BuildConcurrent(null!)); + Assert.Throws("agents", () => AgentWorkflowBuilder.CreateConcurrentBuilderWith(null!)); + } + + [Fact] + public async Task Test_ConcurrentWorkflowBuilder_AgentsRunInParallelAsync() + { + StrongBox> barrier = new(); + StrongBox remaining = new(); + + var workflow = new ConcurrentWorkflowBuilder( + new OrchestrationTestHelpers.DoubleEchoAgentWithBarrier("agent1", barrier, remaining), + new OrchestrationTestHelpers.DoubleEchoAgentWithBarrier("agent2", barrier, remaining)) + .Build(); + + for (int iter = 0; iter < 3; iter++) + { + barrier.Value = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + remaining.Value = 2; + + (string updateText, List? result, _, _) = + await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, "abc")]); + Assert.NotEmpty(updateText); + Assert.NotNull(result); + + // TODO: https://github.com/microsoft/agent-framework/issues/784 + // These asserts are flaky until we guarantee message delivery order. + Assert.Single(Regex.Matches(updateText, "agent1")); + Assert.Single(Regex.Matches(updateText, "agent2")); + Assert.Equal(4, Regex.Matches(updateText, "abc").Count); + Assert.Equal(2, result.Count); + } + } + + [Fact] + public void Test_ConcurrentWorkflowBuilder_DefaultDesignationsMatchSpec() + { + Workflow workflow = new ConcurrentWorkflowBuilder( + new OrchestrationTestHelpers.DoubleEchoAgent("agent1"), + new OrchestrationTestHelpers.DoubleEchoAgent("agent2"), + new OrchestrationTestHelpers.DoubleEchoAgent("agent3")) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + designations.Where(kvp => kvp.Value.Count == 0) + .Should().ContainSingle("ConcurrentEndExecutor is the sole terminal output by default"); + designations.Where(kvp => kvp.Value.Contains(OutputTag.Intermediate)) + .Should().HaveCount(6, "every agent (3) and per-agent accumulator (3) is designated intermediate by default"); + } + + [Fact] + public void Test_ConcurrentWorkflowBuilder_ExplicitDesignationsReplaceDefaults() + { + OrchestrationTestHelpers.DoubleEchoAgent a1 = new("agent1"); + OrchestrationTestHelpers.DoubleEchoAgent a2 = new("agent2"); + OrchestrationTestHelpers.DoubleEchoAgent a3 = new("agent3"); + + Workflow workflow = new ConcurrentWorkflowBuilder(a1, a2, a3) + .WithOutputFrom(a1) + .WithIntermediateOutputFrom([a2]) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Should().HaveCount(2, + "only the two explicitly-designated agents land on the inner builder; the end + accumulator defaults are suppressed"); + designations.Values.Where(tags => tags.Count == 0) + .Should().ContainSingle("agent1 is the only terminal designation"); + designations.Values.Where(tags => tags.Contains(OutputTag.Intermediate)) + .Should().ContainSingle("agent2 is the only intermediate designation"); + } + + [Fact] + public void Test_ConcurrentWorkflowBuilder_DesignationForNonParticipantThrows() + { + OrchestrationTestHelpers.DoubleEchoAgent participant = new("p1"); + OrchestrationTestHelpers.DoubleEchoAgent stranger = new("stranger"); + + ConcurrentWorkflowBuilder builder = new ConcurrentWorkflowBuilder(participant) + .WithIntermediateOutputFrom([stranger]); + + Action build = () => builder.Build(); + build.Should().Throw().WithMessage("*stranger*"); + } + + [Fact] + public void Test_ConcurrentWorkflowBuilder_WithNamePropagatesToWorkflow() + { + Workflow workflow = new ConcurrentWorkflowBuilder(new OrchestrationTestHelpers.DoubleEchoAgent("agent1")) + .WithName("named-concurrent") + .Build(); + + workflow.Name.Should().Be("named-concurrent"); + } + + [Fact] + public void Test_ConcurrentWorkflowBuilder_WithDescriptionPropagatesToWorkflow() + { + Workflow workflow = new ConcurrentWorkflowBuilder(new OrchestrationTestHelpers.DoubleEchoAgent("agent1")) + .WithDescription("describes the concurrent fan-out/fan-in") + .Build(); + + workflow.Description.Should().Be("describes the concurrent fan-out/fan-in"); + } + + [Collection(FuturesSerialCollection.Name)] + public class AsAgentForwarding + { + [Fact] + public async Task Test_ConcurrentWorkflowBuilder_AsAgent_OnlyTerminalDesignationSurfacesAsync() + { + using FuturesScope _ = new(enabled: true); + + OrchestrationTestHelpers.DoubleEchoAgent agent1 = new("agent1"); + OrchestrationTestHelpers.DoubleEchoAgent agent2 = new("agent2"); + + // Designate only agent1 as a terminal output source — agent2 and the fan-in + // aggregator default-intermediate designations are suppressed. + Workflow workflow = new ConcurrentWorkflowBuilder(agent1, agent2) + .WithOutputFrom(agent1) + .Build(); + + List updates = await workflow + .AsAIAgent("WorkflowAgent") + .RunStreamingAsync(new ChatMessage(ChatRole.User, "abc")) + .ToListAsync(); + + HashSet authoredBy = updates + .Select(u => u.AuthorName) + .Where(n => !string.IsNullOrEmpty(n)) + .Select(n => n!) + .ToHashSet(); + + authoredBy.Should().Contain("agent1", "the designated agent must surface"); + authoredBy.Should().NotContain("agent2", + "the undesignated agent must not surface when only one is designated under Futures-on"); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/Futures.AgentResponseOutputFilteringAndTaggingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/Futures.AgentResponseOutputFilteringAndTaggingTests.cs new file mode 100644 index 0000000000..955a354332 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/Futures.AgentResponseOutputFilteringAndTaggingTests.cs @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests.Futures; + +/// +/// Runner-level coverage for . +/// Exercises every combination of (flag on/off) × (designation kind) × (payload shape) to pin the +/// runner's behavior in both the legacy bypass path and the unified filter-and-tag path. +/// +public static partial class FuturesTests +{ + [Collection(FuturesSerialCollection.Name)] + public class AgentResponseOutputFilteringAndTaggingTests + { + private const string SourceId = "yielder"; + + private static AgentResponse SampleResponse(string text = "hi") + => new(new ChatMessage(ChatRole.Assistant, text)); + + private static AgentResponseUpdate SampleUpdate(string text = "tick") + => new(ChatRole.Assistant, text); + + private static async Task> RunAsync(Workflow workflow, T input) where T : notnull + { + List events = []; + await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input).ConfigureAwait(false); + await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) + { + events.Add(evt); + } + return events; + } + + private static Workflow BuildAgentResponseWorkflow(Action? designate = null) + { + YieldAgentResponseExecutor exec = new(SourceId); + WorkflowBuilder builder = new(exec); + designate?.Invoke(builder, exec); + return builder.Build(); + } + + private static Workflow BuildAgentResponseUpdateWorkflow(Action? designate = null) + { + YieldAgentResponseUpdateExecutor exec = new(SourceId); + WorkflowBuilder builder = new(exec); + designate?.Invoke(builder, exec); + return builder.Build(); + } + + private static Workflow BuildPocoWorkflow(Action? designate = null) + { + YieldPocoExecutor exec = new(SourceId); + WorkflowBuilder builder = new(exec); + designate?.Invoke(builder, exec); + return builder.Build(); + } + + // F1 + [Fact] + public async Task Test_Runner_LegacyAgentResponseBypass_RaisesUntaggedEventAsync() + { + using FuturesScope _ = new(enabled: false); + Workflow workflow = BuildAgentResponseWorkflow(designate: null); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.ExecutorId.Should().Be(SourceId); + emitted.Tags.Should().BeEmpty("legacy bypass attaches no tags"); + emitted.IsIntermediate().Should().BeFalse(); + } + + // F2 + [Fact] + public async Task Test_Runner_LegacyAgentResponseUpdateBypass_RaisesUntaggedEventAsync() + { + using FuturesScope _ = new(enabled: false); + Workflow workflow = BuildAgentResponseUpdateWorkflow(designate: null); + + List events = await RunAsync(workflow, "go"); + + AgentResponseUpdateEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEmpty(); + } + + // F3 + [Fact] + public async Task Test_Runner_LegacyBypassIgnoresDesignationAsync() + { + using FuturesScope _ = new(enabled: false); + Workflow workflow = BuildAgentResponseWorkflow(static (b, e) => b.WithIntermediateOutputFrom([e])); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEmpty("legacy bypass ignores the designation entirely"); + emitted.IsIntermediate().Should().BeFalse("legacy bypass does not propagate tags"); + } + + // F4 + [Fact] + public async Task Test_Runner_LegacyPocoIsFilteredAsync() + { + using FuturesScope _ = new(enabled: false); + Workflow workflow = BuildPocoWorkflow(designate: null); + + List events = await RunAsync(workflow, "go"); + + events.OfType().Should().BeEmpty("POCO outputs always go through the filter; undesignated source is dropped"); + } + + // F5 + [Fact] + public async Task Test_Runner_UndesignatedAgentResponseIsFilteredWhenFuturesOnAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseWorkflow(designate: null); + + List events = await RunAsync(workflow, "go"); + + events.OfType().Should().BeEmpty( + "with the future on, AgentResponse must be designated to surface"); + } + + // F6 + [Fact] + public async Task Test_Runner_DesignatedTerminalAgentResponseHasEmptyTagsAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseWorkflow(static (b, e) => b.WithOutputFrom(e)); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEmpty("terminal designation carries no tag"); + emitted.IsIntermediate().Should().BeFalse(); + } + + // F7 + [Fact] + public async Task Test_Runner_DesignatedIntermediateAgentResponseHasIntermediateTagAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseWorkflow(static (b, e) => b.WithIntermediateOutputFrom([e])); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + emitted.IsIntermediate().Should().BeTrue(); + } + + // F8 + [Fact] + public async Task Test_Runner_DesignatedIntermediateAgentResponseUpdateHasIntermediateTagAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseUpdateWorkflow(static (b, e) => b.WithIntermediateOutputFrom([e])); + + List events = await RunAsync(workflow, "go"); + + AgentResponseUpdateEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + emitted.IsIntermediate().Should().BeTrue(); + } + + // F9 + [Fact] + public async Task Test_Runner_TagsAccumulateOutputThenIntermediateAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseWorkflow(static (b, e) => + { + b.WithOutputFrom(e); + b.WithIntermediateOutputFrom([e]); + }); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }, + "terminal+intermediate union is {{ Intermediate }} (terminal contributes the entry but no tag)"); + emitted.IsIntermediate().Should().BeTrue(); + } + + // F10 + [Fact] + public async Task Test_Runner_TagsAccumulateIntermediateThenOutputAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseWorkflow(static (b, e) => + { + b.WithIntermediateOutputFrom([e]); + b.WithOutputFrom(e); + }); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }, "designation order is irrelevant"); + emitted.IsIntermediate().Should().BeTrue(); + } + + // F11 + [Fact] + public async Task Test_Runner_DesignatedIntermediatePocoHasIntermediateTagAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildPocoWorkflow(static (b, e) => b.WithIntermediateOutputFrom([e])); + + List events = await RunAsync(workflow, "go"); + + WorkflowOutputEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Should().NotBeOfType(); + emitted.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + emitted.IsIntermediate().Should().BeTrue(); + } + + // F12 + [Fact] + public async Task Test_Runner_DesignatedTerminalPocoHasEmptyTagsAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildPocoWorkflow(static (b, e) => b.WithOutputFrom(e)); + + List events = await RunAsync(workflow, "go"); + + WorkflowOutputEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEmpty(); + emitted.IsIntermediate().Should().BeFalse(); + } + + // F13 + [Fact] + public async Task Test_Runner_RepeatedTerminalDesignationDedupesAsync() + { + using FuturesScope _ = new(enabled: true); + Workflow workflow = BuildAgentResponseWorkflow(static (b, e) => + { + b.WithOutputFrom(e); + b.WithOutputFrom(e); + }); + + List events = await RunAsync(workflow, "go"); + + AgentResponseEvent emitted = events.OfType().Should().ContainSingle().Subject; + emitted.Tags.Should().BeEmpty("repeated terminal designation contributes no tag"); + } + + // ---- Executors ----------------------------------------------------------- + + internal sealed class YieldAgentResponseExecutor(string id) : Executor(id) + { + protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) + => protocolBuilder.ConfigureRoutes(rb => rb.AddHandler(this.HandleAsync)); + + private ValueTask HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken) + => new(SampleResponse(input)); + } + + internal sealed class YieldAgentResponseUpdateExecutor(string id) : Executor(id) + { + protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) + => protocolBuilder.ConfigureRoutes(rb => rb.AddHandler(this.HandleAsync)); + + private ValueTask HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken) + => new(SampleUpdate(input)); + } + + public sealed record Poco(string Value); + + internal sealed class YieldPocoExecutor(string id) : Executor(id) + { + protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) + => protocolBuilder.ConfigureRoutes(rb => rb.AddHandler(this.HandleAsync)); + + private ValueTask HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken) + => new(new Poco(input)); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesScope.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesScope.cs new file mode 100644 index 0000000000..46798e96b0 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesScope.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Agents.AI.Workflows.UnitTests.Futures; + +/// +/// Sets for +/// the lifetime of the scope, restoring the prior value on dispose. Pair every use with +/// using and run inside the FuturesSerial xUnit collection to avoid leaking +/// state across parallel tests. +/// +internal sealed class FuturesScope : IDisposable +{ + private readonly bool _previous; + + public FuturesScope(bool enabled) + { + this._previous = Workflows.Futures.EnableAgentResponseOutputTaggingAndFiltering; + Workflows.Futures.EnableAgentResponseOutputTaggingAndFiltering = enabled; + } + + public void Dispose() + { + Workflows.Futures.EnableAgentResponseOutputTaggingAndFiltering = this._previous; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesSerialCollection.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesSerialCollection.cs new file mode 100644 index 0000000000..c8e716e119 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Futures/FuturesSerialCollection.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Agents.AI.Workflows.UnitTests.Futures; + +/// +/// xUnit collection marker for tests that mutate the process-global +/// switches. Membership in this collection serializes +/// the tests against each other so that cannot leak state +/// into a concurrently running test. +/// +[CollectionDefinition(Name, DisableParallelization = true)] +[SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", + Justification = "xUnit's [CollectionDefinition] pattern names the marker type after the collection's purpose; the 'Collection' suffix is idiomatic.")] +public sealed class FuturesSerialCollection +{ + public const string Name = "FuturesSerial"; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatWorkflowBuilderTests.cs new file mode 100644 index 0000000000..1eb2d32434 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/GroupChatWorkflowBuilderTests.cs @@ -0,0 +1,477 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Agents.AI.Workflows.InProc; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +public class GroupChatWorkflowBuilderTests +{ + [Fact] + public void BuildGroupChat_InvalidArguments_Throws() + { + Assert.Throws("managerFactory", () => AgentWorkflowBuilder.CreateGroupChatBuilderWith(null!)); + + var groupChat = AgentWorkflowBuilder.CreateGroupChatBuilderWith(_ => new RoundRobinGroupChatManager([new OrchestrationTestHelpers.DoubleEchoAgent("a1")])); + Assert.NotNull(groupChat); + Assert.Throws("agents", () => groupChat.AddParticipants(null!)); + Assert.Throws("agents", () => groupChat.AddParticipants([null!])); + Assert.Throws("agents", () => groupChat.AddParticipants(new OrchestrationTestHelpers.DoubleEchoAgent("a1"), null!)); + + Assert.Throws("agents", () => new RoundRobinGroupChatManager(null!)); + } + + [Fact] + public void GroupChatManager_MaximumIterationCount_Invalid_Throws() + { + var manager = new RoundRobinGroupChatManager([new OrchestrationTestHelpers.DoubleEchoAgent("a1")]); + + const int DefaultMaxIterations = 40; + Assert.Equal(DefaultMaxIterations, manager.MaximumIterationCount); + Assert.Throws("value", void () => manager.MaximumIterationCount = 0); + Assert.Throws("value", void () => manager.MaximumIterationCount = -1); + Assert.Equal(DefaultMaxIterations, manager.MaximumIterationCount); + + manager.MaximumIterationCount = 30; + Assert.Equal(30, manager.MaximumIterationCount); + + manager.MaximumIterationCount = 1; + Assert.Equal(1, manager.MaximumIterationCount); + + manager.MaximumIterationCount = int.MaxValue; + Assert.Equal(int.MaxValue, manager.MaximumIterationCount); + } + + [Fact] + public void BuildGroupChat_WithNameAndDescription_SetsWorkflowNameAndDescription() + { + const string WorkflowName = "Test Group Chat"; + const string WorkflowDescription = "A test group chat workflow"; + + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) + .AddParticipants(new OrchestrationTestHelpers.DoubleEchoAgent("agent1"), new OrchestrationTestHelpers.DoubleEchoAgent("agent2")) + .WithName(WorkflowName) + .WithDescription(WorkflowDescription) + .Build(); + + Assert.Equal(WorkflowName, workflow.Name); + Assert.Equal(WorkflowDescription, workflow.Description); + } + + [Fact] + public void BuildGroupChat_WithNameOnly_SetsWorkflowName() + { + const string WorkflowName = "Named Group Chat"; + + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) + .AddParticipants(new OrchestrationTestHelpers.DoubleEchoAgent("agent1")) + .WithName(WorkflowName) + .Build(); + + Assert.Equal(WorkflowName, workflow.Name); + Assert.Null(workflow.Description); + } + + [Fact] + public void BuildGroupChat_WithoutNameOrDescription_DefaultsToNull() + { + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 }) + .AddParticipants(new OrchestrationTestHelpers.DoubleEchoAgent("agent1")) + .Build(); + + Assert.Null(workflow.Name); + Assert.Null(workflow.Description); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public async Task BuildGroupChat_AgentsRunInOrderAsync(int maxIterations) + { + const int NumAgents = 3; + var workflow = AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxIterations }) + .AddParticipants(new OrchestrationTestHelpers.DoubleEchoAgent("agent1"), new OrchestrationTestHelpers.DoubleEchoAgent("agent2")) + .AddParticipants(new OrchestrationTestHelpers.DoubleEchoAgent("agent3")) + .Build(); + + for (int iter = 0; iter < 3; iter++) + { + const string UserInput = "abc"; + (string updateText, List? result, _, _) = await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + + Assert.NotNull(result); + Assert.Equal(maxIterations + 1, result.Count); + + Assert.Equal(ChatRole.User, result[0].Role); + Assert.Null(result[0].AuthorName); + Assert.Equal(UserInput, result[0].Text); + + // The group-chat host broadcasts each new message (initial user input + each speaker's + // response) to every participant except the speaker that produced it. The selected + // speaker therefore sees only what's been broadcast to it since its previous turn. + string[] agentIds = ["agent1", "agent2", "agent3"]; + List[] buffers = new List[NumAgents]; + for (int a = 0; a < NumAgents; a++) + { + buffers[a] = [UserInput]; + } + + string[] texts = new string[maxIterations + 1]; + texts[0] = UserInput; + string expectedTotal = string.Empty; + for (int i = 1; i < maxIterations + 1; i++) + { + int speakerIdx = (i - 1) % NumAgents; + string id = agentIds[speakerIdx]; + string concatReceived = string.Concat(buffers[speakerIdx]); + texts[i] = $"{id}{Double(concatReceived)}"; + buffers[speakerIdx].Clear(); + for (int a = 0; a < NumAgents; a++) + { + if (a == speakerIdx) + { + continue; + } + + buffers[a].Add(texts[i]); + } + + Assert.Equal(ChatRole.Assistant, result[i].Role); + Assert.Equal(id, result[i].AuthorName); + Assert.Equal(texts[i], result[i].Text); + expectedTotal += texts[i]; + } + + Assert.Equal(expectedTotal, updateText); + Assert.Equal(UserInput + expectedTotal, string.Concat(result)); + + static string Double(string s) => s + s; + } + } + + [Fact] + public void Test_GroupChatWorkflowBuilder_DefaultDesignationsMatchSpec() + { + OrchestrationTestHelpers.DoubleEchoAgent a1 = new("agent1"); + OrchestrationTestHelpers.DoubleEchoAgent a2 = new("agent2"); + OrchestrationTestHelpers.DoubleEchoAgent a3 = new("agent3"); + + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 1 }) + .AddParticipants(a1, a2, a3) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Where(kvp => kvp.Value.Count == 0) + .Should().ContainSingle("group-chat host is the sole terminal output executor by default"); + designations.Where(kvp => kvp.Value.Contains(OutputTag.Intermediate)) + .Should().HaveCount(3, "every participant is designated intermediate by default"); + } + + [Fact] + public void Test_GroupChatWorkflowBuilder_ExplicitDesignationsReplaceDefaults() + { + OrchestrationTestHelpers.DoubleEchoAgent a1 = new("agent1"); + OrchestrationTestHelpers.DoubleEchoAgent a2 = new("agent2"); + OrchestrationTestHelpers.DoubleEchoAgent a3 = new("agent3"); + + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 1 }) + .AddParticipants(a1, a2, a3) + .WithOutputFrom(a1) + .WithIntermediateOutputFrom([a2]) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Should().HaveCount(2, + "only the two explicitly-designated agents land on the inner builder; the host default is suppressed"); + designations.Values.Where(tags => tags.Count == 0) + .Should().ContainSingle("agent1 is the only terminal designation"); + designations.Values.Where(tags => tags.Contains(OutputTag.Intermediate)) + .Should().ContainSingle("agent2 is the only intermediate designation"); + } + + [Fact] + public void Test_GroupChatWorkflowBuilder_DesignationForNonParticipantThrows() + { + OrchestrationTestHelpers.DoubleEchoAgent participant = new("p1"); + OrchestrationTestHelpers.DoubleEchoAgent stranger = new("stranger"); + + GroupChatWorkflowBuilder builder = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 1 }) + .AddParticipants(participant) + .WithOutputFrom(stranger); + + Action build = () => builder.Build(); + build.Should().Throw().WithMessage("*stranger*"); + } + + private sealed class RecordingAgent(string name) : AIAgent + { + public List> Invocations { get; } = []; + + public override string Name => name; + + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => new(new RecordingAgentSession()); + + protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => new(new RecordingAgentSession()); + + protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => default; + + protected override Task RunCoreAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Yield(); + + this.Invocations.Add(messages.Select(m => m.Text).ToList()); + + string id = Guid.NewGuid().ToString("N"); + yield return new AgentResponseUpdate(ChatRole.Assistant, name) { AuthorName = name, MessageId = id }; + } + } + + private sealed class RecordingAgentSession() : AgentSession(); + + [Fact] + public async Task BuildGroupChat_BroadcastsDeltaAndTargetsTurnTokenToSpeakerOnlyAsync() + { + var agentA = new RecordingAgent("agentA"); + var agentB = new RecordingAgent("agentB"); + var agentC = new RecordingAgent("agentC"); + + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 4 }) + .AddParticipants(agentA, agentB, agentC) + .Build(); + + const string UserInput = "hello"; + (_, List? result, _, _) = await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + + Assert.NotNull(result); + Assert.Equal(5, result.Count); // initial user input + 4 agent turns + Assert.Collection( + result, + m => Assert.Equal(UserInput, m.Text), + m => Assert.Equal("agentA", m.Text), + m => Assert.Equal("agentB", m.Text), + m => Assert.Equal("agentC", m.Text), + m => Assert.Equal("agentA", m.Text)); + + // Each agent's TurnToken fires exactly when it is the selected speaker — invocation counts + // confirm only the chosen participant receives a TurnToken on each round. + Assert.Equal(2, agentA.Invocations.Count); + Assert.Single(agentB.Invocations); + Assert.Single(agentC.Invocations); + + // Turn 1: agentA is the first speaker. Initial broadcast went to every participant, so + // agentA's only buffered message is the user input. + Assert.Equal([UserInput], agentA.Invocations[0]); + + // Turn 2: agentB. It received the initial broadcast (user input) plus turn-1 broadcast of + // agentA's response (agentA itself is excluded as the last speaker). + Assert.Equal([UserInput, "agentA"], agentB.Invocations[0]); + + // Turn 3: agentC. It also received every broadcast so far (it has never been excluded). + Assert.Equal([UserInput, "agentA", "agentB"], agentC.Invocations[0]); + + // Turn 4: agentA again. It was excluded on turn 2's broadcast (its own response), but + // received turn-3 (agentB's response) and turn-4 (agentC's response) deltas. + Assert.Equal(["agentB", "agentC"], agentA.Invocations[1]); + } + + [Fact] + public async Task BuildGroupChat_UpdateHistoryAsync_FiltersBroadcastPayloadAsync() + { + var agentA = new RecordingAgent("agentA"); + var agentB = new RecordingAgent("agentB"); + + var workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new PrefixingGroupChatManager(agents, "[broadcast] ") { MaximumIterationCount = 2 }) + .AddParticipants(agentA, agentB) + .Build(); + + const string UserInput = "hello"; + await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + + // Turn 1: agentA's buffer contains only the initial broadcast, which UpdateHistoryAsync + // prefixed. + Assert.Equal(["[broadcast] hello"], agentA.Invocations[0]); + + // Turn 2: agentB received both the initial broadcast and agentA's response — both passed + // through UpdateHistoryAsync before being broadcast. + Assert.Equal(["[broadcast] hello", "[broadcast] agentA"], agentB.Invocations[0]); + } + + [Fact] + public async Task BuildGroupChat_CheckpointResumeMidConversation_PreservesIterationCursorAndBroadcastExclusionAsync() + { + const string UserInput = "hello"; + const int MaxIterations = 6; + + // --- Baseline: run the full conversation under checkpointing and capture every checkpoint + // plus the final transcript. The same workflow + agents are reused for the resume, + // because the runner enforces workflow-shape compatibility on ResumeStreamingAsync. --- + BaselineRunResult baseline = await RunGroupChatBaselineAsync(UserInput, MaxIterations); + + // We need at least one mid-conversation checkpoint to resume from. The baseline produces a + // checkpoint per superstep, which for MaxIterations=6 yields many; we pick a checkpoint + // captured roughly midway so the resumed run still has work to do. + Assert.True(baseline.Checkpoints.Count >= 5, + $"expected at least 5 checkpoints in the baseline, got {baseline.Checkpoints.Count}"); + + int midIndex = baseline.Checkpoints.Count / 2; + CheckpointInfo midCheckpoint = baseline.Checkpoints[midIndex]; + + // Snapshot per-agent invocation counts before the resume so we can isolate the invocations + // produced after the checkpoint is restored. + int aPreCount = baseline.AgentA.Invocations.Count; + int bPreCount = baseline.AgentB.Invocations.Count; + int cPreCount = baseline.AgentC.Invocations.Count; + + // --- Resume the same workflow from the mid-conversation checkpoint. --- + List? resumedResult = null; + await using (StreamingRun resumed = await baseline.Environment + .ResumeStreamingAsync(baseline.Workflow, midCheckpoint)) + { + await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false)) + { + if (evt is WorkflowOutputEvent o) + { + resumedResult = o.As>(); + } + else if (evt is WorkflowErrorEvent err) + { + Assert.Fail($"Resumed workflow failed: {err.Exception}"); + } + } + } + + // (1) Iteration-count continuity: the resumed run terminates with exactly the same number + // of turns the baseline produced — proves IterationCount was rehydrated and the manager + // honored MaximumIterationCount across the boundary. + Assert.NotNull(resumedResult); + Assert.Equal(baseline.Result.Count, resumedResult!.Count); + + // (2) Next-speaker consistency: the full transcript (initial input + every speaker's turn, + // in order) matches the baseline — proves the round-robin cursor was restored. + List baselineTranscript = [.. baseline.Result.Select(m => m.Text)]; + List resumedTranscript = [.. resumedResult.Select(m => m.Text)]; + Assert.Equal(baselineTranscript, resumedTranscript); + + // (3) Broadcast exclusion holds across resume: a RecordingAgent's response text is just its + // own Name. Examine only the invocations recorded after the resume. If the host failed + // to exclude the current speaker from its post-resume broadcasts, an agent's next + // invocation buffer would contain its own previously produced response. Asserting that + // no post-resume invocation input contains the invoking agent's own name proves the + // exclusion was preserved through checkpoint+restore. + AssertPostResumeBroadcastExclusion(baseline.AgentA, aPreCount); + AssertPostResumeBroadcastExclusion(baseline.AgentB, bPreCount); + AssertPostResumeBroadcastExclusion(baseline.AgentC, cPreCount); + + // Sanity: at least one agent was actually invoked after the resume; otherwise the test + // would trivially pass even if the host stopped scheduling turns after restore. + int totalPost = baseline.AgentA.Invocations.Count - aPreCount + + (baseline.AgentB.Invocations.Count - bPreCount) + + (baseline.AgentC.Invocations.Count - cPreCount); + Assert.True(totalPost > 0, "at least one agent should be invoked after resuming from the mid-conversation checkpoint"); + + static void AssertPostResumeBroadcastExclusion(RecordingAgent agent, int preCount) + { + for (int i = preCount; i < agent.Invocations.Count; i++) + { + Assert.DoesNotContain(agent.Name, agent.Invocations[i]); + } + } + } + + private sealed record BaselineRunResult( + Workflow Workflow, + InProcessExecutionEnvironment Environment, + RecordingAgent AgentA, + RecordingAgent AgentB, + RecordingAgent AgentC, + List Result, + List Checkpoints, + CheckpointManager CheckpointManager); + + private static async Task RunGroupChatBaselineAsync(string userInput, int maxIterations) + { + var agentA = new RecordingAgent("agentA"); + var agentB = new RecordingAgent("agentB"); + var agentC = new RecordingAgent("agentC"); + + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = maxIterations }) + .AddParticipants(agentA, agentB, agentC) + .Build(); + + CheckpointManager checkpointMgr = CheckpointManager.CreateInMemory(); + InProcessExecutionEnvironment env = ExecutionEnvironment.InProcess_Lockstep + .ToWorkflowExecutionEnvironment() + .WithCheckpointing(checkpointMgr); + + List checkpoints = []; + List? finalResult = null; + + await using (StreamingRun run = await env.OpenStreamingAsync(workflow)) + { + await run.TrySendMessageAsync(new List { new(ChatRole.User, userInput) }); + await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false)) + { + switch (evt) + { + case SuperStepCompletedEvent step when step.CompletionInfo?.Checkpoint is { } cp: + checkpoints.Add(cp); + break; + case WorkflowOutputEvent o: + finalResult = o.As>(); + break; + case WorkflowErrorEvent err: + Assert.Fail($"Baseline workflow failed: {err.Exception}"); + break; + } + } + } + + Assert.NotNull(finalResult); + return new BaselineRunResult(workflow, env, agentA, agentB, agentC, finalResult!, checkpoints, checkpointMgr); + } + + private sealed class PrefixingGroupChatManager(IReadOnlyList agents, string prefix) : RoundRobinGroupChatManager(agents) + { + protected internal override ValueTask> UpdateHistoryAsync( + IReadOnlyList history, + CancellationToken cancellationToken = default) + { + IEnumerable prefixed = + history.Select(m => new ChatMessage(m.Role, $"{prefix}{m.Text}") { AuthorName = m.AuthorName }); + + return new(prefixed); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffWorkflowBuilderTests.cs new file mode 100644 index 0000000000..858b4a5cf7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/HandoffWorkflowBuilderTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +/// +/// Tests focused on 's output-designation surface — +/// the Python-aligned defaults applied at +/// when the user has not made explicit designations, and the memoized +/// WithOutputFrom / WithIntermediateOutputFrom replay otherwise. +/// +#pragma warning disable MAAIW001 // Experimental: HandoffWorkflowBuilder +public class HandoffWorkflowBuilderTests +{ + [Fact] + public void Test_HandoffWorkflowBuilder_DefaultDesignationsMatchSpec() + { + OrchestrationTestHelpers.DoubleEchoAgent coordinator = new("coordinator"); + OrchestrationTestHelpers.DoubleEchoAgent specialist = new("specialist"); + + Workflow workflow = AgentWorkflowBuilder + .CreateHandoffBuilderWith(coordinator) + .WithHandoff(coordinator, specialist) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Where(kvp => kvp.Value.Count == 0) + .Should().ContainSingle("the handoff end executor is the sole terminal output by default"); + designations.Where(kvp => kvp.Value.Contains(OutputTag.Intermediate)) + .Should().HaveCount(2, "both the coordinator and the specialist are designated intermediate by default"); + } + + [Fact] + public void Test_HandoffWorkflowBuilder_ExplicitDesignationsReplaceDefaults() + { + OrchestrationTestHelpers.DoubleEchoAgent coordinator = new("coordinator"); + OrchestrationTestHelpers.DoubleEchoAgent specialist = new("specialist"); + + Workflow workflow = AgentWorkflowBuilder + .CreateHandoffBuilderWith(coordinator) + .WithHandoff(coordinator, specialist) + .WithOutputFrom(coordinator) + .WithIntermediateOutputFrom([specialist]) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Should().HaveCount(2, + "only the user-specified designations land on the inner builder; the handoff-end default is suppressed"); + designations.Values.Where(tags => tags.Count == 0) + .Should().ContainSingle("coordinator is the only terminal designation"); + designations.Values.Where(tags => tags.Contains(OutputTag.Intermediate)) + .Should().ContainSingle("specialist is the only intermediate designation"); + } + + [Fact] + public void Test_HandoffWorkflowBuilder_DesignationForNonParticipantThrows() + { + OrchestrationTestHelpers.DoubleEchoAgent coordinator = new("coordinator"); + OrchestrationTestHelpers.DoubleEchoAgent specialist = new("specialist"); + OrchestrationTestHelpers.DoubleEchoAgent stranger = new("stranger"); + + HandoffWorkflowBuilder builder = AgentWorkflowBuilder + .CreateHandoffBuilderWith(coordinator) + .WithHandoff(coordinator, specialist) + .WithIntermediateOutputFrom([stranger]); + + Action build = () => builder.Build(); + build.Should().Throw().WithMessage("*stranger*"); + } +} +#pragma warning restore MAAIW001 diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterAndOutputFilterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterTests.cs similarity index 74% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterAndOutputFilterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterTests.cs index c7c231c63e..a849b602bd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterAndOutputFilterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterTests.cs @@ -122,50 +122,3 @@ public sealed class InputWaiterTests : IDisposable await waitTask; } } - -public class OutputFilterTests -{ - private static OutputFilter CreateFilterWithOutputFrom(string outputExecutorId) - { - NoOpExecutor start = new("start"); - NoOpExecutor end = new("end"); - - Workflow workflow = new WorkflowBuilder("start") - .AddEdge(start, end) - .WithOutputFrom(outputExecutorId == "end" ? end : start) - .Build(); - - return new OutputFilter(workflow); - } - - [Fact] - public void OutputFilter_CanOutput_ReturnsTrueForRegisteredExecutor() - { - OutputFilter filter = CreateFilterWithOutputFrom("end"); - - filter.CanOutput("end", "some output").Should().BeTrue("the executor was registered via WithOutputFrom"); - } - - [Fact] - public void OutputFilter_CanOutput_ReturnsFalseForUnregisteredExecutor() - { - OutputFilter filter = CreateFilterWithOutputFrom("end"); - - filter.CanOutput("start", "some output").Should().BeFalse("start was not registered as an output executor"); - } - - [Fact] - public void OutputFilter_CanOutput_ReturnsFalseForNonExistentExecutor() - { - OutputFilter filter = CreateFilterWithOutputFrom("end"); - - filter.CanOutput("nonexistent", "some output").Should().BeFalse("an executor not in the workflow should not be an output executor"); - } - - private sealed class NoOpExecutor(string id) : Executor(id) - { - protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) - => protocolBuilder.ConfigureRoutes(routeBuilder => - routeBuilder.AddHandler((msg, ctx) => ctx.SendMessageAsync(msg))); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs index 744c9264a2..8d053334f9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs @@ -187,8 +187,12 @@ public class JsonSerializationTests actual.InputType.Should().Match(prototype.InputType.CreateValidator()); actual.StartExecutorId.Should().Be(prototype.StartExecutorId); - actual.OutputExecutorIds.Should().HaveCount(prototype.OutputExecutorIds.Count) - .And.AllSatisfy(id => prototype.OutputExecutorIds.Contains(id)); + actual.OutputExecutorIds.Should().HaveCount(prototype.OutputExecutorIds.Count); + foreach (KeyValuePair> kvp in prototype.OutputExecutorIds) + { + actual.OutputExecutorIds.Should().ContainKey(kvp.Key); + actual.OutputExecutorIds[kvp.Key].Should().BeEquivalentTo(kvp.Value); + } void ValidateExecutorDictionary(Dictionary expected, Dictionary> expectedEdges, diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticWorkflowBuilderTests.cs new file mode 100644 index 0000000000..eb795ae36f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticWorkflowBuilderTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +/// +/// Tests focused on 's output-designation surface — +/// the Python-aligned defaults applied at when +/// the user has not made explicit designations, and the memoized +/// WithOutputFrom / WithIntermediateOutputFrom replay otherwise. +/// +#pragma warning disable MAAIW001 // Experimental: MagenticWorkflowBuilder +public class MagenticWorkflowBuilderTests +{ + [Fact] + public void Test_MagenticWorkflowBuilder_DefaultDesignationsMatchSpec() + { + TestReplayAgent manager = new(name: "Manager"); + TestEchoAgent member1 = new(name: "Worker1"); + TestEchoAgent member2 = new(name: "Worker2"); + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(member1, member2) + .RequirePlanSignoff(false) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Where(kvp => kvp.Value.Count == 0) + .Should().ContainSingle("the Magentic orchestrator is the sole terminal output by default"); + designations.Where(kvp => kvp.Value.Contains(OutputTag.Intermediate)) + .Should().HaveCount(2, "every team member is designated intermediate by default"); + } + + [Fact] + public void Test_MagenticWorkflowBuilder_ExplicitDesignationsReplaceDefaults() + { + TestReplayAgent manager = new(name: "Manager"); + TestEchoAgent member1 = new(name: "Worker1"); + TestEchoAgent member2 = new(name: "Worker2"); + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(member1, member2) + .RequirePlanSignoff(false) + .WithOutputFrom(member1) + .WithIntermediateOutputFrom([member2]) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Should().HaveCount(2, + "only the user-specified designations land on the inner builder; the orchestrator default is suppressed"); + designations.Values.Where(tags => tags.Count == 0) + .Should().ContainSingle("member1 is the only terminal designation"); + designations.Values.Where(tags => tags.Contains(OutputTag.Intermediate)) + .Should().ContainSingle("member2 is the only intermediate designation"); + } + + [Fact] + public void Test_MagenticWorkflowBuilder_DesignationForNonParticipantThrows() + { + TestReplayAgent manager = new(name: "Manager"); + TestEchoAgent member = new(name: "Worker"); + TestEchoAgent stranger = new(name: "Stranger"); + + MagenticWorkflowBuilder builder = new MagenticWorkflowBuilder(manager) + .AddParticipants(member) + .RequirePlanSignoff(false) + .WithIntermediateOutputFrom([stranger]); + + Action build = () => builder.Build(); + build.Should().Throw().WithMessage("*Stranger*"); + } +} +#pragma warning restore MAAIW001 diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OrchestrationTestHelpers.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OrchestrationTestHelpers.cs new file mode 100644 index 0000000000..a883838029 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OrchestrationTestHelpers.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.InProc; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +/// +/// Container for shared test helpers used by every orchestration-builder test class — +/// the DoubleEchoAgent family and the RunWorkflow* methods. The actual +/// test methods live in per-builder files (SequentialWorkflowBuilderTests, +/// ConcurrentWorkflowBuilderTests, GroupChatWorkflowBuilderTests, etc.). +/// +public static class OrchestrationTestHelpers +{ + internal class DoubleEchoAgent(string name) : AIAgent + { + public override string Name => name; + + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => new(new DoubleEchoAgentSession()); + + protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => new(new DoubleEchoAgentSession()); + + protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => default; + + protected override Task RunCoreAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Yield(); + + var contents = messages.SelectMany(m => m.Contents).ToList(); + string id = Guid.NewGuid().ToString("N"); + yield return new AgentResponseUpdate(ChatRole.Assistant, this.Name) { AuthorName = this.Name, MessageId = id }; + yield return new AgentResponseUpdate(ChatRole.Assistant, contents) { AuthorName = this.Name, MessageId = id }; + yield return new AgentResponseUpdate(ChatRole.Assistant, contents) { AuthorName = this.Name, MessageId = id }; + } + } + + internal sealed class DoubleEchoAgentSession() : AgentSession(); + + internal sealed class DoubleEchoAgentWithBarrier(string name, StrongBox> barrier, StrongBox remaining) : DoubleEchoAgent(name) + { + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (Interlocked.Decrement(ref remaining.Value) == 0) + { + barrier.Value!.SetResult(true); + } + + await barrier.Value!.Task.ConfigureAwait(false); + + await foreach (var update in base.RunCoreStreamingAsync(messages, session, options, cancellationToken)) + { + await Task.Yield(); + yield return update; + } + } + } + + internal sealed record WorkflowRunResult(string UpdateText, List? Result, CheckpointInfo? LastCheckpoint, List PendingRequests); + + internal static async Task RunWorkflowCheckpointedAsync( + Workflow workflow, List input, InProcessExecutionEnvironment environment, CheckpointInfo? fromCheckpoint = null) + { + await using StreamingRun run = + fromCheckpoint != null ? await environment.ResumeStreamingAsync(workflow, fromCheckpoint) + : await environment.OpenStreamingAsync(workflow); + + await run.TrySendMessageAsync(input); + await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + return await ProcessWorkflowRunAsync(run); + } + + internal static async Task ProcessWorkflowRunAsync(StreamingRun run) + { + StringBuilder sb = new(); + WorkflowOutputEvent? output = null; + CheckpointInfo? lastCheckpoint = null; + + List pendingRequests = []; + + await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false).ConfigureAwait(false)) + { + switch (evt) + { + case AgentResponseUpdateEvent responseUpdate: + sb.Append(responseUpdate.Data); + break; + + case RequestInfoEvent requestInfo: + pendingRequests.Add(requestInfo); + break; + + case WorkflowOutputEvent e: + output = e; + break; + + case WorkflowErrorEvent errorEvent: + Assert.Fail($"Workflow execution failed with error: {errorEvent.Exception}"); + break; + + case SuperStepCompletedEvent stepCompleted: + lastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint; + break; + } + } + + return new(sb.ToString(), output?.As>(), lastCheckpoint, pendingRequests); + } + + internal static Task RunWorkflowAsync( + Workflow workflow, List input, ExecutionEnvironment executionEnvironment = ExecutionEnvironment.InProcess_Lockstep) + => RunWorkflowCheckpointedAsync(workflow, input, executionEnvironment.ToWorkflowExecutionEnvironment()); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputFilterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputFilterTests.cs new file mode 100644 index 0000000000..6114a74e8e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputFilterTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Agents.AI.Workflows.Execution; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +public class OutputFilterTests +{ + private static OutputFilter CreateFilterWithOutputFrom(string outputExecutorId) + { + NoOpExecutor start = new("start"); + NoOpExecutor end = new("end"); + + Workflow workflow = new WorkflowBuilder("start") + .AddEdge(start, end) + .WithOutputFrom(outputExecutorId == "end" ? end : start) + .Build(); + + return new OutputFilter(workflow); + } + + [Fact] + public void OutputFilter_CanOutput_ReturnsTrueForRegisteredExecutor() + { + OutputFilter filter = CreateFilterWithOutputFrom("end"); + + filter.CanOutput("end", "some output").Should().BeTrue("the executor was registered via WithOutputFrom"); + } + + [Fact] + public void OutputFilter_CanOutput_ReturnsFalseForUnregisteredExecutor() + { + OutputFilter filter = CreateFilterWithOutputFrom("end"); + + filter.CanOutput("start", "some output").Should().BeFalse("start was not registered as an output executor"); + } + + [Fact] + public void OutputFilter_CanOutput_ReturnsFalseForNonExistentExecutor() + { + OutputFilter filter = CreateFilterWithOutputFrom("end"); + + filter.CanOutput("nonexistent", "some output").Should().BeFalse("an executor not in the workflow should not be an output executor"); + } + + [Fact] + public void Test_OutputFilter_ReturnsEmptyTagSetWhenRegisteredViaWithOutputFrom() + { + OutputFilter filter = CreateFilterWithOutputFrom("end"); + + filter.TryGetTags("end", out HashSet? tags).Should().BeTrue(); + tags.Should().NotBeNull().And.BeEmpty("terminal designation carries no tag"); + } + + [Fact] + public void Test_OutputFilter_ReturnsIntermediateTagWhenRegisteredViaWithIntermediateOutputFrom() + { + NoOpExecutor start = new("start"); + NoOpExecutor end = new("end"); + + Workflow workflow = new WorkflowBuilder("start") + .AddEdge(start, end) + .WithIntermediateOutputFrom([end]) + .Build(); + + OutputFilter filter = new(workflow); + + filter.TryGetTags("end", out HashSet? tags).Should().BeTrue(); + tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } + + [Fact] + public void Test_OutputFilter_ReturnsIntermediateTagForAccumulatedDesignation() + { + NoOpExecutor start = new("start"); + NoOpExecutor end = new("end"); + + Workflow workflow = new WorkflowBuilder("start") + .AddEdge(start, end) + .WithOutputFrom(end) + .WithIntermediateOutputFrom([end]) + .Build(); + + OutputFilter filter = new(workflow); + + filter.TryGetTags("end", out HashSet? tags).Should().BeTrue(); + tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }, + "terminal designation contributes no tag; the union is the intermediate set"); + } + + [Fact] + public void Test_OutputFilter_TryGetTagsReturnsFalseForUnregisteredExecutor() + { + OutputFilter filter = CreateFilterWithOutputFrom("end"); + + filter.TryGetTags("start", out HashSet? tags).Should().BeFalse(); + tags.Should().BeNull(); + } + + private sealed class NoOpExecutor(string id) : Executor(id) + { + protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) + => protocolBuilder.ConfigureRoutes(routeBuilder => + routeBuilder.AddHandler((msg, ctx) => ctx.SendMessageAsync(msg))); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputTagTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputTagTests.cs new file mode 100644 index 0000000000..369d0d97d2 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/OutputTagTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using FluentAssertions; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +public class OutputTagTests +{ + [Fact] + public void Test_OutputTag_KnownValues() + { + OutputTag.Intermediate.Value.Should().Be("intermediate"); + } + + [Fact] + public void Test_OutputTag_EqualityIsOrdinalOnValue() + { + OutputTag.Intermediate.Should().Be(OutputTag.Intermediate); + (OutputTag.Intermediate == OutputTag.Intermediate).Should().BeTrue(); + + // Same Value via independent construction (via JSON round-trip below) is equal. + OutputTag rebuilt = JsonSerializer.Deserialize("\"intermediate\"", WorkflowsJsonUtilities.DefaultOptions); + rebuilt.Should().Be(OutputTag.Intermediate); + } + + [Fact] + public void Test_OutputTag_DefaultStructValueIsDistinct() + { + OutputTag def = default; + def.Value.Should().BeNull(); + def.Should().NotBe(OutputTag.Intermediate); + def.GetHashCode().Should().Be(0); + + HashSet set = [OutputTag.Intermediate]; + set.Contains(def).Should().BeFalse("default(OutputTag) must not collide with the well-known singleton in a HashSet"); + } + + [Fact] + public void Test_OutputTag_GetHashCodeMatchesEquals() + { + OutputTag a = OutputTag.Intermediate; + OutputTag b = JsonSerializer.Deserialize("\"intermediate\"", WorkflowsJsonUtilities.DefaultOptions); + + a.Equals(b).Should().BeTrue(); + a.GetHashCode().Should().Be(b.GetHashCode()); + } + + [Fact] + public void Test_OutputTag_JsonConverter_RoundtripsValueAsString() + { + string intermediateJson = JsonSerializer.Serialize(OutputTag.Intermediate, WorkflowsJsonUtilities.DefaultOptions); + intermediateJson.Should().Be("\"intermediate\""); + + OutputTag back = JsonSerializer.Deserialize("\"intermediate\"", WorkflowsJsonUtilities.DefaultOptions); + back.Should().Be(OutputTag.Intermediate); + + OutputTag fromUnknown = JsonSerializer.Deserialize("\"custom\"", WorkflowsJsonUtilities.DefaultOptions); + fromUnknown.Value.Should().Be("custom"); + } + + [Fact] + public void Test_OutputTag_ConstructorIsInternal() + { + ConstructorInfo? ctor = typeof(OutputTag).GetConstructor( + BindingFlags.Instance | BindingFlags.NonPublic, + binder: null, + types: [typeof(string)], + modifiers: null); + + ctor.Should().NotBeNull("OutputTag(string) must exist as an internal constructor"); + ctor!.IsAssembly.Should().BeTrue("OutputTag(string) must be `internal` so external assemblies cannot synthesize tags"); + ctor.IsPublic.Should().BeFalse(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SequentialWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SequentialWorkflowBuilderTests.cs new file mode 100644 index 0000000000..f22c7aa782 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SequentialWorkflowBuilderTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Agents.AI.Workflows.UnitTests.Futures; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +public class SequentialWorkflowBuilderTests +{ + [Fact] + public void Test_SequentialWorkflowBuilder_InvalidArguments_Throws() + { + Assert.Throws("agents", () => new SequentialWorkflowBuilder(null!)); + Assert.Throws("agents", () => new SequentialWorkflowBuilder().Build()); + + Assert.Throws("agents", () => AgentWorkflowBuilder.BuildSequential(workflowName: null!, null!)); + Assert.Throws("agents", () => AgentWorkflowBuilder.BuildSequential()); + Assert.Throws("agents", () => AgentWorkflowBuilder.CreateSequentialBuilderWith(null!)); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public async Task Test_SequentialWorkflowBuilder_AgentsRunInOrderAsync(int numAgents) + { + var workflow = new SequentialWorkflowBuilder( + from i in Enumerable.Range(1, numAgents) + select new OrchestrationTestHelpers.DoubleEchoAgent($"agent{i}")) + .Build(); + + for (int iter = 0; iter < 3; iter++) + { + const string UserInput = "abc"; + (string updateText, List? result, _, _) = + await OrchestrationTestHelpers.RunWorkflowAsync(workflow, [new ChatMessage(ChatRole.User, UserInput)]); + + Assert.NotNull(result); + Assert.Equal(numAgents + 1, result.Count); + + Assert.Equal(ChatRole.User, result[0].Role); + Assert.Null(result[0].AuthorName); + Assert.Equal(UserInput, result[0].Text); + + string[] texts = new string[numAgents + 1]; + texts[0] = UserInput; + string expectedTotal = string.Empty; + for (int i = 1; i < numAgents + 1; i++) + { + string id = $"agent{((i - 1) % numAgents) + 1}"; + texts[i] = $"{id}{Double(string.Concat(texts.Take(i)))}"; + Assert.Equal(ChatRole.Assistant, result[i].Role); + Assert.Equal(id, result[i].AuthorName); + Assert.Equal(texts[i], result[i].Text); + expectedTotal += texts[i]; + } + + Assert.Equal(expectedTotal, updateText); + Assert.Equal(UserInput + expectedTotal, string.Concat(result)); + + static string Double(string s) => s + s; + } + } + + [Fact] + public void Test_SequentialWorkflowBuilder_DefaultDesignationsMatchSpec() + { + Workflow workflow = new SequentialWorkflowBuilder( + new OrchestrationTestHelpers.DoubleEchoAgent("agent1"), + new OrchestrationTestHelpers.DoubleEchoAgent("agent2"), + new OrchestrationTestHelpers.DoubleEchoAgent("agent3")) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + designations.Where(kvp => kvp.Value.Count == 0) + .Should().ContainSingle("OutputMessagesExecutor is the sole terminal output by default"); + designations.Where(kvp => kvp.Value.Contains(OutputTag.Intermediate)) + .Should().HaveCount(3, "every pipeline agent is designated intermediate by default"); + } + + [Fact] + public void Test_SequentialWorkflowBuilder_ExplicitDesignationsReplaceDefaults() + { + OrchestrationTestHelpers.DoubleEchoAgent a1 = new("agent1"); + OrchestrationTestHelpers.DoubleEchoAgent a2 = new("agent2"); + OrchestrationTestHelpers.DoubleEchoAgent a3 = new("agent3"); + + Workflow workflow = new SequentialWorkflowBuilder(a1, a2, a3) + .WithOutputFrom(a1) + .WithIntermediateOutputFrom([a2]) + .Build(); + + Dictionary> designations = workflow.OutputExecutors; + + designations.Should().HaveCount(2, + "only the two explicitly-designated agents land on the inner builder; the end default is suppressed"); + designations.Values.Where(tags => tags.Count == 0) + .Should().ContainSingle("agent1 is the only terminal designation"); + designations.Values.Where(tags => tags.Contains(OutputTag.Intermediate)) + .Should().ContainSingle("agent2 is the only intermediate designation"); + } + + [Fact] + public void Test_SequentialWorkflowBuilder_DesignationForNonParticipantThrows() + { + OrchestrationTestHelpers.DoubleEchoAgent participant = new("p1"); + OrchestrationTestHelpers.DoubleEchoAgent stranger = new("stranger"); + + SequentialWorkflowBuilder builder = new SequentialWorkflowBuilder(participant) + .WithIntermediateOutputFrom([stranger]); + + Action build = () => builder.Build(); + build.Should().Throw().WithMessage("*stranger*"); + } + + [Fact] + public void Test_SequentialWorkflowBuilder_WithNamePropagatesToWorkflow() + { + Workflow workflow = new SequentialWorkflowBuilder(new OrchestrationTestHelpers.DoubleEchoAgent("agent1")) + .WithName("named-sequential") + .Build(); + + workflow.Name.Should().Be("named-sequential"); + } + + [Fact] + public void Test_SequentialWorkflowBuilder_WithDescriptionPropagatesToWorkflow() + { + Workflow workflow = new SequentialWorkflowBuilder(new OrchestrationTestHelpers.DoubleEchoAgent("agent1")) + .WithDescription("describes the sequential pipeline") + .Build(); + + workflow.Description.Should().Be("describes the sequential pipeline"); + } + + [Collection(FuturesSerialCollection.Name)] + public class AsAgentForwarding + { + [Fact] + public async Task Test_SequentialWorkflowBuilder_AsAgent_OnlyTerminalDesignationSurfacesAsync() + { + using FuturesScope _ = new(enabled: true); + + OrchestrationTestHelpers.DoubleEchoAgent agent1 = new("agent1"); + OrchestrationTestHelpers.DoubleEchoAgent agent2 = new("agent2"); + OrchestrationTestHelpers.DoubleEchoAgent agent3 = new("agent3"); + + // Explicitly designate ONLY the last agent — defaults (which would tag every agent + // intermediate) are suppressed, so under Futures-on, agent1/agent2 produce no + // AgentResponse(Update)Events and nothing of theirs reaches the AsAgent stream. + Workflow workflow = new SequentialWorkflowBuilder(agent1, agent2, agent3) + .WithOutputFrom(agent3) + .Build(); + + List updates = await workflow + .AsAIAgent("WorkflowAgent") + .RunStreamingAsync(new ChatMessage(ChatRole.User, "abc")) + .ToListAsync(); + + // Filter by AuthorName — distinguishes which agent originated each update + // (text-content checks are unreliable because agent3 echoes earlier agents' markers + // as part of the cumulative pipeline payload). + HashSet authoredBy = updates + .Select(u => u.AuthorName) + .Where(n => !string.IsNullOrEmpty(n)) + .Select(n => n!) + .ToHashSet(); + + authoredBy.Should().Contain("agent3", "the terminal agent must surface"); + authoredBy.Should().NotContain("agent1", + "the intermediate agent must not surface when only the terminal is designated"); + authoredBy.Should().NotContain("agent2", + "the intermediate agent must not surface when only the terminal is designated"); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowBuilderSmokeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowBuilderTests.cs similarity index 83% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowBuilderSmokeTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowBuilderTests.cs index c2b855b8bf..dfeeaea510 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowBuilderSmokeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowBuilderTests.cs @@ -6,7 +6,7 @@ using FluentAssertions; namespace Microsoft.Agents.AI.Workflows.UnitTests; -public partial class WorkflowBuilderSmokeTests +public partial class WorkflowBuilderTests { private sealed class NoOpExecutor(string id) : Executor(id) { @@ -455,4 +455,112 @@ public partial class WorkflowBuilderSmokeTests /// private static Edge GetSingleEdge(Workflow workflow, string sourceId) => workflow.Edges[sourceId].Should().ContainSingle().Subject; + + // --- Tag-aware WithOutputFrom / WithIntermediateOutputFrom tests --- + + [Fact] + public void Test_WithOutputFrom_RegistersWithEmptyTagSet() + { + NoOpExecutor a = new("a"); + NoOpExecutor b = new("b"); + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, b) + .WithOutputFrom(b) + .Build(); + + workflow.OutputExecutors.Should().ContainKey("b"); + workflow.OutputExecutors["b"].Should().BeEmpty("regular outputs are untagged"); + } + + [Fact] + public void Test_WithIntermediateOutputFrom_AddsIntermediateTag() + { + NoOpExecutor a = new("a"); + NoOpExecutor b = new("b"); + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, b) + .WithIntermediateOutputFrom([b]) + .Build(); + + workflow.OutputExecutors["b"].Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } + + [Fact] + public void Test_WithOutputFrom_MultipleExecutorsAllUntagged() + { + NoOpExecutor a = new("a"); + NoOpExecutor b = new("b"); + NoOpExecutor c = new("c"); + + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, b).AddEdge(a, c) + .WithOutputFrom(b, c) + .Build(); + + workflow.OutputExecutors.Should().HaveCount(2); + workflow.OutputExecutors["b"].Should().BeEmpty(); + workflow.OutputExecutors["c"].Should().BeEmpty(); + } + + [Fact] + public void Test_WithOutputFrom_ThenIntermediate_AccumulatesTags() + { + NoOpExecutor a = new("a"); + NoOpExecutor b = new("b"); + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, b) + .WithOutputFrom(b) + .WithIntermediateOutputFrom([b]) + .Build(); + + // WithOutputFrom doesn't add a tag; WithIntermediateOutputFrom adds Intermediate. + workflow.OutputExecutors["b"].Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } + + [Fact] + public void Test_WithIntermediateOutputFrom_RepeatedDedupes() + { + NoOpExecutor a = new("a"); + NoOpExecutor b = new("b"); + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, b) + .WithIntermediateOutputFrom([b]) + .WithIntermediateOutputFrom([b]) + .Build(); + + workflow.OutputExecutors["b"].Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } + + [Fact] + public void Test_WithIntermediateOutputFrom_OnlyRegistersWithoutPriorWithOutputFrom() + { + // WithIntermediateOutputFrom on its own is sufficient to register the executor as an + // output source — the call ensures the id is in the dict with the Intermediate tag. + NoOpExecutor a = new("a"); + NoOpExecutor b = new("b"); + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, b) + .WithIntermediateOutputFrom([b]) + .Build(); + + workflow.OutputExecutors.Should().ContainKey("b"); + workflow.OutputExecutors["b"].Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } + + [Fact] + public void Test_WithOutputFrom_TracksExecutorBinding() + { + // A placeholder binding referenced via WithOutputFrom must end up bound by the time we Build. + NoOpExecutor a = new("a"); + NoOpExecutor future = new("future"); + + Workflow workflow = new WorkflowBuilder("a") + .AddEdge(a, "future") + .WithIntermediateOutputFrom(["future"]) + .BindExecutor(future) + .Build(); + + workflow.OutputExecutors.Should().ContainKey("future"); + workflow.OutputExecutors["future"].Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs index 7b9b428871..4402142fae 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs @@ -824,4 +824,130 @@ public class WorkflowHostSmokeTests : AIAgentHostingExecutorTestsBase Workflow handoffWorkflow = new HandoffWorkflowBuilder(agent).Build(); return this.Run_AsAgent_OutgoingMessagesInHistoryAsync(handoffWorkflow, runAsync); } + + // ----- Phase 5: Workflow-as-Agent intermediate forwarding ----------------- + + [Collection(Futures.FuturesSerialCollection.Name)] + public class IntermediateForwarding + { + private const string InterText = "progress"; + private const string FinalText = "final"; + + private static async Task> RunStreamingAsync( + Workflow workflow, + bool includeWorkflowOutputsInResponse = false) + { + return await workflow + .AsAIAgent("WorkflowAgent", includeWorkflowOutputsInResponse: includeWorkflowOutputsInResponse) + .RunStreamingAsync(new ChatMessage(ChatRole.User, "hi")) + .ToListAsync(); + } + + [Fact] + public async Task Test_WorkflowHostAgent_IntermediateAgentResponseForwardedInStreamingAsync() + { + using Futures.FuturesScope _ = new(enabled: true); + TestReplayAgent agent = new(TestReplayAgent.ToChatMessages(InterText)); + ExecutorBinding binding = agent.BindAsExecutor(new AIAgentHostOptions { EmitAgentResponseEvents = true }); + Workflow workflow = new WorkflowBuilder(binding) + .WithIntermediateOutputFrom([binding]) + .Build(); + + // Under Futures-on, AgentResponseEvent mirrors AgentResponseUpdateEvent: always + // forwarded regardless of the include flag. The intermediate tag is observable on + // the surfaced event for consumers that care to distinguish. + List updates = await RunStreamingAsync(workflow, includeWorkflowOutputsInResponse: false); + + updates.Any(u => u.RawRepresentation is AgentResponseEvent are && are.IsIntermediate() && u.Text == InterText) + .Should().BeTrue("AgentResponseEvent is forwarded under Futures-on regardless of the include flag"); + } + + [Fact] + public async Task Test_WorkflowHostAgent_TerminalAgentResponseForwardedUnconditionallyWhenFuturesOnAsync() + { + using Futures.FuturesScope _ = new(enabled: true); + TestReplayAgent agent = new(TestReplayAgent.ToChatMessages(FinalText)); + ExecutorBinding binding = agent.BindAsExecutor(new AIAgentHostOptions { EmitAgentResponseEvents = true }); + Workflow workflow = new WorkflowBuilder(binding) + .WithOutputFrom(binding) + .Build(); + + // Even a terminal-only designation surfaces without the include flag — the gating + // asymmetry between AgentResponse and AgentResponseUpdate is gone under Futures-on. + List updates = await RunStreamingAsync(workflow, includeWorkflowOutputsInResponse: false); + + updates.Any(u => u.RawRepresentation is AgentResponseEvent && u.Text == FinalText) + .Should().BeTrue("terminal AgentResponseEvent is forwarded under Futures-on regardless of the include flag"); + } + + [Fact] + public async Task Test_WorkflowHostAgent_TerminalAgentResponseGatedWhenFuturesOffAsync() + { + using Futures.FuturesScope _ = new(enabled: false); + + static Workflow Build() + { + TestReplayAgent agent = new(TestReplayAgent.ToChatMessages(FinalText)); + ExecutorBinding binding = agent.BindAsExecutor(new AIAgentHostOptions { EmitAgentResponseEvents = true }); + return new WorkflowBuilder(binding).WithOutputFrom(binding).Build(); + } + + // Legacy semantics: AgentResponseEvent stays behind the include flag when Futures + // is off. Two fresh workflows because in-process runs aren't reentrant. + List gated = await RunStreamingAsync(Build(), includeWorkflowOutputsInResponse: false); + gated.Any(u => u.RawRepresentation is AgentResponseEvent && u.Text == FinalText) + .Should().BeFalse("terminal AgentResponseEvent stays gated under Futures-off"); + + List included = await RunStreamingAsync(Build(), includeWorkflowOutputsInResponse: true); + included.Any(u => u.RawRepresentation is AgentResponseEvent && u.Text == FinalText) + .Should().BeTrue("opting in via includeWorkflowOutputsInResponse surfaces it"); + } + + [Fact] + public async Task Test_WorkflowHostAgent_UndesignatedExecutorEmitsNoAgentResponseEventWhenFuturesOnAsync() + { + using Futures.FuturesScope _ = new(enabled: true); + TestReplayAgent agent = new(TestReplayAgent.ToChatMessages(InterText)); + ExecutorBinding binding = agent.BindAsExecutor(new AIAgentHostOptions { EmitAgentResponseEvents = true }); + // No designation — under Futures-on, the AgentResponse is dropped by the filter. + Workflow workflow = new WorkflowBuilder(binding).Build(); + + List updates = await RunStreamingAsync(workflow, includeWorkflowOutputsInResponse: true); + + updates.Any(u => u.RawRepresentation is AgentResponseEvent) + .Should().BeFalse("an undesignated AIAgent executor produces no AgentResponseEvent under Futures-on"); + } + + [Fact] + public async Task Test_WorkflowHostAgent_UndesignatedAgentResponseSurfacesWhenFuturesOffAsync() + { + using Futures.FuturesScope _ = new(enabled: false); + TestReplayAgent agent = new(TestReplayAgent.ToChatMessages(InterText)); + ExecutorBinding binding = agent.BindAsExecutor(new AIAgentHostOptions { EmitAgentResponseEvents = true }); + Workflow workflow = new WorkflowBuilder(binding).Build(); + + List updates = await RunStreamingAsync(workflow, includeWorkflowOutputsInResponse: true); + + updates.Any(u => u.RawRepresentation is AgentResponseEvent && u.Text == InterText) + .Should().BeTrue("legacy bypass still emits AgentResponseEvent regardless of designation"); + } + + [Fact] + public async Task Test_WorkflowHostAgent_IntermediateTagAvailableViaRawRepresentationAsync() + { + using Futures.FuturesScope _ = new(enabled: true); + TestReplayAgent agent = new(TestReplayAgent.ToChatMessages(InterText)); + ExecutorBinding binding = agent.BindAsExecutor(new AIAgentHostOptions { EmitAgentResponseEvents = true }); + Workflow workflow = new WorkflowBuilder(binding) + .WithIntermediateOutputFrom([binding]) + .Build(); + + List updates = await RunStreamingAsync(workflow); + + AgentResponseUpdate progress = updates.First(u => u.RawRepresentation is AgentResponseEvent && u.Text == InterText); + AgentResponseEvent raw = (AgentResponseEvent)progress.RawRepresentation!; + raw.IsIntermediate().Should().BeTrue(); + raw.Tags.Should().BeEquivalentTo(new[] { OutputTag.Intermediate }); + } + } } From 1fccf16f1103107186c12fb5f4ad58b8a6ba7f62 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 28 May 2026 20:03:19 -0400 Subject: [PATCH 14/61] feat: Remove [Experimental] tag from .NET Orchestrations (#6164) --- .../Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs | 3 --- .../HandoffWorkflowBuilder.cs | 7 ------- .../MagenticPlanReviewRequest.cs | 2 -- .../MagenticPlanReviewResponse.cs | 2 -- .../MagenticProgressLedger.cs | 3 +-- .../MagenticWorkflowBuilder.cs | 2 -- .../Specialized/HandoffAgentExecutor.cs | 1 - .../Specialized/HandoffMessagesFilter.cs | 3 --- .../Specialized/Magentic/MagenticOrchestrator.cs | 6 ------ 9 files changed, 1 insertion(+), 28 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs index 007920f6bc..8321efd99e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; @@ -104,7 +103,6 @@ public static partial class AgentWorkflowBuilder /// The must be capable of understanding those provided. If the agent /// ignores the tools or is otherwise unable to advertize them to the underlying provider, handoffs will not occur. /// - [Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public static HandoffWorkflowBuilder CreateHandoffBuilderWith(AIAgent initialAgent) { Throw.IfNull(initialAgent); @@ -150,7 +148,6 @@ public static partial class AgentWorkflowBuilder /// Creates a new with the given . /// The LLM-powered manager agent that coordinates the team. /// The builder for creating a Magentic workflow. - [Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public static MagenticWorkflowBuilder CreateMagenticBuilderWith(AIAgent managerAgent) { Throw.IfNull(managerAgent); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs index 30ec899edc..906ebab885 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs @@ -15,11 +15,6 @@ using ExecutorFactoryFunc = System.Func [ExcludeFromCodeCoverage] // This is obsolete, and 1:1 equivalent to HandoffWorkflowBuilder (no "s") [Obsolete("Prefer HandoffWorkflowBuilder (no 's') instead, which has the same API but the preferred name. This will be removed in a future release before GA.")] @@ -30,7 +25,6 @@ public sealed class HandoffsWorkflowBuilder(AIAgent initialAgent) : HandoffWorkf } /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkflowBuilderCore(initialAgent) { } @@ -38,7 +32,6 @@ public sealed class HandoffWorkflowBuilder(AIAgent initialAgent) : HandoffWorkfl /// /// Provides a builder for specifying the handoff relationships between agents and building the resulting workflow. /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public class HandoffWorkflowBuilderCore : OrchestrationBuilderBase where TBuilder : HandoffWorkflowBuilderCore { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewRequest.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewRequest.cs index 7e66cd4c1a..fd7b82afe6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewRequest.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewRequest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Extensions.AI; @@ -16,7 +15,6 @@ namespace Microsoft.Agents.AI.Workflows; /// contain the latest progress ledger that determined that no progress has been made or the workflow was in /// a loop. /// Whether the workflow is currently stalled. -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public record MagenticPlanReviewRequest(ChatMessage Plan, MagenticProgressLedger? CurrentProgress, bool IsStalled) { /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewResponse.cs index 952cd9fade..0a72ccfa0f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticPlanReviewResponse.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows; @@ -13,7 +12,6 @@ namespace Microsoft.Agents.AI.Workflows; /// /// Review feedback for a generated plan. Empty if the plan is approved as-is and changes are requested. /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public record MagenticPlanReviewResponse(List Review) { internal bool IsApproved => this.Review.Count == 0; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticProgressLedger.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticProgressLedger.cs index 65058e7430..698c2e890c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticProgressLedger.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticProgressLedger.cs @@ -14,7 +14,6 @@ namespace Microsoft.Agents.AI.Workflows; /// /// Maintains a ledger of progress made by the Magentic workflow. /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public class MagenticProgressLedger { internal static readonly BooleanProgressLedgerSlot IsRequestSatisfiedSlot = new("is_request_satisfied", @@ -76,7 +75,7 @@ public class MagenticProgressLedger this.InstructionOrQuestion = instructionOrQuestion!; } - // TODO: To what extent do we want to enforce that the additional questions are also answered? + // TODO: To what extent do we want to enforce that the additional questions are also answered? return requiredQuestionsAnswered; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs index c75a3d045b..6ae123c55d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/MagenticWorkflowBuilder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Specialized.Magentic; @@ -27,7 +26,6 @@ namespace Microsoft.Agents.AI.Workflows; /// not supported on the ManagerAgent. /// /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public class MagenticWorkflowBuilder(AIAgent managerAgent) : OrchestrationBuilderBase { private readonly List _team = new(); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs index 02c48ea48d..914a96bfbe 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs @@ -81,7 +81,6 @@ internal sealed record StateRef(string Key, string? ScopeName) } /// Executor used to represent an agent in a handoffs workflow, responding to events. -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] internal sealed class HandoffAgentExecutor : StatefulExecutor { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffMessagesFilter.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffMessagesFilter.cs index 61eebc0e2b..2ca73ca3d8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffMessagesFilter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffMessagesFilter.cs @@ -2,12 +2,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.Specialized; -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] internal sealed class HandoffMessagesFilter { private readonly HandoffToolCallFilteringBehavior _filteringBehavior; @@ -17,7 +15,6 @@ internal sealed class HandoffMessagesFilter this._filteringBehavior = filteringBehavior; } - [Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] internal static bool IsHandoffFunctionName(string name) { return name.StartsWith(HandoffWorkflowBuilder.FunctionPrefix, StringComparison.Ordinal); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs index 30a93c6850..ff85a65c71 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; using System.Threading; @@ -18,7 +17,6 @@ namespace Microsoft.Agents.AI.Workflows.Specialized.Magentic; [JsonDerivedType(typeof(MagenticPlanCreatedEvent))] [JsonDerivedType(typeof(MagenticReplannedEvent))] [JsonDerivedType(typeof(MagenticProgressLedgerUpdatedEvent))] -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public abstract class MagenticOrchestratorEvent(object? data) : WorkflowEvent(data) { } @@ -27,7 +25,6 @@ public abstract class MagenticOrchestratorEvent(object? data) : WorkflowEvent(da /// Represents the creation of the initial plan /// /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public sealed class MagenticPlanCreatedEvent(ChatMessage fullTaskLeger) : MagenticOrchestratorEvent(fullTaskLeger) { /// @@ -40,7 +37,6 @@ public sealed class MagenticPlanCreatedEvent(ChatMessage fullTaskLeger) : Magent /// Represents the creation of a new plan in response to a stall. /// /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public sealed class MagenticReplannedEvent(ChatMessage fullTaskLeger) : MagenticOrchestratorEvent(fullTaskLeger) { /// @@ -53,7 +49,6 @@ public sealed class MagenticReplannedEvent(ChatMessage fullTaskLeger) : Magentic /// Represents an update to the when running a coordination round. /// /// -[Experimental(DiagnosticConstants.ExperimentalFeatureDiagnostic)] public sealed class MagenticProgressLedgerUpdatedEvent(MagenticProgressLedger progressLedger) : MagenticOrchestratorEvent(progressLedger) { /// @@ -138,7 +133,6 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta to the conversation and enters the inner loop. - If revision requested, append the review comments to the chat history, trigger replanning via the manager, emit a REPLANNED event, then run the outer loop. - */ if (this._taskContext == null || this._taskContext.TaskLedger == null) { From d2d5384f28b60a91771e7e70c7452da89d3c7e02 Mon Sep 17 00:00:00 2001 From: Daria Korenieva Date: Fri, 29 May 2026 00:20:56 -0700 Subject: [PATCH 15/61] Python: Add Mistral AI embedding client package (#5480) * Python: Add Mistral AI embedding client package Signed-off-by: Daria Korenieva * Address review feedback: fix dimensions check, sort embeddings by index, align docs Signed-off-by: Daria Korenieva * Address review feedback: downgrade to alpha, remove integration tests - Change version to 1.0.0a260505 (alpha) - Update classifier to Development Status :: 3 - Alpha - Update PACKAGE_STATUS.md to alpha - Remove Mistral from integration test workflows (no API keys yet) Signed-off-by: Daria Korenieva * Add samples directory for alpha package compliance Per python-package-management skill: alpha packages must include samples inside the package directory. Signed-off-by: Daria Korenieva * Fix ruff formatting in sample file Signed-off-by: Daria Korenieva --------- Signed-off-by: Daria Korenieva --- python/.env.example | 3 + python/PACKAGE_STATUS.md | 1 + python/packages/mistral/AGENTS.md | 26 + python/packages/mistral/LICENSE | 21 + python/packages/mistral/README.md | 42 + .../agent_framework_mistral/__init__.py | 17 + .../_embedding_client.py | 250 ++ .../mistral/agent_framework_mistral/py.typed | 1 + python/packages/mistral/pyproject.toml | 105 + python/packages/mistral/samples/README.md | 15 + python/packages/mistral/samples/__init__.py | 1 + .../mistral/samples/mistral_embeddings.py | 77 + .../mistral/test_mistral_embedding_client.py | 267 ++ python/pyproject.toml | 6 +- python/uv.lock | 3351 ++++++++--------- 15 files changed, 2473 insertions(+), 1710 deletions(-) create mode 100644 python/packages/mistral/AGENTS.md create mode 100644 python/packages/mistral/LICENSE create mode 100644 python/packages/mistral/README.md create mode 100644 python/packages/mistral/agent_framework_mistral/__init__.py create mode 100644 python/packages/mistral/agent_framework_mistral/_embedding_client.py create mode 100644 python/packages/mistral/agent_framework_mistral/py.typed create mode 100644 python/packages/mistral/pyproject.toml create mode 100644 python/packages/mistral/samples/README.md create mode 100644 python/packages/mistral/samples/__init__.py create mode 100644 python/packages/mistral/samples/mistral_embeddings.py create mode 100644 python/packages/mistral/tests/mistral/test_mistral_embedding_client.py diff --git a/python/.env.example b/python/.env.example index eab84910b1..26fed3fb1c 100644 --- a/python/.env.example +++ b/python/.env.example @@ -44,6 +44,9 @@ GEMINI_MODEL="" # Ollama OLLAMA_ENDPOINT="" OLLAMA_MODEL="" +# Mistral AI +MISTRAL_API_KEY="" +MISTRAL_EMBEDDING_MODEL="" # Observability (instrumentation is enabled by default; set "ENABLE_INSTRUMENTATION" to "false" to opt out) ENABLE_SENSITIVE_DATA=true OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/" diff --git a/python/PACKAGE_STATUS.md b/python/PACKAGE_STATUS.md index 1f336f1cd8..736a18bf1b 100644 --- a/python/PACKAGE_STATUS.md +++ b/python/PACKAGE_STATUS.md @@ -37,6 +37,7 @@ Status is grouped into these buckets: | `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` | | `agent-framework-lab` | `python/packages/lab` | `beta` | | `agent-framework-mem0` | `python/packages/mem0` | `beta` | +| `agent-framework-mistral` | `python/packages/mistral` | `alpha` | | `agent-framework-monty` | `python/packages/monty` | `alpha` | | `agent-framework-ollama` | `python/packages/ollama` | `beta` | | `agent-framework-openai` | `python/packages/openai` | `released` | diff --git a/python/packages/mistral/AGENTS.md b/python/packages/mistral/AGENTS.md new file mode 100644 index 0000000000..267c6e7024 --- /dev/null +++ b/python/packages/mistral/AGENTS.md @@ -0,0 +1,26 @@ +# Mistral Package (agent-framework-mistral) + +Integration with Mistral AI for embedding generation. + +## Main Classes + +- **`MistralEmbeddingClient`** - Embedding client for Mistral AI models +- **`MistralEmbeddingOptions`** - Options TypedDict for Mistral-specific embedding parameters +- **`MistralEmbeddingSettings`** - TypedDict settings for Mistral configuration + +## Usage + +```python +from agent_framework_mistral import MistralEmbeddingClient + +# Requires MISTRAL_API_KEY environment variable (or pass api_key= directly) +client = MistralEmbeddingClient(model="mistral-embed") +result = await client.get_embeddings(["Hello, world!"]) +print(result[0].vector) +``` + +## Import Path + +```python +from agent_framework_mistral import MistralEmbeddingClient +``` diff --git a/python/packages/mistral/LICENSE b/python/packages/mistral/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/mistral/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/mistral/README.md b/python/packages/mistral/README.md new file mode 100644 index 0000000000..41460dd890 --- /dev/null +++ b/python/packages/mistral/README.md @@ -0,0 +1,42 @@ +# Get Started with Microsoft Agent Framework Mistral AI + +Please install this package: + +```bash +pip install agent-framework-mistral --pre +``` + +and see the [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information. + +## Embedding Client + +The `MistralEmbeddingClient` provides embedding generation using Mistral AI models. + +### Quick Start + +```python +from agent_framework_mistral import MistralEmbeddingClient + +# Using environment variables (MISTRAL_API_KEY, MISTRAL_EMBEDDING_MODEL) +client = MistralEmbeddingClient() + +# Or passing parameters directly +client = MistralEmbeddingClient( + model="mistral-embed", + api_key="your-api-key", +) + +# Generate embeddings +result = await client.get_embeddings(["Hello, world!", "How are you?"]) +for embedding in result: + print(f"Dimensions: {embedding.dimensions}") + print(f"Vector: {embedding.vector[:5]}...") +``` + +### Configuration + +| Environment Variable | Description | +|---|---| +| `MISTRAL_API_KEY` | Your Mistral AI API key | +| `MISTRAL_EMBEDDING_MODEL` | Embedding model name (e.g., `mistral-embed`) | +| `MISTRAL_SERVER_URL` | Optional server URL override | diff --git a/python/packages/mistral/agent_framework_mistral/__init__.py b/python/packages/mistral/agent_framework_mistral/__init__.py new file mode 100644 index 0000000000..58d4677a82 --- /dev/null +++ b/python/packages/mistral/agent_framework_mistral/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib.metadata + +from ._embedding_client import MistralEmbeddingClient, MistralEmbeddingOptions, MistralEmbeddingSettings + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +__all__ = [ + "MistralEmbeddingClient", + "MistralEmbeddingOptions", + "MistralEmbeddingSettings", + "__version__", +] diff --git a/python/packages/mistral/agent_framework_mistral/_embedding_client.py b/python/packages/mistral/agent_framework_mistral/_embedding_client.py new file mode 100644 index 0000000000..4b07d7d120 --- /dev/null +++ b/python/packages/mistral/agent_framework_mistral/_embedding_client.py @@ -0,0 +1,250 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import logging +import sys +from collections.abc import Sequence +from typing import Any, ClassVar, Generic, TypedDict + +from agent_framework import ( + BaseEmbeddingClient, + Embedding, + EmbeddingGenerationOptions, + GeneratedEmbeddings, + UsageDetails, + load_settings, +) +from agent_framework._settings import SecretString +from agent_framework.observability import EmbeddingTelemetryLayer +from mistralai.client import Mistral + +if sys.version_info >= (3, 13): + from typing import TypeVar # type: ignore # pragma: no cover +else: + from typing_extensions import TypeVar # type: ignore # pragma: no cover + + +logger = logging.getLogger("agent_framework.mistral") + + +class MistralEmbeddingOptions(EmbeddingGenerationOptions, total=False): + """Mistral AI-specific embedding options. + + Extends EmbeddingGenerationOptions with Mistral-specific fields. + + Examples: + .. code-block:: python + + from agent_framework_mistral import MistralEmbeddingOptions + + options: MistralEmbeddingOptions = { + "model": "mistral-embed", + "dimensions": 1024, + } + """ + + +MistralEmbeddingOptionsT = TypeVar( + "MistralEmbeddingOptionsT", + bound=TypedDict, # type: ignore[valid-type] + default="MistralEmbeddingOptions", + covariant=True, +) + + +class MistralEmbeddingSettings(TypedDict, total=False): + """Mistral AI embedding settings. + + Fields: + api_key: Mistral API key. Resolved from ``MISTRAL_API_KEY``. + embedding_model: Embedding model name. Resolved from ``MISTRAL_EMBEDDING_MODEL``. + server_url: Optional server URL override. Resolved from ``MISTRAL_SERVER_URL``. + """ + + api_key: str | None + embedding_model: str | None + server_url: str | None + + +class RawMistralEmbeddingClient( + BaseEmbeddingClient[str, list[float], MistralEmbeddingOptionsT], + Generic[MistralEmbeddingOptionsT], +): + """Raw Mistral AI embedding client without telemetry. + + Keyword Args: + model: The Mistral embedding model (e.g. "mistral-embed"). + Can also be set via environment variable ``MISTRAL_EMBEDDING_MODEL``. + api_key: Mistral API key. Defaults to ``MISTRAL_API_KEY`` environment variable. + server_url: Optional server URL override. Defaults to ``MISTRAL_SERVER_URL`` + environment variable, or the Mistral default. + client: Optional pre-configured ``Mistral`` client instance. + additional_properties: Additional properties stored on the client instance. + env_file_path: Path to ``.env`` file for settings. + env_file_encoding: Encoding for ``.env`` file. + """ + + INJECTABLE: ClassVar[set[str]] = {"client"} + + def __init__( + self, + *, + model: str | None = None, + api_key: str | SecretString | None = None, + server_url: str | None = None, + client: Mistral | None = None, + additional_properties: dict[str, Any] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a raw Mistral AI embedding client.""" + mistral_settings = load_settings( + MistralEmbeddingSettings, + env_prefix="MISTRAL_", + required_fields=["embedding_model", "api_key"], + api_key=str(api_key) if isinstance(api_key, SecretString) else api_key, + embedding_model=model, + server_url=server_url, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + self.model: str = mistral_settings["embedding_model"] # type: ignore[assignment] + resolved_api_key: str = mistral_settings["api_key"] # type: ignore[assignment] + resolved_server_url = mistral_settings.get("server_url") + + if client is not None: + self.client = client + else: + client_kwargs: dict[str, Any] = {"api_key": resolved_api_key} + if resolved_server_url: + client_kwargs["server_url"] = resolved_server_url + self.client = Mistral(**client_kwargs) + + self.server_url = resolved_server_url + super().__init__(additional_properties=additional_properties) + + def service_url(self) -> str: + """Get the URL of the service.""" + return self.server_url or "https://api.mistral.ai" + + async def get_embeddings( + self, + values: Sequence[str], + *, + options: MistralEmbeddingOptionsT | None = None, + ) -> GeneratedEmbeddings[list[float], MistralEmbeddingOptionsT]: + """Call the Mistral AI embeddings API. + + Args: + values: The text values to generate embeddings for. + options: Optional embedding generation options. + + Returns: + Generated embeddings with usage metadata. + + Raises: + ValueError: If model is not provided or values is empty. + """ + if not values: + return GeneratedEmbeddings([], options=options) + + opts: dict[str, Any] = options or {} # type: ignore + model = opts.get("model") or self.model + if not model: + raise ValueError("model is required") + + kwargs: dict[str, Any] = {"model": model, "inputs": list(values)} + if "dimensions" in opts: + kwargs["output_dimension"] = opts["dimensions"] + + response = await self.client.embeddings.create_async(**kwargs) + + embeddings: list[Embedding[list[float]]] = [] + if response and response.data: + items = sorted(response.data, key=lambda d: d.index if d.index is not None else 0) + for item in items: + vector = list(item.embedding) if item.embedding else [] + embeddings.append( + Embedding( + vector=vector, + dimensions=len(vector), + model=response.model or model, + ) + ) + + usage_dict: UsageDetails | None = None + if response and response.usage: + usage_dict = { + "input_token_count": response.usage.prompt_tokens, + "total_token_count": response.usage.total_tokens, + } + + return GeneratedEmbeddings(embeddings, options=options, usage=usage_dict) + + +class MistralEmbeddingClient( + EmbeddingTelemetryLayer[str, list[float], MistralEmbeddingOptionsT], + RawMistralEmbeddingClient[MistralEmbeddingOptionsT], + Generic[MistralEmbeddingOptionsT], +): + """Mistral AI embedding client with telemetry support. + + Keyword Args: + model: The Mistral embedding model (e.g. "mistral-embed"). + Can also be set via environment variable ``MISTRAL_EMBEDDING_MODEL``. + api_key: Mistral API key. Defaults to ``MISTRAL_API_KEY`` environment variable. + server_url: Optional server URL override. Defaults to ``MISTRAL_SERVER_URL`` + environment variable, or the Mistral default. + client: Optional pre-configured ``Mistral`` client instance. + otel_provider_name: Optional telemetry provider name override. + env_file_path: Path to ``.env`` file for settings. + env_file_encoding: Encoding for ``.env`` file. + + Examples: + .. code-block:: python + + from agent_framework_mistral import MistralEmbeddingClient + + # Using environment variables + # Set MISTRAL_API_KEY=your-key + # Set MISTRAL_EMBEDDING_MODEL=mistral-embed + client = MistralEmbeddingClient() + + # Or passing parameters directly + client = MistralEmbeddingClient( + model="mistral-embed", + api_key="your-api-key", + ) + + # Generate embeddings + result = await client.get_embeddings(["Hello, world!"]) + print(result[0].vector) + """ + + OTEL_PROVIDER_NAME: ClassVar[str] = "mistralai" + + def __init__( + self, + *, + model: str | None = None, + api_key: str | SecretString | None = None, + server_url: str | None = None, + client: Mistral | None = None, + otel_provider_name: str | None = None, + additional_properties: dict[str, Any] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a Mistral AI embedding client.""" + super().__init__( + model=model, + api_key=api_key, + server_url=server_url, + client=client, + additional_properties=additional_properties, + otel_provider_name=otel_provider_name, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) diff --git a/python/packages/mistral/agent_framework_mistral/py.typed b/python/packages/mistral/agent_framework_mistral/py.typed new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/python/packages/mistral/agent_framework_mistral/py.typed @@ -0,0 +1 @@ + diff --git a/python/packages/mistral/pyproject.toml b/python/packages/mistral/pyproject.toml new file mode 100644 index 0000000000..30e7942fd1 --- /dev/null +++ b/python/packages/mistral/pyproject.toml @@ -0,0 +1,105 @@ +[project] +name = "agent-framework-mistral" +description = "Mistral AI integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0a260505" +license-files = ["LICENSE"] +urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Framework :: Pydantic :: 2", + "Typing :: Typed", +] +dependencies = [ + "agent-framework-core>=1.1.0,<2", + "mistralai>=2.0.0,<3", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" + +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [] +markers = [ + "integration: marks tests as integration tests that require external services", +] +timeout = 120 + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W"] + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extends = "../../pyproject.toml" +include = ["agent_framework_mistral"] +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true +disallow_any_unimported = true + +[tool.bandit] +targets = ["agent_framework_mistral"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" + +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mistral" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_mistral --cov-report=term-missing:skip-covered tests' + +[tool.uv.build-backend] +module-name = "agent_framework_mistral" +module-root = "" + +[build-system] +requires = ["uv_build>=0.8.2,<0.9.0"] +build-backend = "uv_build" diff --git a/python/packages/mistral/samples/README.md b/python/packages/mistral/samples/README.md new file mode 100644 index 0000000000..b58ecf6ef2 --- /dev/null +++ b/python/packages/mistral/samples/README.md @@ -0,0 +1,15 @@ +# Mistral AI Embedding Examples + +This folder contains examples demonstrating how to use Mistral AI embedding models with the Agent Framework. + +## Examples + +| File | Description | +|------|-------------| +| [`mistral_embeddings.py`](mistral_embeddings.py) | Basic embedding generation with the Mistral AI embedding client. | + +## Environment Variables + +- `MISTRAL_API_KEY`: Your Mistral AI API key +- `MISTRAL_EMBEDDING_MODEL`: Embedding model name (e.g., `mistral-embed`) +- `MISTRAL_SERVER_URL` (optional): Server URL override for custom deployments diff --git a/python/packages/mistral/samples/__init__.py b/python/packages/mistral/samples/__init__.py new file mode 100644 index 0000000000..2a50eae894 --- /dev/null +++ b/python/packages/mistral/samples/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/packages/mistral/samples/mistral_embeddings.py b/python/packages/mistral/samples/mistral_embeddings.py new file mode 100644 index 0000000000..17677b8697 --- /dev/null +++ b/python/packages/mistral/samples/mistral_embeddings.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Shows how to generate embeddings using the Mistral AI embedding client. + +Requires ``MISTRAL_API_KEY`` and ``MISTRAL_EMBEDDING_MODEL`` environment variables. +""" + +import asyncio + +from dotenv import load_dotenv + +from agent_framework_mistral import MistralEmbeddingClient + +load_dotenv() + + +async def basic_embedding_example() -> None: + """Generate embeddings for a list of texts.""" + print("=== Basic Embedding Generation ===") + + # 1. Create the embedding client (uses MISTRAL_API_KEY and MISTRAL_EMBEDDING_MODEL env vars). + client = MistralEmbeddingClient() + + # 2. Generate embeddings for multiple texts. + texts = ["Hello, world!", "How are you?", "Agent Framework with Mistral AI"] + result = await client.get_embeddings(texts) + + # 3. Print results. + print(f"Generated {len(result)} embeddings") + for i, embedding in enumerate(result): + print(f" Text {i + 1}: dimensions={embedding.dimensions}, vector={embedding.vector[:5]}...") + + if result.usage: + print( + f" Usage: {result.usage['input_token_count']} input tokens, " + f"{result.usage['total_token_count']} total tokens" + ) + + +async def embedding_with_options_example() -> None: + """Generate embeddings with custom dimensions.""" + print("\n=== Embedding with Custom Dimensions ===") + + from agent_framework_mistral import MistralEmbeddingOptions + + client = MistralEmbeddingClient() + + # Request a specific output dimension (model must support it). + options: MistralEmbeddingOptions = {"dimensions": 256} + result = await client.get_embeddings(["Dimensionality reduction example"], options=options) + + print(f" Dimensions: {result[0].dimensions}") + print(f" Vector (first 5): {result[0].vector[:5]}...") + + +async def main() -> None: + """Run embedding examples.""" + await basic_embedding_example() + await embedding_with_options_example() + + +if __name__ == "__main__": + asyncio.run(main()) + +""" +Sample output: +=== Basic Embedding Generation === +Generated 3 embeddings + Text 1: dimensions=1024, vector=[0.0123, -0.0456, 0.0789, -0.0012, 0.0345]... + Text 2: dimensions=1024, vector=[0.0234, -0.0567, 0.0891, -0.0023, 0.0456]... + Text 3: dimensions=1024, vector=[0.0345, -0.0678, 0.0912, -0.0034, 0.0567]... + Usage: 15 input tokens, 15 total tokens + +=== Embedding with Custom Dimensions === + Dimensions: 256 + Vector (first 5): [0.0456, -0.0789, 0.0123, -0.0456, 0.0789]... +""" diff --git a/python/packages/mistral/tests/mistral/test_mistral_embedding_client.py b/python/packages/mistral/tests/mistral/test_mistral_embedding_client.py new file mode 100644 index 0000000000..cecd03b3e2 --- /dev/null +++ b/python/packages/mistral/tests/mistral/test_mistral_embedding_client.py @@ -0,0 +1,267 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from agent_framework import Embedding, GeneratedEmbeddings + +from agent_framework_mistral import MistralEmbeddingClient, MistralEmbeddingOptions + +# region: Unit Tests + + +def test_mistral_embedding_construction(monkeypatch: pytest.MonkeyPatch) -> None: + """Test construction with environment variables.""" + monkeypatch.setenv("MISTRAL_EMBEDDING_MODEL", "mistral-embed") + monkeypatch.setenv("MISTRAL_API_KEY", "test-key") + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_cls.return_value = MagicMock() + client = MistralEmbeddingClient() + assert client.model == "mistral-embed" + + +def test_mistral_embedding_construction_with_params() -> None: + """Test construction with explicit parameters.""" + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_cls.return_value = MagicMock() + client = MistralEmbeddingClient( + model="mistral-embed", + api_key="test-key", + ) + assert client.model == "mistral-embed" + mock_cls.assert_called_once_with(api_key="test-key") + + +def test_mistral_embedding_construction_with_server_url() -> None: + """Test construction with custom server URL.""" + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_cls.return_value = MagicMock() + client = MistralEmbeddingClient( + model="mistral-embed", + api_key="test-key", + server_url="https://custom.mistral.ai", + ) + assert client.model == "mistral-embed" + assert client.server_url == "https://custom.mistral.ai" + mock_cls.assert_called_once_with( + api_key="test-key", + server_url="https://custom.mistral.ai", + ) + + +def test_mistral_embedding_construction_with_client() -> None: + """Test construction with a pre-configured client.""" + mock_client = MagicMock() + with patch("agent_framework_mistral._embedding_client.Mistral"): + client = MistralEmbeddingClient( + model="mistral-embed", + api_key="test-key", + client=mock_client, + ) + assert client.client is mock_client + + +def test_mistral_embedding_construction_missing_model_raises(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that missing model raises an error.""" + monkeypatch.delenv("MISTRAL_EMBEDDING_MODEL", raising=False) + monkeypatch.setenv("MISTRAL_API_KEY", "test-key") + from agent_framework.exceptions import SettingNotFoundError + + with pytest.raises(SettingNotFoundError): + MistralEmbeddingClient() + + +def test_mistral_embedding_construction_missing_api_key_raises(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that missing API key raises an error.""" + monkeypatch.delenv("MISTRAL_API_KEY", raising=False) + monkeypatch.setenv("MISTRAL_EMBEDDING_MODEL", "mistral-embed") + from agent_framework.exceptions import SettingNotFoundError + + with pytest.raises(SettingNotFoundError): + MistralEmbeddingClient() + + +def test_mistral_embedding_service_url() -> None: + """Test service_url returns the correct URL.""" + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_cls.return_value = MagicMock() + client = MistralEmbeddingClient( + model="mistral-embed", + api_key="test-key", + ) + assert client.service_url() == "https://api.mistral.ai" + + +def test_mistral_embedding_service_url_custom() -> None: + """Test service_url returns custom URL when set.""" + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_cls.return_value = MagicMock() + client = MistralEmbeddingClient( + model="mistral-embed", + api_key="test-key", + server_url="https://custom.mistral.ai", + ) + assert client.service_url() == "https://custom.mistral.ai" + + +async def test_mistral_embedding_get_embeddings() -> None: + """Test generating embeddings via the Mistral API.""" + mock_response = MagicMock() + mock_response.data = [ + MagicMock(embedding=[0.1, 0.2, 0.3], index=0, object="embedding"), + MagicMock(embedding=[0.4, 0.5, 0.6], index=1, object="embedding"), + ] + mock_response.model = "mistral-embed" + mock_response.usage = MagicMock(prompt_tokens=10, total_tokens=10) + + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_client = MagicMock() + mock_client.embeddings = MagicMock() + mock_client.embeddings.create_async = AsyncMock(return_value=mock_response) + mock_cls.return_value = mock_client + + client = MistralEmbeddingClient(model="mistral-embed", api_key="test-key") + result = await client.get_embeddings(["hello", "world"]) + + assert isinstance(result, GeneratedEmbeddings) + assert len(result) == 2 + assert result[0].vector == [0.1, 0.2, 0.3] + assert result[1].vector == [0.4, 0.5, 0.6] + assert result[0].model == "mistral-embed" + assert result.usage == {"input_token_count": 10, "total_token_count": 10} + + mock_client.embeddings.create_async.assert_called_once_with( + model="mistral-embed", + inputs=["hello", "world"], + ) + + +async def test_mistral_embedding_get_embeddings_empty_input() -> None: + """Test generating embeddings with empty input.""" + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_client = MagicMock() + mock_cls.return_value = mock_client + + client = MistralEmbeddingClient(model="mistral-embed", api_key="test-key") + result = await client.get_embeddings([]) + + assert isinstance(result, GeneratedEmbeddings) + assert len(result) == 0 + + +async def test_mistral_embedding_get_embeddings_with_dimensions() -> None: + """Test generating embeddings with custom dimensions option.""" + mock_response = MagicMock() + mock_response.data = [ + MagicMock(embedding=[0.1, 0.2], index=0, object="embedding"), + ] + mock_response.model = "mistral-embed" + mock_response.usage = MagicMock(prompt_tokens=5, total_tokens=5) + + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_client = MagicMock() + mock_client.embeddings = MagicMock() + mock_client.embeddings.create_async = AsyncMock(return_value=mock_response) + mock_cls.return_value = mock_client + + client = MistralEmbeddingClient(model="mistral-embed", api_key="test-key") + options: MistralEmbeddingOptions = {"dimensions": 512} + result = await client.get_embeddings(["hello"], options=options) + + assert len(result) == 1 + mock_client.embeddings.create_async.assert_called_once_with( + model="mistral-embed", + inputs=["hello"], + output_dimension=512, + ) + + +async def test_mistral_embedding_get_embeddings_no_model_raises() -> None: + """Test that missing model at call time raises ValueError.""" + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_client = MagicMock() + mock_cls.return_value = mock_client + + client = MistralEmbeddingClient(model="mistral-embed", api_key="test-key") + client.model = None # type: ignore[assignment] + + with pytest.raises(ValueError, match="model is required"): + await client.get_embeddings(["hello"]) + + +async def test_mistral_embedding_get_embeddings_model_override() -> None: + """Test that model can be overridden via options.""" + mock_response = MagicMock() + mock_response.data = [ + MagicMock(embedding=[0.1, 0.2, 0.3], index=0, object="embedding"), + ] + mock_response.model = "custom-embed" + mock_response.usage = MagicMock(prompt_tokens=5, total_tokens=5) + + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_client = MagicMock() + mock_client.embeddings = MagicMock() + mock_client.embeddings.create_async = AsyncMock(return_value=mock_response) + mock_cls.return_value = mock_client + + client = MistralEmbeddingClient(model="mistral-embed", api_key="test-key") + options: MistralEmbeddingOptions = {"model": "custom-embed"} + result = await client.get_embeddings(["hello"], options=options) + + assert len(result) == 1 + assert result[0].model == "custom-embed" + mock_client.embeddings.create_async.assert_called_once_with( + model="custom-embed", + inputs=["hello"], + ) + + +async def test_mistral_embedding_get_embeddings_no_usage() -> None: + """Test handling response without usage information.""" + mock_response = MagicMock() + mock_response.data = [ + MagicMock(embedding=[0.1, 0.2, 0.3], index=0, object="embedding"), + ] + mock_response.model = "mistral-embed" + mock_response.usage = None + + with patch("agent_framework_mistral._embedding_client.Mistral") as mock_cls: + mock_client = MagicMock() + mock_client.embeddings = MagicMock() + mock_client.embeddings.create_async = AsyncMock(return_value=mock_response) + mock_cls.return_value = mock_client + + client = MistralEmbeddingClient(model="mistral-embed", api_key="test-key") + result = await client.get_embeddings(["hello"]) + + assert len(result) == 1 + assert result.usage is None + + +# region: Integration Tests + +skip_if_mistral_embedding_integration_tests_disabled = pytest.mark.skipif( + os.getenv("MISTRAL_EMBEDDING_MODEL", "") in ("", "test-model") or os.getenv("MISTRAL_API_KEY", "") == "", + reason="No real Mistral embedding model or API key provided; skipping integration tests.", +) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_mistral_embedding_integration_tests_disabled +async def test_mistral_embedding_integration() -> None: + """Integration test for Mistral AI embedding client.""" + client = MistralEmbeddingClient() + result = await client.get_embeddings(["Hello, world!", "How are you?"]) + + assert isinstance(result, GeneratedEmbeddings) + assert len(result) == 2 + for embedding in result: + assert isinstance(embedding, Embedding) + assert isinstance(embedding.vector, list) + assert len(embedding.vector) > 0 + assert all(isinstance(v, float) for v in embedding.vector) + assert result.usage is not None + assert result.usage["input_token_count"] is not None + assert result.usage["input_token_count"] > 0 diff --git a/python/pyproject.toml b/python/pyproject.toml index 49a2843456..454862ac82 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -54,7 +54,9 @@ package = false prerelease = "if-necessary-or-explicit" # Security floors for transitive deps; overrides bypass litellm[proxy]'s strict pins. constraint-dependencies = ["litellm>=1.83.7", "fastapi-sso>=0.19.0"] -override-dependencies = ["mcp[ws]>=1.27.0", "uvicorn[standard]>=0.34.0"] +# Allow opentelemetry-semantic-conventions 0.61b0 for mistralai compatibility +# (mistralai pins <0.61 but 0.61b0 is compatible at runtime). +override-dependencies = ["mcp[ws]>=1.27.0", "uvicorn[standard]>=0.34.0", "opentelemetry-semantic-conventions>=0.60b1"] environments = [ "sys_platform == 'darwin'", "sys_platform == 'linux'", @@ -88,6 +90,7 @@ agent-framework-github-copilot = { workspace = true } agent-framework-hyperlight = { workspace = true } agent-framework-lab = { workspace = true } agent-framework-mem0 = { workspace = true } +agent-framework-mistral = { workspace = true } agent-framework-monty = { workspace = true } agent-framework-ollama = { workspace = true } agent-framework-openai = { workspace = true } @@ -211,6 +214,7 @@ executionEnvironments = [ { root = "packages/lab/lightning/tests", reportPrivateUsage = "none" }, { root = "packages/lab/tau2/tests", reportPrivateUsage = "none" }, { root = "packages/mem0/tests", reportPrivateUsage = "none" }, + { root = "packages/mistral/tests", reportPrivateUsage = "none" }, { root = "packages/ollama/tests", reportPrivateUsage = "none" }, { root = "packages/orchestrations/tests", reportPrivateUsage = "none" }, { root = "packages/purview/tests", reportPrivateUsage = "none" }, diff --git a/python/uv.lock b/python/uv.lock index 4ac7509f35..a67c495e62 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -50,6 +50,7 @@ members = [ "agent-framework-hyperlight", "agent-framework-lab", "agent-framework-mem0", + "agent-framework-mistral", "agent-framework-monty", "agent-framework-ollama", "agent-framework-openai", @@ -64,6 +65,7 @@ constraints = [ ] overrides = [ { name = "mcp", extras = ["ws"], specifier = ">=1.27.0" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.60b1" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, ] @@ -653,7 +655,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.4.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.4.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 = "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'" }, ] @@ -722,6 +724,21 @@ requires-dist = [ { name = "mem0ai", specifier = ">=1.0.0,<2" }, ] +[[package]] +name = "agent-framework-mistral" +version = "1.0.0a260505" +source = { editable = "packages/mistral" } +dependencies = [ + { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mistralai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework-core", editable = "packages/core" }, + { name = "mistralai", specifier = ">=2.0.0,<3" }, +] + [[package]] name = "agent-framework-monty" version = "1.0.0a260521" @@ -802,7 +819,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.4.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.4.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 = "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'" }, ] @@ -893,7 +910,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -905,110 +922,110 @@ dependencies = [ { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, - { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, - { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, - { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, - { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, - { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, - { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, - { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, + { url = "https://files.pythonhosted.org/packages/2c/05/6817e0390eb47b0867cf8efdb535298191662192281bc3ca62a0cb7973eb/aiohttp-3.13.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6290fe12fe8cefa6ea3c1c5b969d32c010dfe191d4392ff9b599a3f473cbe722", size = 753094, upload-time = "2026-03-28T17:14:59.928Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/e5b7f25f6dd1ab57da92aa9d226b2c8b56f223dd20475d3ddfddaba86ab8/aiohttp-3.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7520d92c0e8fbbe63f36f20a5762db349ff574ad38ad7bc7732558a650439845", size = 505213, upload-time = "2026-03-28T17:15:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e5/8f42033c7ce98b54dfd3791f03e60231cfe4a2db4471b5fc188df2b8a6ad/aiohttp-3.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2710ae1e1b81d0f187883b6e9d66cecf8794b50e91aa1e73fc78bfb5503b5d9", size = 498580, upload-time = "2026-03-28T17:15:03.879Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/bbc989f5362066b81930da1a66084a859a971d03faab799dc59a3ce3a220/aiohttp-3.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:717d17347567ded1e273aa09918650dfd6fd06f461549204570c7973537d4123", size = 1692718, upload-time = "2026-03-28T17:15:05.541Z" }, + { url = "https://files.pythonhosted.org/packages/1c/72/3775116969931f151be116689d2ae6ddafff2ec2887d8f9b4e7043f32e74/aiohttp-3.13.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:383880f7b8de5ac208fa829c7038d08e66377283b2de9e791b71e06e803153c2", size = 1660714, upload-time = "2026-03-28T17:15:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e8/d2f1a2da2743e32fe348ebf8a4c59caad14a92f5f18af616fd33381275e1/aiohttp-3.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1867087e2c1963db1216aedf001efe3b129835ed2b05d97d058176a6d08b5726", size = 1744152, upload-time = "2026-03-28T17:15:10.828Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a6/575886f417ac3c08e462f2ca237cc49f436bd992ca3f7ff95b7dd9c44205/aiohttp-3.13.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6234bf416a38d687c3ab7f79934d7fb2a42117a5b9813aca07de0a5398489023", size = 1836278, upload-time = "2026-03-28T17:15:12.537Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4c/0051d4550fb9e8b5ca4e0fe1ccd58652340915180c5164999e6741bf2083/aiohttp-3.13.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdd3393130bf6588962441ffd5bde1d3ea2d63a64afa7119b3f3ba349cebbe7", size = 1687953, upload-time = "2026-03-28T17:15:14.248Z" }, + { url = "https://files.pythonhosted.org/packages/c9/54/841e87b8c51c2adc01a3ceb9919dc45c7899fe4c21deb70aada734ea5a38/aiohttp-3.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d0dbc6c76befa76865373d6aa303e480bb8c3486e7763530f7f6e527b471118", size = 1572484, upload-time = "2026-03-28T17:15:15.911Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/21cbf5f7fa1e267af6301f886cab9b314f085e4d0097668d189d165cd7da/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10fb7b53262cf4144a083c9db0d2b4d22823d6708270a9970c4627b248c6064c", size = 1662851, upload-time = "2026-03-28T17:15:17.822Z" }, + { url = "https://files.pythonhosted.org/packages/40/15/bcad6b68d7bef27ae7443288215767263c7753ede164267cf6cf63c94a87/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:eb10ce8c03850e77f4d9518961c227be569e12f71525a7e90d17bca04299921d", size = 1671984, upload-time = "2026-03-28T17:15:19.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/ab316931afc7a73c7f493bb1b30fbd61e28ec2d3ea50353336e76293e8ec/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7c65738ac5ae32b8feef699a4ed0dc91a0c8618b347781b7461458bbcaaac7eb", size = 1713880, upload-time = "2026-03-28T17:15:21.589Z" }, + { url = "https://files.pythonhosted.org/packages/1c/45/314e8e64c7f328174964b6db511dd5e9e60c9121ab5457bc2c908b7d03a4/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6b335919ffbaf98df8ff3c74f7a6decb8775882632952fd1810a017e38f15aee", size = 1560315, upload-time = "2026-03-28T17:15:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/18/e7/93d5fa06fe00219a81466577dacae9e3732f3b4f767b12b2e2cc8c35c970/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ec75fc18cb9f4aca51c2cbace20cf6716e36850f44189644d2d69a875d5e0532", size = 1735115, upload-time = "2026-03-28T17:15:25.77Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/f64b95392ddd4e204fd9ab7cd33dd18d14ac9e4b86866f1f6a69b7cda83d/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:463fa18a95c5a635d2b8c09babe240f9d7dbf2a2010a6c0b35d8c4dff2a0e819", size = 1673916, upload-time = "2026-03-28T17:15:27.526Z" }, + { url = "https://files.pythonhosted.org/packages/52/c1/bb33be79fd285c69f32e5b074b299cae8847f748950149c3965c1b3b3adf/aiohttp-3.13.4-cp310-cp310-win32.whl", hash = "sha256:13168f5645d9045522c6cef818f54295376257ed8d02513a37c2ef3046fc7a97", size = 440277, upload-time = "2026-03-28T17:15:29.173Z" }, + { url = "https://files.pythonhosted.org/packages/23/f9/7cf1688da4dd0885f914ee40bc8e1dce776df98fe6518766de975a570538/aiohttp-3.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:a7058af1f53209fdf07745579ced525d38d481650a989b7aa4a3b484b901cdab", size = 463015, upload-time = "2026-03-28T17:15:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, + { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, + { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, + { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, + { url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" }, + { url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" }, + { url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" }, + { url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" }, + { url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" }, + { url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" }, + { url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" }, + { url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" }, + { url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" }, + { url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" }, + { url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" }, + { url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" }, + { url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" }, ] [[package]] @@ -1249,28 +1266,28 @@ wheels = [ [[package]] name = "azure-core" -version = "1.41.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests", 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/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, ] [[package]] name = "azure-core-tracing-opentelemetry" -version = "1.0.0b13" +version = "1.0.0b12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/ab/a937e4af8afec9d437d55252f2a3a4419fc3fc7d5e5d54022622bd11b2b6/azure_core_tracing_opentelemetry-1.0.0b13.tar.gz", hash = "sha256:6cb2f8dfd5dee6c11843db0205fc92e2434e1a272c169c953afe92483aafc7eb", size = 25832, upload-time = "2026-05-01T00:59:57.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/7f/5de13a331a5f2919417819cc37dcf7c897018f02f83aa82b733e6629a6a6/azure_core_tracing_opentelemetry-1.0.0b12.tar.gz", hash = "sha256:bb454142440bae11fd9d68c7c1d67ae38a1756ce808c5e4d736730a7b4b04144", size = 26010, upload-time = "2025-03-21T00:18:37.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/01/8898c2506cae6a57c1b76d930d2af94764a65354bc863feb2684235851ce/azure_core_tracing_opentelemetry-1.0.0b13-py3-none-any.whl", hash = "sha256:4dacd3a9f117f11f98e89305e161c951b8df85b984f3b56130614de9cd9887f9", size = 12112, upload-time = "2026-05-01T00:59:59.149Z" }, + { url = "https://files.pythonhosted.org/packages/76/5e/97a471f66935e7f89f521d0e11ae49c7f0871ca38f5c319dccae2155c8d8/azure_core_tracing_opentelemetry-1.0.0b12-py3-none-any.whl", hash = "sha256:38fd42709f1cc4bbc4f2797008b1c30a6a01617e49910c05daa3a0d0c65053ac", size = 11962, upload-time = "2025-03-21T00:18:38.581Z" }, ] [[package]] @@ -1358,7 +1375,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b52" +version = "1.0.0b51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1368,9 +1385,9 @@ dependencies = [ { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/7e/bfc03436b88c48f5adc21a3ebbf4392b6b7fbbfe33ef3b1e88d07ba9f380/azure_monitor_opentelemetry_exporter-1.0.0b52.tar.gz", hash = "sha256:7eac679fca32dee9e426df65f2a538161db4514fc322fc66107f7826567d86e1", size = 326179, upload-time = "2026-05-11T22:47:02.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/a4/a6cd2d389bc1009300bcd57c9e2ace4b7e7ae1e5dc0bda415ee803629cf2/azure_monitor_opentelemetry_exporter-1.0.0b51.tar.gz", hash = "sha256:a6171c34326bcd6216938bb40d715c15f1f22984ac1986fc97231336d8ac4c3c", size = 319837, upload-time = "2026-04-06T21:45:46.378Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/e8/d13e6a74c98ecc3011bce9ab09fc2e75aec48ab46288f72be57c2fa21460/azure_monitor_opentelemetry_exporter-1.0.0b52-py2.py3-none-any.whl", hash = "sha256:a38c503e5e2cc0ec8a4bf336b23cce23488719f5361a45cdd01a514080f0e7fc", size = 244751, upload-time = "2026-05-11T22:47:04.304Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1a/6b0b7a6181b42709103a65a676c89fd5055cb1d1b281ebe10c49254a170f/azure_monitor_opentelemetry_exporter-1.0.0b51-py2.py3-none-any.whl", hash = "sha256:6572cac11f96e3b18ae1187cb35cf3b40d0004655dae8048896c41c765bea530", size = 242104, upload-time = "2026-04-06T21:45:47.856Z" }, ] [[package]] @@ -1432,158 +1449,39 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.1" +version = "1.42.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "s3transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/36/028c12ed6ed85009a21b5472eb76c27f9b0341c6986f06f83475b40aaf51/boto3-1.43.1.tar.gz", hash = "sha256:9e4f85a7884797ff0f52c257094730ed228aaa07fa8134775ff8f86909cf4f2a", size = 113175, upload-time = "2026-04-30T20:27:04.569Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz", hash = "sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size = 112725, upload-time = "2026-02-27T20:25:33.228Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/d1/b8b2d5420c51cd8f7ec044ceecbf24b060156680b26519e1d482e160c3c8/boto3-1.43.1-py3-none-any.whl", hash = "sha256:3840bf0345b9aefcc5915176a19d227f63cfba7778c65e6e52d61c6ea0a10fdc", size = 140498, upload-time = "2026-04-30T20:27:01.791Z" }, + { url = "https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl", hash = "sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size = 140557, upload-time = "2026-02-27T20:25:31.774Z" }, ] [[package]] name = "botocore" -version = "1.43.11" +version = "1.42.81" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dateutil", 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/e9/fa/4bec16fa5a4cde7b593e549238bfeb8ed1bdba9d427888a18c460a1f2352/botocore-1.43.11.tar.gz", hash = "sha256:d7d479cc2809ec2728f2898521003adfb79bfe6a4615c59dfd222ec52b0cee6b", size = 15364020, upload-time = "2026-05-19T19:39:58.317Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/5f/b0bb9a8768398fb131e1fe722c9cc5b18f74d21ca1970efe8576912b2c6e/botocore-1.42.81.tar.gz", hash = "sha256:48e6f6f52de1cc107a34810309b8ca998ea9bb719a3fe4c06f903a604b3138cb", size = 15129980, upload-time = "2026-04-01T19:35:23.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9a/9f1d955c2eebefb6bd20de740ae7a05e7b015c63f0f01dba338dcf29cc68/botocore-1.43.11-py3-none-any.whl", hash = "sha256:0108b5604df5a26918936c845e1e761866ee9ea8d1c1f9358ed3c69afdc37436", size = 15043467, upload-time = "2026-05-19T19:39:53.176Z" }, -] - -[[package]] -name = "cachebox" -version = "5.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/f6/85f176d2518cf1d1be5f981fc2dadf6b131e33fefd721f36b330e3434d6c/cachebox-5.2.3.tar.gz", hash = "sha256:b1f68246685aa739bbbd2734befb1465363a1e1042407c154feadb065f17a099", size = 63686, upload-time = "2026-04-10T12:21:35.028Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/9e/88193fcb7a2a43fe8ed9d9888374d43fa5c7176aa802651e68b28f1aee4a/cachebox-5.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c2c89720547271d36e10cad2c7302bbe11f46eb39eead0a2c321c2d371b8f8b6", size = 374393, upload-time = "2026-04-10T12:20:20.424Z" }, - { url = "https://files.pythonhosted.org/packages/98/8d/e0b13d9bfd43f295cce7824ebaac1970f818a7027c16f290de404934cafe/cachebox-5.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7f33d24e90dc8aa26762e25898c91a1223b66685420a28a3628fa2e006924f5", size = 356318, upload-time = "2026-04-10T12:20:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/bc/02/8ae1b63dbdebb2ebf600523f48b54e9bfb10db5a28551c3432346f49e1dd/cachebox-5.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cb03ec6289a2ac5daf7422d755683324f02d821bfa796087100df2a7ebd5de", size = 395782, upload-time = "2026-04-10T12:18:50.054Z" }, - { url = "https://files.pythonhosted.org/packages/e4/2f/79a8a0057f354581c25a1a00ddabbd5db4b8631d192670d7a0cc4271dbb7/cachebox-5.2.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71a71df463ba4c86bc843fa01c3a2a721033adefad888af28c6b65e1915a75c", size = 353194, upload-time = "2026-04-10T12:19:03.083Z" }, - { url = "https://files.pythonhosted.org/packages/3b/57/a1fead35cf481432bd87def0653cd4a069b1ea5847589255795e49ae74b8/cachebox-5.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbe4655371d19fc9f4f5874312bcb6e5b5b6182989979ac33d93c34c8d10c012", size = 371090, upload-time = "2026-04-10T12:19:16.019Z" }, - { url = "https://files.pythonhosted.org/packages/8c/58/53f1fab8bcc3238fd6c533ef3ab146097986a8acb722863c688a2410c1b2/cachebox-5.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4974476d1779961df89d6e6f79e6103a1659289d3ee11c92adcb52e236a8aaeb", size = 390902, upload-time = "2026-04-10T12:19:28.258Z" }, - { url = "https://files.pythonhosted.org/packages/11/2f/5abff74666f8388d2c9516c265f99c33484c827f7fcb3cd703c2f3cbb17e/cachebox-5.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad16d733219f4cab3eec6533af30ab7b9c919c6e3e22ad1ef4eb82629a62edef", size = 395855, upload-time = "2026-04-10T12:19:54.207Z" }, - { url = "https://files.pythonhosted.org/packages/dd/11/30b429db12ab5df663aa108bcfac42805f733da65b0bf452f60bfaf4a530/cachebox-5.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:12a9e0a93774ca2b3a9fe8a2a0d0812e399fac4af0fce6246a5bca1e7009b8fc", size = 425760, upload-time = "2026-04-10T12:19:41.138Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b4/fdac1bb902b954c03d23eb301d645a328c9664caff5898930fdbd92fde80/cachebox-5.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be89497a011eb7a638d13cc520244d77579c0f515b95bf759b3de0b90a015203", size = 564988, upload-time = "2026-04-10T12:20:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/76cd5405b0339f15bf86593258bf9bc5608f10a5e0fa6f37a282b42a6caa/cachebox-5.2.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dd01fc0c1934cccb76493eb4b149a9232d299e5e0275f557adf875c3d25cec81", size = 669110, upload-time = "2026-04-10T12:20:49.039Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bc/52d154aa0407bafce94d1d8d3ff27ca5e842f8311be43cfabdefcbb0f6b7/cachebox-5.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a0dfd97b0968f8bd48c33098a03d10f797964559c3a437c84bf97a9973545714", size = 643768, upload-time = "2026-04-10T12:21:04.095Z" }, - { url = "https://files.pythonhosted.org/packages/51/d9/82627eb8cecaf5e7e601bbc65d474a1c3053a2fbc21618ddc6aac19c47dc/cachebox-5.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:223ccf7ac60f595def258e7bc74c0b1d6f43991c9cae6d06749c803d22786d99", size = 610047, upload-time = "2026-04-10T12:21:19.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2e/cc5b303746418fde00c93ddbc295733b4e2d131d2e8f5afbc6f45f50454e/cachebox-5.2.3-cp310-cp310-win32.whl", hash = "sha256:745b805fdd99931c3ce1d87d2ee21ca3fb62cba6b4e1f674907af87aad73dce4", size = 275529, upload-time = "2026-04-10T12:21:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/31/72/fb10d6f779d041f701b89f0b7830329f51d1846fbc600869f9f7d635b7b5/cachebox-5.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:a87b19c0a3d8d665a9805b5b4afd64b40082395b70ebe2756131ed1edb0c8f02", size = 287988, upload-time = "2026-04-10T12:21:36.41Z" }, - { url = "https://files.pythonhosted.org/packages/81/88/154179d492f2c000fe6efab3c3ff6b8eb94fbfaa09efe47999bce6b1e29f/cachebox-5.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:996f49d04b234082530afcc650bdd00556afbebc19c6c0daaafb85950340cb3c", size = 374245, upload-time = "2026-04-10T12:20:22.042Z" }, - { url = "https://files.pythonhosted.org/packages/7d/9d/3b03f2e063161bcb1a5e0969d521b5c622c2da02252a5c8bd4ef0e4f9914/cachebox-5.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23a3300ebbb526fa12ce6fa53699002f5fba6da23b4bbbaf8ba8b18a3f03e6b3", size = 356308, upload-time = "2026-04-10T12:20:09.149Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9b/8da38af731e3832e9f987548e4bfb610d7f3054019e12c44a94ba9272b37/cachebox-5.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c63ee1589364caa04c018405e625d2e44e0bf9994f2715b2f322075d8c45b6", size = 395666, upload-time = "2026-04-10T12:18:51.89Z" }, - { url = "https://files.pythonhosted.org/packages/01/dd/1522aa808f94c904c5eb3640991799fed14dd43c1dd99a9f7b71bd95b1e3/cachebox-5.2.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebd0f8d4ebc3943c1ddcbbdc54f1a8ddf95505c862ed5731319cebd1eb98ae41", size = 353362, upload-time = "2026-04-10T12:19:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/95bf883ec9b69a76f3a7d9fb14d015d9a4bdab0143a3eff62ceebc8b1419/cachebox-5.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:569966efcc6309aa7d774443e3513cdbb8671efae0158138ba2ebb7d8cc9d8ed", size = 371007, upload-time = "2026-04-10T12:19:17.484Z" }, - { url = "https://files.pythonhosted.org/packages/4b/3d/cc02066d5ccfcb8b35adbaf867977fdb54572cda56ace56da396f0caa3bf/cachebox-5.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5774d06f0da37dd566239a4376d6ca8cf983d3e4c3228712ec22b4130f662f21", size = 390670, upload-time = "2026-04-10T12:19:29.685Z" }, - { url = "https://files.pythonhosted.org/packages/b3/50/8e4d59b3e344405d8393d6cc5cc92754d3cc1d81134041ebffd3f5ab73e6/cachebox-5.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5bf8755bc66bcf42e7ca5c42d703a041a7aaad58f9a0c3be54d5b1cefd2641", size = 395765, upload-time = "2026-04-10T12:19:56.169Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d4/d731cff1c4cec22404bd3ddda05b233c5efaa5f13d7abf4e2728905b7cdd/cachebox-5.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63f061cc6a5ca70bbce2e6be0588fe2fee00a93a1b0581b1086d54b10288cdb6", size = 425707, upload-time = "2026-04-10T12:19:42.714Z" }, - { url = "https://files.pythonhosted.org/packages/36/01/3ec8aadceb0dcc66dbd0b9b32966cf7b6928ed84471424c24d21b0af62d0/cachebox-5.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577c781f18b559f4dc9eea176c6aed008843ef4b8e045cf61bb519e09dccc9ef", size = 564759, upload-time = "2026-04-10T12:20:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/31cbc8623ecc2e25900f7e8f20f11bfb84786989a59a8046e70b27cbea6d/cachebox-5.2.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7f691e25572a3ddbb018e19d796f774713bd6b0f7ce9be2e71f6e18572de264a", size = 669309, upload-time = "2026-04-10T12:20:51.117Z" }, - { url = "https://files.pythonhosted.org/packages/34/29/5a9e92bdc7b32dc865e73dd776638244f900136daee5bb0591a67e1530fa/cachebox-5.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:33368adf86669c29b936fbae5d6219cf90aacd4b1db71dae2e23d584a8219cd6", size = 643705, upload-time = "2026-04-10T12:21:05.882Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/5273a412855fdc11f674e4749aee6d5ec0a91f5c1a9f6e922f7fa0cb7a83/cachebox-5.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38ce67b7b45713e49459a09411d07f82de04022c04aecde6202cd32f934c2b1f", size = 609751, upload-time = "2026-04-10T12:21:21.331Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a4/0fadb5e6a00f373cc3fe56b4415cdea2fc0147f6ec475611762d16eb4b05/cachebox-5.2.3-cp311-cp311-win32.whl", hash = "sha256:a7cd2c81347063ab6c512d0f569aeb5f75fc2dfe686c8486258ffd08052324f4", size = 275485, upload-time = "2026-04-10T12:21:51.563Z" }, - { url = "https://files.pythonhosted.org/packages/03/83/67c1bf83f815294d2c3acd7631f25b5cbe6067e1d56495f76829dd60057b/cachebox-5.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:7e45798d6b969794840bb302857946d710ecb32af78dfcb3ab40f4e68ee7fdaf", size = 288024, upload-time = "2026-04-10T12:21:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e7/6fa6abfc9c4c07b88f09a88466fa93c7081fd679d8e06f8f558bb4ac845c/cachebox-5.2.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09c0340e9daa7b4530801e5a570cb0c1a1ad941a85d245d360020d3986d0e787", size = 377791, upload-time = "2026-04-10T12:20:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/3a/79/89e4423352d0ca33bbf80fc1b4b665e654a93de8b16cf41e96fcac81801a/cachebox-5.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3162758792626685ec34950eedd565d015b115d0ff0d751d2716031fc32d51b", size = 359562, upload-time = "2026-04-10T12:20:10.626Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ab/e533c2751e6a3411ebe369277aaed03199b9e4586a48f0a3712a1f4b418b/cachebox-5.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a189a780c3ccd7b9d157074ba6bf3e191e522b39abbdb590075111851f02d50d", size = 397910, upload-time = "2026-04-10T12:18:53.336Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0d/b8492d6ca53278499a37c9f9d51afd4ad77bfbe813d6281944d45b97a1e7/cachebox-5.2.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:410b67baa99d433644199b11289627f7ebba4ee5786f95ca9858f238afcee157", size = 353699, upload-time = "2026-04-10T12:19:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/78/d4/fd20b3a5362651303fa12d3ee62f56af2bd396e4a7303d7014a1a1e5b392/cachebox-5.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81474dc19d3865fa5e57263f834bc6bbc00e471a594fb9d934ed552732c02fd", size = 372510, upload-time = "2026-04-10T12:19:18.997Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/3ec55c946d300cc4eaed3a0f79740051ac6e11ef4032421332c6ca15f5d5/cachebox-5.2.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85ccd827193b3e3e887a88a16b88ef7ed174e7e65be515b5253322aa75e665c3", size = 392802, upload-time = "2026-04-10T12:19:31.196Z" }, - { url = "https://files.pythonhosted.org/packages/01/b1/1a3c4e436ad8a4c4ba3e70f4c62e1f927cbbb3c943a9bba5813b8b815bde/cachebox-5.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1e7d3cb8a5e7e68996a8619e3ef8771a124d14568c251f9e586eba88d759c1", size = 398223, upload-time = "2026-04-10T12:19:57.583Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/d36ad3976c4396b350b96a1582411b7a00e56c144eec0bb5ba5f36ce7d86/cachebox-5.2.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adcedfcfcb933b21e7fdcfe560c79887bc8287abceab0586aa3730417dd0277d", size = 427696, upload-time = "2026-04-10T12:19:44.361Z" }, - { url = "https://files.pythonhosted.org/packages/a8/36/71845b5c7a9ffbd85e6fdb470c11a174f499bd5238fa37b1214157c2454d/cachebox-5.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7f0c72c51a3a9e7049ea6ff2a43cd3877ab7fee966eb65771a59621563b75e3", size = 567854, upload-time = "2026-04-10T12:20:38.357Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a2/baf0e5a8392e64e352b137ccd7356b3d98068c842fd19f510a7790c05d34/cachebox-5.2.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c48c10e498d573511aafbd545570e7f43b40a7428dc282183bf5adc334d9e1a8", size = 670306, upload-time = "2026-04-10T12:20:52.903Z" }, - { url = "https://files.pythonhosted.org/packages/a5/22/cd4e4c1d624b8ef9fb4b8bebf0bf5d2d74a399cf1ac46b667bb79d15359a/cachebox-5.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2f1e086ab5ffd082a68bb63699d517655a59b06414927bfc84e01df91b81e34d", size = 645943, upload-time = "2026-04-10T12:21:08.238Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d6/55859981f5ec6a9e412baaa4db6aa5973a00008750b3f054cdefcb6491fc/cachebox-5.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:649d18399f13735bb82daa33800196f815529c49e967767c40ca221723e68afa", size = 612309, upload-time = "2026-04-10T12:21:23.404Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1e/313f650467ac85824c4199188f8f1ee3386cd12eb665dbf7c88d372e4956/cachebox-5.2.3-cp312-cp312-win32.whl", hash = "sha256:0a17aeb4e5b1c6ef1c3db8fc5186f9986e215ba5ea5a5d08baa45bcf55f261b2", size = 279789, upload-time = "2026-04-10T12:21:53.215Z" }, - { url = "https://files.pythonhosted.org/packages/c5/50/3b334f887accfa811cf5c7533b8ce22c523eb009363a86401198899dadd2/cachebox-5.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:cfd69114141ab362acaa2099e425a1b965cf7b021a539a4e953143d593930b74", size = 290917, upload-time = "2026-04-10T12:21:39.696Z" }, - { url = "https://files.pythonhosted.org/packages/31/3b/16d5c295f6ec2913ef595b39986dc7b7cc179fdd2e73f5ebd1814c38fd51/cachebox-5.2.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9527c5c70f8735f2d696331d8bcf77254f03b4dc8542046807823bd36ed4e8ba", size = 377408, upload-time = "2026-04-10T12:20:25.444Z" }, - { url = "https://files.pythonhosted.org/packages/cd/87/45f834154f79721e5b64a80ffab4f9710834c4f9c01fa977f94a9116c32a/cachebox-5.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40ac878af00d5969862c1f6bc076de1e34ca248662fce6aecca1761f52e33e32", size = 359274, upload-time = "2026-04-10T12:20:12.127Z" }, - { url = "https://files.pythonhosted.org/packages/46/17/794e5f93e0a172aa14ecd692f6d89bdf094f71eb35fa923d0a0af25cef1c/cachebox-5.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5ff26bfd8f7e95b3becf6d5f65c25edaca50fa68078868648b70d79bcccc260", size = 397520, upload-time = "2026-04-10T12:18:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/19/9470b1a96de6e480192b1a92b2fafa72aa052efc2509a5418a5652205b33/cachebox-5.2.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82e7002dd343afeeba2fcf0e483131b342a27ec3bc34b2214dc617691bda40d6", size = 353183, upload-time = "2026-04-10T12:19:07.797Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2b/72813f80397ed4640e337cbd1a14ab7eaafe33e479291d3623b6a6a55fec/cachebox-5.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ccbdc54a6c4b5758408c1083bdfa217bd382894a8331c7d0a54b84ba0cf51e5b", size = 372239, upload-time = "2026-04-10T12:19:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/47dc9687288fa55486573627089ecd9aae124de5924a4bce008af96d80b6/cachebox-5.2.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df5135a168f143d186b1cc3be0ca16b66446897ab5cedc03bd80bcc926fcd403", size = 392568, upload-time = "2026-04-10T12:19:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/13/95/450765b971a3bed9d7cf003c3833c1976482eb83b0241b6dbb840a25b43b/cachebox-5.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10bedf96db8f9766cc956f9adcc623e604264e5d6fa2e255432f8c2ed7519143", size = 397920, upload-time = "2026-04-10T12:19:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3e/dd8f4c1f92e58d479913ce9cbaa3227c911128e6046c82f4fd44309f685a/cachebox-5.2.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f22732d0d69bb84ad2dca7480bffdfd0430c647152d488936e152ecbbfee52fb", size = 427332, upload-time = "2026-04-10T12:19:45.888Z" }, - { url = "https://files.pythonhosted.org/packages/7e/20/80d8c26ce63e78da3874a5bb07a3a78de53a2b0356ba80583a4927f0a074/cachebox-5.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:26ae0b68979204d360327f4c0725cfdc95cfc34ab73ab1a8f528e3bd2f6d023c", size = 567494, upload-time = "2026-04-10T12:20:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/10/35/7249885dfed3602b3b48c1e67781197dcdc536c50f72caeabe3944348af8/cachebox-5.2.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f3d628b816e28a6e7661d460e02dd5b421247cc2cd275814f80ea79621245fc4", size = 669968, upload-time = "2026-04-10T12:20:55.155Z" }, - { url = "https://files.pythonhosted.org/packages/2d/8a/e5b58f0bbd6fef74da5d8e5ab49e67898ce7e6df28c16280a0f2b78461f7/cachebox-5.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:64057caa6b741320655cd3c5997fe642dae5dbff571eb530e6f53e58272bb43b", size = 645547, upload-time = "2026-04-10T12:21:09.948Z" }, - { url = "https://files.pythonhosted.org/packages/d8/25/51783a4c6f25ca87ef1b4b762ff0364bd98053a02d597b30d26ff4cf13c5/cachebox-5.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa325306084aa2dc0b21e07723d7700f4d43dece3732c7fdaf7a269dc5e35aa7", size = 611844, upload-time = "2026-04-10T12:21:25.286Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c5/b26c4b046e296d0e249448fe297626b3caca2e851837712f03c358662cb7/cachebox-5.2.3-cp313-cp313-win32.whl", hash = "sha256:55003089d21c2f5515089c307be063b45558e884a4a1cc9593944374c89975c4", size = 279421, upload-time = "2026-04-10T12:21:54.921Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7f/a49420670393bfea618de7a893d45cae9294cf3293d7b158e7af20e8f39e/cachebox-5.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:dcc5edb6ecf2b516e90b773d232360c5e4ed8fdcda038b19441da2ed9cf208ab", size = 290702, upload-time = "2026-04-10T12:21:41.458Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0b/bf83bda13ef6fc490d208a1d4dd712034624526a88f61713cca0edc9884f/cachebox-5.2.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a4b7559fa4994c4032dd07466c2041d57e055feb814762e1f73f4e8beef188d0", size = 371704, upload-time = "2026-04-10T12:20:27.253Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ea/aa5162273238e84f9e41b33600c69299572dc1c8f0f768d07660b71be07d/cachebox-5.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f57afada3d9327adf87f3b5cf0094348c6fd49354ab2e9bd20b044648eb094ae", size = 353385, upload-time = "2026-04-10T12:20:13.668Z" }, - { url = "https://files.pythonhosted.org/packages/47/96/3ca013e2e48df5c1d7855669b208f4bf8014ccb842ccf7a3a0eaac07bee0/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8342ff350ce86f062492752d612e9f056ac5dc56375713d75c3bf6e83b4d18db", size = 392181, upload-time = "2026-04-10T12:18:56.385Z" }, - { url = "https://files.pythonhosted.org/packages/63/ca/1bacb4efa0b0ce8065d1fb7c8dc7c382ec4e1cc3f007eb08417732be2725/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:405f9cc8492fc9d953b5a6b9e2b661e99583755c6639ab8d09a287fdf336503c", size = 349494, upload-time = "2026-04-10T12:19:09.505Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2e/75db4bda3768658f5baa5a54f6a4f643bc2de1a16788e40581a080e803c7/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94aae393ec1d9b26565d346445bb6afa3963d2a0d3eb5e4188d0e510fab871a0", size = 369216, upload-time = "2026-04-10T12:19:22.224Z" }, - { url = "https://files.pythonhosted.org/packages/f5/82/e1f833be0d57e29a8c5eb0a0275cd34b962f3c7f5b9e0517ec4bf75e7cc3/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8b0b575066fc09f6fae0d4bd30d6ff56584a6870cbe7d202916c5e0d725cfd4", size = 385922, upload-time = "2026-04-10T12:19:34.198Z" }, - { url = "https://files.pythonhosted.org/packages/53/d6/615a3c16c1d63839f2c67644eb414c4dc9769ab2e169d935110fd8e268d5/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41e99c1240106d39b63ce7868a6cd8c9da9243fef08848b85d428164e0769fd2", size = 393276, upload-time = "2026-04-10T12:20:00.925Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a6/7844c9c84b170dae1005b22da174639968e64c8055d66a209a1598663771/cachebox-5.2.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:432ca62b99f7eafc21af669d76c88c1b7377db179b89fb6fca3ea93b8f9fff19", size = 421355, upload-time = "2026-04-10T12:19:47.691Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/43f62355846cae3dc41cb4daccac0a4bb2b7b8b3c7d77d1b6a220bae6d54/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e51d9c59006b53447f806145406eb37a7fc3c25553d4fd24c3887f3b268d214e", size = 561656, upload-time = "2026-04-10T12:20:42.161Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fc/a453813c6d000d69a41a06c6a3143a6c4d0d0e41f23c155db2f82ea0edfa/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:5e48a405f699fb001b8af120a6e0b4a981277f84eb5dd66a1faa21e4b6fe9485", size = 665791, upload-time = "2026-04-10T12:20:56.842Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a3/f6a9e75f1e602b67b6d67088a9a766adfc4e0a740a9c4b68e4e6207c1006/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8cbfc007ea78af61d75d7d26e5854df53dc5da6877d074afd4b4696c074f4ee7", size = 640975, upload-time = "2026-04-10T12:21:11.641Z" }, - { url = "https://files.pythonhosted.org/packages/a3/15/4ac98277f7fd9d855c8ed337e8e2a3386d17997cce2dd3eadb23dedc08e3/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6a94d0da8133b3a0707ae11c9ea321f8fc37e3b5a14517019a05d632218b0f56", size = 607242, upload-time = "2026-04-10T12:21:27.27Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0b/ce61907a803f75854e0cc91b84c16e14dce0e4e939efbda26293eb4c8784/cachebox-5.2.3-cp313-cp313t-win32.whl", hash = "sha256:5fee33549877c03c2494ec5359a57a7667f872fe8e296a7f39d3dfe08dd3914c", size = 271619, upload-time = "2026-04-10T12:21:56.768Z" }, - { url = "https://files.pythonhosted.org/packages/b0/06/fece190ad5173d06b2779494aaad5528907f2e55c809618e5b67c2e3dbb5/cachebox-5.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:67548a05cd41fcc4f7af80a2f97f742fef3d436537ac2e1a1dce0fcba5d41190", size = 283133, upload-time = "2026-04-10T12:21:43.037Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8b/72c0e80aad08e09867ce14a621bce689a733552f20cdf2ef96d4b052da10/cachebox-5.2.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:37fa0891f0defee053c09f5f43f802f731e36e6e6ca055d7d174af07f77232ca", size = 380523, upload-time = "2026-04-10T12:20:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fc/62/33aaade81b181d5191cc39c867c297aa7c65f3191aa9749bf99b77496b88/cachebox-5.2.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dc6315902f2ef4afbf10bc8e08c54ff34de5ce124546b8e0016c9b0d327be21e", size = 362424, upload-time = "2026-04-10T12:20:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0b/3eedaf9ea4b41c931f4340bfa42056efe2bb5fe3a79649d6c8a1dce585a5/cachebox-5.2.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7df1735ca778480d51b8232fed397ffe3935158f20d34fb1c5ed171b53d5a6e2", size = 399572, upload-time = "2026-04-10T12:18:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/be/69/c79b8a6a5b889ac4a60800bacea3553cb3b86f6fd13b2262bade1cb962c6/cachebox-5.2.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e22451cde8f884051e941b21870e4fc91fcf58d0d8c285bb8964107e1f02445c", size = 353803, upload-time = "2026-04-10T12:19:11.21Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c3/bc7838de51039f8c50506d8dc82f22ff9a652794339a223b12af595e1d2f/cachebox-5.2.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcbccf3015d9a42bcf41260fa5cc048a5bdb75aa10997d514d6c976117f30ee2", size = 374474, upload-time = "2026-04-10T12:19:23.658Z" }, - { url = "https://files.pythonhosted.org/packages/65/61/e5231ad2ae952ca482f9b9df55df4b96add1a80de28de537c5f574605987/cachebox-5.2.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:311eae5079e256cbbfafdc3dcff1714b6598a767f9c1ef8c3709e74ea0cc12b0", size = 393045, upload-time = "2026-04-10T12:19:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/78/c4/c9b3fa764ac5420a9e079ad53fa8840d4a26b74c4ccda56acbef49cf76ff/cachebox-5.2.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f4d2a80a5cd3380739c67f7d89e596634f5897b8d5a4a3dc1598312cb077535", size = 398700, upload-time = "2026-04-10T12:20:02.513Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3e/c4e3acd4cb04e01c5fb7cc7a4de16059b9594d90672fff85af8670275267/cachebox-5.2.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3977515b727a5203f494c44c4566fb936c4b940351c01d3d8e7b5d104dff4f53", size = 426725, upload-time = "2026-04-10T12:19:49.385Z" }, - { url = "https://files.pythonhosted.org/packages/25/5d/610b79479719951581109d985244d34c97f86a308c3d7c83443e2b1dac46/cachebox-5.2.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c5be17dd5c4fabcfecd5bcf6d54f9c6fb719daed3ef01ac1c03a14af0e2b26c1", size = 570042, upload-time = "2026-04-10T12:20:43.793Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/cad8a05db4d0c0f5ba6bccb32e57d15c472276de9476f56004445b40711f/cachebox-5.2.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6d37334fc218fdaee31db8a4f938938716e7c3b1b4059e25de27c8447fc95fde", size = 670974, upload-time = "2026-04-10T12:20:58.528Z" }, - { url = "https://files.pythonhosted.org/packages/54/d1/9cff7c2b9048d1c38b7ad8199ce856596d09720b3bea74043f3bad71970b/cachebox-5.2.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e5f1b7e23411b748d919348c3b65db1f9f8927ab8f6f3acae19bd617543df2d", size = 646213, upload-time = "2026-04-10T12:21:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/27/ae/2e1ad162ec13903e84469c8a753baf385f1bc324279d6c7cb6365e7099df/cachebox-5.2.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7b06a75a898b31fd73c4d8bf727a9b9f8b5b7738cccd0ab5e6fd2a9cf659d3c", size = 612787, upload-time = "2026-04-10T12:21:29.271Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8a/07b5ffd841e1ff534bb6e8721c39fdfe0d7cdaac1398e1783b2a0c37bd22/cachebox-5.2.3-cp314-cp314-win32.whl", hash = "sha256:3b798052719f09a2ce7bf9fa9452dc0a7d4dc53b50a2d3aba6ce6ebc12d39df7", size = 278559, upload-time = "2026-04-10T12:21:58.482Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/b88a82ce9ec7a2fa0f09ed1cdd031692c8664c41f9ab71831e177c7ce2df/cachebox-5.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:4afc8b8575e3228a42ad8d819de5fbbecc6bd0b521295966b00244be37ae3b9b", size = 291928, upload-time = "2026-04-10T12:21:44.621Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/8c79c07c8c6517fb2fe7d479dd87044e38aac5b9af0245b33fcd695eae37/cachebox-5.2.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:0e8a34b82be30d3d9fb7dfaf9a86ec2b3ab9bc264715909ef27fc3d3587324d2", size = 374325, upload-time = "2026-04-10T12:20:30.923Z" }, - { url = "https://files.pythonhosted.org/packages/7f/51/0fc26b923e80ab857ac99d5f7f3784dc941e7b4de361c204835233176ddf/cachebox-5.2.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4d4e336aebf866463878ccd28a4d0ef4003ea216708cf4a02a7f198481b3af81", size = 355444, upload-time = "2026-04-10T12:20:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/c1/6d/a6b399221f8dc4b3e01b37d3240ef5b8a7eb78cd9bfbb99b0e655dd01649/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b102fcdd97b0602bf5d6ba1a571bba3e3d6fa912b89fd768b0da5427408eab8", size = 393978, upload-time = "2026-04-10T12:18:59.753Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f1/4c8f998c117c1941a82bd824d6687280c50167f21fea6392e41531d641e2/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245a79fb2c5d3bff252f4263f76210ef3ad7c2ff9b0234859b26974830a80491", size = 349298, upload-time = "2026-04-10T12:19:12.843Z" }, - { url = "https://files.pythonhosted.org/packages/d1/dd/683bc5a32a0da660d02fa248b880b71a2b834e9b54b8d272b5801282f402/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd0e8dbd8fd4cf664c645c08f9e10508e133353756705c4a738e90a5406224b5", size = 370619, upload-time = "2026-04-10T12:19:25.298Z" }, - { url = "https://files.pythonhosted.org/packages/81/49/d6c47c78a7769b355076c5b635c2b538c8b88e8ceeb408e104d0f269b515/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdb74294bdc33e39e26606919a9b2229038d5fac0edb80c9056683c08584d4a9", size = 385988, upload-time = "2026-04-10T12:19:37.638Z" }, - { url = "https://files.pythonhosted.org/packages/70/e2/b669555ada7fa1392e4cdb8a19f3367db5c6abef0fde8ab034a9747760df/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bba3e9a7f52fa196b434522f39675f3b32a076976ef2373ded6f1065e99f4d20", size = 394090, upload-time = "2026-04-10T12:20:03.978Z" }, - { url = "https://files.pythonhosted.org/packages/8f/01/42916249e53fe4fcbdf0419fb55dbc09b9f377475376e1d7f4ae9c9bd6cd/cachebox-5.2.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abb21f0f937fb66528f1b9f1a04874d6aa503e78bbb26f4cf33bf67faddbdd68", size = 421632, upload-time = "2026-04-10T12:19:51.048Z" }, - { url = "https://files.pythonhosted.org/packages/a1/54/34eebe18c6ed8ba27b1331b5e3d08bd8bb62f03ba81fbf47a2db0fa646f7/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dab6fd3189b0c746fb03e1915fd947aaca9112cedf26ef3a0c39383acf87d2e5", size = 563871, upload-time = "2026-04-10T12:20:45.417Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b2/f92da0d54e4f18609588709090de8c81dd7c8b20ed6ac30f9b91bedbedf5/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e7d2935b9df11d3717f99c7237b6780f1f8c70e6a99b69b8430d89929ec825", size = 665677, upload-time = "2026-04-10T12:21:00.512Z" }, - { url = "https://files.pythonhosted.org/packages/43/9d/bf2d3dc949afe4d21fc7eb15b7524255e834b9252df6bba111e6686d1c6f/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:611aa260fe1b2506330ff72f415e2cb4053c9c4e3776ac68fe2eedee0e1b91b1", size = 642067, upload-time = "2026-04-10T12:21:15.727Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4f/a789eda189550d239fbaf165b9810f148e733e97a2a4eda7c4192295c7f8/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ffb8514a9cb49bacff7995b7c767625cb2239692bd6524245e8579e375cc", size = 608048, upload-time = "2026-04-10T12:21:31.156Z" }, - { url = "https://files.pythonhosted.org/packages/41/c3/590e161c04ffbd36e33933e6dcca5ffa40b5548e3121a21d77aad42af138/cachebox-5.2.3-cp314-cp314t-win32.whl", hash = "sha256:83988dd8e9075ee837e8407e26db49a9944ae74924d5db57b477444d7d98622c", size = 271694, upload-time = "2026-04-10T12:22:00.589Z" }, - { url = "https://files.pythonhosted.org/packages/66/f4/f60b8506df467261178afe918801df37c02c46ec2b8ce019760a14e2abe7/cachebox-5.2.3-cp314-cp314t-win_amd64.whl", hash = "sha256:dbda6390fa5070a19157ae35ab8066d3fe468634e0e9e21452c68ce7999c7d0c", size = 284212, upload-time = "2026-04-10T12:21:46.241Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7b/5eead1ca0d437b1993a742c6571079ae58ae4db50d94d42e87b514aed6c3/cachebox-5.2.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c798cddfb780156db09d3d96ed5da4c2d5fc01dad4bc7b54db5b20c34f221926", size = 376199, upload-time = "2026-04-10T12:20:32.674Z" }, - { url = "https://files.pythonhosted.org/packages/77/e3/5e45042f9b552a5087cafc2e0fed834e632531fca17818201d72e78593ce/cachebox-5.2.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c8f3de4afeb3fd721620be3d02f2338bcbc3fdbd464ca14e1c474088c9669db0", size = 357109, upload-time = "2026-04-10T12:20:18.554Z" }, - { url = "https://files.pythonhosted.org/packages/d4/51/3c4743b718b42e4b80166fa61f8722b603eba7bf206768a7892c4699dce7/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39022c258872185327acffa9ad42d6bdf42f37d006d35c825a684eb5fa98d40", size = 396433, upload-time = "2026-04-10T12:19:01.463Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/678da91187bdb2836db2b8da62519da75359b46bc28697799a7caa314519/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a0599fb85dcb6df9a86502435643fe90c793bbcd50b5d85217c70f2bc2e38fc", size = 354287, upload-time = "2026-04-10T12:19:14.55Z" }, - { url = "https://files.pythonhosted.org/packages/df/06/769446da6c9f2855499aaa19e2d7260aa47934bc2e15a931e5b737f8685a/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cdbe8f1b7716a44dc82ef3a6830a612260c7379478cfa80804632e2e6252b8e", size = 372507, upload-time = "2026-04-10T12:19:26.763Z" }, - { url = "https://files.pythonhosted.org/packages/79/cf/86c60994a7be734abef0395e440dc11714f84ffcd369cbcd8e61c3d58126/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:783d1b9a0b3c77c43e7ae331b9d6561ad75827e16b2484e2a6cc289ec4d392ee", size = 390831, upload-time = "2026-04-10T12:19:39.591Z" }, - { url = "https://files.pythonhosted.org/packages/9d/db/acfb55f8d5ee4ea1c5f2d32ede25d4d04e944ba09d2832c27c085022490d/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c6476a2a842906fee782d92f8fbcb03ecfd22eecc39adb7fb5b047d7e1cf020", size = 396277, upload-time = "2026-04-10T12:20:05.735Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4f/35e27e85a48e15671c5863addcabde910eb311800a621c3e47c04bd36d17/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:184bbcfa1370415b6d1f09e4fb74ab697dac8df09f522aa217a2fac65f973744", size = 426980, upload-time = "2026-04-10T12:19:52.622Z" }, - { url = "https://files.pythonhosted.org/packages/09/4b/50f2cadf20c02db9e449f2e9fee95f3eb5768ab1804dd0a5eba6c98119ad/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f89df36b46f8f5e11c0c49701ec3cebddf51191f96afb7bb75c394faf3c1cbc8", size = 565539, upload-time = "2026-04-10T12:20:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/43/53/b8e948cadb48b8bcf1d13c2aa4a788ff0e95b50ddb808c18e998499b4680/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fb0bdcd9e28686e3b91d5210c843542858f0f10de151181aee27a7978fe4992e", size = 670870, upload-time = "2026-04-10T12:21:02.141Z" }, - { url = "https://files.pythonhosted.org/packages/29/7b/d68ca3f59a9d6963c2f6b19bc4b1926a37db2e4a4f6c9891d12788e49ce2/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:5196f0d2c2f99c92ddf0d2c37803ff90509d14a5df211b7754feb8b61ffd8740", size = 644542, upload-time = "2026-04-10T12:21:17.541Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c8/44ae6d5dff09f044d61a92591e6a8db17f3b2ee51a54d375cce90271527b/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:73671850d8c3634ab217398c83715d3feb52589ec97bd8e2f4d22e472741ea48", size = 610235, upload-time = "2026-04-10T12:21:32.93Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1b/31cf2449da9a296f6c6c0002c7ae91a25c3a4bfef071763bbeb85300b402/cachebox-5.2.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:70c718f6bb77e6ba142b9a055b81ce85412a0c0e5e82a154489b45e6f91d09ec", size = 287614, upload-time = "2026-04-10T12:21:47.909Z" }, + { url = "https://files.pythonhosted.org/packages/d7/33/c7a01649a6cb7219b233d2ed071ab925e52cdb64e15ce935024c0007376f/botocore-1.42.81-py3-none-any.whl", hash = "sha256:bcef8c93c20ebeba95e4f8b9edfbffbc78a0e11235425a92ee32e48fd8e03c37", size = 14807198, upload-time = "2026-04-01T19:35:20.437Z" }, ] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -1792,14 +1690,14 @@ wheels = [ [[package]] name = "click" -version = "8.4.0" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -1914,7 +1812,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.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.4.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')" }, ] 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 = [ @@ -1993,115 +1891,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.14.0" +version = "7.13.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, - { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, - { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, - { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, - { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, - { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, - { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, - { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, - { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, - { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, - { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, - { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, - { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, - { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, - { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, - { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, - { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, - { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, - { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, - { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, - { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, - { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, - { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, - { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, - { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, - { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, - { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, - { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, - { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, - { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, - { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, - { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, - { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, - { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, - { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, - { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, - { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, - { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, - { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, - { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, - { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, - { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, - { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, - { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, - { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, - { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, - { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, - { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [package.optional-dependencies] @@ -2205,15 +2103,14 @@ wheels = [ [[package]] name = "deepdiff" -version = "9.1.0" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachebox", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "orderly-set", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/6b/6a4a5aaf38535eb332c2856aa08e73ed7c549d0851b1215401af0a2db1a7/deepdiff-9.1.0.tar.gz", hash = "sha256:07e9e366fab4297755153c4eab795ad4ef3cbd0d51660e847f5751c6bd727687", size = 382149, upload-time = "2026-05-15T20:18:05.751Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/20/63dd34163ed07393968128dc8c7ab948c96e47c4ce76976ea533de64909d/deepdiff-9.0.0.tar.gz", hash = "sha256:4872005306237b5b50829803feff58a1dfd20b2b357a55de22e7ded65b2008a7", size = 151952, upload-time = "2026-03-30T05:52:23.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/26/4a2bad8eb430d8d805a4642c4bff25103a37548d74ab346f8b1e024abcc5/deepdiff-9.1.0-py3-none-any.whl", hash = "sha256:80c0460e1993b04f6f0ca79abf25548b129fd218478c4ebb08f80560f5d10610", size = 184662, upload-time = "2026-05-15T20:18:03.956Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c4/da7089cd7aa4ab554f56e18a7fb08dcfed8fd2ae91fa528f5b1be207a148/deepdiff-9.0.0-py3-none-any.whl", hash = "sha256:b1ae0dd86290d86a03de5fbee728fde43095c1472ae4974bdab23ab4656305bd", size = 170540, upload-time = "2026-03-30T05:52:22.008Z" }, ] [[package]] @@ -2236,11 +2133,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.18.0" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] [[package]] @@ -2293,6 +2190,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "eval-type-backport" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -2410,11 +2316,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.0" +version = "3.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] @@ -2470,59 +2376,59 @@ wheels = [ [[package]] name = "fonttools" -version = "4.63.0" +version = "4.62.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, - { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, - { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, - { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, - { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, - { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, - { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, - { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, - { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, - { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, - { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, - { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, - { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, - { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, - { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, - { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, - { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, - { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, - { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, - { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, - { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, - { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, - { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, - { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, - { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, - { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, - { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] [[package]] @@ -2675,11 +2581,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.4.0" +version = "2026.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] [[package]] @@ -2714,7 +2620,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.30.3" +version = "2.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2723,22 +2629,22 @@ dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/0b/b6e296aff70bef900766934cf4e83eaacc3f244adb61936b66d24b204080/google_api_core-2.30.1.tar.gz", hash = "sha256:7304ef3bd7e77fd26320a36eeb75868f9339532bfea21694964f4765b37574ee", size = 176742, upload-time = "2026-03-30T22:50:52.637Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, + { url = "https://files.pythonhosted.org/packages/43/86/a00ea4596780ef3f0721c1f073c0c5ae992da4f35cf12f0d8c92d19267a6/google_api_core-2.30.1-py3-none-any.whl", hash = "sha256:3be893babbb54a89c6807b598383ddf212112130e3d24d06c681b5d18f082e08", size = 173238, upload-time = "2026-03-30T22:48:50.586Z" }, ] [[package]] name = "google-auth" -version = "2.53.0" +version = "2.49.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, ] [package.optional-dependencies] @@ -2748,7 +2654,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.75.0" +version = "1.68.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2762,21 +2668,21 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/59/3ed61240ef20b3ae6ed54e82c6f8b6d1f194947bc6679679dd6cdb037594/google_genai-1.75.0.tar.gz", hash = "sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf", size = 539039, upload-time = "2026-05-04T22:48:54.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/2c/f059982dbcb658cc535c81bbcbe7e2c040d675f4b563b03cdb01018a4bc3/google_genai-1.68.0.tar.gz", hash = "sha256:ac30c0b8bc630f9372993a97e4a11dae0e36f2e10d7c55eacdca95a9fa14ca96", size = 511285, upload-time = "2026-03-18T01:03:18.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/b6/552d40e96da22921eb1fead7c14b00b5b5473a20e45959488660fab35ee2/google_genai-1.75.0-py3-none-any.whl", hash = "sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c", size = 793726, upload-time = "2026-05-04T22:48:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/84/de/7d3ee9c94b74c3578ea4f88d45e8de9405902f857932334d81e89bce3dfa/google_genai-1.68.0-py3-none-any.whl", hash = "sha256:a1bc9919c0e2ea2907d1e319b65471d3d6d58c54822039a249fe1323e4178d15", size = 750912, upload-time = "2026-03-18T01:03:15.983Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.75.0" +version = "1.73.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/c0/4a54c386282c13449eca8bbe2ddb518181dc113e78d240458a68856b4d69/googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6", size = 147506, upload-time = "2026-03-26T22:17:38.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, ] [[package]] @@ -2790,65 +2696,68 @@ wheels = [ [[package]] name = "greenlet" -version = "3.5.0" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, - { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, - { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, - { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, - { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, - { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, - { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, - { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, - { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, - { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, - { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, - { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, - { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, - { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, - { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, - { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, - { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] -name = "griffelib" -version = "2.0.2" +name = "griffe" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, ] [[package]] @@ -2948,34 +2857,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.5.0" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, - { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, - { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, - { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, - { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, - { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, - { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, - { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, ] [[package]] @@ -3002,11 +2911,11 @@ wheels = [ [[package]] name = "httpdbg" -version = "2.1.7" +version = "2.1.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/a2/5ccf3dab272bf8b07b5211da5748bec75b231d7c3471afd8f7f2a399289d/httpdbg-2.1.7.tar.gz", hash = "sha256:6ee7db35ad4d5d71cc75b1d5088567e4f71a65b987dfd934c8ac7d1d6cb6cc26", size = 80657, upload-time = "2026-05-01T10:34:34.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/47/69b5ceb2bbd03657b0a1458d8f3fdccd331d05bfa139569e6f293a2c4353/httpdbg-2.1.6.tar.gz", hash = "sha256:7f0925718faa0c94f5855075b168dc95bb16f9cd6826eb8449c8af9e720ea42b", size = 80616, upload-time = "2026-03-28T11:04:27.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/c4/46ab6052e3cae4489087b3c8bda83bc8b4911a126339cdd249f72fb6ab30/httpdbg-2.1.7-py3-none-any.whl", hash = "sha256:e29578bfdca82361805adc19938e64fcabcf5029a9c9783757b1ffac2a4dc2f8", size = 87966, upload-time = "2026-05-01T10:34:32.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/a8/6d101e4d58563e8647fe010a5ae61ba41ceb61e2abd789170573599d7d43/httpdbg-2.1.6-py3-none-any.whl", hash = "sha256:e3d5be9cd5eeb262b77e4faf113c356b7da127bb848b3360c99e394bb2dc9590", size = 87938, upload-time = "2026-03-28T11:04:26.035Z" }, ] [[package]] @@ -3083,7 +2992,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.15.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3096,9 +3005,9 @@ dependencies = [ { name = "typer", 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/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/2a/a847fd02261cd051da218baf99f90ee7c7040c109a01833db4f838f25256/huggingface_hub-1.8.0.tar.gz", hash = "sha256:c5627b2fd521e00caf8eff4ac965ba988ea75167fad7ee72e17f9b7183ec63f3", size = 735839, upload-time = "2026-03-25T16:01:28.152Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/8a3a16ea4d202cb641b51d2681bdd3d482c1c592d7570b3fa264730829ce/huggingface_hub-1.8.0-py3-none-any.whl", hash = "sha256:d3eb5047bd4e33c987429de6020d4810d38a5bef95b3b40df9b17346b7f353f2", size = 625208, upload-time = "2026-03-25T16:01:26.603Z" }, ] [[package]] @@ -3164,23 +3073,23 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "importlib-metadata" -version = "8.7.1" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] [[package]] @@ -3224,105 +3133,99 @@ wheels = [ [[package]] name = "jiter" -version = "0.15.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, - { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, - { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, - { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, - { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, - { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, - { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, - { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, - { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, - { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, - { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, - { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, - { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, - { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, - { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, - { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, - { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, - { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, - { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, - { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, - { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, - { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, - { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, - { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, - { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, - { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, - { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, - { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, - { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, - { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, - { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, - { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, - { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, - { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, - { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, - { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, - { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, - { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, - { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] @@ -3361,9 +3264,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, ] +[[package]] +name = "jsonpath-python" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/db/2f4ecc24da35c6142b39c353d5b7c16eef955cc94b35a48d3fa47996d7c3/jsonpath_python-1.1.5.tar.gz", hash = "sha256:ceea2efd9e56add09330a2c9631ea3d55297b9619348c1055e5bfb9cb0b8c538", size = 87352, upload-time = "2026-03-17T06:16:40.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl", hash = "sha256:a60315404d70a65e76c9a782c84e50600480221d94a58af47b7b4d437351cb4b", size = 14090, upload-time = "2026-03-17T06:16:39.152Z" }, +] + [[package]] name = "jsonschema" -version = "4.26.0" +version = "4.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3371,9 +3283,9 @@ dependencies = [ { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [[package]] @@ -3514,7 +3426,7 @@ wheels = [ [[package]] name = "langfuse" -version = "4.6.1" +version = "4.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3526,99 +3438,99 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/31/4b7157be23e7c8c3581ac5f6547c5c003e232e7044c92398c468ef78a809/langfuse-4.6.1.tar.gz", hash = "sha256:7f256c669e610909c2e93ca3e9e4168dbef344b753b6874f14b0edd673863f17", size = 281379, upload-time = "2026-05-08T14:08:15.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/d0/6d79ed5614f86f27f5df199cf10c6facf6874ff6f91b828ae4dad90aa86d/langfuse-4.0.6.tar.gz", hash = "sha256:83a6f8cc8f1431fa2958c91e2673bc4179f993297e9b1acd1dbf001785e6cf83", size = 274094, upload-time = "2026-04-01T20:04:15.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/bf/3a6082f7809bdcc1269e9920c07d7c7f92a53cc265a4a879e59c92b23b36/langfuse-4.6.1-py3-none-any.whl", hash = "sha256:a696ac3089a0c8431bf7f1b47b7f6417da311f418dd04ce9ef62d63608fd8797", size = 481237, upload-time = "2026-05-08T14:08:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/088048e37b6d7ec1b52c6a11bc33101454285a22eaab8303dcccfd78344d/langfuse-4.0.6-py3-none-any.whl", hash = "sha256:0562b1dcf83247f9d8349f0f755eaed9a7f952fee67e66580970f0738bf3adbf", size = 472841, upload-time = "2026-04-01T20:04:16.451Z" }, ] [[package]] name = "librt" -version = "0.11.0" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, - { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, - { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, - { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, - { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, - { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, - { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, - { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, - { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, - { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, - { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, - { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, - { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, - { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, - { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, - { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, - { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] name = "litellm" -version = "1.85.0" +version = "1.83.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3634,9 +3546,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/3b/d5/3c9b560db2ffa9e498655d0dfd74f408bc5b32ede858b5731c2a5fa4c752/litellm-1.85.0.tar.gz", hash = "sha256:babdd569809af913d08a08a7eb55df1ed3e6a3960ee365c6cef4ad031c9bc72a", size = 15344387, upload-time = "2026-05-17T01:59:15.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/38/e6a4abb062e039d18d59538cc4e6fc370c2c10cd2bff4a2e546acb69dcb9/litellm-1.85.0-py3-none-any.whl", hash = "sha256:2bb449153610691faffd76f5b94a8c29e4b66fc5394156ebf54fd4fe92759b1a", size = 16978229, upload-time = "2026-05-17T01:59:11.902Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, ] [package.optional-dependencies] @@ -3671,20 +3583,20 @@ proxy = [ [[package]] name = "litellm-enterprise" -version = "0.1.40" +version = "0.1.39" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/b5/c1ef8cbb4555564dc47489d65758d4132bb1a3a5c0991264c57e0069b059/litellm_enterprise-0.1.40.tar.gz", hash = "sha256:f2bc6d8ed3863f51d2aaafa99f2a5cda6ccf9fc64a9c646825ab8051a789bc62", size = 70107, upload-time = "2026-05-05T23:28:14.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/0b/79fb68abf7c787d951dd367f662c52b922278548f244f5d36e623cdb2161/litellm_enterprise-0.1.39.tar.gz", hash = "sha256:434e2c15280218bb9224adbbac878bcffe0b8a75b0b46deeb0b90bc4f2e2152b", size = 69465, upload-time = "2026-04-26T03:09:36.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/45/af3e0922805f81262bdfc6002567e004205b144fd2d011c2891da6f48265/litellm_enterprise-0.1.40-py3-none-any.whl", hash = "sha256:bf0eada2309053556aef09d9071297f3c5952e787df9c283de13a393b6b85251", size = 137250, upload-time = "2026-05-05T23:28:13.82Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/30df9b36366559efd9c1fae39c67856481c7418056eb2196266bda605bc8/litellm_enterprise-0.1.39-py3-none-any.whl", hash = "sha256:e5f48745fb127dc4f72fd1fa7cdeba0ddd4066dc5f0d9e8e87eea4e4571d42b3", size = 136645, upload-time = "2026-04-26T03:09:35.492Z" }, ] [[package]] name = "litellm-proxy-extras" -version = "0.4.72" +version = "0.4.69" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/fb/da50c6cb89901b316e8c29c5049bfd4f916ac04e5d49b2971183c066274f/litellm_proxy_extras-0.4.72.tar.gz", hash = "sha256:a9543165eb5e8b440a2a92963a67fcf143a15457bfdcb8fc05e68325aee75c0e", size = 43483, upload-time = "2026-05-14T05:42:17.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/e8/0176368d64ffaaf7ff7da07a7833ef05cd92484cf21167a9291cb311568f/litellm_proxy_extras-0.4.69.tar.gz", hash = "sha256:8c24a01a4dffb137e95c709a47ab68053591ccdf7d78a038c57348f5b2ab990d", size = 41220, upload-time = "2026-04-26T03:12:12.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/e1/899e05e740695c0562040b62df897e6182b3e6f88ce719a3c53bffeebc1c/litellm_proxy_extras-0.4.72-py3-none-any.whl", hash = "sha256:869c40cb459e0c40c93d29eb4d4d8b3a197050c0673d7eca1765a034a690fb93", size = 117821, upload-time = "2026-05-14T05:42:16.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/165a96b061fa90824ffbce13191262d4a0089510284a973805e5854e2c03/litellm_proxy_extras-0.4.69-py3-none-any.whl", hash = "sha256:4aee8dab05d1a6f91ba89da729d241122eaad4cbe64f39b19ea6a855543146c4", size = 113230, upload-time = "2026-04-26T03:12:10.731Z" }, ] [[package]] @@ -3702,14 +3614,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.2.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -3799,7 +3711,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.9" +version = "3.10.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy", version = "1.3.2", 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')" }, @@ -3808,73 +3720,73 @@ 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.4.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.4.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 = "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'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, - { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, - { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, - { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, - { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, - { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, - { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, - { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, - { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, - { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, - { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, - { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, - { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, - { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, - { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, - { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, - { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, - { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, - { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, - { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, - { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, - { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, - { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, - { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, - { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, - { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, - { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, - { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, - { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, - { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, - { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, - { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, ] [[package]] name = "mcp" -version = "1.27.1" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3892,9 +3804,9 @@ dependencies = [ { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, ] [package.optional-dependencies] @@ -3913,7 +3825,7 @@ wheels = [ [[package]] name = "mem0ai" -version = "1.0.11" +version = "1.0.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3924,9 +3836,9 @@ dependencies = [ { name = "qdrant-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "sqlalchemy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/1e/2f8a8cc4b8e7f6126f3367d27dc65eac5cd4ceb854888faa3a8f62a2c0a0/mem0ai-1.0.11.tar.gz", hash = "sha256:ddb803bedc22bd514606d262407782e88df929f6991b59f6972fb8a25cc06001", size = 201758, upload-time = "2026-04-06T11:31:43.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/d9/1fbf24b055f9f14ec61d593bc95b75b033fd1a69bfddee33d6453b21e5d0/mem0ai-1.0.10.tar.gz", hash = "sha256:f3e22c9aff695ca6c66631c4e79ceef92457c8d3355c56359ab4257fa031c046", size = 200416, upload-time = "2026-04-01T18:23:27.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/b5/f822c94e1b901f8a700af134c2473646de9a7db26364566f6a72d527d235/mem0ai-1.0.11-py3-none-any.whl", hash = "sha256:bcf4d678dc0a4d4e8eccaebe05562eae022fcdc825a0e3095d02f28cf61a5b6d", size = 297138, upload-time = "2026-04-06T11:31:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/2c6ea68c404757e683da23b942dfff6987fe283ccbf2fa1fb0c128ddbdc6/mem0ai-1.0.10-py3-none-any.whl", hash = "sha256:9ff586c3a39a834042ce6755fc9da2315e284fb622ee773cd344ecca756ccad5", size = 295374, upload-time = "2026-04-01T18:23:25.022Z" }, ] [[package]] @@ -3969,13 +3881,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/1b/543ddaa2daf8593911a02a07a6a78366d4a6a0053ec86a557c19fa97b60e/microsoft_agents_hosting_core-0.3.1-py3-none-any.whl", hash = "sha256:a4b41556b15321b74f539c5a0a89f70955459b7ec57e9e4b24e61bba27f1cbbc", size = 94573, upload-time = "2025-09-09T23:19:53.855Z" }, ] +[[package]] +name = "mistralai" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonpath-python", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", 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 = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/0e67c37f305b127d4df005f4a216d3570c4cf3330aa775415bde90d2f662/mistralai-2.4.2.tar.gz", hash = "sha256:7896ffa763e0be1ec05e5b436d2c21ae089e4b5438cda9033dcd1b25bc3021a2", size = 416708, upload-time = "2026-04-23T15:11:00.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/3e/d6df4c222e1845d16c75c5d12981b15fc4f6413a968cd7647691bf8d03d3/mistralai-2.4.2-py3-none-any.whl", hash = "sha256:caf57734078b5a6f2777157a8cd5ffe6a7d530078755f18b9884092d918299f4", size = 980037, upload-time = "2026-04-23T15:10:59.223Z" }, +] + [[package]] name = "ml-dtypes" 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.4.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.4.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')" }, ] 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 = [ @@ -4026,16 +3957,16 @@ wheels = [ [[package]] name = "msal" -version = "1.36.0" +version = "1.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/cb/b02b0f748ac668922364ccb3c3bff5b71628a05f5adfec2ba2a5c3031483/msal-1.36.0.tar.gz", hash = "sha256:3f6a4af2b036b476a4215111c4297b4e6e236ed186cd804faefba23e4990978b", size = 174217, upload-time = "2026-04-09T10:20:33.525Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/d3/414d1f0a5f6f4fe5313c2b002c54e78a3332970feb3f5fed14237aa17064/msal-1.36.0-py3-none-any.whl", hash = "sha256:36ecac30e2ff4322d956029aabce3c82301c29f0acb1ad89b94edcabb0e58ec4", size = 121547, upload-time = "2026-04-09T10:20:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/96/86/16815fddf056ca998853c6dc525397edf0b43559bb4073a80d2bc7fe8009/msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3", size = 119909, upload-time = "2026-03-04T23:38:50.452Z" }, ] [[package]] @@ -4273,11 +4204,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.21.2" +version = "2.18.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/a0/6198c56d42ef2f3c6ed0c42ba30dbcefdc86a91262d7d449010770ae085b/narwhals-2.21.2.tar.gz", hash = "sha256:5c5b2d0b47aef7c73ea412cfcbcd467f2f2d5be73e3c2ab19d78f4a97718790a", size = 632176, upload-time = "2026-05-16T08:49:08.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/96/45218c2fdec4c9f22178f905086e85ef1a6d63862dcc3cd68eb60f1867f5/narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b", size = 620578, upload-time = "2026-03-24T15:11:25.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.2-py3-none-any.whl", hash = "sha256:7bb57c3700486039215455b9bf2d64261915cc0fd845cc30272d631df696b251", size = 451201, upload-time = "2026-05-16T08:49:05.536Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad", size = 444952, upload-time = "2026-03-24T15:11:23.801Z" }, ] [[package]] @@ -4358,7 +4289,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.6" +version = "2.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -4374,79 +4305,79 @@ 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/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, - { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, - { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, - { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, - { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, - { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, - { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, - { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, - { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, - { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, - { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, - { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, - { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, - { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, - { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] [[package]] @@ -4473,7 +4404,7 @@ wheels = [ [[package]] name = "openai" -version = "2.37.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -4485,28 +4416,27 @@ 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/32/50/5901f01ef14e6c27788beb91e54fef5d6204fb5fb9e97402fc8a14de2e32/openai-2.37.0.tar.gz", hash = "sha256:f4bc562cc5f3a43d40d678105572d9d44765f6e0f50c125f63055419b72f4bd9", size = 754706, upload-time = "2026-05-15T22:30:35.428Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/4c/bce61680d0699a78a405fd9a67989b175ba020590428831aab2ab1d2be7c/openai-2.37.0-py3-none-any.whl", hash = "sha256:814633888b8f3b1ffd6615697c6e4ef93632d08b7c2e28c8c5ef3556e5a10107", size = 1303238, upload-time = "2026-05-15T22:30:32.767Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] name = "openai-agents" -version = "0.17.3" +version = "0.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffelib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "mcp", extra = ["ws"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "openai", 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 = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "types-requests", 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'" }, - { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/16/b79c1849125eb6d19cae98c21ff35caa2e55b5ec8d7a02b354b711917ef7/openai_agents-0.17.3.tar.gz", hash = "sha256:63b6dda6bd4fb51169e2a2cbd5d187a4e5ce823bbd15f965c8ed1d3b89072eec", size = 5406135, upload-time = "2026-05-19T01:28:15.971Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/38/f47644c5de02f1853483d1a16d1fb7d12cc2c219c5548ec26a3e9aee1c29/openai_agents-0.10.5.tar.gz", hash = "sha256:73cef5263eeb98437b874b29c800694617af7d9626be19514b4ed6f434874c1e", size = 2511640, upload-time = "2026-03-05T20:43:59.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/ec/775a14cfd5f12f4ffe458c7ac9527831093c72e8c1aef682898fc6394106/openai_agents-0.17.3-py3-none-any.whl", hash = "sha256:a048bb0752d40913d18bccf6562f56260b603bb57c972597b6da58f60123f4bd", size = 841541, upload-time = "2026-05-19T01:28:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/e2/07/ad27018fb42d6f1e70f471a5ca3f6398a2159575b623edf86e1ddde66ce4/openai_agents-0.10.5-py3-none-any.whl", hash = "sha256:6c92491c61ba85b4790d76562b4af2e6e230c8844f9c12fed8a721400a320c86", size = 413046, upload-time = "2026-03-05T20:43:57.874Z" }, ] [[package]] @@ -5030,7 +4960,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.3" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -5047,59 +4977,59 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.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.4.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 = "python-dateutil", 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 = "tzdata", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, - { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, - { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, ] [[package]] @@ -5113,11 +5043,11 @@ wheels = [ [[package]] name = "pathspec" -version = "1.1.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -5220,24 +5150,24 @@ wheels = [ [[package]] name = "pip" -version = "26.1.1" +version = "26.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, ] [[package]] name = "plotly" -version = "6.7.0" +version = "6.6.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/3a/7f/0f100df1172aadf88a929a9dbb902656b0880ba4b960fe5224867159d8f4/plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6", size = 6911286, upload-time = "2026-04-09T20:36:45.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/fb/41efe84970cfddefd4ccf025e2cbfafe780004555f583e93dba3dac2cdef/plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c", size = 7027956, upload-time = "2026-03-02T21:10:25.321Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0", size = 9898444, upload-time = "2026-04-09T20:36:39.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0", size = 9910315, upload-time = "2026-03-02T21:10:18.131Z" }, ] [[package]] @@ -5305,17 +5235,19 @@ wheels = [ [[package]] name = "posthog" -version = "7.15.0" +version = "7.9.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { 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/a1/98/e4f414fd390bba6242be37f0a899325abba5ab22e3e83213cbb20253f807/posthog-7.15.0.tar.gz", hash = "sha256:22270215d7b062cd66badee3dfbe957e3f05e29b1b0e8c125a214ebc50bc77c8", size = 212310, upload-time = "2026-05-19T12:57:03.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/a7/2865487853061fbd62383492237b546d2d8f7c1846272350d2b9e14138cd/posthog-7.9.12.tar.gz", hash = "sha256:ebabf2eb2e1c1fbf22b0759df4644623fa43cc6c9dcbe9fd429b7937d14251ec", size = 176828, upload-time = "2026-03-12T09:01:15.184Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/4a/7b50a53e9a557c7e78deb36811eb6b0d05f40f20c3b633e379e206426e37/posthog-7.15.0-py3-none-any.whl", hash = "sha256:0ada8fe94c7fb9b7ad507180eed308fea3b45063ddb4088cc8eaddb9cdea15c4", size = 248461, upload-time = "2026-05-19T12:57:01.575Z" }, + { url = "https://files.pythonhosted.org/packages/65/a9/7a803aed5a5649cf78ea7b31e90d0080181ba21f739243e1741a1e607f1f/posthog-7.9.12-py3-none-any.whl", hash = "sha256:7175bd1698a566bfea98a016c64e3456399f8046aeeca8f1d04ae5bf6c5a38d0", size = 202469, upload-time = "2026-03-12T09:01:13.38Z" }, ] [[package]] @@ -5366,142 +5298,128 @@ wheels = [ [[package]] name = "propcache" -version = "0.5.2" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, - { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, - { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, - { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, - { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, - { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, - { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, - { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, - { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, - { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, - { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, - { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, - { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, - { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, - { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, - { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, - { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, - { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, - { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, - { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, - { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, - { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, - { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, - { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, - { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, - { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, - { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, - { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, - { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, - { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, - { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, - { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, - { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] [[package]] name = "proto-plus" -version = "1.28.0" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, ] [[package]] @@ -5536,59 +5454,59 @@ wheels = [ [[package]] name = "pyarrow" -version = "24.0.0" +version = "23.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", size = 35076681, upload-time = "2026-04-21T08:51:46.845Z" }, - { url = "https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", size = 36684260, upload-time = "2026-04-21T08:51:53.642Z" }, - { url = "https://files.pythonhosted.org/packages/57/02/9b9320e673dd8a99411fac78690f3df92f6dd6f59754c750110bca66d64e/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", size = 45698566, upload-time = "2026-04-21T10:46:02.133Z" }, - { url = "https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", size = 48835562, upload-time = "2026-04-21T10:46:10.278Z" }, - { url = "https://files.pythonhosted.org/packages/a5/63/097510448e47e4091faa41c43ba92f97cecaab8f4535b56a3d149578f634/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", size = 49394997, upload-time = "2026-04-21T10:46:18.08Z" }, - { url = "https://files.pythonhosted.org/packages/60/6b/c047d6222ab279024a062742d1807e2fbaf27bba88a98637299ff47b9236/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", size = 51911424, upload-time = "2026-04-21T10:46:25.347Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ba/464cc70761c2a525d97ebd84e21c31ebd47f3ef4bdcee117009f51c46f24/pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", size = 27251730, upload-time = "2026-04-21T10:46:30.913Z" }, - { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, - { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, - { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, - { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, - { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, - { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a8/24e5dc6855f50a62936ceb004e6e9645e4219a8065f304145d7fb8a79d5d/pyarrow-23.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", size = 34307390, upload-time = "2026-02-16T10:08:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8e/4be5617b4aaae0287f621ad31c6036e5f63118cfca0dc57d42121ff49b51/pyarrow-23.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", size = 35853761, upload-time = "2026-02-16T10:08:17.811Z" }, + { url = "https://files.pythonhosted.org/packages/2e/08/3e56a18819462210432ae37d10f5c8eed3828be1d6c751b6e6a2e93c286a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", size = 44493116, upload-time = "2026-02-16T10:08:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/f8/82/c40b68001dbec8a3faa4c08cd8c200798ac732d2854537c5449dc859f55a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", size = 47564532, upload-time = "2026-02-16T10:08:34.27Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/73f611989116b6f53347581b02177f9f620efdf3cd3f405d0e83cdf53a83/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", size = 48183685, upload-time = "2026-02-16T10:08:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/6c6b3ecdae2a8c3aced99956187e8302fc954cc2cca2a37cf2111dad16ce/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", size = 50605582, upload-time = "2026-02-16T10:08:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/8d/94/d359e708672878d7638a04a0448edf7c707f9e5606cee11e15aaa5c7535a/pyarrow-23.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", size = 27521148, upload-time = "2026-02-16T10:08:58.077Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, ] [[package]] @@ -5623,7 +5541,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5631,9 +5549,9 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies] @@ -5655,118 +5573,120 @@ wheels = [ [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] @@ -5842,16 +5762,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -6059,11 +5979,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.27" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -6089,11 +6009,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.2" +version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] @@ -6184,21 +6104,21 @@ wheels = [ [[package]] name = "qdrant-client" -version = "1.18.0" +version = "1.17.1" 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.4.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.4.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 = "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/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" }, + { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, ] [[package]] @@ -6221,7 +6141,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.4.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.4.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 = "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'" }, @@ -6249,128 +6169,128 @@ wheels = [ [[package]] name = "regex" -version = "2026.5.9" +version = "2026.3.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/93/5ab3e899c47fa7994e524447135a71cd121685a35c8fe35029005f8b236f/regex-2026.3.32.tar.gz", hash = "sha256:f1574566457161678297a116fa5d1556c5a4159d64c5ff7c760e7c564bf66f16", size = 415605, upload-time = "2026-03-28T21:49:22.012Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, - { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, - { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, - { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, - { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, - { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, - { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, - { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, - { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/ae29a505fdfcec85978f35d30e6de7c0ae37eaf7c287f6e88abd04be27b3/regex-2026.3.32-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:462a041d2160090553572f6bb0be417ab9bb912a08de54cb692829c871ee88c1", size = 489575, upload-time = "2026-03-28T21:45:27.167Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fd/7a56c6a86213e321a309161673667091991630287d7490c5e9ec3db29607/regex-2026.3.32-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c6f6b027d10f84bfe65049028892b5740878edd9eae5fea0d1710b09b1d257", size = 291288, upload-time = "2026-03-28T21:45:30.886Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/ac2b481011b23f79994d4d80df03d9feccb64fbfc7bbe8dad2c3e8efc50c/regex-2026.3.32-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:879ae91f2928a13f01a55cfa168acedd2b02b11b4cd8b5bb9223e8cde777ca52", size = 289336, upload-time = "2026-03-28T21:45:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/cf7dfef7a4182e84acbe8919ce7ff50e3545007c2743219e92271b2fbc1c/regex-2026.3.32-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:887a9fa74418d74d645281ee0edcf60694053bd1bc2ebc49eb5e66bfffc6d107", size = 786358, upload-time = "2026-03-28T21:45:34.025Z" }, + { url = "https://files.pythonhosted.org/packages/fb/cb/42bfeb4597206e3171e70c973ca1d39190b48f6cda7546c25f9cb283285f/regex-2026.3.32-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d571f0b2eec3513734ea31a16ce0f7840c0b85a98e7edfa0e328ed144f9ef78f", size = 854179, upload-time = "2026-03-28T21:45:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/90/d8/9f4a7d7edffe7117de23b94696c52065b68e70267d71576d74429d598d9b/regex-2026.3.32-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ada7bd5bb6511d12177a7b00416ce55caee49fbf8c268f26b909497b534cacb", size = 898810, upload-time = "2026-03-28T21:45:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/05/e6/80335c06ddf7fd7a28b97402ebe1ea4fe80a3aa162fba0f7364175f625d1/regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:918db4e34a7ef3d0beee913fa54b34231cc3424676f1c19bdb85f01828d3cd37", size = 790605, upload-time = "2026-03-28T21:45:39.207Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/91436a89c1636090903d753d90b076784b11b8c67b79b3bde9851a45c4d7/regex-2026.3.32-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:69a847a6ffaa86e8af7b9e7037606e05a6f663deec516ad851e8e05d9908d16a", size = 786550, upload-time = "2026-03-28T21:45:40.993Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fc/ea7364b5e9abd220cebf547f2f8a42044878e9d8b02b3a652f8b807c0cbc/regex-2026.3.32-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c8d402ea3dfe674288fe3962016affd33b5b27213d2b5db1823ffa4de524c57", size = 770223, upload-time = "2026-03-28T21:45:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/aff4ad741e914cc493e7500431cdf14e51bc808b14f1f205469d353a970b/regex-2026.3.32-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6b39a2cc5625bbc4fda18919a891eab9aab934eecf83660a90ce20c53621a9a", size = 774436, upload-time = "2026-03-28T21:45:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e7/060779f504c92320f75b90caab4e57324816020986c27f57414b0a1ebcc9/regex-2026.3.32-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f7cc00089b4c21847852c0ad76fb3680f9833b855a0d30bcec94211c435bff6b", size = 849400, upload-time = "2026-03-28T21:45:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8e/6544b27f70bfd14e9c50ff5527027acc9b8f9830d352a746f843da7b0627/regex-2026.3.32-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd03e38068faeef937cc6761a250a4aaa015564bd0d61481fefcf15586d31825", size = 757934, upload-time = "2026-03-28T21:45:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6f/abf2234b3f51da1e693f13bb85e7dbb3bbdd07c04e12e0e105b9bc6006a6/regex-2026.3.32-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e006ea703d5c0f3d112b51ba18af73b58209b954acfe3d8da42eacc9a00e4be6", size = 838479, upload-time = "2026-03-28T21:45:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/db/3c/653f43c3a3643fd221bfaf61ed4a4c8f0ccc25e31a8faa8f1558a892c22c/regex-2026.3.32-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6980ceb5c1049d4878632f08ba0bf7234c30e741b0dc9081da0f86eca13189d3", size = 778478, upload-time = "2026-03-28T21:45:51.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/5e6bd702d7efc3f2a29bf65dfa46f5159653b3c6f846ddf693e1a7f9a739/regex-2026.3.32-cp310-cp310-win32.whl", hash = "sha256:6128dd0793a87287ea1d8bf16b4250dd96316c464ee15953d5b98875a284d41e", size = 266343, upload-time = "2026-03-28T21:45:53.548Z" }, + { url = "https://files.pythonhosted.org/packages/c4/89/39d04329e858956d2db1d08a10f02be8f6837c964663513ac4393158bef9/regex-2026.3.32-cp310-cp310-win_amd64.whl", hash = "sha256:5aa78c857c1731bdd9863923ffadc816d823edf475c7db6d230c28b53b7bdb5e", size = 278632, upload-time = "2026-03-28T21:45:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/c7e9ff3c2648408f4cda7224e195ad7a0d68724225d8d9a55eca9055504f/regex-2026.3.32-cp310-cp310-win_arm64.whl", hash = "sha256:34c905a721ddee0f84c99e3e3b59dd4a5564a6fe338222bc89dd4d4df166115c", size = 270593, upload-time = "2026-03-28T21:45:56.994Z" }, + { url = "https://files.pythonhosted.org/packages/92/c1/c68163a6ce455996db71e249a65234b1c9f79a914ea2108c6c9af9e1812a/regex-2026.3.32-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d7855f5e59fcf91d0c9f4a51dc5d8847813832a2230c3e8e35912ccf20baaa2", size = 489568, upload-time = "2026-03-28T21:45:58.791Z" }, + { url = "https://files.pythonhosted.org/packages/96/9c/0bdd47733b832b5caa11e63df14dccdb311b41ab33c1221e249af4421f8f/regex-2026.3.32-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18eb45f711e942c27dbed4109830bd070d8d618e008d0db39705f3f57070a4c6", size = 291287, upload-time = "2026-03-28T21:46:00.46Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ff/1977a595f15f8dc355f9cebd875dab67f3faeca1f36b905fe53305bbcaed/regex-2026.3.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed3b8281c5d0944d939c82db4ec2300409dd69ee087f7a75a94f2e301e855fb4", size = 289325, upload-time = "2026-03-28T21:46:02.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/68/dfa21aef5af4a144702befeb5ff20ea9f9fbe40a4dfd08d56148b5b48b0a/regex-2026.3.32-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad5c53f2e8fcae9144009435ebe3d9832003508cf8935c04542a1b3b8deefa15", size = 790898, upload-time = "2026-03-28T21:46:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/36/26/9424e43e0e31ac3ce1ba0e7232ee91e113a04a579c53331bc0f16a4a5bf7/regex-2026.3.32-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70c634e39c5cda0da05c93d6747fdc957599f7743543662b6dbabdd8d3ba8a96", size = 862462, upload-time = "2026-03-28T21:46:05.923Z" }, + { url = "https://files.pythonhosted.org/packages/63/a8/06573154ac891c6b55b74a88e0fb7c10081c20916b82dd0abc8cef938e13/regex-2026.3.32-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e0f6648fd48f4c73d801c55ab976cd602e2da87de99c07bff005b131f269c6a", size = 906522, upload-time = "2026-03-28T21:46:07.988Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/46673bb18448c51222c6272c850484a0092f364fae8d0315be9aa1e4baa7/regex-2026.3.32-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5e0fdb5744caf1036dec5510f543164f2144cb64932251f6dfd42fa872b7f9c", size = 798289, upload-time = "2026-03-28T21:46:09.959Z" }, + { url = "https://files.pythonhosted.org/packages/4d/cb/804f1bd5ff08687258e6a92b040aba9b770e626b8d3ba21fffdfa21db2db/regex-2026.3.32-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dab4178a0bc1ef13178832b12db7bc7f562e8f028b2b5be186e370090dc50652", size = 774823, upload-time = "2026-03-28T21:46:12.049Z" }, + { url = "https://files.pythonhosted.org/packages/e5/94/28a58258f8d822fb949c8ff87fc7e5f2a346922360ec084c193b3c95e51c/regex-2026.3.32-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f95bd07f301135771559101c060f558e2cf896c7df00bec050ca7f93bf11585a", size = 781381, upload-time = "2026-03-28T21:46:13.746Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f3/71e69dbe0543586a3e3532cf36e8c9b38d6d93033161a9799c1e9090eb78/regex-2026.3.32-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2dcca2bceb823c9cc610e57b86a265d7ffc30e9fe98548c609eba8bd3c0c2488", size = 855968, upload-time = "2026-03-28T21:46:15.762Z" }, + { url = "https://files.pythonhosted.org/packages/6d/99/850feec404a02b62e048718ec1b4b98b5c3848cd9ca2316d0bdb65a53f6a/regex-2026.3.32-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:567b57eb987547a23306444e4f6f85d4314f83e65c71d320d898aa7550550443", size = 762785, upload-time = "2026-03-28T21:46:17.394Z" }, + { url = "https://files.pythonhosted.org/packages/40/04/808ab0462a2d19b295a3b42134f5183692f798addfe6a8b6aa5f7c7a35b2/regex-2026.3.32-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b6acb765e7c1f2fa08ac9057a33595e26104d7d67046becae184a8f100932dd9", size = 845797, upload-time = "2026-03-28T21:46:19.269Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/8afcf0fd4bd55440b48442c86cddfe61b0d21c92d96e384c0c47d769f4c3/regex-2026.3.32-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1ed17104d1be7f807fdec35ec99777168dd793a09510d753f8710590ba54cdd", size = 785200, upload-time = "2026-03-28T21:46:20.939Z" }, + { url = "https://files.pythonhosted.org/packages/99/4d/23d992ab4115456fec520d6c3aae39e0e33739b244ddb39aa4102a0f7ef0/regex-2026.3.32-cp311-cp311-win32.whl", hash = "sha256:c60f1de066eb5a0fd8ee5974de4194bb1c2e7692941458807162ffbc39887303", size = 266351, upload-time = "2026-03-28T21:46:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/62/74/27c3cdb3a3fbbf67f7231b872877416ec817ae84271573d2fd14bf8723d3/regex-2026.3.32-cp311-cp311-win_amd64.whl", hash = "sha256:8fe14e24124ef41220e5992a0f09432f890037df6f93fd3d6b7a0feff2db16b2", size = 278639, upload-time = "2026-03-28T21:46:24.016Z" }, + { url = "https://files.pythonhosted.org/packages/0a/12/6a67bd509f38aec021d63096dbc884f39473e92adeb1e35d6fb6d89cbd59/regex-2026.3.32-cp311-cp311-win_arm64.whl", hash = "sha256:ded4fc0edf3de792850cb8b04bbf3c5bd725eeaf9df4c27aad510f6eed9c4e19", size = 270594, upload-time = "2026-03-28T21:46:25.857Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/69492c45b0e61b027109d8433a5c3d4f7a90709184c057c7cfc60acb1bfa/regex-2026.3.32-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ad8d372587e659940568afd009afeb72be939c769c552c9b28773d0337251391", size = 490572, upload-time = "2026-03-28T21:46:28.031Z" }, + { url = "https://files.pythonhosted.org/packages/92/0a/7dcffeebe0fcac45a1f9caf80712002d3cbd66d7d69d719315ee142b280f/regex-2026.3.32-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f5747501b69299c6b0b047853771e4ed390510bada68cb16da9c9c2078343f7", size = 292078, upload-time = "2026-03-28T21:46:29.789Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ec/988486058ef49eb931476419bae00f164c4ceb44787c45dc7a54b7de0ea4/regex-2026.3.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db976be51375bca900e008941639448d148c655c9545071965d0571ecc04f5d0", size = 289786, upload-time = "2026-03-28T21:46:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cf/1955bb5567bc491bd63068e17f75ab0c9ff5e9d08466beec7e347f5e768d/regex-2026.3.32-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66a5083c3ffe5a5a95f8281ea47a88072d4f24001d562d1d9d28d4cdc005fec5", size = 796431, upload-time = "2026-03-28T21:46:33.101Z" }, + { url = "https://files.pythonhosted.org/packages/27/8a/67fcbca511b792107540181ee0690df6de877bfbcb41b7ecae7028025ca5/regex-2026.3.32-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e83ce8008b48762be296f1401f19afd9ea29f3d035d1974e0cecb74e9afbd1df", size = 865785, upload-time = "2026-03-28T21:46:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/0677bc44f2c28305edcabc11933777b9ad34e9e8ded7ba573d24e4bc3ee7/regex-2026.3.32-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3aa21bad31db904e0b9055e12c8282df62d43169c4a9d2929407060066ebc74", size = 913593, upload-time = "2026-03-28T21:46:36.835Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/661043d1c263b0d9d10c6ff4e9c9745f3df9641c62b51f96a3473638e7ce/regex-2026.3.32-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f54840bea73541652f1170dc63402a5b776fc851ad36a842da9e5163c1f504a0", size = 801512, upload-time = "2026-03-28T21:46:38.587Z" }, + { url = "https://files.pythonhosted.org/packages/ff/27/74c986061380e1811a46cf04cdf9c939db9f8c0e63953eddfe37ffd633ea/regex-2026.3.32-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ffbadc647325dd4e3118269bda93ded1eb5f5b0c3b7ba79a3da9fbd04f248e9", size = 776182, upload-time = "2026-03-28T21:46:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c8/d833397b70cd1bacfcdc0a611f0e2c1f5b91fee8eedd88affcee770cbbb6/regex-2026.3.32-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66d3126afe7eac41759cd5f0b3b246598086e88e70527c0d68c9e615b81771c4", size = 785837, upload-time = "2026-03-28T21:46:42.926Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/fa226b72989b5b93db6926fab5478115e085dfcf077e18d2cb386be0fd23/regex-2026.3.32-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f785f44a44702dea89b28bce5bc82552490694ce4e144e21a4f0545e364d2150", size = 860612, upload-time = "2026-03-28T21:46:44.8Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/bdd2fc0c055a1b15702bd4084829bbb6b06095f27990e5bee52b2898ea03/regex-2026.3.32-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b7836aa13721dbdef658aebd11f60d00de633a95726521860fe1f6be75fa225a", size = 765285, upload-time = "2026-03-28T21:46:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/21f5e2a35a191b27e5a47cccb3914c99e139b49b1342d3f36e64e8cc60f7/regex-2026.3.32-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5336b1506142eb0f23c96fb4a34b37c4fefd4fed2a7042069f3c8058efe17855", size = 851963, upload-time = "2026-03-28T21:46:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/f4/04ed04ebf335a44083695c22772be6a42efa31900415555563acf02cb4de/regex-2026.3.32-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b56993a7aeb4140c4770f4f7965c9e5af4f024457d06e23c01b0d47501cb18ed", size = 788332, upload-time = "2026-03-28T21:46:50.454Z" }, + { url = "https://files.pythonhosted.org/packages/21/25/5355908f479d0dc13d044f88270cdcabc8723efc12e4c2b19e5a94ff1a96/regex-2026.3.32-cp312-cp312-win32.whl", hash = "sha256:d363660f9ef8c734495598d2f3e527fb41f745c73159dc0d743402f049fb6836", size = 266847, upload-time = "2026-03-28T21:46:52.125Z" }, + { url = "https://files.pythonhosted.org/packages/00/e5/3be71c781a031db5df00735b613895ad5fdbf86c6e3bbea5fbbd7bfb5902/regex-2026.3.32-cp312-cp312-win_amd64.whl", hash = "sha256:c9f261ad3cd97257dc1d9355bfbaa7dd703e06574bffa0fa8fe1e31da915ee38", size = 278034, upload-time = "2026-03-28T21:46:54.096Z" }, + { url = "https://files.pythonhosted.org/packages/31/5f/27f1e0b1eea4faa99c66daca34130af20c44fae0237bbc98b87999dbc4a8/regex-2026.3.32-cp312-cp312-win_arm64.whl", hash = "sha256:89e50667e7e8c0e7903e4d644a2764fffe9a3a5d6578f72ab7a7b4205bf204b7", size = 270673, upload-time = "2026-03-28T21:46:56.046Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ba/9c1819f302b42b5fbd4139ead6280e9ec37d19bbe33379df0039b2a57bb4/regex-2026.3.32-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c6d9c6e783b348f719b6118bb3f187b2e138e3112576c9679eb458cc8b2e164b", size = 490394, upload-time = "2026-03-28T21:46:58.112Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/f62b0ce79eb83ca82fffea1736289d29bc24400355968301406789bcebd2/regex-2026.3.32-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f21ae18dfd15752cdd98d03cbd7a3640be826bfd58482a93f730dbd24d7b9fb", size = 291993, upload-time = "2026-03-28T21:47:00.198Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d8/ba0f8f81f88cd20c0b27acc123561ac5495ea33f800f0b8ebed2038b23eb/regex-2026.3.32-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:844d88509c968dd44b30daeefac72b038b1bf31ac372d5106358ab01d393c48b", size = 289618, upload-time = "2026-03-28T21:47:02.269Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0d/b47a0e68bc511c195ff129c0311a4cd79b954b8676193a9d03a97c623a91/regex-2026.3.32-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fc918cd003ba0d066bf0003deb05a259baaaab4dc9bd4f1207bbbe64224857a", size = 796427, upload-time = "2026-03-28T21:47:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/32b05aa8fde7789ba316533c0f30e87b6b5d38d6d7f8765eadc5aab84671/regex-2026.3.32-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbc458a292aee57d572075f22c035fa32969cdb7987d454e3e34d45a40a0a8b4", size = 865850, upload-time = "2026-03-28T21:47:05.982Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/828d8095501f237b83f630d4069eea8c0e5cb6a204e859cf0b67c223ce12/regex-2026.3.32-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:987cdfcfb97a249abc3601ad53c7de5c370529f1981e4c8c46793e4a1e1bfe8e", size = 913578, upload-time = "2026-03-28T21:47:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f8/acf1eb80f58852e85bd39a6ddfa78ce2243ddc8de8da7582e6ba657da593/regex-2026.3.32-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d88fa37ba5e8a80ca8d956b9ea03805cfa460223ac94b7d4854ee5e30f3173", size = 801536, upload-time = "2026-03-28T21:47:10.206Z" }, + { url = "https://files.pythonhosted.org/packages/9f/05/986cdf8d12693451f5889aaf4ea4f65b2c49b1152ae814fa1fb75439e40b/regex-2026.3.32-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d082be64e51671dd5ee1c208c92da2ddda0f2f20d8ef387e57634f7e97b6aae", size = 776226, upload-time = "2026-03-28T21:47:12.891Z" }, + { url = "https://files.pythonhosted.org/packages/32/02/945a6a2348ca1c6608cb1747275c8affd2ccd957d4885c25218a86377912/regex-2026.3.32-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1d7fa44aece1fa02b8927441614c96520253a5cad6a96994e3a81e060feed55", size = 785933, upload-time = "2026-03-28T21:47:14.795Z" }, + { url = "https://files.pythonhosted.org/packages/53/12/c5bab6cc679ad79a45427a98c4e70809586ac963c5ad54a9217533c4763e/regex-2026.3.32-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d478a2ca902b6ef28ffc9521e5f0f728d036abe35c0b250ee8ae78cfe7c5e44e", size = 860671, upload-time = "2026-03-28T21:47:16.985Z" }, + { url = "https://files.pythonhosted.org/packages/bf/68/8d85f98c2443469facabef62b82b851d369b13f92bec2ca7a3808deaa47b/regex-2026.3.32-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2820d2231885e97aff0fcf230a19ebd5d2b5b8a1ba338c20deb34f16db1c7897", size = 765335, upload-time = "2026-03-28T21:47:18.872Z" }, + { url = "https://files.pythonhosted.org/packages/89/a7/d8a9c270916107a501fca63b748547c6c77e570d19f16a29b557ce734f3d/regex-2026.3.32-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc8ced733d6cd9af5e412f256a32f7c61cd2d7371280a65c689939ac4572499f", size = 851913, upload-time = "2026-03-28T21:47:20.793Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/03d392b26679914ccf21f83d18ad4443232d2f8c3e2c30a962d4e3918d9c/regex-2026.3.32-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:847087abe98b3c1ebf1eb49d6ef320dbba75a83ee4f83c94704580f1df007dd4", size = 788447, upload-time = "2026-03-28T21:47:22.628Z" }, + { url = "https://files.pythonhosted.org/packages/cf/df/692227d23535a50604333068b39eb262626db780ab1e1b19d83fc66853aa/regex-2026.3.32-cp313-cp313-win32.whl", hash = "sha256:d21a07edddb3e0ca12a8b8712abc8452481c3d3db19ae87fc94e9842d005964b", size = 266834, upload-time = "2026-03-28T21:47:24.778Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/13e4e56adc16ba607cffa1fe880f233eb9ded8ab8a8580619683c9e4ce48/regex-2026.3.32-cp313-cp313-win_amd64.whl", hash = "sha256:3c054e39a9f85a3d76c62a1d50c626c5e9306964eaa675c53f61ff7ec1204bbb", size = 277972, upload-time = "2026-03-28T21:47:26.627Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1c/80a86dbb2b416fec003b1801462bdcebbf1d43202ed5acb176e99c1ba369/regex-2026.3.32-cp313-cp313-win_arm64.whl", hash = "sha256:b2e9c2ea2e93223579308263f359eab8837dc340530b860cb59b713651889f14", size = 270649, upload-time = "2026-03-28T21:47:28.551Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/e38372da599dc1c39c599907ec535016d110034bd3701ce36554f59767ef/regex-2026.3.32-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5d86e3fb08c94f084a625c8dc2132a79a3a111c8bf6e2bc59351fa61753c2f6e", size = 494495, upload-time = "2026-03-28T21:47:30.642Z" }, + { url = "https://files.pythonhosted.org/packages/5f/27/6e29ece8c9ce01001ece1137fa21c8707529c2305b22828f63623b0eb262/regex-2026.3.32-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b6f366a5ef66a2df4d9e68035cfe9f0eb8473cdfb922c37fac1d169b468607b0", size = 293988, upload-time = "2026-03-28T21:47:32.553Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/8752e18bb87a2fe728b73b0f83c082eb162a470766063f8028759fb26844/regex-2026.3.32-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b8fca73e16c49dd972ce3a88278dfa5b93bf91ddef332a46e9443abe21ca2f7c", size = 292634, upload-time = "2026-03-28T21:47:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7b/d7729fe294e23e9c7c3871cb69d49059fa7d65fd11e437a2cbea43f6615d/regex-2026.3.32-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b953d9d496d19786f4d46e6ba4b386c6e493e81e40f9c5392332458183b0599d", size = 810532, upload-time = "2026-03-28T21:47:36.839Z" }, + { url = "https://files.pythonhosted.org/packages/fd/49/4dae7b000659f611b17b9c1541fba800b0569e4060debc4635ef1b23982c/regex-2026.3.32-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b565f25171e04d4fad950d1fa837133e3af6ea6f509d96166eed745eb0cf63bc", size = 871919, upload-time = "2026-03-28T21:47:39.192Z" }, + { url = "https://files.pythonhosted.org/packages/83/85/aa8ad3977b9399861db3df62b33fe5fef6932ee23a1b9f4f357f58f2094b/regex-2026.3.32-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f28eac18a8733a124444643a66ac96fef2c0ad65f50034e0a043b90333dc677f", size = 916550, upload-time = "2026-03-28T21:47:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c0/6379d7f5b59ff0656ba49cf666d5013ecee55e83245275b310b0ffc79143/regex-2026.3.32-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cdd508664430dd51b8888deb6c5b416d8de046b2e11837254378d31febe4a98", size = 814988, upload-time = "2026-03-28T21:47:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/2c/af/2dfddc64074bd9b70e27e170ee9db900542e2870210b489ad4471416ba86/regex-2026.3.32-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c35d097f509cf7e40d20d5bee548d35d6049b36eb9965e8d43e4659923405b9", size = 786337, upload-time = "2026-03-28T21:47:46.076Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2f/4eb8abd705236402b4fe0e130971634deffb1855e2028bf02a2b7c0e841c/regex-2026.3.32-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:85c9b0c131427470a6423baa0a9330be6fd8c3630cc3ee6fdee03360724cbec5", size = 800029, upload-time = "2026-03-28T21:47:48.356Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/77d9ca2c9df483b51b4b1291c96d79c9ae301077841c4db39bc822f6b4c6/regex-2026.3.32-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e50af656c15e2723eeb7279c0837e07accc594b95ec18b86821a4d44b51b24bf", size = 865843, upload-time = "2026-03-28T21:47:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/48/10/306f477a509f4eed699071b1f031d89edd5a2b5fa28c8ede5b2638eaba82/regex-2026.3.32-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4bc32b4dbdb4f9f300cf9f38f8ea2ce9511a068ffaa45ac1373ee7a943f1d810", size = 772473, upload-time = "2026-03-28T21:47:52.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f6/54bd83ec46ac037de2beb049afc9dd5d2769c6ecaadf7856254ce610e62a/regex-2026.3.32-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3e5d1802cba785210a4a800e63fcee7a228649a880f3bf7f2aadccb151a834b", size = 856805, upload-time = "2026-03-28T21:47:55.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/ee0e7d14de1fc6582d5782f072db6c61465a38a4142f88e175dda494b536/regex-2026.3.32-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ef250a3f5e93182193f5c927c5e9575b2cb14b80d03e258bc0b89cc5de076b60", size = 801875, upload-time = "2026-03-28T21:47:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/8a/06/0fa9daca59d07b6aabd8e0468d3b86fd578576a157206fbcddbfc2298f7d/regex-2026.3.32-cp313-cp313t-win32.whl", hash = "sha256:9cf7036dfa2370ccc8651521fcbb40391974841119e9982fa312b552929e6c85", size = 269892, upload-time = "2026-03-28T21:47:59.674Z" }, + { url = "https://files.pythonhosted.org/packages/13/47/77f16b5ad9f10ca574f03d84a354b359b0ac33f85054f2f2daafc9f7b807/regex-2026.3.32-cp313-cp313t-win_amd64.whl", hash = "sha256:c940e00e8d3d10932c929d4b8657c2ea47d2560f31874c3e174c0d3488e8b865", size = 281318, upload-time = "2026-03-28T21:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/c6/47/db4446faaea8d01c8315c9c89c7dc6abbb3305e8e712e9b23936095c4d58/regex-2026.3.32-cp313-cp313t-win_arm64.whl", hash = "sha256:ace48c5e157c1e58b7de633c5e257285ce85e567ac500c833349c363b3df69d4", size = 272366, upload-time = "2026-03-28T21:48:03.748Z" }, + { url = "https://files.pythonhosted.org/packages/32/68/ff024bf6131b7446a791a636dbbb7fa732d586f33b276d84b3460ea49393/regex-2026.3.32-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a416ee898ecbc5d8b283223b4cf4d560f93244f6f7615c1bd67359744b00c166", size = 490430, upload-time = "2026-03-28T21:48:05.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/72/039d9164817ee298f2a2d0246001afe662241dcbec0eedd1fe03e2a2555e/regex-2026.3.32-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d76d62909bfb14521c3f7cfd5b94c0c75ec94b0a11f647d2f604998962ec7b6c", size = 291948, upload-time = "2026-03-28T21:48:07.666Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/77f684d90ffe3e99b828d3cabb87a0f1601d2b9decd1333ff345809b1d02/regex-2026.3.32-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:631f7d95c83f42bccfe18946a38ad27ff6b6717fb4807e60cf24860b5eb277fc", size = 289786, upload-time = "2026-03-28T21:48:09.562Z" }, + { url = "https://files.pythonhosted.org/packages/83/70/bd76069a0304e924682b2efd8683a01617a7e1da9b651af73039d8da76a4/regex-2026.3.32-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12917c6c6813ffcdfb11680a04e4d63c5532b88cf089f844721c5f41f41a63ad", size = 796672, upload-time = "2026-03-28T21:48:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/c2d7d9a5671e111a2c16d57e0cb03e1ce35b28a115901590528aa928bb5b/regex-2026.3.32-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e221b615f83b15887636fcb90ed21f1a19541366f8b7ba14ba1ad8304f4ded4", size = 866556, upload-time = "2026-03-28T21:48:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b9/9921a31931d0bc3416ac30205471e0e2ed60dcbd16fc922bbd69b427322b/regex-2026.3.32-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f9ae4755fa90f1dc2d0d393d572ebc134c0fe30fcfc0ab7e67c1db15f192041", size = 912787, upload-time = "2026-03-28T21:48:16.548Z" }, + { url = "https://files.pythonhosted.org/packages/41/ab/2c1bc8ab99f63cdabdbc7823af8f4cfcd6ddbb2babf01861826c3f1ad44d/regex-2026.3.32-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a094e9dcafedfb9d333db5cf880304946683f43a6582bb86688f123335122929", size = 800879, upload-time = "2026-03-28T21:48:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/49/e5/0be716eb2c0b2ae3a439e44432534e82b2f81848af64cb21c0473ad8ae46/regex-2026.3.32-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1cecea3e477af105f32ef2119b8d895f297492e41d317e60d474bc4bffd62ff", size = 776332, upload-time = "2026-03-28T21:48:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/114a61bd25dec7d1070930eaef82aadf9b05961a37629e7cca7bc3fc2257/regex-2026.3.32-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f26262900edd16272b6360014495e8d68379c6c6e95983f9b7b322dc928a1194", size = 786384, upload-time = "2026-03-28T21:48:23.277Z" }, + { url = "https://files.pythonhosted.org/packages/0c/78/be0a6531f8db426e8e60d6356aeef8e9cc3f541655a648c4968b63c87a88/regex-2026.3.32-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb22fa9ee6a0acb22fc9aecce5f9995fe4d2426ed849357d499d62608fbd7f9", size = 861381, upload-time = "2026-03-28T21:48:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/45/b1/e5076fbe45b8fb39672584b1b606d512f5bd3a43155be68a95f6b88c1fc5/regex-2026.3.32-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9b9118a78e031a2e4709cd2fcc3028432e89b718db70073a8da574c249b5b249", size = 765434, upload-time = "2026-03-28T21:48:27.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/da/fd65d68b897f8b52b1390d20d776fa753582484724a9cb4f4c26de657ae5/regex-2026.3.32-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b193ed199848aa96618cd5959c1582a0bf23cd698b0b900cb0ffe81b02c8659c", size = 851501, upload-time = "2026-03-28T21:48:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d6/1e9c991c32022a9312e9124cc974961b3a2501338de2cd1cce75a3612d7a/regex-2026.3.32-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:10fb2aaae1aaadf7d43c9f3c2450404253697bf8b9ce360bd5418d1d16292298", size = 788076, upload-time = "2026-03-28T21:48:32.025Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5b/b23c72f6d607cbb24ef42acf0c7c2ef4eee1377a9f7ba43b312f889edfbb/regex-2026.3.32-cp314-cp314-win32.whl", hash = "sha256:110ba4920721374d16c4c8ea7ce27b09546d43e16aea1d7f43681b5b8f80ba61", size = 272255, upload-time = "2026-03-28T21:48:34.355Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ec/32bbcc42366097a8cea2c481e02964be6c6fa5ccfb0fa9581686af0bec5f/regex-2026.3.32-cp314-cp314-win_amd64.whl", hash = "sha256:245667ad430745bae6a1e41081872d25819d86fbd9e0eec485ba00d9f78ad43d", size = 281160, upload-time = "2026-03-28T21:48:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/89038a028cb68e719fa03ab1ad603649fc199bcda12270d2ac7b471b8f5d/regex-2026.3.32-cp314-cp314-win_arm64.whl", hash = "sha256:1ca02ff0ef33e9d8276a1fcd6d90ff6ea055a32c9149c0050b5b67e26c6d2c51", size = 273688, upload-time = "2026-03-28T21:48:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/87caccd608837a1fa4f8c7edc48e206103452b9bbc94fc724fa39340e807/regex-2026.3.32-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:51fb7e26f91f9091fd8ec6a946f99b15d3bc3667cb5ddc73dd6cb2222dd4a1cc", size = 494506, upload-time = "2026-03-28T21:48:41.327Z" }, + { url = "https://files.pythonhosted.org/packages/16/53/a922e6b24694d70bdd68fc3fd076950e15b1b418cff9d2cc362b3968d86f/regex-2026.3.32-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:51a93452034d671b0e21b883d48ea66c5d6a05620ee16a9d3f229e828568f3f0", size = 293986, upload-time = "2026-03-28T21:48:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/60/e4/0cb32203c1aebad0577fcd5b9af1fe764869e617d5234bc6a0ad284299ea/regex-2026.3.32-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:03c2ebd15ff51e7b13bb3dc28dd5ac18cd39e59ebb40430b14ae1a19e833cff1", size = 292677, upload-time = "2026-03-28T21:48:45.772Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/5006b70291469d4174dd66ad162802e2f68419c0f2a7952d0c76c1288cfa/regex-2026.3.32-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bf2f3c2c5bd8360d335c7dcd4a9006cf1dabae063ee2558ee1b07bbc8a20d88", size = 810661, upload-time = "2026-03-28T21:48:48.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9b/438763a20d22cd1f65f95c8f030dd25df2d80a941068a891d21a5f240456/regex-2026.3.32-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a4a3189a99ecdd1c13f42513ab3fc7fa8311b38ba7596dd98537acb8cd9acc3", size = 872156, upload-time = "2026-03-28T21:48:50.739Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5b/1341287887ac982ed9f5f60125e440513ffe354aa7e3681940495af7c12a/regex-2026.3.32-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c0bbfbd38506e1ea96a85da6782577f06239cb9fcf9696f1ea537c980c0680b", size = 916749, upload-time = "2026-03-28T21:48:53.57Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/1d2b48b8e94debfffc6fefb84d2a86a178cc208652a1d6493d5f29821c70/regex-2026.3.32-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aaf8ee8f34b677f90742ca089b9c83d64bdc410528767273c816a863ed57327", size = 814788, upload-time = "2026-03-28T21:48:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d9/7dacb34c43adaeb954518d851f3e5d3ce495ac00a9d6010e3b4b59917c4a/regex-2026.3.32-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ea568832eca219c2be1721afa073c1c9eb8f98a9733fdedd0a9747639fc22a5", size = 786594, upload-time = "2026-03-28T21:48:58.404Z" }, + { url = "https://files.pythonhosted.org/packages/ea/72/28295068c92dbd6d3ce4fd22554345cf504e957cc57dadeda4a64fa86a57/regex-2026.3.32-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e4c8fa46aad1a11ae2f8fcd1c90b9d55e18925829ac0d98c5bb107f93351745", size = 800167, upload-time = "2026-03-28T21:49:01.226Z" }, + { url = "https://files.pythonhosted.org/packages/ca/17/b10745adeca5b8d52da050e7c746137f5d01dabc6dbbe6e8d9d821dc65c1/regex-2026.3.32-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cec365d44835b043d7b3266487797639d07d621bec9dc0ea224b00775797cc1", size = 865906, upload-time = "2026-03-28T21:49:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/45/9d/1acbcce765044ac0c87f453f4876e0897f7a61c10315262f960184310798/regex-2026.3.32-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:09e26cad1544d856da85881ad292797289e4406338afe98163f3db9f7fac816c", size = 772642, upload-time = "2026-03-28T21:49:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/24/41/1ef8b4811355ad7b9d7579d3aeca00f18b7bc043ace26c8c609b9287346d/regex-2026.3.32-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:6062c4ef581a3e9e503dccf4e1b7f2d33fdc1c13ad510b287741ac73bc4c6b27", size = 856927, upload-time = "2026-03-28T21:49:09.373Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/0dc1d361be80ec1b8b707ada041090181133a7a29d438e432260a4b26f9a/regex-2026.3.32-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88ebc0783907468f17fca3d7821b30f9c21865a721144eb498cb0ff99a67bcac", size = 801910, upload-time = "2026-03-28T21:49:11.818Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/1a23f767fa250844772a9464306d34e0fafe2c317303b88a1415096b6324/regex-2026.3.32-cp314-cp314t-win32.whl", hash = "sha256:e480d3dac06c89bc2e0fd87524cc38c546ac8b4a38177650745e64acbbcfdeba", size = 275714, upload-time = "2026-03-28T21:49:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/c2/2b/616d31b125ca76079d74d6b1d84ec0860ffdb41c379151135d06e35a8633/regex-2026.3.32-cp314-cp314t-win_amd64.whl", hash = "sha256:67015a8162d413af9e3309d9a24e385816666fbf09e48e3ec43342c8536f7df6", size = 285722, upload-time = "2026-03-28T21:49:16.642Z" }, + { url = "https://files.pythonhosted.org/packages/7e/91/043d9a00d6123c5fa22a3dc96b10445ce434a8110e1d5e53efb01f243c8b/regex-2026.3.32-cp314-cp314t-win_arm64.whl", hash = "sha256:1a6ac1ed758902e664e0d95c1ee5991aa6fb355423f378ed184c6ec47a1ec0e9", size = 275700, upload-time = "2026-03-28T21:49:19.348Z" }, ] [[package]] name = "requests" -version = "2.34.2" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -6378,9 +6298,9 @@ dependencies = [ { name = "idna", 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/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -6582,14 +6502,14 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.17.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] @@ -6661,7 +6581,7 @@ resolution-markers = [ ] dependencies = [ { name = "joblib", 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.4.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.4.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 = "scipy", version = "1.17.1", 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 = "(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')" }, ] @@ -6785,7 +6705,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.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.4.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')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -6858,9 +6778,9 @@ 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.4.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.4.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 = "pandas", version = "2.3.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 = "pandas", version = "3.0.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 = "pandas", version = "3.0.2", 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/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } wheels = [ @@ -7007,75 +6927,75 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.49" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(platform_machine == 'AMD64' and sys_platform == 'darwin') or (platform_machine == 'WIN32' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'darwin') or (platform_machine == 'amd64' and sys_platform == 'darwin') or (platform_machine == 'ppc64le' and sys_platform == 'darwin') or (platform_machine == 'win32' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'WIN32' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'amd64' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 'win32' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'WIN32' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'win32') or (platform_machine == 'amd64' and sys_platform == 'win32') or (platform_machine == 'ppc64le' and sys_platform == 'win32') or (platform_machine == 'win32' and sys_platform == 'win32') or (platform_machine == 'x86_64' and 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/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, - { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, - { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, - { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, - { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, - { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, - { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, - { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, - { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, - { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, - { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, - { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, - { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, - { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, - { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, - { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, - { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, - { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, - { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, - { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, - { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, - { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, - { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, - { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, - { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, - { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, - { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] name = "sse-starlette" -version = "3.4.4" +version = "3.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, ] [[package]] @@ -7140,7 +7060,7 @@ dependencies = [ { name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pandas", version = "2.3.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 = "pandas", version = "3.0.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 = "pandas", version = "3.0.2", 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 = "plotly", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic-argparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -7188,90 +7108,93 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.13.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4", size = 1036627, upload-time = "2026-05-15T04:50:11.229Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9", size = 984699, upload-time = "2026-05-15T04:50:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e", size = 1118690, upload-time = "2026-05-15T04:50:14.234Z" }, - { url = "https://files.pythonhosted.org/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5", size = 1138423, upload-time = "2026-05-15T04:50:15.668Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d", size = 1185077, upload-time = "2026-05-15T04:50:17.19Z" }, - { url = "https://files.pythonhosted.org/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1", size = 1241702, upload-time = "2026-05-15T04:50:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910", size = 876565, upload-time = "2026-05-15T04:50:20.268Z" }, - { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, - { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, - { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, - { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, - { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, - { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, - { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, - { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, - { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, - { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, - { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, - { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, - { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, - { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, - { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, - { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, - { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, - { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, - { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, - { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, - { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] [[package]] name = "tokenizers" -version = "0.23.1" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, - { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, - { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, - { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, - { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] [[package]] @@ -7360,7 +7283,7 @@ wheels = [ [[package]] name = "typer" -version = "0.25.1" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -7368,9 +7291,9 @@ dependencies = [ { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "shellingham", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, ] [[package]] @@ -7393,14 +7316,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.33.0.20260518" +version = "2.33.0.20260402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/7b/a06527d20af1441d813360b8e0ce152a75b7d8e4aab7c7d0a156f405d7ec/types_requests-2.33.0.20260402.tar.gz", hash = "sha256:1bdd3ada9b869741c5c4b887d2c8b4e38284a1449751823b5ebbccba3eefd9da", size = 23851, upload-time = "2026-04-02T04:19:55.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/51/65/3853bb6bac5ae789dc7e28781154705c27859eccc8e46282c3f36780f5f5/types_requests-2.33.0.20260402-py3-none-any.whl", hash = "sha256:c98372d7124dd5d10af815ee25c013897592ff92af27b27e22c98984102c3254", size = 20739, upload-time = "2026-04-02T04:19:54.955Z" }, ] [[package]] @@ -7426,11 +7349,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] @@ -7447,11 +7370,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] @@ -7482,16 +7405,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.47.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { 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')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [package.optional-dependencies] @@ -7571,119 +7494,105 @@ wheels = [ [[package]] name = "watchfiles" -version = "1.2.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9", size = 400471, upload-time = "2026-05-18T04:31:08.908Z" }, - { url = "https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4", size = 394599, upload-time = "2026-05-18T04:30:19.845Z" }, - { url = "https://files.pythonhosted.org/packages/18/52/752dcc7dc817baef5e89518732925795ce52e36a683a9a3c9fb68b21504e/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631", size = 455458, upload-time = "2026-05-18T04:30:29.126Z" }, - { url = "https://files.pythonhosted.org/packages/12/48/366ebbb22fcc504c2f72b45f0b7e72f40a18795cc01752c16066d597b67a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994", size = 460513, upload-time = "2026-05-18T04:31:40.85Z" }, - { url = "https://files.pythonhosted.org/packages/ad/44/1f9e1b15e7a729062e0d0c3d0d7225ea4ab98b2267ef87287153be2495fc/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e", size = 493616, upload-time = "2026-05-18T04:30:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/7e/55/8b1086dcc8a1d6a697a62767bd7ea368e74c61c6fd171683cfe24a3fe5d2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19", size = 573154, upload-time = "2026-05-18T04:30:37.903Z" }, - { url = "https://files.pythonhosted.org/packages/14/7a/242f400cc77fafa7b18d53d19d9cb64fc6a6f61f28c55913bae7c674d92a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8", size = 467046, upload-time = "2026-05-18T04:30:41.869Z" }, - { url = "https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07", size = 457100, upload-time = "2026-05-18T04:31:13.064Z" }, - { url = "https://files.pythonhosted.org/packages/81/36/519f6dbb7a95e4fe7c1513ed25b1520295ef9905a27f1f2226a73892bfb7/watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551", size = 467038, upload-time = "2026-05-18T04:30:32.915Z" }, - { url = "https://files.pythonhosted.org/packages/2f/12/951af6b9f89097e02511122258402cb3578443021930b70cf968d6310dc0/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310", size = 632563, upload-time = "2026-05-18T04:30:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/0cba1f0a6117b7ec117271bdc3cb3a5a252005959755a2c09a745e0942cc/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df", size = 660851, upload-time = "2026-05-18T04:31:53.186Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f2/26347558cc8bf6877845e66b315f644d03c173906aa09e233a3f4fd23928/watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1", size = 277023, upload-time = "2026-05-18T04:30:18.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/68/a5e67b6b68e94f4c1511d61c46c55eba0737583620b6febf194c7b9cc23f/watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d", size = 290107, upload-time = "2026-05-18T04:32:09.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, - { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, - { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, - { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, - { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, - { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, - { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, - { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, - { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, - { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, - { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, - { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, - { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, - { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, - { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, - { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, - { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, - { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, - { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, - { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, - { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, - { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, - { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, - { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, - { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, - { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, - { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, - { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, - { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, - { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, - { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, - { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, - { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, - { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, - { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] [[package]] @@ -7747,14 +7656,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.8" +version = "3.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700, upload-time = "2026-03-24T01:08:07.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295, upload-time = "2026-03-24T01:08:06.133Z" }, ] [[package]] @@ -7849,125 +7758,149 @@ wheels = [ [[package]] name = "yarl" -version = "1.24.2" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/df/f1c7a3de0831cd83194f1a85c5bb431b13f81e6b45079314c86d1c4ef3f2/yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12", size = 129057, upload-time = "2026-05-19T21:27:47.564Z" }, - { url = "https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0", size = 91545, upload-time = "2026-05-19T21:27:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75", size = 91380, upload-time = "2026-05-19T21:27:51.953Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1b/8bafab7db23b0567ae9db749099b329d91e3b82bc6028b2050ba583e116c/yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727", size = 105957, upload-time = "2026-05-19T21:27:53.98Z" }, - { url = "https://files.pythonhosted.org/packages/7f/77/21030c2f8d21d21559719beafc772ada2014be933418ed1eaed9cc800e42/yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413", size = 97242, upload-time = "2026-05-19T21:27:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/50/d8/f9ea63d1b6aa910a866e089d871fff6cbd49caab29b86b35221a62dfa0d5/yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9", size = 114719, upload-time = "2026-05-19T21:27:58.037Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a3/04e0ee98ac58a249ea7ed75223f5f901ba81a834f0b4921b58e5cec11757/yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2", size = 112140, upload-time = "2026-05-19T21:27:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90", size = 106721, upload-time = "2026-05-19T21:28:02.586Z" }, - { url = "https://files.pythonhosted.org/packages/65/e7/a52478ebfc66ec989e085c6ae038b9f1bfa4190baa193b133b669c709e2f/yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643", size = 106478, upload-time = "2026-05-19T21:28:04.523Z" }, - { url = "https://files.pythonhosted.org/packages/04/d8/5508530fea8472542de00013ae280765fc938ee196fc4030c43a498afb36/yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac", size = 105423, upload-time = "2026-05-19T21:28:06.515Z" }, - { url = "https://files.pythonhosted.org/packages/84/f1/ece28505e9628e8b756e11bb4f28864a17cc33b6b44db4d2aaf0622bf630/yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f", size = 99878, upload-time = "2026-05-19T21:28:08.637Z" }, - { url = "https://files.pythonhosted.org/packages/3f/52/fb5d34529b46dd84013afcfb30b8d2bc2832ed03d412736f577d604fa393/yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36", size = 114025, upload-time = "2026-05-19T21:28:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/43/f0/ff9d31aaab024f7a251c0ed308a98ae29bf9f7dc344e78f28b1322431ca2/yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a", size = 105613, upload-time = "2026-05-19T21:28:12.784Z" }, - { url = "https://files.pythonhosted.org/packages/31/7d/3296fb3f3ecd52bf9ae6c16b0895c1cda7e9170a2083861552b683f70264/yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53", size = 111665, upload-time = "2026-05-19T21:28:14.393Z" }, - { url = "https://files.pythonhosted.org/packages/1a/74/77aa6ddaca4fbf42e45e675a465c43956dd40702281049975a2aa04eae59/yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342", size = 106914, upload-time = "2026-05-19T21:28:15.893Z" }, - { url = "https://files.pythonhosted.org/packages/d8/02/7611f22cd1d4ed7373eb7f9ee21fde1046edba2e7c0e514880d760352f48/yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4", size = 92658, upload-time = "2026-05-19T21:28:17.471Z" }, - { url = "https://files.pythonhosted.org/packages/91/00/671d0add79938127292839ae44506ce2f7fe8909c72d5a931864f128fd0b/yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39", size = 87887, upload-time = "2026-05-19T21:28:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, - { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, - { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, - { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, - { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, - { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, - { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, - { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, - { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, - { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, - { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, - { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, - { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, - { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, - { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, - { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, - { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, - { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, - { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, - { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, - { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, - { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, - { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, - { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, - { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, - { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, - { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, - { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, - { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, - { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, - { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, - { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, - { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] [[package]] name = "zipp" -version = "4.1.0" +version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] From e8ff541ebf07339b12585a3d4984d78a13e7e1aa Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Fri, 29 May 2026 09:21:14 +0200 Subject: [PATCH 16/61] Python: consolidate MCP reliability fixes (#6145) * Python: consolidate MCP reliability fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix MCP cleanup and metadata typing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Satisfy MCP metadata mypy typing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Pyright metadata mapping type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/core/agent_framework/_mcp.py | 92 +++++++-- python/packages/core/tests/core/test_mcp.py | 206 +++++++++++++++++++ 2 files changed, 278 insertions(+), 20 deletions(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index b2942de2a0..d872b2b92d 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -10,7 +10,7 @@ import logging import re import sys from abc import abstractmethod -from collections.abc import Callable, Collection, Coroutine, Sequence +from collections.abc import Callable, Collection, Coroutine, Mapping, Sequence from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore from datetime import timedelta from functools import partial @@ -142,6 +142,13 @@ def _inject_otel_into_mcp_meta(meta: dict[str, Any] | None = None) -> dict[str, return meta +def _url_origin(url: Any) -> tuple[str, str, int | None]: + port = url.port + if port is None: + port = 443 if url.scheme == "https" else 80 if url.scheme == "http" else None + return (url.scheme, url.host or "", port) + + def streamable_http_client(*args: Any, **kwargs: Any) -> _AsyncGeneratorContextManager[Any, None]: """Lazily import the MCP streamable HTTP transport.""" try: @@ -255,6 +262,7 @@ class MCPTool: self._exit_stack = AsyncExitStack() self._lifecycle_lock = asyncio.Lock() self._lifecycle_request_lock = asyncio.Lock() + self._function_load_lock = asyncio.Lock() self._lifecycle_queue: asyncio.Queue[tuple[str, bool, bool, asyncio.Future[None]]] | None = None self._lifecycle_owner_task: asyncio.Task[None] | None = None self.session = session @@ -655,6 +663,11 @@ class MCPTool: raise except asyncio.CancelledError: logger.warning("Could not cleanly close MCP exit stack because the lifecycle owner task was cancelled.") + except Exception as e: + if type(e).__name__ == "ExceptionGroup": + logger.warning("Could not cleanly close MCP exit stack due to cleanup error group. Error: %s", e) + else: + raise async def _close_and_check_cancelled(self, ex: BaseException) -> bool: """Close the exit stack and return True if *ex* is a genuine task cancellation. @@ -1018,6 +1031,10 @@ class MCPTool: Raises: ToolExecutionException: If the MCP server is not connected. """ + async with self._function_load_lock: + await self._load_prompts_locked() + + async def _load_prompts_locked(self) -> None: from anyio import ClosedResourceError from mcp import types @@ -1100,6 +1117,10 @@ class MCPTool: Raises: ToolExecutionException: If the MCP server is not connected. """ + async with self._function_load_lock: + await self._load_tools_locked() + + async def _load_tools_locked(self) -> None: from anyio import ClosedResourceError from mcp import types @@ -1109,7 +1130,7 @@ class MCPTool: # Track existing function names to prevent duplicates existing_names = {func.name for func in self._functions} - self._tool_call_meta_by_name.clear() + tool_call_meta_by_name: dict[str, dict[str, Any]] = {} params: types.PaginatedRequestParams | None = None while True: @@ -1145,7 +1166,7 @@ class MCPTool: for tool in tool_list.tools: if tool.meta is not None: - self._tool_call_meta_by_name[tool.name] = dict(tool.meta) + tool_call_meta_by_name[tool.name] = dict(tool.meta) normalized_name = _normalize_mcp_name(tool.name) local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix) @@ -1194,6 +1215,8 @@ class MCPTool: break params = types.PaginatedRequestParams(cursor=tool_list.nextCursor) + self._tool_call_meta_by_name = tool_call_meta_by_name + async def _close_on_owner(self) -> None: # Cancel any pending reload tasks before tearing down the session. tasks = list(self._pending_reload_tasks) @@ -1276,7 +1299,11 @@ class MCPTool: tool_name: The name of the tool to call. Keyword Args: - kwargs: Arguments to pass to the tool. + _meta: Optional ``dict[str, Any]`` of MCP request metadata. This reserved key is passed as the + ``meta`` parameter of the underlying ``session.call_tool`` call rather than as a tool argument. + User-supplied keys override metadata from ``tools/list``; OpenTelemetry propagation fills in + non-conflicting keys. + kwargs: Remaining arguments to pass to the tool. Returns: A list of Content items representing the tool output. The default @@ -1294,6 +1321,19 @@ class MCPTool: raise ToolExecutionException( "Tools are not loaded for this server, please set load_tools=True in the constructor." ) + + raw_user_meta: object | None = kwargs.get("_meta") + user_meta: dict[str, Any] | None = None + if raw_user_meta is not None and not isinstance(raw_user_meta, dict): + raise ToolExecutionException("MCP tool metadata provided via _meta must be a dict.") + if isinstance(raw_user_meta, dict): + raw_user_meta_dict = cast(Mapping[object, object], raw_user_meta) + user_meta = {} + for key, value in raw_user_meta_dict.items(): + if not isinstance(key, str): + raise ToolExecutionException("MCP tool metadata provided via _meta must use string keys.") + user_meta[key] = value + # Filter out framework kwargs that cannot be serialized by the MCP SDK. # These are internal objects passed through the function invocation pipeline # that should not be forwarded to external MCP servers. @@ -1313,12 +1353,16 @@ class MCPTool: "conversation_id", "options", "response_format", + "_meta", } } # Some MCP proxies require their tools/list metadata to be echoed on tools/call. tool_meta = self._tool_call_meta_by_name.get(tool_name) - meta = _inject_otel_into_mcp_meta(dict(tool_meta) if tool_meta is not None else None) + request_meta = dict(tool_meta) if tool_meta is not None else None + if user_meta is not None: + request_meta = {**(request_meta or {}), **user_meta} + meta = _inject_otel_into_mcp_meta(request_meta) parser = self.parse_tool_results or self._parse_tool_result_from_mcp # Try the operation, reconnecting once if the connection is closed @@ -1336,28 +1380,33 @@ class MCPTool: return parser(result) except ToolExecutionException: raise - except ClosedResourceError as cl_ex: + except (ClosedResourceError, McpError) as call_ex: + is_session_terminated = ( + isinstance(call_ex, McpError) and "session terminated" in call_ex.error.message.lower() + ) + is_connection_lost = isinstance(call_ex, ClosedResourceError) or is_session_terminated + if not is_connection_lost: + error_message = call_ex.error.message if isinstance(call_ex, McpError) else str(call_ex) + raise ToolExecutionException(error_message, inner_exception=call_ex) from call_ex + if attempt == 0: - # First attempt failed, try reconnecting - logger.info("MCP connection closed unexpectedly. Reconnecting...") + # First attempt failed, try reconnecting. + logger.info("MCP connection closed or terminated unexpectedly. Reconnecting...") try: await self.connect(reset=True) - continue # Retry the operation + continue except Exception as reconn_ex: raise ToolExecutionException( "Failed to reconnect to MCP server.", inner_exception=reconn_ex, ) from reconn_ex - else: - # Second attempt also failed, give up - logger.error(f"MCP connection closed unexpectedly after reconnection: {cl_ex}") - raise ToolExecutionException( - f"Failed to call tool '{tool_name}' - connection lost.", - inner_exception=cl_ex, - ) from cl_ex - except McpError as mcp_exc: - error_message = mcp_exc.error.message - raise ToolExecutionException(error_message, inner_exception=mcp_exc) from mcp_exc + + # Second attempt also failed, give up. + logger.error("MCP connection closed unexpectedly after reconnection: %s", call_ex) + raise ToolExecutionException( + f"Failed to call tool '{tool_name}' - connection lost.", + inner_exception=call_ex, + ) from call_ex except Exception as ex: raise ToolExecutionException(f"Failed to call tool '{tool_name}'.", inner_exception=ex) from ex raise ToolExecutionException(f"Failed to call tool '{tool_name}' after retries.") @@ -1718,10 +1767,11 @@ class MCPStreamableHTTPTool(MCPTool): Returns: An async context manager for the streamable HTTP client transport. """ - from httpx import AsyncClient, Request, Timeout + from httpx import URL, AsyncClient, Request, Timeout http_client = self._httpx_client if self._header_provider is not None: + target_origin = _url_origin(URL(self.url)) if http_client is None: http_client = AsyncClient( follow_redirects=True, @@ -1732,6 +1782,8 @@ class MCPStreamableHTTPTool(MCPTool): if not hasattr(self, "_inject_headers_hook"): async def _inject_headers(request: Request) -> None: # noqa: RUF029 + if _url_origin(request.url) != target_origin: + return headers = _mcp_call_headers.get({}) for key, value in headers.items(): request.headers[key] = value diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 6273eb76e6..519d8e5db3 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -1161,6 +1161,43 @@ async def test_local_mcp_server_function_execution_error(): await func.invoke(param="test_value") +async def test_mcp_tool_reconnects_after_session_terminated_error(): + """Session termination errors should reconnect once and retry the tool call.""" + + class TestServer(MCPTool): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.connect_count = 0 + self.sessions: list[Any] = [] + + async def connect(self, *, reset: bool = False) -> None: + self.connect_count += 1 + self.session = Mock(spec=ClientSession) + self.sessions.append(self.session) + if self.connect_count == 1: + self.session.call_tool = AsyncMock( + side_effect=McpError(types.ErrorData(code=-32000, message="Session terminated")) + ) + else: + self.session.call_tool = AsyncMock( + return_value=types.CallToolResult(content=[types.TextContent(type="text", text="recovered")]) + ) + self.is_connected = True + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + await server.connect() + + result = await server.call_tool("test_tool", param="test_value") + + assert _mcp_result_to_text(result) == "recovered" + assert server.connect_count == 2 + assert server.sessions[0].call_tool.await_count == 1 + assert server.sessions[1].call_tool.await_count == 1 + + async def test_mcp_tool_call_tool_raises_on_is_error(): """Test that call_tool raises ToolExecutionException when MCP returns isError=True.""" @@ -3260,6 +3297,68 @@ async def test_load_prompts_pagination_with_duplicates(): assert [f.name for f in tool._functions] == ["prompt_1", "prompt_2"] +async def test_load_tools_concurrent_reload_does_not_duplicate_tools_and_preserves_meta(): + """Concurrent tool reloads should not duplicate functions or lose tools/list metadata.""" + tool = MCPTool(name="test_tool") + mock_session = AsyncMock() + tool.session = mock_session + tool.load_tools_flag = True + + page = Mock() + page.tools = [ + types.Tool( + name="tool_1", + description="First tool", + inputSchema={"type": "object", "properties": {"param": {"type": "string"}}}, + _meta={"echo": "tool_1"}, + ), + ] + page.nextCursor = None + + async def mock_list_tools(params: Any = None) -> Any: + assert params is None + await asyncio.sleep(0) + return page + + mock_session.list_tools = AsyncMock(side_effect=mock_list_tools) + + await asyncio.wait_for(asyncio.gather(tool.load_tools(), tool.load_tools()), timeout=1) + + assert mock_session.list_tools.call_count == 2 + assert [f.name for f in tool._functions] == ["tool_1"] + assert tool._tool_call_meta_by_name == {"tool_1": {"echo": "tool_1"}} + + +async def test_load_prompts_concurrent_reload_does_not_duplicate_prompts(): + """Concurrent prompt reloads should not duplicate functions.""" + tool = MCPTool(name="test_tool") + mock_session = AsyncMock() + tool.session = mock_session + tool.load_prompts_flag = True + + page = Mock() + page.prompts = [ + types.Prompt( + name="prompt_1", + description="First prompt", + arguments=[types.PromptArgument(name="arg1", description="Arg 1", required=True)], + ), + ] + page.nextCursor = None + + async def mock_list_prompts(params: Any = None) -> Any: + assert params is None + await asyncio.sleep(0) + return page + + mock_session.list_prompts = AsyncMock(side_effect=mock_list_prompts) + + await asyncio.wait_for(asyncio.gather(tool.load_prompts(), tool.load_prompts()), timeout=1) + + assert mock_session.list_prompts.call_count == 2 + assert [f.name for f in tool._functions] == ["prompt_1"] + + async def test_load_tools_pagination_exception_handling(): """Test that load_tools handles exceptions during pagination gracefully.""" from unittest.mock import AsyncMock @@ -3891,6 +3990,31 @@ async def test_mcp_tool_safe_close_handles_cancelled_error(): mock_exit_stack.aclose.assert_called_once() +async def test_mcp_tool_safe_close_handles_cleanup_exception_group(): + """Cleanup task groups should not hide the original connect failure.""" + import builtins + from contextlib import AsyncExitStack + + exception_group_type = getattr(builtins, "ExceptionGroup", None) + if exception_group_type is None: + pytest.skip("ExceptionGroup is not available on this Python version") + + tool = MCPStreamableHTTPTool( + name="test", + url="http://example.com/mcp", + load_tools=False, + load_prompts=False, + ) + + mock_exit_stack = AsyncMock(spec=AsyncExitStack) + mock_exit_stack.aclose = AsyncMock(side_effect=exception_group_type("cleanup failed", [RuntimeError("reader")])) + tool._exit_stack = mock_exit_stack + + await tool._safe_close_exit_stack() + + mock_exit_stack.aclose.assert_called_once() + + async def test_connect_sets_logging_level_when_logger_level_is_set(): """Test that connect() sets the MCP server logging level when the logger level is not NOTSET.""" @@ -4389,6 +4513,52 @@ async def test_mcp_tool_call_tool_forwards_tool_list_meta(): assert server.session.call_tool.call_args.kwargs["meta"] == tool_meta +async def test_mcp_tool_call_tool_user_meta_merges_with_tool_list_meta(): + """User-provided _meta should be sent as MCP request metadata, not tool arguments.""" + from opentelemetry import trace + + tool_meta = {"from_tool": "tool-value", "shared": "tool-value"} + user_meta = {"from_user": "user-value", "shared": "user-value"} + + class TestServer(MCPTool): + async def connect(self) -> None: + self.session = Mock(spec=ClientSession) + self.session.list_tools = AsyncMock( + return_value=types.ListToolsResult( + tools=[ + types.Tool( + name="test_tool", + description="Test tool", + inputSchema={"type": "object", "properties": {"param": {"type": "string"}}}, + _meta=tool_meta, + ) + ] + ) + ) + self.session.call_tool = AsyncMock( + return_value=types.CallToolResult(content=[types.TextContent(type="text", text="result")]) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + async with server: + await server.load_tools() + + with trace.use_span(trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT)): + await server.call_tool("test_tool", param="test_value", _meta=user_meta) + + call_kwargs = server.session.call_tool.call_args.kwargs + assert call_kwargs["arguments"] == {"param": "test_value"} + assert call_kwargs["meta"] == { + "from_tool": "tool-value", + "from_user": "user-value", + "shared": "user-value", + } + assert user_meta == {"from_user": "user-value", "shared": "user-value"} + + async def test_mcp_streamable_http_tool_hook_not_duplicated_on_repeated_get_mcp_client(): """Test that calling get_mcp_client multiple times does not accumulate duplicate hooks.""" tool = MCPStreamableHTTPTool( @@ -4641,6 +4811,42 @@ async def test_mcp_streamable_http_tool_header_provider_with_httpx_event_hook(): await tool._httpx_client.aclose() +async def test_mcp_streamable_http_tool_header_provider_skips_cross_origin_redirect(): + """The request hook must not re-add caller headers after a cross-origin redirect.""" + import httpx + + from agent_framework._mcp import _mcp_call_headers + + tool = MCPStreamableHTTPTool( + name="test", + url="http://example.com/mcp", + header_provider=lambda kw: {"Authorization": f"Bearer {kw.get('token', '')}"}, + ) + + try: + with patch("agent_framework._mcp.streamable_http_client"): + tool.get_mcp_client() + + assert tool._httpx_client is not None + hooks = tool._httpx_client.event_hooks.get("request", []) + assert len(hooks) == 1 + + token = _mcp_call_headers.set({"Authorization": "Bearer secret"}) + try: + same_origin = httpx.Request("POST", "http://example.com/redirected") + await hooks[0](same_origin) + assert same_origin.headers.get("Authorization") == "Bearer secret" + + cross_origin = httpx.Request("POST", "http://attacker.example/capture") + await hooks[0](cross_origin) + assert "Authorization" not in cross_origin.headers + finally: + _mcp_call_headers.reset(token) + finally: + if getattr(tool, "_httpx_client", None) is not None: + await tool._httpx_client.aclose() + + async def test_mcp_streamable_http_tool_header_provider_with_user_httpx_client(): """Test that header_provider works when the user provides their own httpx client.""" import httpx From dd9a4b6321f8922cb4505f84ed5c3e206dfbddb7 Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Fri, 29 May 2026 01:11:13 -0700 Subject: [PATCH 17/61] Python: [A2A] Set message_id on AgentResponseUpdate for message-bearing paths (#6163) Map A2A protocol message_id to AgentResponseUpdate.message_id in two paths where it was previously omitted, aligning with .NET behavior: 1. Standalone A2AMessage: set message_id=msg.message_id (matches .NET ConvertToAgentResponseUpdate(Message) which sets both ResponseId and MessageId to message.MessageId) 2. TaskStatusUpdateEvent (terminal/input_required): set message_id=message.message_id (matches .NET which sets MessageId=statusUpdateEvent.Status.Message?.MessageId) Fixes #5949 Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/a2a/agent_framework_a2a/_agent.py | 2 ++ python/packages/a2a/tests/test_a2a_agent.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index c620176779..33c8239a1c 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -492,6 +492,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): contents=contents, role="assistant" if msg.role == A2ARole.ROLE_AGENT else "user", response_id=msg.message_id or str(uuid.uuid4()), + message_id=msg.message_id, additional_properties={"a2a_metadata": metadata} if metadata else None, raw_representation=msg, ) @@ -732,6 +733,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): contents=contents, role="assistant" if message.role == A2ARole.ROLE_AGENT else "user", response_id=update_event.task_id, + message_id=message.message_id, additional_properties={"a2a_metadata": merged_metadata} if merged_metadata else None, raw_representation=update_event, ) diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py index 0ff5978c87..746a9b8664 100644 --- a/python/packages/a2a/tests/test_a2a_agent.py +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -420,6 +420,7 @@ async def test_run_streaming_with_message_response(a2a_agent: A2AAgent, mock_a2a assert content.text == "Streaming response from agent!" assert updates[0].response_id == "msg-stream-123" + assert updates[0].message_id == "msg-stream-123" assert mock_a2a_client.call_count == 1 @@ -1422,7 +1423,7 @@ async def test_streaming_status_update_event_yields_content( status=TaskStatus( state=TaskState.TASK_STATE_COMPLETED, message=A2AMessage( - message_id=str(uuid4()), + message_id="msg-status-done", role=A2ARole.ROLE_AGENT, parts=[Part(text="Done")], ), @@ -1437,6 +1438,7 @@ async def test_streaming_status_update_event_yields_content( assert len(updates) == 1 assert updates[0].text == "Done" assert updates[0].role == "assistant" + assert updates[0].message_id == "msg-status-done" assert updates[0].raw_representation == update_event @@ -1449,7 +1451,7 @@ async def test_streaming_input_required_emits_content(a2a_agent: A2AAgent, mock_ status=TaskStatus( state=TaskState.TASK_STATE_INPUT_REQUIRED, message=A2AMessage( - message_id=str(uuid4()), + message_id="msg-input-req", role=A2ARole.ROLE_AGENT, parts=[Part(text="What is your name?")], ), @@ -1463,6 +1465,7 @@ async def test_streaming_input_required_emits_content(a2a_agent: A2AAgent, mock_ assert len(updates) == 1 assert updates[0].text == "What is your name?" + assert updates[0].message_id == "msg-input-req" @mark.asyncio From 6510d6e3c89fc9dcf4c45596f24517b73196ff08 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 17:09:12 +0000 Subject: [PATCH 18/61] .NET: Quarantine flaky DevUI test (#6159) * Bump Microsoft.Extensions.AI packages to 10.6.0 * Align transitive package versions for Microsoft.Extensions.AI 10.6.0 * Initial plan * Temporarily skip flaky DevUI keyed/default workflow test * Revert Microsoft.Extensions.AI package bumps, keep only flaky test quarantine --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --- .../DevUIIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs index 029a650785..bbecb7fc01 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs @@ -172,7 +172,7 @@ public class DevUIIntegrationTests Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-three" && e.Type == "workflow"); } - [Fact] + [Fact(Skip = "Flaky in merge_group; see https://github.com/microsoft/agent-framework/issues/5845")] public async Task TestServerWithDevUI_ResolvesWorkflows_WithKeyedAndDefaultRegistrationAsync() { // Arrange From 11c8d89ab2853ce3c71f9f0b730ee73c8df4bf22 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Fri, 29 May 2026 12:07:48 -0700 Subject: [PATCH 19/61] .NET: Fix InvokeMcpTool approval path for declarative workflows (#6177) * Fix InvokeMcpTool approval path for declarative workflows * Added more test for coverage. --- .../ObjectModel/InvokeMcpToolExecutor.cs | 10 +- .../ObjectModel/InvokeMcpToolExecutorTest.cs | 147 ++++++++++++++++++ 2 files changed, 150 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs index 7796a6f409..c4c490551a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs @@ -75,18 +75,14 @@ internal sealed class InvokeMcpToolExecutor( if (requireApproval) { - // Create tool call content for approval request + // Create tool call content for approval request. + // Transport headers (e.g. Authorization) are intentionally excluded from the + // approval event: they must not cross into the externally-surfaced approval request. McpServerToolCallContent toolCall = new(this.Id, toolName, serverLabel ?? serverUrl) { Arguments = arguments }; - if (headers != null) - { - toolCall.AdditionalProperties ??= []; - toolCall.AdditionalProperties.Add(headers); - } - ToolApprovalRequestContent approvalRequest = new(this.Id, toolCall); ChatMessage requestMessage = new(ChatRole.Assistant, [approvalRequest]); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs index d047badaf7..b8d936dab9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; @@ -290,6 +292,151 @@ public sealed class InvokeMcpToolExecutorTest(ITestOutputHelper output) : Workfl await this.ExecuteTestAsync(model); } + [Fact] + public async Task InvokeMcpToolApprovalRequestExcludesTransportHeadersAsync() + { + // Arrange + this.State.InitializeSystem(); + InvokeMcpTool model = this.CreateModel( + displayName: nameof(InvokeMcpToolApprovalRequestExcludesTransportHeadersAsync), + serverUrl: TestServerUrl, + serverLabel: TestServerLabel, + toolName: TestToolName, + requireApproval: true, + headerKey: "Authorization", + headerValue: "Bearer super-secret-token"); + MockMcpToolProvider mockProvider = new(); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + ExternalInputRequest? capturedRequest = null; + + // Act + await this.ExecuteAsync( + [ + action, + new DelegateActionExecutor( + InvokeMcpToolExecutor.Steps.ExternalInput(action.Id), + this.State, + CaptureRequestAsync) + ], + isDiscrete: false); + + // Assert - the approval event must not carry any transport headers (e.g. Authorization). + Assert.NotNull(capturedRequest); + ToolApprovalRequestContent approvalRequest = + capturedRequest!.AgentResponse.Messages + .SelectMany(message => message.Contents) + .OfType() + .Single(); + + AdditionalPropertiesDictionary? additionalProperties = approvalRequest.ToolCall.AdditionalProperties; + Assert.True(additionalProperties is null || additionalProperties.Count == 0); + + // Defense in depth: the credential value must not appear anywhere in the serialized approval content. + string serializedApproval = System.Text.Json.JsonSerializer.Serialize(capturedRequest.AgentResponse); + Assert.DoesNotContain("super-secret-token", serializedApproval); + + ValueTask CaptureRequestAsync(IWorkflowContext context, ExternalInputRequest request, CancellationToken cancellationToken) + { + capturedRequest = request; + return default; + } + } + + [Fact] + public async Task InvokeMcpToolInvocationForwardsHeadersToTransportAsync() + { + // Arrange + this.State.InitializeSystem(); + const string HeaderKey = "Authorization"; + const string HeaderValue = "Bearer super-secret-token"; + InvokeMcpTool model = this.CreateModel( + displayName: nameof(InvokeMcpToolInvocationForwardsHeadersToTransportAsync), + serverUrl: TestServerUrl, + serverLabel: TestServerLabel, + toolName: TestToolName, + requireApproval: false, + headerKey: HeaderKey, + headerValue: HeaderValue); + + IDictionary? capturedHeaders = null; + Mock mockProvider = new(); + mockProvider + .Setup(provider => provider.InvokeToolAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny?>(), + It.IsAny?>(), + It.IsAny(), + It.IsAny())) + .Callback?, IDictionary?, string?, CancellationToken>( + (_, _, _, _, headers, _, _) => capturedHeaders = headers) + .ReturnsAsync(new McpServerToolResultContent("mock-call-id") { Outputs = [new TextContent("ok")] }); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + // Act + await this.ExecuteAsync(action, isDiscrete: false); + + // Assert - headers remain available to the actual transport invocation. + Assert.NotNull(capturedHeaders); + Assert.True(capturedHeaders!.TryGetValue(HeaderKey, out string? forwardedValue)); + Assert.Equal(HeaderValue, forwardedValue); + } + + [Fact] + public async Task InvokeMcpToolApprovedCaptureResponseForwardsHeadersToTransportAsync() + { + // Arrange - exercises the post-approval CaptureResponseAsync resume path to prove the + // fix did not regress header forwarding on the path that the vulnerability actually targets. + this.State.InitializeSystem(); + const string HeaderKey = "Authorization"; + const string HeaderValue = "Bearer super-secret-token"; + InvokeMcpTool model = this.CreateModel( + displayName: nameof(InvokeMcpToolApprovedCaptureResponseForwardsHeadersToTransportAsync), + serverUrl: TestServerUrl, + serverLabel: TestServerLabel, + toolName: TestToolName, + requireApproval: true, + headerKey: HeaderKey, + headerValue: HeaderValue); + + IDictionary? capturedHeaders = null; + Mock mockProvider = new(); + mockProvider + .Setup(provider => provider.InvokeToolAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny?>(), + It.IsAny?>(), + It.IsAny(), + It.IsAny())) + .Callback?, IDictionary?, string?, CancellationToken>( + (_, _, _, _, headers, _, _) => capturedHeaders = headers) + .ReturnsAsync(new McpServerToolResultContent("mock-call-id") { Outputs = [new TextContent("ok")] }); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + Mock mockContext = new(MockBehavior.Loose); + + // Build an approved response matching this action's request id. + McpServerToolCallContent toolCall = new(action.Id, TestToolName, TestServerLabel); + ToolApprovalRequestContent approvalRequest = new(action.Id, toolCall); + ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved: true); + ExternalInputResponse response = new(new ChatMessage(ChatRole.User, [approvalResponse])); + + // Act - call CaptureResponseAsync directly so the post-approval branch actually executes. + await action.CaptureResponseAsync(mockContext.Object, response, CancellationToken.None); + + // Assert - headers reach the transport invocation on the approved path. + Assert.NotNull(capturedHeaders); + Assert.True(capturedHeaders!.TryGetValue(HeaderKey, out string? forwardedValue)); + Assert.Equal(HeaderValue, forwardedValue); + } + [Fact] public async Task InvokeMcpToolExecuteWithEmptyHeaderValueAsync() { From fa2a6af4434fdfc310168d1b1f65cef2561b8eae Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 29 May 2026 20:42:07 +0100 Subject: [PATCH 20/61] Bump Azure.AI.AgentServer.* packages and align Azure.Core/System.ClientModel (#6178) * Bump Azure.AI.AgentServer.* package versions * Align Azure.Core/System.ClientModel to AgentServer transitive deps Bump Azure.Core 1.55->1.56 and System.ClientModel 1.11->1.12 to match Azure.AI.AgentServer.* requirements, and add explicit references in transitive-pinning-off Foundry consumers to avoid CS1705/MSB3277 version conflicts. --- dotnet/Directory.Packages.props | 10 +++++----- .../SessionFilesClient/SessionFilesClient.csproj | 2 ++ .../Using-Samples/SimpleAgent/SimpleAgent.csproj | 2 ++ .../Microsoft.Agents.AI.Foundry.UnitTests.csproj | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index efa9a70227..2ae94d5cd2 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -22,14 +22,14 @@ - - - + + + - + @@ -44,7 +44,7 @@ - + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj index 954036ba3b..995de85710 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj @@ -13,8 +13,10 @@ + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj index 3c739b96d0..5e6d95dbf6 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj @@ -13,8 +13,10 @@ + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 713c55aaa6..7862225653 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -8,6 +8,8 @@ + + From 07a1e834926b496b1d48eaed6e824740e1a9037a Mon Sep 17 00:00:00 2001 From: Hasan Ghomi Date: Sat, 30 May 2026 01:41:25 +0400 Subject: [PATCH 21/61] .NET: Forward Magentic participant replies to manager (#6156) MagenticOrchestrator.TakeTurnAsync dropped the `messages` parameter on subsequent turns, so participant replies never reached the manager's ChatHistory. The manager kept re-dispatching the same speaker every round until MaxRounds. Append the incoming messages to taskContext.ChatHistory before running the coordination round (matches Python's _handle_response). Adds RecordingReplayAgent + regression test that asserts the worker's reply reaches round-2's progress-ledger call. Co-authored-by: Jacob Alber --- .../Magentic/MagenticOrchestrator.cs | 7 ++- .../MagenticOrchestrationTests.cs | 58 +++++++++++++++++++ .../RecordingReplayAgent.cs | 32 ++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs index ff85a65c71..31e80d8725 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/Magentic/MagenticOrchestrator.cs @@ -195,7 +195,12 @@ internal class MagenticOrchestrator(AIAgent managerAgent, List team, Ta } else { - // Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan) + // Subsequent turns: agent returned control, go directly to coordination (progress ledger only, no replan). + // Capture the participant's reply into the manager-visible chat history so the progress ledger can see it. + if (messages is { Count: > 0 }) + { + this._taskContext.ChatHistory.AddRange(messages); + } await this.RunCoordinationRoundAsync(this._taskContext, context, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs index 937e047886..30be5bd873 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs @@ -361,6 +361,64 @@ public class MagenticOrchestrationTests runResult.Result![0].Text.Should().Contain("Multi-round task completed!"); } + [Fact] + public async Task RunCoordinationRound_Forwards_Participant_Reply_To_ManagerAsync() + { + // Regression: MagenticOrchestrator.TakeTurnAsync used to drop the `messages` + // parameter on subsequent turns, so participant replies never reached the + // manager's ChatHistory. The manager then re-dispatched the same speaker + // every round until MaxRounds. Assert that round-2's progress-ledger call + // actually sees the worker's reply in its input. + + const string TaskPrompt = "Echo back this exact magentic-regression-marker"; + + List factsResponse = CreatePlanResponse("Facts"); + List planResponse = CreatePlanResponse("Plan"); + List round1Ledger = CreateProgressLedgerResponse( + isRequestSatisfied: false, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: TaskPrompt); + List round2Ledger = CreateProgressLedgerResponse( + isRequestSatisfied: true, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: "Done"); + List finalAnswer = CreateFinalAnswerResponse("All good"); + + RecordingReplayAgent manager = new( + [factsResponse, planResponse, round1Ledger, round2Ledger, finalAnswer], + name: "Manager"); + TestEchoAgent worker = new(name: "Worker"); + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(worker) + .RequirePlanSignoff(false) + .Build(); + + WorkflowRunResult runResult = await RunMagenticWorkflowAsync( + workflow, + [new ChatMessage(ChatRole.User, TaskPrompt)]); + + runResult.Result.Should().NotBeNull(); + runResult.Result![0].Text.Should().Contain("All good"); + + // Calls in order: facts, plan, ledger1, ledger2, finalAnswer. + manager.RecordedInputs.Should().HaveCount(5); + + manager.RecordedInputs[3].Should().Contain( + m => m.Role == ChatRole.Assistant + && m.AuthorName == "Worker" + && m.Text.Contains(TaskPrompt), + "round-2 progress ledger must see the worker's reply; without it the manager loops to MaxRounds"); + + manager.RecordedInputs[4].Should().Contain( + m => m.Role == ChatRole.Assistant && m.AuthorName == "Worker", + "final-answer synthesis must see what participants actually said"); + } + [Fact] public async Task PlanReview_Revised_Triggers_ReplanAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs new file mode 100644 index 0000000000..ff4386a461 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/RecordingReplayAgent.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +/// +/// A that records the input messages it receives on each call. +/// Used by tests that need to assert what context the agent was actually handed. +/// +internal sealed class RecordingReplayAgent(List> messages, string? id = null, string? name = null) + : TestReplayAgent(messages, id, name) +{ + public List> RecordedInputs { get; } = []; + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + this.RecordedInputs.Add(messages.ToList()); + await foreach (AgentResponseUpdate update in base.RunCoreStreamingAsync(messages, session, options, cancellationToken)) + { + yield return update; + } + } +} From edcc786651ca3741ba005d7e5aef971ad24602cc Mon Sep 17 00:00:00 2001 From: Nicole Serafino <74378487+nicoleserafino@users.noreply.github.com> Date: Fri, 29 May 2026 16:41:40 -0500 Subject: [PATCH 22/61] .NET: Preserve and propagate CreatedAt through workflows (#3930) * Preserve per-message CreatedAt attribute if it's available * Add unit test --------- Co-authored-by: Sam Chang Co-authored-by: samchang-msft --- .../AgentResponse.cs | 2 +- .../AgentResponseTests.cs | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs index 081e054efc..13680e8bcb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs @@ -297,7 +297,7 @@ public class AgentResponse AgentId = this.AgentId, ResponseId = this.ResponseId, MessageId = message.MessageId, - CreatedAt = this.CreatedAt, + CreatedAt = message.CreatedAt ?? this.CreatedAt, }; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentResponseTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentResponseTests.cs index 6d24c821bc..7a8203dea7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentResponseTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentResponseTests.cs @@ -222,6 +222,42 @@ public class AgentResponseTests Assert.Equal(100, usageContent.Details.TotalTokenCount); } + [Fact] + public void ToAgentResponseUpdatesPropagatesCreatedAt() + { + // Sets different CreatedAt values on the AgentResponse and the ChatMessage to verify that the ChatMessage.CreatedAt is the one that gets propagated to the AgentResponseUpdate + AgentResponse response = new(new ChatMessage(new ChatRole("customRole"), "Text") { MessageId = "someMessage", CreatedAt = new DateTimeOffset(2024, 11, 11, 9, 20, 0, TimeSpan.Zero) }) + { + AgentId = "agentId", + ResponseId = "12345", + CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), + AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 }, + Usage = new UsageDetails + { + TotalTokenCount = 100 + }, + }; + + AgentResponseUpdate[] updates = response.ToAgentResponseUpdates(); + Assert.NotNull(updates); + Assert.Equal(2, updates.Length); + + AgentResponseUpdate update0 = updates[0]; + Assert.Equal("agentId", update0.AgentId); + Assert.Equal("12345", update0.ResponseId); + Assert.Equal("someMessage", update0.MessageId); + Assert.Equal(new DateTimeOffset(2024, 11, 11, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt); + Assert.Equal("customRole", update0.Role?.Value); + Assert.Equal("Text", update0.Text); + + AgentResponseUpdate update1 = updates[1]; + Assert.Equal("value1", update1.AdditionalProperties?["key1"]); + Assert.Equal(42, update1.AdditionalProperties?["key2"]); + Assert.IsType(update1.Contents[0]); + UsageContent usageContent = (UsageContent)update1.Contents[0]; + Assert.Equal(100, usageContent.Details.TotalTokenCount); + } + [Fact] public void ParseAsStructuredOutputWithJSOSuccess() { From 5affc9c33392b2636e5a7b860a3d9628f4cd6e1e Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:09:11 -0700 Subject: [PATCH 23/61] Python: Reorganize A2A samples and use package A2AExecutor (#6165) * Reorganize A2A samples: client demos in 02-agents, use package A2AExecutor - Move client samples (agent_with_a2a, a2a_agent_as_function_tools) to samples/02-agents/a2a/ - Add new concept samples: polling, stream reconnection, protocol selection - Replace sample agent_executor.py with package-level A2AExecutor (stream=True) - Update 04-hosting/a2a to focus on server-side, point to 02-agents for clients - Add README.md for the new 02-agents/a2a/ sample collection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix streaming artifact coalescing and address PR review feedback A2AExecutor fix: - Generate a stable artifact_id per stream in _run_stream so all streaming chunks share the same ID, enabling proper append=True coalescing per the A2A spec (TaskArtifactUpdateEvent with same artifactId). - Previously, item.message_id was None for OpenAI/Foundry streaming updates, causing the SDK to generate a new random UUID per token (100+ separate artifacts instead of 1 appended artifact). Sample improvements: - Replace join workaround with response.text now that coalescing works - Add background=True to stream reconnection resume call (required for continuation token emission on in-progress tasks) - Fix type ignore specificity in polling sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../a2a/agent_framework_a2a/_a2a_executor.py | 35 ++++- python/samples/02-agents/a2a/README.md | 54 ++++++++ .../a2a/a2a_agent_as_function_tools.py | 2 +- python/samples/02-agents/a2a/a2a_polling.py | 96 ++++++++++++++ .../02-agents/a2a/a2a_protocol_selection.py | 84 ++++++++++++ .../02-agents/a2a/a2a_stream_reconnection.py | 124 ++++++++++++++++++ .../a2a/agent_with_a2a.py | 17 +-- python/samples/04-hosting/a2a/README.md | 48 ++----- python/samples/04-hosting/a2a/a2a_server.py | 4 +- .../samples/04-hosting/a2a/agent_executor.py | 83 ------------ 10 files changed, 409 insertions(+), 138 deletions(-) create mode 100644 python/samples/02-agents/a2a/README.md rename python/samples/{04-hosting => 02-agents}/a2a/a2a_agent_as_function_tools.py (99%) create mode 100644 python/samples/02-agents/a2a/a2a_polling.py create mode 100644 python/samples/02-agents/a2a/a2a_protocol_selection.py create mode 100644 python/samples/02-agents/a2a/a2a_stream_reconnection.py rename python/samples/{04-hosting => 02-agents}/a2a/agent_with_a2a.py (88%) delete mode 100644 python/samples/04-hosting/a2a/agent_executor.py diff --git a/python/packages/a2a/agent_framework_a2a/_a2a_executor.py b/python/packages/a2a/agent_framework_a2a/_a2a_executor.py index 949ce10167..3685e82742 100644 --- a/python/packages/a2a/agent_framework_a2a/_a2a_executor.py +++ b/python/packages/a2a/agent_framework_a2a/_a2a_executor.py @@ -2,6 +2,7 @@ import base64 import logging +import uuid from asyncio import CancelledError from collections.abc import Mapping from functools import partial @@ -181,9 +182,18 @@ class A2AExecutor(AgentExecutor): """Run the agent in streaming mode and publish updates to the task updater.""" response_stream = self._agent.run(query, session=session, stream=True, **self._run_kwargs) streamed_artifact_ids: set[str] = set() + # Generate a stable artifact ID for the entire stream so all chunks share the same ID. + # This ensures clients can coalesce streaming tokens into a single artifact/message + # per the A2A spec (TaskArtifactUpdateEvent with append=True on same artifactId). + default_artifact_id = str(uuid.uuid4()) await ( response_stream.with_transform_hook( - partial(self.handle_events, updater=updater, streamed_artifact_ids=streamed_artifact_ids) + partial( + self.handle_events, + updater=updater, + streamed_artifact_ids=streamed_artifact_ids, + default_artifact_id=default_artifact_id, + ) ) ).get_final_response() @@ -199,7 +209,11 @@ class A2AExecutor(AgentExecutor): await self.handle_events(message, updater) async def handle_events( - self, item: Message | AgentResponseUpdate, updater: TaskUpdater, streamed_artifact_ids: set[str] | None = None + self, + item: Message | AgentResponseUpdate, + updater: TaskUpdater, + streamed_artifact_ids: set[str] | None = None, + default_artifact_id: str | None = None, ) -> None: """Convert agent response items (Messages or Updates) to A2A protocol events. @@ -213,7 +227,10 @@ class A2AExecutor(AgentExecutor): item: The agent response item (Message or AgentResponseUpdate) to process. updater: The task updater to publish events to. streamed_artifact_ids: A set of artifact IDs that have already been streamed. - Used to prevent duplicate updates for the same artifact. + Used to track which artifacts need append=True on subsequent chunks. + default_artifact_id: A stable artifact ID to use when the item does not provide one. + This ensures all streaming chunks for a single response share the same artifact ID, + allowing clients to coalesce them into a single message. Example: .. code-block:: python @@ -224,6 +241,7 @@ class A2AExecutor(AgentExecutor): item: Message | AgentResponseUpdate, updater: TaskUpdater, streamed_artifact_ids: set[str] | None = None, + default_artifact_id: str | None = None, ) -> None: # Custom logic to transform item contents if item.role == "assistant" and item.contents: @@ -260,19 +278,22 @@ class A2AExecutor(AgentExecutor): if parts: if isinstance(item, AgentResponseUpdate): + # Resolve artifact ID: use item's message_id if available, otherwise fall back + # to the stable default_artifact_id so all streaming chunks share the same ID. + artifact_id = item.message_id or default_artifact_id # For streaming updates, we send TaskArtifactUpdateEvent via add_artifact await updater.add_artifact( parts=parts, - artifact_id=item.message_id, + artifact_id=artifact_id, metadata=metadata, append=( True - if streamed_artifact_ids is not None and item.message_id in (streamed_artifact_ids or set()) + if streamed_artifact_ids is not None and artifact_id in streamed_artifact_ids else None ), ) - if item.message_id and streamed_artifact_ids is not None: - streamed_artifact_ids.add(item.message_id) + if artifact_id and streamed_artifact_ids is not None: + streamed_artifact_ids.add(artifact_id) else: # For final messages, we send TaskStatusUpdateEvent with 'working' state await updater.update_status( diff --git a/python/samples/02-agents/a2a/README.md b/python/samples/02-agents/a2a/README.md new file mode 100644 index 0000000000..e4247a95e9 --- /dev/null +++ b/python/samples/02-agents/a2a/README.md @@ -0,0 +1,54 @@ +# A2A Client Samples + +These samples demonstrate how to **consume** remote A2A-compliant agents using the Agent Framework's `A2AAgent` class. + +For hosting your own agents as A2A servers, see [`samples/04-hosting/a2a/`](../../04-hosting/a2a/). + +## Samples + +| Sample | Concept | +|--------|---------| +| [`agent_with_a2a.py`](agent_with_a2a.py) | Basic consumption — non-streaming and streaming | +| [`a2a_agent_as_function_tools.py`](a2a_agent_as_function_tools.py) | Expose A2A skills as function tools for a host agent | +| [`a2a_polling.py`](a2a_polling.py) | Poll a long-running task with continuation tokens | +| [`a2a_stream_reconnection.py`](a2a_stream_reconnection.py) | Resume an interrupted stream via continuation token | +| [`a2a_protocol_selection.py`](a2a_protocol_selection.py) | Configure preferred protocol bindings (JSONRPC, GRPC, HTTP+JSON) | + +## Prerequisites + +- A running A2A-compliant agent server (see `samples/04-hosting/a2a/` to start one) +- Set `A2A_AGENT_HOST` environment variable to the server URL +- For `a2a_agent_as_function_tools.py`: also set `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL` + +## Running + +```bash +cd python/samples/02-agents/a2a + +# Start an A2A server in another terminal first: +# cd python/samples/04-hosting/a2a && uv run python a2a_server.py + +export A2A_AGENT_HOST="http://localhost:5001/" +uv run python agent_with_a2a.py +``` + +## Key APIs + +```python +from agent_framework.a2a import A2AAgent + +# Connect to a remote agent +async with A2AAgent(url="http://localhost:5001/", agent_card=card) as agent: + # Non-streaming + response = await agent.run("Hello") + + # Streaming + stream = agent.run("Hello", stream=True) + async for update in stream: + print(update.text) + + # Background + polling + response = await agent.run("Long task", background=True) + while response.continuation_token: + response = await agent.poll_task(response.continuation_token) +``` diff --git a/python/samples/04-hosting/a2a/a2a_agent_as_function_tools.py b/python/samples/02-agents/a2a/a2a_agent_as_function_tools.py similarity index 99% rename from python/samples/04-hosting/a2a/a2a_agent_as_function_tools.py rename to python/samples/02-agents/a2a/a2a_agent_as_function_tools.py index ca753f2d42..c441fe77e1 100644 --- a/python/samples/04-hosting/a2a/a2a_agent_as_function_tools.py +++ b/python/samples/02-agents/a2a/a2a_agent_as_function_tools.py @@ -33,7 +33,7 @@ Prerequisites: - Set FOUNDRY_MODEL to the model deployment name (e.g. gpt-4o) To run this sample: - cd python/samples/04-hosting/a2a + cd python/samples/02-agents/a2a uv run python a2a_agent_as_function_tools.py """ diff --git a/python/samples/02-agents/a2a/a2a_polling.py b/python/samples/02-agents/a2a/a2a_polling.py new file mode 100644 index 0000000000..cccce69d72 --- /dev/null +++ b/python/samples/02-agents/a2a/a2a_polling.py @@ -0,0 +1,96 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +import httpx +from a2a.client import A2ACardResolver +from agent_framework.a2a import A2AAgent +from dotenv import load_dotenv + +load_dotenv() + +""" +A2A Polling for Task Completion + +This sample demonstrates how to poll a long-running A2A task for completion +using continuation tokens. When `background=True`, the agent returns immediately +with a continuation token that you can use to check progress later. + +Key concepts demonstrated: +- Starting a background A2A task with `background=True` +- Receiving a continuation token for in-progress tasks +- Polling with `poll_task()` until the task reaches a terminal state + +This is the A2A equivalent of the .NET A2AAgent_PollingForTaskCompletion sample. + +Prerequisites: +- Set A2A_AGENT_HOST to the URL of a running A2A server + +To run this sample: + cd python/samples/02-agents/a2a + uv run python a2a_polling.py +""" + + +async def main() -> None: + """Demonstrates polling a long-running A2A task for completion.""" + a2a_agent_host = os.getenv("A2A_AGENT_HOST") + if not a2a_agent_host: + raise ValueError("A2A_AGENT_HOST environment variable is not set") + + # 1. Resolve agent card and create agent. + async with httpx.AsyncClient(timeout=60.0) as http_client: + resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host) + agent_card = await resolver.get_agent_card() + + async with A2AAgent( + name=agent_card.name, + agent_card=agent_card, + url=a2a_agent_host, + ) as agent: + # 2. Start a background task — the agent returns immediately. + print("Starting background task...") + response = await agent.run( + "Write a detailed research report on quantum computing advances in 2025", + background=True, + ) + + # 3. Check if we got a continuation token (task still in progress). + if response.continuation_token is None: + # Task completed immediately — no polling needed. + print("Task completed immediately:") + print(f" {response.text}") + return + + # 4. Poll until the task completes. + token = response.continuation_token + poll_count = 0 + while token is not None: + poll_count += 1 + print(f" Poll #{poll_count} — task still in progress, waiting 2s...") + await asyncio.sleep(2) + + response = await agent.poll_task(token) # type: ignore[arg-type] + token = response.continuation_token + + # 5. Task is done — print the final response. + print(f"\nTask completed after {poll_count} poll(s):") + print(f" {response.text[:200]}...") + + +if __name__ == "__main__": + asyncio.run(main()) + + +""" +Sample output: + +Starting background task... + Poll #1 — task still in progress, waiting 2s... + Poll #2 — task still in progress, waiting 2s... + Poll #3 — task still in progress, waiting 2s... + +Task completed after 3 poll(s): + Quantum computing has seen remarkable progress in 2025, with breakthroughs in... +""" diff --git a/python/samples/02-agents/a2a/a2a_protocol_selection.py b/python/samples/02-agents/a2a/a2a_protocol_selection.py new file mode 100644 index 0000000000..c005d7365b --- /dev/null +++ b/python/samples/02-agents/a2a/a2a_protocol_selection.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +import httpx +from a2a.client import A2ACardResolver +from agent_framework.a2a import A2AAgent +from dotenv import load_dotenv + +load_dotenv() + +""" +A2A Protocol Selection + +This sample demonstrates how to configure which protocol binding the A2A client +uses when connecting to a remote agent. The A2A specification defines three +standard bindings: JSONRPC, GRPC, and HTTP+JSON. Agents declare their supported +bindings in their AgentCard, and clients can express a preference. + +Key concepts demonstrated: +- Configuring `supported_protocol_bindings` on A2AAgent +- The client selects a binding that matches the remote agent's capabilities +- Fallback behavior when preferred binding is unavailable + +This is the A2A equivalent of the .NET A2AAgent_ProtocolSelection sample. + +Prerequisites: +- Set A2A_AGENT_HOST to the URL of a running A2A server + +To run this sample: + cd python/samples/02-agents/a2a + uv run python a2a_protocol_selection.py +""" + + +async def main() -> None: + """Demonstrates configuring A2A protocol binding preferences.""" + a2a_agent_host = os.getenv("A2A_AGENT_HOST") + if not a2a_agent_host: + raise ValueError("A2A_AGENT_HOST environment variable is not set") + + # 1. Resolve agent card to see what bindings are available. + async with httpx.AsyncClient(timeout=60.0) as http_client: + resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host) + agent_card = await resolver.get_agent_card() + + print(f"Agent: {agent_card.name}") + print("Supported interfaces:") + for interface in agent_card.supported_interfaces: + print(f" - {interface.protocol_binding} @ {interface.url}") + + # 2. Create agent with explicit protocol binding preference. + # The list is ordered by preference — the SDK will select the first + # binding that matches a supported interface on the agent card. + # + # This matters when a server exposes multiple interfaces (e.g. JSONRPC + # on / and HTTP+JSON on /api/). If only one binding is available, the + # client uses it regardless of your preference list. + async with A2AAgent( + name=agent_card.name, + agent_card=agent_card, + url=a2a_agent_host, + supported_protocol_bindings=["HTTP+JSON", "JSONRPC"], + ) as agent: + print("\nConfigured bindings: ['HTTP+JSON', 'JSONRPC']") + response = await agent.run("Tell me a short joke") + print(f"Response: {response.text}") + + +if __name__ == "__main__": + asyncio.run(main()) + + +""" +Sample output: + +Agent: PolicyAgent +Supported interfaces: + - JSONRPC @ http://localhost:5001/ + +Configured bindings: ['HTTP+JSON', 'JSONRPC'] +Response: Here's a short joke for you... +""" diff --git a/python/samples/02-agents/a2a/a2a_stream_reconnection.py b/python/samples/02-agents/a2a/a2a_stream_reconnection.py new file mode 100644 index 0000000000..c9fd0a8891 --- /dev/null +++ b/python/samples/02-agents/a2a/a2a_stream_reconnection.py @@ -0,0 +1,124 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +import httpx +from a2a.client import A2ACardResolver +from agent_framework.a2a import A2AAgent +from dotenv import load_dotenv + +load_dotenv() + +""" +A2A Stream Reconnection + +This sample demonstrates how to reconnect to an interrupted A2A stream +using a continuation token. When streaming a long-running task, you can +capture the continuation token from any update and use it to resume the +stream later if the connection is lost. + +Key concepts demonstrated: +- Streaming an A2A response with `stream=True` +- Capturing continuation tokens from in-progress updates +- Simulating a stream interruption (break) +- Resuming the stream with `run(continuation_token=..., stream=True)` + +This is the A2A equivalent of the .NET A2AAgent_StreamReconnection sample. + +Prerequisites: +- Set A2A_AGENT_HOST to the URL of a running A2A server + +To run this sample: + cd python/samples/02-agents/a2a + uv run python a2a_stream_reconnection.py +""" + + +async def main() -> None: + """Demonstrates reconnecting to an interrupted A2A stream.""" + a2a_agent_host = os.getenv("A2A_AGENT_HOST") + if not a2a_agent_host: + raise ValueError("A2A_AGENT_HOST environment variable is not set") + + # 1. Resolve agent card and create agent. + async with httpx.AsyncClient(timeout=60.0) as http_client: + resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host) + agent_card = await resolver.get_agent_card() + + async with A2AAgent( + name=agent_card.name, + agent_card=agent_card, + url=a2a_agent_host, + ) as agent: + # 2. Start a streaming background task. + print("Starting streaming task...") + stream = agent.run( + "Write a long essay about the history of artificial intelligence", + stream=True, + background=True, + ) + + # 3. Read a few updates, capture the continuation token, then "disconnect". + saved_token = None + update_count = 0 + async for update in stream: + update_count += 1 + if update.continuation_token: + saved_token = update.continuation_token + for content in update.contents: + if content.text: + print(content.text, end="", flush=True) + + # Simulate a disconnect after receiving 3 updates. + if update_count >= 3: + print("\n\n--- Connection interrupted! ---\n") + break + + if saved_token is None: + print("No continuation token received — task may have completed before interruption.") + return + + # 4. Reconnect using the saved continuation token. + # background=True is required so that in-progress task updates + # surface continuation tokens (matching the A2AAgent contract). + print(f"Reconnecting with continuation token (task_id={saved_token['task_id']})...") + resumed_stream = agent.run( + continuation_token=saved_token, + stream=True, + background=True, + ) + + # 5. Continue receiving updates from where we left off. + async for update in resumed_stream: + update_count += 1 + for content in update.contents: + if content.text: + print(content.text, end="", flush=True) + print() # newline after streaming completes + + response = await resumed_stream.get_final_response() + print(f"\nStream completed. Total updates: {update_count}") + print(f"Final response: {len(response.messages)} message(s)") + + +if __name__ == "__main__": + asyncio.run(main()) + + +""" +Sample output: + +Starting streaming task... +Policy: + +--- Connection interrupted! --- + +Reconnecting with continuation token (task_id=task-abc123)... + Short Shipment Dispute Handling Policy V2.1 + +Summary: "For short shipments reported by customers, first verify internal..." + +Stream completed. Total updates: 106 +Final response: 103 message(s) +""" diff --git a/python/samples/04-hosting/a2a/agent_with_a2a.py b/python/samples/02-agents/a2a/agent_with_a2a.py similarity index 88% rename from python/samples/04-hosting/a2a/agent_with_a2a.py rename to python/samples/02-agents/a2a/agent_with_a2a.py index 58415b038c..c35fbb234c 100644 --- a/python/samples/04-hosting/a2a/agent_with_a2a.py +++ b/python/samples/02-agents/a2a/agent_with_a2a.py @@ -22,7 +22,7 @@ technologies to communicate seamlessly. By default the A2AAgent waits for the remote agent to finish before returning (background=False). This means long-running A2A tasks are handled transparently — the caller simply awaits the result. For advanced scenarios where you need to poll or resubscribe to in-progress tasks, see the -background_responses sample: samples/concepts/background_responses.py +a2a_polling and a2a_stream_reconnection samples in this folder. For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/ @@ -70,9 +70,7 @@ async def main(): print("\n--- Non-streaming response ---") response = await agent.run("What are your capabilities?") - print("Agent Response:") - for message in response.messages: - print(f" {message.text}") + print(f"Agent Response:\n {response.text}") # 5. Stream a response — the natural model for A2A. # Updates arrive as Server-Sent Events, letting you observe @@ -82,12 +80,11 @@ async def main(): async for update in stream: for content in update.contents: if content.text: - print(f" {content.text}") + print(content.text, end="", flush=True) + print() # newline after streaming completes response = await stream.get_final_response() - print(f"\nFinal response ({len(response.messages)} message(s)):") - for message in response.messages: - print(f" {message.text}") + print(f"\nFinal response:\n {response.text}") if __name__ == "__main__": @@ -105,8 +102,8 @@ Agent Response: I can help with code generation, analysis, and general Q&A. --- Streaming response --- - I am an AI assistant built to help with various tasks. +I am an AI assistant built to help with various tasks. -Final response (1 message(s)): +Final response: I am an AI assistant built to help with various tasks. """ diff --git a/python/samples/04-hosting/a2a/README.md b/python/samples/04-hosting/a2a/README.md index bc76d124f6..803bab3f9e 100644 --- a/python/samples/04-hosting/a2a/README.md +++ b/python/samples/04-hosting/a2a/README.md @@ -1,39 +1,30 @@ -# A2A Agent Examples +# A2A Server Hosting Examples -This sample demonstrates how to host and consume agents using the [A2A (Agent2Agent) protocol](https://a2a-protocol.org/latest/) with the `agent_framework` package. There are three runnable entry points: +This sample demonstrates how to **host** Agent Framework agents as A2A-compliant servers using the [A2A (Agent2Agent) protocol](https://a2a-protocol.org/latest/). + +> **Looking for client samples?** See [`samples/02-agents/a2a/`](../../02-agents/a2a/) for consuming remote A2A agents. + +## Server Samples | Run this file | To... | |---------------|-------| -| **[`a2a_server.py`](a2a_server.py)** | Host an Agent Framework agent as an A2A-compliant server. | -| **[`agent_with_a2a.py`](agent_with_a2a.py)** | Connect to an A2A server and send requests (non-streaming and streaming). | -| **[`a2a_agent_as_function_tools.py`](a2a_agent_as_function_tools.py)** | Convert A2A agent skills into function tools for a host agent. | +| **[`a2a_server.py`](a2a_server.py)** | Host an Agent Framework agent as an A2A-compliant server (multi-agent). | +| **[`agent_framework_to_a2a.py`](agent_framework_to_a2a.py)** | Minimal example: expose a single agent as an A2A server. | -The remaining files are supporting modules used by the server: +## Supporting Modules | File | Description | |------|-------------| -| [`agent_framework_to_a2a.py`](agent_framework_to_a2a.py) | Exposes an agent_framework agent as an A2A-compliant server. Demonstrates how to wrap an agent_framework agent and expose it as an A2A service that other A2A clients can discover and communicate with. | | [`agent_definitions.py`](agent_definitions.py) | Agent and AgentCard factory definitions for invoice, policy, and logistics agents. | -| [`agent_executor.py`](agent_executor.py) | Bridges the a2a-sdk `AgentExecutor` interface to Agent Framework agents. | | [`invoice_data.py`](invoice_data.py) | Mock invoice data and tool functions for the invoice agent. | | [`a2a_server.http`](a2a_server.http) | REST Client requests for testing the server directly from VS Code. | ## Environment Variables -Make sure to set the following environment variables before running the examples: - ### Required (Server) - `FOUNDRY_PROJECT_ENDPOINT` — Your Azure AI Foundry project endpoint - `FOUNDRY_MODEL` — Model deployment name (e.g. `gpt-4o`) -### Required (Client) -- `A2A_AGENT_HOST` — URL of the A2A server (e.g. `http://localhost:5001/`) - -### Required (Function Tools Sample) -- `A2A_AGENT_HOST` — URL of the A2A server (e.g. `http://localhost:5000/`) -- `FOUNDRY_PROJECT_ENDPOINT` — Your Azure AI Foundry project endpoint -- `FOUNDRY_MODEL` — Model deployment name (e.g. `gpt-4o`) - ## Quick Start All commands below should be run from this directory: @@ -67,7 +58,7 @@ uv run python a2a_server.py --agent-type policy ### 1. Start the A2A Server -> **Note (Option A — pip users):** Replace `uv run python` with `python` in all `uv run` commands below (e.g. `python a2a_server.py ...`). `uv` is not required once the virtual environment is activated. +> **Note (Option A — pip users):** Replace `uv run python` with `python` in all `uv run` commands below. `uv` is not required once the virtual environment is activated. Pick an agent type and start the server (each in its own terminal): @@ -79,25 +70,12 @@ uv run python a2a_server.py --agent-type logistics --port 5002 You can run one agent or all three — each listens on its own port. -### 2. Run the A2A Client +### 2. Run a Client -In a separate terminal (from the same directory), point the client at a running server: +Once a server is running, use any of the client samples in [`samples/02-agents/a2a/`](../../02-agents/a2a/): ```powershell +cd python/samples/02-agents/a2a $env:A2A_AGENT_HOST = "http://localhost:5001/" uv run python agent_with_a2a.py - -# A2A server exposing an agent_framework agent -uv run python agent_framework_to_a2a.py -``` - -### 3. Run the Function Tools Sample - -This sample resolves the remote agent's skills and registers each one as a function tool -on a host Foundry-backed agent. The host agent then autonomously selects the right skill -to handle the user's request. - -```powershell -$env:A2A_AGENT_HOST = "http://localhost:5000/" -uv run python a2a_agent_as_function_tools.py ``` diff --git a/python/samples/04-hosting/a2a/a2a_server.py b/python/samples/04-hosting/a2a/a2a_server.py index 8eaf3a7363..185d9da048 100644 --- a/python/samples/04-hosting/a2a/a2a_server.py +++ b/python/samples/04-hosting/a2a/a2a_server.py @@ -9,7 +9,7 @@ from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes from a2a.server.tasks import InMemoryTaskStore from agent_definitions import AGENT_CARD_FACTORIES, AGENT_FACTORIES -from agent_executor import AgentFrameworkExecutor +from agent_framework.a2a import A2AExecutor from agent_framework.foundry import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -92,7 +92,7 @@ def main() -> None: # Build the A2A server components url = f"http://{args.host}:{args.port}/" agent_card = AGENT_CARD_FACTORIES[args.agent_type](url) - executor = AgentFrameworkExecutor(agent) + executor = A2AExecutor(agent, stream=True) task_store = InMemoryTaskStore() request_handler = DefaultRequestHandler( agent_executor=executor, diff --git a/python/samples/04-hosting/a2a/agent_executor.py b/python/samples/04-hosting/a2a/agent_executor.py deleted file mode 100644 index 3dcefff09f..0000000000 --- a/python/samples/04-hosting/a2a/agent_executor.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -"""AgentExecutor bridge between the a2a-sdk server and Agent Framework agents. - -Implements the a2a-sdk ``AgentExecutor`` interface so that incoming A2A -requests are forwarded to an Agent Framework agent and the response is -published back through the a2a-sdk event queue. -""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -from a2a.helpers import new_task_from_user_message -from a2a.server.agent_execution.agent_executor import AgentExecutor -from a2a.server.tasks import TaskUpdater -from a2a.types import Part, TaskState - -if TYPE_CHECKING: - from a2a.server.agent_execution.context import RequestContext - from a2a.server.events.event_queue import EventQueue - from agent_framework import Agent - - -class AgentFrameworkExecutor(AgentExecutor): - """Bridges A2A protocol requests to an Agent Framework agent. - - For each incoming ``execute`` call the executor: - 1. Extracts the user's text from the A2A ``RequestContext``. - 2. Runs the Agent Framework agent (non-streaming). - 3. Publishes the result as an A2A ``Message`` to the ``EventQueue``. - """ - - def __init__(self, agent: Agent) -> None: - self.agent = agent - - async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: - """Run the agent and publish the response.""" - user_text = context.get_user_input() - if not user_text: - user_text = "Hello" - - # v1.0 requires a Task object in the queue before any TaskStatusUpdateEvent - task = context.current_task - if not task and context.message: - task = new_task_from_user_message(context.message) - await event_queue.enqueue_event(task) - - task_id = task.id if task else context.task_id - updater = TaskUpdater(event_queue, task_id, context.context_id) - - # Signal that the agent is working - await updater.start_work() - - try: - response = await self.agent.run(user_text) - - # Build response text from agent messages - response_parts: list[Part] = [] - for msg in response.messages: - if msg.text: - response_parts.append(Part(text=msg.text)) - - if not response_parts: - response_parts.append(Part(text=str(response))) - - # Publish the agent's response and mark as completed - await updater.complete( - message=updater.new_agent_message(response_parts), - ) - except asyncio.CancelledError: - raise - except Exception as e: - await updater.update_status( - state=TaskState.TASK_STATE_FAILED, - message=updater.new_agent_message([Part(text=f"Agent error: {e}")]), - ) - - async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - """Handle cancellation by publishing a canceled status.""" - updater = TaskUpdater(event_queue, context.task_id, context.context_id) - await updater.update_status(state=TaskState.TASK_STATE_CANCELED) From 8b0db48d331354867c1066c32e05e822b17b6e45 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:12:31 +0900 Subject: [PATCH 24/61] Add community PR limit workflow (#6229) * Add community PR limit workflow * Address PR limit workflow review feedback --- .github/scripts/pr_limit_moderation.js | 163 ++++++++++++ .github/tests/test_pr_limit_moderation.js | 286 ++++++++++++++++++++++ .github/workflows/limit-community-prs.yml | 83 +++++++ 3 files changed, 532 insertions(+) create mode 100644 .github/scripts/pr_limit_moderation.js create mode 100644 .github/tests/test_pr_limit_moderation.js create mode 100644 .github/workflows/limit-community-prs.yml diff --git a/.github/scripts/pr_limit_moderation.js b/.github/scripts/pr_limit_moderation.js new file mode 100644 index 0000000000..489a06ea78 --- /dev/null +++ b/.github/scripts/pr_limit_moderation.js @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft. All rights reserved. + +function getPullRequest(context) { + const pullRequest = context.payload.pull_request; + if (!pullRequest?.number || !pullRequest.user?.login) { + throw new Error('This script must be run from a pull_request_target event.'); + } + + return { + author: pullRequest.user.login, + labels: pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [], + number: pullRequest.number, + }; +} + +async function ensureLabel({ github, owner, repo, labelName }) { + try { + await github.rest.issues.getLabel({ + owner, + repo, + name: labelName, + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + + try { + await github.rest.issues.createLabel({ + owner, + repo, + name: labelName, + color: 'd93f0b', + description: 'Community author has exceeded the open pull request limit.', + }); + } catch (createError) { + if (createError.status !== 422) { + throw createError; + } + } + } +} + +function hasLabel(labels, labelName) { + if (!labelName) { + return false; + } + + return labels.some((label) => label.toLowerCase() === labelName.toLowerCase()); +} + +function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) { + return [ + `Thank you for your contribution, @${author}.`, + '', + `To keep the review queue manageable, we currently limit community contributors to ${maxOpenPrs} ` + + `open pull requests at a time. This PR would put you at ${openPrCount} open pull requests, ` + + 'so we are closing it automatically.', + '', + 'Please focus on getting your existing PRs reviewed, merged, or closed before opening another one. ' + + `If a maintainer asked you to open this PR, they can apply the \`${exemptLabelName}\` label and reopen it.`, + ].join('\n'); +} + +async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber }) { + const query = `repo:${owner}/${repo} is:pr is:open author:${author}`; + const response = await github.rest.search.issuesAndPullRequests({ + q: query, + per_page: 100, + }); + + const indexedPrNumbers = response.data.items.map((item) => item.number); + const currentPrIsIndexed = indexedPrNumbers.includes(pullRequestNumber); + if (currentPrIsIndexed || response.data.total_count >= 100) { + return response.data.total_count; + } + + return response.data.total_count + 1; +} + +async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) { + const { owner, repo } = context.repo; + const { author, labels, number } = getPullRequest(context); + + if (hasLabel(labels, exemptLabelName)) { + core.info(`PR #${number} has the ${exemptLabelName} label; skipping open PR limit enforcement.`); + return { + author, + closed: false, + exempt: true, + openPrCount: null, + }; + } + + const openPrCount = await getOpenPrCount({ + github, + owner, + repo, + author, + pullRequestNumber: number, + }); + + if (openPrCount <= maxOpenPrs) { + core.info( + `${author} has ${openPrCount} open pull request(s), which is within the limit of ${maxOpenPrs}.`, + ); + return { + author, + closed: false, + openPrCount, + }; + } + + await ensureLabel({ + github, + owner, + repo, + labelName, + }); + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: number, + labels: [labelName], + }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: number, + body: buildLimitMessage({ + author, + exemptLabelName, + maxOpenPrs, + openPrCount, + }), + }); + + await github.rest.pulls.update({ + owner, + repo, + pull_number: number, + state: 'closed', + }); + + core.info( + `${author} has ${openPrCount} open pull request(s), which exceeds the limit of ${maxOpenPrs}. ` + + `Closed PR #${number}.`, + ); + + return { + author, + closed: true, + openPrCount, + }; +} + +module.exports = { + buildLimitMessage, + enforcePrLimit, + getOpenPrCount, +}; diff --git a/.github/tests/test_pr_limit_moderation.js b/.github/tests/test_pr_limit_moderation.js new file mode 100644 index 0000000000..67192c8327 --- /dev/null +++ b/.github/tests/test_pr_limit_moderation.js @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft. All rights reserved. + +/** + * Tests for pr_limit_moderation.js. + * + * Run with: node --test .github/tests/test_pr_limit_moderation.js + */ + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { enforcePrLimit } = require('../scripts/pr_limit_moderation.js'); + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function createContext({ author = 'community-user', labels = [], number = 123 } = {}) { + return { + repo: { + owner: 'microsoft', + repo: 'agent-framework', + }, + payload: { + pull_request: { + number, + labels: labels.map((name) => ({ name })), + user: { + login: author, + }, + }, + }, + }; +} + +function createCore() { + const messages = []; + return { + messages, + info(message) { + messages.push(message); + }, + }; +} + +function createGithub({ totalCount, itemNumbers, labelExists = true }) { + const calls = []; + + return { + calls, + rest: { + search: { + async issuesAndPullRequests(params) { + calls.push({ api: 'search.issuesAndPullRequests', params }); + return { + data: { + total_count: totalCount, + items: itemNumbers.map((number) => ({ number })), + }, + }; + }, + }, + issues: { + async getLabel(params) { + calls.push({ api: 'issues.getLabel', params }); + if (!labelExists) { + const error = new Error('Not Found'); + error.status = 404; + throw error; + } + return { data: { name: params.name } }; + }, + async createLabel(params) { + calls.push({ api: 'issues.createLabel', params }); + return { data: { name: params.name } }; + }, + async addLabels(params) { + calls.push({ api: 'issues.addLabels', params }); + return { data: [] }; + }, + async createComment(params) { + calls.push({ api: 'issues.createComment', params }); + return { data: { id: 1 } }; + }, + }, + pulls: { + async update(params) { + calls.push({ api: 'pulls.update', params }); + return { data: { state: params.state } }; + }, + }, + }, + }; +} + + +// --------------------------------------------------------------------------- +// PR limit enforcement +// --------------------------------------------------------------------------- + +describe('PR limit enforcement', () => { + it('does not close the PR when the author is at the open PR limit', async () => { + const github = createGithub({ + totalCount: 10, + itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 123], + }); + + const result = await enforcePrLimit({ + github, + context: createContext(), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, false); + assert.equal(result.openPrCount, 10); + assert.deepEqual( + github.calls.map((call) => call.api), + ['search.issuesAndPullRequests'], + ); + }); + + it('counts the new PR when search has not indexed it yet', async () => { + const github = createGithub({ + totalCount: 10, + itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }); + + const result = await enforcePrLimit({ + github, + context: createContext(), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, true); + assert.equal(result.openPrCount, 11); + assert.deepEqual( + github.calls.map((call) => call.api), + [ + 'search.issuesAndPullRequests', + 'issues.getLabel', + 'issues.addLabels', + 'issues.createComment', + 'pulls.update', + ], + ); + }); + + it('creates the label when it does not already exist', async () => { + const github = createGithub({ + totalCount: 11, + itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], + labelExists: false, + }); + + const result = await enforcePrLimit({ + github, + context: createContext(), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, true); + assert.deepEqual( + github.calls.map((call) => call.api), + [ + 'search.issuesAndPullRequests', + 'issues.getLabel', + 'issues.createLabel', + 'issues.addLabels', + 'issues.createComment', + 'pulls.update', + ], + ); + assert.equal( + github.calls.find((call) => call.api === 'issues.createLabel').params.name, + 'too-many-prs', + ); + }); + + it('tolerates a 422 race when creating the label', async () => { + const github = createGithub({ + totalCount: 11, + itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], + labelExists: false, + }); + github.rest.issues.createLabel = async (params) => { + github.calls.push({ api: 'issues.createLabel', params }); + const error = new Error('Validation Failed'); + error.status = 422; + throw error; + }; + + const result = await enforcePrLimit({ + github, + context: createContext(), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, true); + assert.deepEqual( + github.calls.map((call) => call.api), + [ + 'search.issuesAndPullRequests', + 'issues.getLabel', + 'issues.createLabel', + 'issues.addLabels', + 'issues.createComment', + 'pulls.update', + ], + ); + }); + + it('uses a diplomatic close message with the configured limit', async () => { + const github = createGithub({ + totalCount: 11, + itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], + }); + + await enforcePrLimit({ + github, + context: createContext({ author: 'octo-contributor' }), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + const comment = github.calls.find((call) => call.api === 'issues.createComment').params.body; + assert.match(comment, /Thank you for your contribution/); + assert.match(comment, /limit community contributors to 10 open pull requests/); + assert.match(comment, /@octo-contributor/); + assert.match(comment, /`pr-limit-exempt` label and reopen/); + }); + + it('does not close an exempt PR when it is reopened', async () => { + const github = createGithub({ + totalCount: 11, + itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], + }); + + const result = await enforcePrLimit({ + github, + context: createContext({ labels: ['PR-LIMIT-EXEMPT'] }), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, false); + assert.equal(result.exempt, true); + assert.equal(result.openPrCount, null); + assert.deepEqual(github.calls, []); + }); + + it('does not over-count when the current PR is not on the first search page', async () => { + const github = createGithub({ + totalCount: 101, + itemNumbers: Array.from({ length: 100 }, (_, index) => index + 1), + }); + + const result = await enforcePrLimit({ + github, + context: createContext({ number: 123 }), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, true); + assert.equal(result.openPrCount, 101); + }); +}); diff --git a/.github/workflows/limit-community-prs.yml b/.github/workflows/limit-community-prs.yml new file mode 100644 index 0000000000..2fe66ccd20 --- /dev/null +++ b/.github/workflows/limit-community-prs.yml @@ -0,0 +1,83 @@ +name: Limit community pull requests + +on: + pull_request_target: + types: [opened, reopened] + +permissions: + contents: read + issues: write + pull-requests: write + +concurrency: + group: pr-limit-${{ github.repository }}-${{ github.event.pull_request.user.login }} + cancel-in-progress: false + +env: + MAX_OPEN_PULL_REQUESTS: '10' + PR_LIMIT_EXEMPT_LABEL: pr-limit-exempt + TOO_MANY_PRS_LABEL: too-many-prs + +jobs: + team_check: + runs-on: ubuntu-latest + outputs: + is_team_member: ${{ steps.check.outputs.is_team_member }} + steps: + - name: Checkout scripts + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + sparse-checkout: .github/scripts + fetch-depth: 1 + persist-credentials: false + + - name: Check PR author team membership + id: check + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }} + PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + script: | + const checkTeamMembership = require('./.github/scripts/check_team_membership.js'); + const { author, isTeamMember } = await checkTeamMembership({ + github, + context, + core, + teamSlug: process.env.TEAM_NAME, + issueNumber: process.env.PR_NUMBER, + }); + core.setOutput('is_team_member', isTeamMember ? 'true' : 'false'); + if (isTeamMember) { + core.info(`Author ${author} is a team member; skipping open PR limit.`); + } else { + core.info(`Author ${author} is not a team member; checking open PR limit.`); + } + + limit_open_prs: + runs-on: ubuntu-latest + needs: team_check + if: ${{ needs.team_check.outputs.is_team_member == 'false' }} + steps: + - name: Checkout scripts + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + sparse-checkout: .github/scripts + fetch-depth: 1 + persist-credentials: false + + - name: Enforce open PR limit + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + script: | + const { enforcePrLimit } = require('./.github/scripts/pr_limit_moderation.js'); + await enforcePrLimit({ + github, + context, + core, + exemptLabelName: process.env.PR_LIMIT_EXEMPT_LABEL, + maxOpenPrs: Number.parseInt(process.env.MAX_OPEN_PULL_REQUESTS, 10), + labelName: process.env.TOO_MANY_PRS_LABEL, + }); From b59a854fcdb4b5588615e22bea788572db6da6df Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:18:26 +0900 Subject: [PATCH 25/61] Fix integration test worker crashes in Azure Functions on Py3.13 (#4260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Fix integration test worker crashes on Python 3.13 Three changes to prevent pytest-xdist workers from crashing during Azure Functions integration tests: 1. Add `start_new_session=True` to subprocess on Linux so signals (e.g. from test-timeout) cannot propagate between the func host and the xdist worker process. 2. Add an overall 100-second budget to the fixture setup loop so the retry logic never exceeds the 120-second test timeout. When pytest-timeout's thread method fires during fixture setup and the thread doesn't respond, it calls os._exit() which kills the xdist worker – this is the root cause of the "Not properly terminated" crashes. 3. Remove the `UV_PYTHON: "3.10"` workaround from both workflow files so integration tests actually run on Python 3.13. Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> * Validate integration tests on Python 3.13 Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> * Revert unintentional uv.lock dependency bumps Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> * Use time.monotonic() instead of time.time() for fixture budget timing Addresses review feedback: monotonic clock is immune to NTP/clock adjustments that could skew the budget enforcement. Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> * Fix func worker segfault on Python 3.13 by redirecting worker to Python 3.12 The Azure Functions Python worker crashes with SIGSEGV (exit code 139) on Python 3.13 due to protobuf C extension (google._upb) compatibility issues. When the test runner uses Python >=3.13, the conftest now automatically finds a compatible Python 3.10-3.12 and sets languageWorkers__python__defaultExecutablePath so the func host uses it for the worker process. The CI setup action also ensures Python 3.12 is available on the runner, falling back to uv python install if the system doesn't have it. Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> * Address code review: add path validation, clarify version range and config key format Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> * Run func worker natively on Python 3.13 by disabling dependency isolation Replace the Python 3.12 redirect workaround with the proper fix: set PYTHON_ISOLATE_WORKER_DEPENDENCIES=0 on Python >=3.13. The segfault (exit code 139) is caused by the Azure Functions worker's module isolation mechanism conflicting with protobuf's C extensions (google._upb) on Python 3.13. Disabling isolation lets the worker load dependencies from the app's own environment, which avoids the crash while keeping everything running on Python 3.13. See: https://github.com/Azure/azure-functions-python-worker/issues/1797 Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: larohra <41490930+larohra@users.noreply.github.com> Co-authored-by: Laveesh Rohra --- .../tests/integration_tests/conftest.py | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/python/packages/azurefunctions/tests/integration_tests/conftest.py b/python/packages/azurefunctions/tests/integration_tests/conftest.py index 4f180d4921..7e17b84cfb 100644 --- a/python/packages/azurefunctions/tests/integration_tests/conftest.py +++ b/python/packages/azurefunctions/tests/integration_tests/conftest.py @@ -365,6 +365,14 @@ def _start_function_app(sample_path: Path, port: int) -> subprocess.Popen[Any]: # use the task hub name to separate orchestration state. env["TASKHUB_NAME"] = f"test{uuid.uuid4().hex[:8]}" + # The Azure Functions Python worker's dependency isolation mechanism crashes + # on Python 3.13 with a SIGSEGV in the protobuf C extension (google._upb). + # Disabling isolation lets the worker load dependencies from the app's own + # environment, which avoids the crash. + # See: https://github.com/Azure/azure-functions-python-worker/issues/1797 + if sys.version_info >= (3, 13): + env.setdefault("PYTHON_ISOLATE_WORKER_DEPENDENCIES", "0") + # On Windows, use CREATE_NEW_PROCESS_GROUP to allow proper termination # shell=True only on Windows to handle PATH resolution if sys.platform == "win32": @@ -375,8 +383,15 @@ def _start_function_app(sample_path: Path, port: int) -> subprocess.Popen[Any]: shell=True, env=env, ) - # On Unix, don't use shell=True to avoid shell wrapper issues - return subprocess.Popen(["func", "start", "--port", str(port)], cwd=str(sample_path), env=env) + # On Unix, use start_new_session=True to isolate the process group from the + # pytest-xdist worker. Without this, signals (e.g. from test-timeout) can + # propagate to the func host and vice-versa, potentially killing the worker. + return subprocess.Popen( + ["func", "start", "--port", str(port)], + cwd=str(sample_path), + env=env, + start_new_session=True, + ) def _wait_for_function_app_ready(func_process: subprocess.Popen[Any], port: int, max_wait: int = 60) -> None: @@ -533,18 +548,33 @@ def function_app_for_test(request: pytest.FixtureRequest) -> Iterator[dict[str, _load_and_validate_env(sample_path) max_attempts = 3 + # The overall budget MUST be shorter than the pytest-timeout value + # (--timeout=120 by default) so that the fixture finishes cleanly instead + # of being killed by os._exit() which crashes the xdist worker. + overall_budget = 100 # seconds – leaves headroom below the 120 s test timeout last_error: Exception | None = None func_process: subprocess.Popen[Any] | None = None base_url = "" port = 0 + overall_start = time.monotonic() + attempts_made = 0 for _ in range(max_attempts): + remaining = overall_budget - (time.monotonic() - overall_start) + if remaining < 10: + # Not enough time for another attempt; bail out. + break + + attempts_made += 1 port = _find_available_port() base_url = _build_base_url(port) func_process = _start_function_app(sample_path, port) try: - _wait_for_function_app_ready(func_process, port) + # Cap each attempt's wait to the remaining budget minus a small + # buffer for cleanup. + per_attempt_wait = min(60, int(remaining) - 5) + _wait_for_function_app_ready(func_process, port, max_wait=max(per_attempt_wait, 10)) last_error = None break except FunctionAppStartupError as exc: @@ -553,7 +583,8 @@ def function_app_for_test(request: pytest.FixtureRequest) -> Iterator[dict[str, func_process = None if func_process is None: - error_message = f"Function app failed to start after {max_attempts} attempt(s)." + elapsed = int(time.monotonic() - overall_start) + error_message = f"Function app failed to start after {attempts_made} attempt(s) ({elapsed}s elapsed)." if last_error is not None: error_message += f" Last error: {last_error}" pytest.fail(error_message) From 78d175a1e20eb65c2b80517fa1cc911a9272cee3 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:26:20 +0800 Subject: [PATCH 26/61] Python: coalesce code interpreter history chunks (#5801) * fix: coalesce code interpreter history chunks * fix: narrow content item list types * fix: remove redundant content list casts --- .../packages/core/agent_framework/_types.py | 86 +++++++++++++++++++ .../packages/core/tests/core/test_sessions.py | 57 ++++++++++++ 2 files changed, 143 insertions(+) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index ce47f5fc8e..f30fc04789 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -1973,11 +1973,97 @@ def _coalesce_text_content(contents: list[Content], type_str: Literal["text", "t contents.extend(coalesced_contents) +def _content_items_text(items: Any) -> str | None: + """Return concatenated text when a content item list only contains text.""" + if not isinstance(items, list): + return None + text_parts: list[str] = [] + content_items = cast(list[object], items) + for item in content_items: + if not isinstance(item, Content) or item.type != "text": + return None + text_parts.append(item.text or "") + return "".join(text_parts) + + +def _merge_content_item_lists(existing: Any, incoming: Any) -> Any: + """Merge streamed nested content lists, replacing deltas with a later full value when present.""" + if incoming is None: + return existing + if existing is None: + return deepcopy(incoming) + + existing_text = _content_items_text(existing) + incoming_text = _content_items_text(incoming) + if existing_text is not None and incoming_text is not None: + if incoming_text.startswith(existing_text): + return deepcopy(incoming) + if existing_text.startswith(incoming_text): + return existing + + existing_items = cast(list[Content], existing) + merged = deepcopy(existing_items[0]) + merged.text = existing_text + incoming_text + return [merged] + + if isinstance(existing, list) and isinstance(incoming, list): + existing_list = cast(list[object], existing) + incoming_list = cast(list[object], incoming) + return [*existing_list, *deepcopy(incoming_list)] + return deepcopy(incoming) + + +def _merge_code_interpreter_content(existing: Content, incoming: Content) -> None: + """Merge two code interpreter content items for the same logical call.""" + existing.inputs = _merge_content_item_lists(existing.inputs, incoming.inputs) + existing.outputs = _merge_content_item_lists(existing.outputs, incoming.outputs) + existing.annotations = _combine_annotations(existing.annotations, incoming.annotations) + existing.additional_properties = {**existing.additional_properties, **incoming.additional_properties} + existing.raw_representation = _combine_raw_representations(existing.raw_representation, incoming.raw_representation) + + +def _code_interpreter_key(content: Content) -> tuple[str, str] | None: + """Return the aggregation key for code interpreter call/result content.""" + if content.type not in {"code_interpreter_tool_call", "code_interpreter_tool_result"}: + return None + call_id = content.call_id or content.additional_properties.get("item_id") + if not isinstance(call_id, str) or not call_id: + return None + return content.type, call_id + + +def _coalesce_code_interpreter_content(contents: list[Content]) -> None: + """Coalesce streaming code interpreter chunks by call id.""" + if not contents: + return + + coalesced_contents: list[Content] = [] + seen: dict[tuple[str, str], Content] = {} + for content in contents: + key = _code_interpreter_key(content) + if key is None: + coalesced_contents.append(content) + continue + + existing = seen.get(key) + if existing is None: + copied = deepcopy(content) + seen[key] = copied + coalesced_contents.append(copied) + continue + + _merge_code_interpreter_content(existing, content) + + contents.clear() + contents.extend(coalesced_contents) + + def _finalize_response(response: ChatResponse | AgentResponse) -> None: """Finalizes the response by performing any necessary post-processing.""" for msg in response.messages: _coalesce_text_content(msg.contents, "text") _coalesce_text_content(msg.contents, "text_reasoning") + _coalesce_code_interpreter_content(msg.contents) # region ContinuationToken diff --git a/python/packages/core/tests/core/test_sessions.py b/python/packages/core/tests/core/test_sessions.py index ebb91d0b0d..7c78dba209 100644 --- a/python/packages/core/tests/core/test_sessions.py +++ b/python/packages/core/tests/core/test_sessions.py @@ -307,6 +307,63 @@ class TestHistoryProviderBase: assert provider.stored[0].text == "hello" assert provider.stored[1].text == "hi" + async def test_after_run_stores_coalesced_code_interpreter_chunks(self) -> None: + from agent_framework import AgentResponse, AgentResponseUpdate, Content + + provider = ConcreteHistoryProvider("mem", store_inputs=False) + updates = [ + AgentResponseUpdate( + role="assistant", + contents=[ + Content.from_code_interpreter_tool_result( + call_id="ci_123", + outputs=[], + ) + ], + ), + AgentResponseUpdate( + contents=[ + Content.from_code_interpreter_tool_call( + call_id="ci_123", + inputs=[Content.from_text(text="import")], + additional_properties={"sequence_number": 1}, + ) + ], + ), + AgentResponseUpdate( + contents=[ + Content.from_code_interpreter_tool_call( + call_id="ci_123", + inputs=[Content.from_text(text=" pandas")], + additional_properties={"sequence_number": 2}, + ) + ], + ), + AgentResponseUpdate( + contents=[ + Content.from_code_interpreter_tool_call( + call_id="ci_123", + inputs=[Content.from_text(text="import pandas as pd")], + additional_properties={"sequence_number": 3}, + ) + ], + ), + ] + ctx = SessionContext(session_id="s1", input_messages=[Message(role="user", contents=["make a sheet"])]) + ctx._response = AgentResponse.from_updates(updates) + + await provider.after_run(agent=None, session=AgentSession(), context=ctx, state={}) # type: ignore[arg-type] + + assert len(provider.stored) == 1 + stored_contents = provider.stored[0].contents + calls = [content for content in stored_contents if content.type == "code_interpreter_tool_call"] + results = [content for content in stored_contents if content.type == "code_interpreter_tool_result"] + assert len(calls) == 1 + assert len(results) == 1 + assert calls[0].inputs is not None + assert len(calls[0].inputs) == 1 + assert calls[0].inputs[0].text == "import pandas as pd" + async def test_after_run_skips_inputs_when_disabled(self) -> None: from agent_framework import AgentResponse From 52a8045bb670918ef8c1f970dad051092893ceee Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:20:39 +0100 Subject: [PATCH 27/61] Python: Add background agent support to harness agent (#6155) * Add background agent support to harness agent * Address PR comments --- .../core/agent_framework/_harness/_agent.py | 21 ++++- .../core/tests/core/test_harness_agent.py | 91 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_harness/_agent.py b/python/packages/core/agent_framework/_harness/_agent.py index ce218576db..5896f72141 100644 --- a/python/packages/core/agent_framework/_harness/_agent.py +++ b/python/packages/core/agent_framework/_harness/_agent.py @@ -14,12 +14,13 @@ import logging from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any -from .._agents import Agent +from .._agents import Agent, SupportsAgentRun from .._clients import SupportsWebSearchTool from .._compaction import CompactionProvider, ContextWindowCompactionStrategy, ToolResultCompactionStrategy from .._feature_stage import ExperimentalFeature, experimental from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider from .._skills import SkillsProvider +from ._background_agents import BackgroundAgentsProvider from ._memory import MemoryContextProvider, MemoryStore from ._mode import AgentModeProvider from ._todo import TodoProvider @@ -103,6 +104,8 @@ def _assemble_context_providers( memory_store: MemoryStore | None, skills_provider: SkillsProvider | None, skills_paths: Sequence[str] | None, + background_agents: Sequence[SupportsAgentRun] | None, + background_agents_instructions: str | None, extra_context_providers: Sequence[ContextProvider] | None, ) -> list[ContextProvider]: """Assemble the ordered list of context providers.""" @@ -130,6 +133,10 @@ def _assemble_context_providers( if skills_paths: providers.append(SkillsProvider.from_paths(*skills_paths)) + # Background agents are opt-in: only added when agents are provided. + if background_agents: + providers.append(BackgroundAgentsProvider(background_agents, instructions=background_agents_instructions)) + # Append any user-supplied additional providers. if extra_context_providers: providers.extend(extra_context_providers) @@ -165,6 +172,8 @@ def create_harness_agent( memory_store: MemoryStore | None = None, skills_provider: SkillsProvider | None = None, skills_paths: Sequence[str] | None = None, + background_agents: Sequence[SupportsAgentRun] | None = None, + background_agents_instructions: str | None = None, disable_web_search: bool = False, otel_provider_name: str | None = None, context_providers: Sequence[ContextProvider] | None = None, @@ -182,6 +191,7 @@ def create_harness_agent( - **AgentModeProvider** — plan/execute mode tracking - **MemoryContextProvider** — file-based durable memory (when ``memory_store`` provided) - **SkillsProvider** — skill discovery and progressive loading + - **BackgroundAgentsProvider** — delegate work to background sub-agents - **OpenTelemetry** — observability via ``AgentTelemetryLayer`` Each feature can be disabled or customized via keyword arguments. @@ -253,6 +263,13 @@ def create_harness_agent( skills_paths: Paths for file-based skill discovery (looks for SKILL.md files). Can be combined with ``skills_provider``. When neither ``skills_provider`` nor ``skills_paths`` is provided, no SkillsProvider is added. + background_agents: Collection of agents available for background task delegation. + When provided, a ``BackgroundAgentsProvider`` is automatically included, + enabling the agent to start, monitor, and retrieve results from background tasks. + Each agent must have a non-empty, unique name (case-insensitive). + background_agents_instructions: Optional instruction override for the + ``BackgroundAgentsProvider``. May include ``{background_agents}`` placeholder + which will be replaced with the agent listing. disable_web_search: When True, skip automatic web search tool inclusion. When False (default), the web search tool is automatically added if the client implements SupportsWebSearchTool. A warning is logged if the client @@ -302,6 +319,8 @@ def create_harness_agent( memory_store=memory_store, skills_provider=skills_provider, skills_paths=skills_paths, + background_agents=background_agents, + background_agents_instructions=background_agents_instructions, extra_context_providers=context_providers, ) diff --git a/python/packages/core/tests/core/test_harness_agent.py b/python/packages/core/tests/core/test_harness_agent.py index c53147fd15..58ef3f5f2d 100644 --- a/python/packages/core/tests/core/test_harness_agent.py +++ b/python/packages/core/tests/core/test_harness_agent.py @@ -394,3 +394,94 @@ def test_create_harness_agent_logs_warning_when_no_web_search(caplog: pytest.Log max_output_tokens=16_384, ) assert any("SupportsWebSearchTool" in msg for msg in caplog.messages) + + +# --- Background Agents Tests --- + + +class _FakeBackgroundAgent: + """Minimal agent stub satisfying SupportsAgentRun for background agents tests.""" + + def __init__(self, name: str, description: str | None = None): + self.id = f"agent-{name}" + self.name = name + self.description = description + + def create_session(self, *, session_id: str | None = None) -> AgentSession: + return AgentSession(session_id=session_id) + + def get_session(self, service_session_id: str, *, session_id: str | None = None) -> AgentSession: + return AgentSession(service_session_id=service_session_id, session_id=session_id) + + async def run(self, messages: Any = None, *, stream: bool = False, session: Any = None, **kwargs: Any) -> Any: + from agent_framework import AgentResponse + + return AgentResponse(messages=[], response_id="fake-bg-response") + + +def test_create_harness_agent_no_background_agents_by_default() -> None: + """No BackgroundAgentsProvider should be included when background_agents is not provided.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + agent = create_harness_agent( + client=_FakeChatClient(), # type: ignore[arg-type] + max_context_window_tokens=128_000, + max_output_tokens=16_384, + disable_web_search=True, + ) + providers = agent.context_providers or [] + assert not any(isinstance(p, BackgroundAgentsProvider) for p in providers) + + +def test_create_harness_agent_adds_background_agents_provider() -> None: + """BackgroundAgentsProvider should be included when background_agents are provided.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + bg_agent = _FakeBackgroundAgent("WebSearcher", "Searches the web") + agent = create_harness_agent( + client=_FakeChatClient(), # type: ignore[arg-type] + max_context_window_tokens=128_000, + max_output_tokens=16_384, + disable_web_search=True, + background_agents=[bg_agent], + ) + providers = agent.context_providers or [] + bg_providers = [p for p in providers if isinstance(p, BackgroundAgentsProvider)] + assert len(bg_providers) == 1 + + +def test_create_harness_agent_background_agents_custom_instructions() -> None: + """Custom instructions should be passed to BackgroundAgentsProvider.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + custom_instructions = "## Custom\n\nUse agents wisely.\n\n{background_agents}" + bg_agent = _FakeBackgroundAgent("Helper", "A helper agent") + agent = create_harness_agent( + client=_FakeChatClient(), # type: ignore[arg-type] + max_context_window_tokens=128_000, + max_output_tokens=16_384, + disable_web_search=True, + background_agents=[bg_agent], + background_agents_instructions=custom_instructions, + ) + providers = agent.context_providers or [] + bg_providers = [p for p in providers if isinstance(p, BackgroundAgentsProvider)] + assert len(bg_providers) == 1 + # Verify the custom instructions were used (placeholder replaced with agent list). + assert "Custom" in bg_providers[0]._instructions + assert "Helper" in bg_providers[0]._instructions + + +def test_create_harness_agent_empty_background_agents_list() -> None: + """An empty background_agents list should NOT add a BackgroundAgentsProvider.""" + from agent_framework._harness._background_agents import BackgroundAgentsProvider + + agent = create_harness_agent( + client=_FakeChatClient(), # type: ignore[arg-type] + max_context_window_tokens=128_000, + max_output_tokens=16_384, + disable_web_search=True, + background_agents=[], + ) + providers = agent.context_providers or [] + assert not any(isinstance(p, BackgroundAgentsProvider) for p in providers) From 8091d052d8175e1726a2ef238affa80a381a9b8a Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 1 Jun 2026 19:53:56 +0200 Subject: [PATCH 28/61] Python: refresh dev dependencies and validate runtime bounds (#6238) Updates third-party dev dependencies across the Python workspace and validates that all runtime dependency bounds still hold at both ends. Dev dependency bumps (root, lab, declarative, durabletask): - uv 0.11.6 -> 0.11.17, ruff 0.15.8 -> 0.15.15, pytest-asyncio 1.3.0 -> 1.4.0, mcp 1.27.0 -> 1.27.2, azure-monitor-opentelemetry 1.8.7 -> 1.8.8, poethepoet 0.42.1 -> 0.46.0, prek 0.3.9 -> 0.4.3, types-python-dateutil and types-PyYaml stub bumps. - Transitive Dependabot items swept via lock: idna 3.11 -> 3.17, pip 26.0.1 -> 26.1.2. Deliberately excluded: - opentelemetry-sdk stays 1.40.0: azure-monitor-opentelemetry (incl. 1.8.8) hard-pins opentelemetry-sdk==1.40. - mypy stays 1.20.0 and pyright stays 1.1.408: the 2.1.0 / 1.1.409 bumps introduce new diagnostics that fail type checking and need dedicated PRs. - rich kept as a range: agentlightning (lab[lightning]) forces rich==13.9.4. Code/formatting changes driven by the ruff upgrade: - devui lifespan now uses try/finally so shutdown cleanup always runs (ruff RUF075). - Removed unused TYPE_CHECKING imports in core and foundry flagged by ruff 0.15.15. - Reapplied ruff 0.15.15 formatting to the files it changed. Validation: validate-dependency-bounds-test "*" passes (31/31 lower + 31/31 upper); typing 62/62; lint 31/31; devui tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../a2a/agent_framework_a2a/_a2a_executor.py | 4 +- .../core/agent_framework/_middleware.py | 3 +- .../packages/core/agent_framework/_skills.py | 7 +- .../packages/core/tests/core/test_skills.py | 8 +- python/packages/declarative/pyproject.toml | 2 +- .../devui/agent_framework_devui/_server.py | 14 +- python/packages/durabletask/pyproject.toml | 2 +- .../foundry/agent_framework_foundry/_agent.py | 2 - python/packages/lab/pyproject.toml | 8 +- python/pyproject.toml | 14 +- .../02-agents/harness/harness_research.py | 5 +- .../orchestrations/02_selector_group_chat.py | 12 +- .../orchestrations/03_swarm.py | 9 +- .../single_agent/02_agent_with_tool.py | 3 +- .../single_agent/04_agent_as_tool.py | 6 +- .../orchestrations/sequential.py | 6 +- .../samples/hosting/test_toolbox_endpoint.py | 6 +- python/uv.lock | 408 +++++++++--------- 18 files changed, 271 insertions(+), 248 deletions(-) diff --git a/python/packages/a2a/agent_framework_a2a/_a2a_executor.py b/python/packages/a2a/agent_framework_a2a/_a2a_executor.py index 3685e82742..c0031c406c 100644 --- a/python/packages/a2a/agent_framework_a2a/_a2a_executor.py +++ b/python/packages/a2a/agent_framework_a2a/_a2a_executor.py @@ -287,9 +287,7 @@ class A2AExecutor(AgentExecutor): artifact_id=artifact_id, metadata=metadata, append=( - True - if streamed_artifact_ids is not None and artifact_id in streamed_artifact_ids - else None + True if streamed_artifact_ids is not None and artifact_id in streamed_artifact_ids else None ), ) if artifact_id and streamed_artifact_ids is not None: diff --git a/python/packages/core/agent_framework/_middleware.py b/python/packages/core/agent_framework/_middleware.py index 64467b470e..4f90030368 100644 --- a/python/packages/core/agent_framework/_middleware.py +++ b/python/packages/core/agent_framework/_middleware.py @@ -36,11 +36,10 @@ if TYPE_CHECKING: from pydantic import BaseModel from ._agents import SupportsAgentRun - from ._clients import SupportsChatGetResponse from ._compaction import CompactionStrategy, TokenizerProtocol from ._sessions import AgentSession from ._tools import FunctionTool, ToolTypes - from ._types import ChatOptions, ChatResponse, ChatResponseUpdate + from ._types import ChatOptions ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel) diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 683302b13a..5e313f20d9 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -2134,9 +2134,7 @@ class SkillsProvider(ContextProvider): ), FunctionTool( name="read_skill_resource", - description=( - "Reads a resource associated with a skill, such as references, assets, or dynamic data." - ), + description=("Reads a resource associated with a skill, such as references, assets, or dynamic data."), func=_read_resource, input_model={ "type": "object", @@ -2173,8 +2171,7 @@ class SkillsProvider(ContextProvider): "type": "object", "additionalProperties": True, "description": ( - "Named arguments as key-value pairs " - '(e.g. {"length": 24, "uppercase": true}).' + 'Named arguments as key-value pairs (e.g. {"length": 24, "uppercase": true}).' ), }, { diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index 17fb2cf5ce..31f679c367 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -4086,8 +4086,8 @@ class TestClassSkill: async def test_content_is_cached(self) -> None: skill = _MinimalClassSkill() - content1 = (await skill.get_content()) - content2 = (await skill.get_content()) + content1 = await skill.get_content() + content2 = await skill.get_content() assert content1 is content2 def test_resources_are_lazy_cached(self) -> None: @@ -5587,8 +5587,8 @@ class TestInlineSkillContentCaching: async def test_content_cached_after_first_access(self) -> None: """InlineSkill.content returns the same object on subsequent accesses.""" skill = InlineSkill(frontmatter=SkillFrontmatter(name="test-skill", description="Test"), instructions="Body") - first = (await skill.get_content()) - second = (await skill.get_content()) + first = await skill.get_content() + second = await skill.get_content() assert first is second # Same object (cached) assert "test-skill" in first diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index 1297228860..0efa733daa 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ ] [dependency-groups] dev = [ - "types-PyYaml==6.0.12.20250915" + "types-PyYaml==6.0.12.20260518" ] [tool.uv] diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index 08c454f94e..c4138a0aeb 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -375,13 +375,15 @@ class DevServer: logger.info("Starting Agent Framework Server") await self._ensure_executor() await self._ensure_openai_executor() # Initialize OpenAI executor - yield - # Shutdown - logger.info("Shutting down Agent Framework Server") + try: + yield + finally: + # Shutdown + logger.info("Shutting down Agent Framework Server") - # Cleanup entity resources (e.g., close credentials, clients) - if self.executor: - await self._cleanup_entities() + # Cleanup entity resources (e.g., close credentials, clients) + if self.executor: + await self._cleanup_entities() app = FastAPI( title="Agent Framework Server", diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index ebd8005c2b..9b3f846dd4 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ [dependency-groups] dev = [ - "types-python-dateutil==2.9.0.20260402", + "types-python-dateutil==2.9.0.20260518", ] [tool.uv] diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index c7f2d03d60..1e1157d05a 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -57,8 +57,6 @@ if TYPE_CHECKING: from agent_framework import ( Agent, AgentRunInputs, - ChatAndFunctionMiddlewareTypes, - ContextProvider, MiddlewareTypes, ToolTypes, ) diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index 2630ef84b7..da2095936f 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -57,19 +57,19 @@ math = [ [dependency-groups] dev = [ - "uv==0.11.6", - "ruff==0.15.8", + "uv==0.11.17", + "ruff==0.15.15", "pytest==9.0.3", "mypy==1.20.0", "pyright==1.1.408", #tasks - "poethepoet==0.42.1", + "poethepoet==0.46.0", "rich>=13.7.1,<15.0.0", "tomli==2.4.1", "tomli-w==1.2.0", # tau2 from source (not available on PyPI) "tau2@ git+https://github.com/sierra-research/tau2-bench@5ba9e3e56db57c5e4114bf7f901291f09b2c5619", - "prek==0.3.9", + "prek==0.4.3", ] [project.scripts] diff --git a/python/pyproject.toml b/python/pyproject.toml index 454862ac82..a1bae9a099 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -28,25 +28,25 @@ dependencies = [ [dependency-groups] dev = [ - "uv==0.11.6", + "uv==0.11.17", "flit==3.12.0", - "ruff==0.15.8", + "ruff==0.15.15", "pytest==9.0.3", - "pytest-asyncio==1.3.0", + "pytest-asyncio==1.4.0", "pytest-cov==7.1.0", "pytest-xdist[psutil]==3.8.0", "pytest-timeout==2.4.0", "pytest-retry==1.7.0", "mypy==1.20.0", "pyright==1.1.408", - "mcp[ws]==1.27.0", + "mcp[ws]==1.27.2", "opentelemetry-sdk==1.40.0", - "azure-monitor-opentelemetry==1.8.7", + "azure-monitor-opentelemetry==1.8.8", #tasks - "poethepoet==0.42.1", + "poethepoet==0.46.0", "rich>=13.7.1,<16.0.0", "tomli==2.4.1", - "prek==0.3.9", + "prek==0.4.3", ] [tool.uv] diff --git a/python/samples/02-agents/harness/harness_research.py b/python/samples/02-agents/harness/harness_research.py index f1cb66228a..977c26f049 100644 --- a/python/samples/02-agents/harness/harness_research.py +++ b/python/samples/02-agents/harness/harness_research.py @@ -109,7 +109,10 @@ async def main() -> None: print(f"\n [calling tool: {content.name}]", flush=True) print(" ", end="", flush=True) # Show web search activity when the result arrives with action details. - elif content.type in ("search_tool_call", "search_tool_result") and getattr(content, "tool_name", None) == "web_search": + elif ( + content.type in ("search_tool_call", "search_tool_result") + and getattr(content, "tool_name", None) == "web_search" + ): action = None if content.type == "search_tool_result" and isinstance(content.result, dict): action = content.result.get("action", {}) diff --git a/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py b/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py index 40973abf72..08bbf5d617 100644 --- a/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py +++ b/python/samples/autogen-migration/orchestrations/02_selector_group_chat.py @@ -74,19 +74,22 @@ async def run_agent_framework() -> None: client = OpenAIChatClient(model="gpt-4.1-mini") # Create specialized agents - python_expert = Agent(client=client, + python_expert = Agent( + client=client, name="python_expert", instructions="You are a Python programming expert. Answer Python-related questions.", description="Expert in Python programming", ) - javascript_expert = Agent(client=client, + javascript_expert = Agent( + client=client, name="javascript_expert", instructions="You are a JavaScript programming expert. Answer JavaScript-related questions.", description="Expert in JavaScript programming", ) - database_expert = Agent(client=client, + database_expert = Agent( + client=client, name="database_expert", instructions="You are a database expert. Answer SQL and database-related questions.", description="Expert in databases and SQL", @@ -95,7 +98,8 @@ async def run_agent_framework() -> None: workflow = GroupChatBuilder( participants=[python_expert, javascript_expert, database_expert], max_rounds=1, - orchestrator_agent=Agent(client=client, + orchestrator_agent=Agent( + client=client, name="selector_manager", instructions="Based on the conversation, select the most appropriate expert to respond next.", ), diff --git a/python/samples/autogen-migration/orchestrations/03_swarm.py b/python/samples/autogen-migration/orchestrations/03_swarm.py index e89e8ef573..a7e03cee2b 100644 --- a/python/samples/autogen-migration/orchestrations/03_swarm.py +++ b/python/samples/autogen-migration/orchestrations/03_swarm.py @@ -113,7 +113,8 @@ async def run_agent_framework() -> None: client = OpenAIChatClient(model="gpt-4.1-mini") # Create triage agent - triage_agent = Agent(client=client, + triage_agent = Agent( + client=client, name="triage", instructions=( "You are a triage agent. Analyze the user's request and route to the appropriate specialist:\n" @@ -125,7 +126,8 @@ async def run_agent_framework() -> None: ) # Create billing specialist - billing_agent = Agent(client=client, + billing_agent = Agent( + client=client, name="billing_agent", instructions="You are a billing specialist. Help with payment and billing questions. Provide clear assistance.", description="Handles billing and payment questions", @@ -133,7 +135,8 @@ async def run_agent_framework() -> None: ) # Create technical support specialist - tech_support = Agent(client=client, + tech_support = Agent( + client=client, name="technical_support", instructions="You are technical support. Help with technical issues. Provide clear assistance.", description="Handles technical support questions", diff --git a/python/samples/autogen-migration/single_agent/02_agent_with_tool.py b/python/samples/autogen-migration/single_agent/02_agent_with_tool.py index c4a75586c9..75cebac2f4 100644 --- a/python/samples/autogen-migration/single_agent/02_agent_with_tool.py +++ b/python/samples/autogen-migration/single_agent/02_agent_with_tool.py @@ -73,7 +73,8 @@ async def run_agent_framework() -> None: # Create agent with tool client = OpenAIChatClient(model="gpt-4.1-mini") - agent = Agent(client=client, + agent = Agent( + client=client, name="assistant", instructions="You are a helpful assistant. Use available tools to answer questions.", tools=[get_weather], diff --git a/python/samples/autogen-migration/single_agent/04_agent_as_tool.py b/python/samples/autogen-migration/single_agent/04_agent_as_tool.py index 79c420b415..4144ac1b83 100644 --- a/python/samples/autogen-migration/single_agent/04_agent_as_tool.py +++ b/python/samples/autogen-migration/single_agent/04_agent_as_tool.py @@ -61,7 +61,8 @@ async def run_agent_framework() -> None: client = OpenAIChatClient(model="gpt-4.1-mini") # Create specialized writer agent - writer = Agent(client=client, + writer = Agent( + client=client, name="writer", instructions="You are a creative writer. Write short, engaging content.", ) @@ -75,7 +76,8 @@ async def run_agent_framework() -> None: ) # Create coordinator agent with writer tool - coordinator = Agent(client=client, + coordinator = Agent( + client=client, name="coordinator", instructions="You coordinate with specialized agents. Delegate writing tasks to the writer agent.", tools=[writer_tool], diff --git a/python/samples/semantic-kernel-migration/orchestrations/sequential.py b/python/samples/semantic-kernel-migration/orchestrations/sequential.py index 51a6eb78cc..22a2be6f23 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/sequential.py +++ b/python/samples/semantic-kernel-migration/orchestrations/sequential.py @@ -78,12 +78,14 @@ async def sk_agent_response_callback( async def run_agent_framework_example(prompt: str) -> list[Message]: client = OpenAIChatCompletionClient(credential=AzureCliCredential()) - writer = Agent(client=client, + writer = Agent( + client=client, instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = Agent(client=client, + reviewer = Agent( + client=client, instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) diff --git a/python/tests/samples/hosting/test_toolbox_endpoint.py b/python/tests/samples/hosting/test_toolbox_endpoint.py index b43b889315..b08c5e58a9 100644 --- a/python/tests/samples/hosting/test_toolbox_endpoint.py +++ b/python/tests/samples/hosting/test_toolbox_endpoint.py @@ -30,11 +30,7 @@ for _mod_name in _MISSING_MODULES: # Load the two sample modules by file path to avoid needing them on sys.path. # --------------------------------------------------------------------------- _RESPONSES_DIR = ( - Path(__file__).parent.parent.parent.parent - / "samples" - / "04-hosting" - / "foundry-hosted-agents" - / "responses" + Path(__file__).parent.parent.parent.parent / "samples" / "04-hosting" / "foundry-hosted-agents" / "responses" ) diff --git a/python/uv.lock b/python/uv.lock index a67c495e62..5e4ae35369 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -2,17 +2,20 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.15' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.15' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -145,24 +148,24 @@ requires-dist = [{ name = "agent-framework-core", extras = ["all"], editable = " [package.metadata.requires-dev] dev = [ - { name = "azure-monitor-opentelemetry", specifier = "==1.8.7" }, + { name = "azure-monitor-opentelemetry", specifier = "==1.8.8" }, { name = "flit", specifier = "==3.12.0" }, - { name = "mcp", extras = ["ws"], specifier = "==1.27.0" }, + { name = "mcp", extras = ["ws"], specifier = "==1.27.2" }, { name = "mypy", specifier = "==1.20.0" }, { name = "opentelemetry-sdk", specifier = "==1.40.0" }, - { name = "poethepoet", specifier = "==0.42.1" }, - { name = "prek", specifier = "==0.3.9" }, + { name = "poethepoet", specifier = "==0.46.0" }, + { name = "prek", specifier = "==0.4.3" }, { name = "pyright", specifier = "==1.1.408" }, { name = "pytest", specifier = "==9.0.3" }, - { name = "pytest-asyncio", specifier = "==1.3.0" }, + { name = "pytest-asyncio", specifier = "==1.4.0" }, { name = "pytest-cov", specifier = "==7.1.0" }, { name = "pytest-retry", specifier = "==1.7.0" }, { name = "pytest-timeout", specifier = "==2.4.0" }, { name = "pytest-xdist", extras = ["psutil"], specifier = "==3.8.0" }, { name = "rich", specifier = ">=13.7.1,<16.0.0" }, - { name = "ruff", specifier = "==0.15.8" }, + { name = "ruff", specifier = "==0.15.15" }, { name = "tomli", specifier = "==2.4.1" }, - { name = "uv", specifier = "==0.11.6" }, + { name = "uv", specifier = "==0.11.17" }, ] [[package]] @@ -457,7 +460,7 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "types-pyyaml", specifier = "==6.0.12.20250915" }] +dev = [{ name = "types-pyyaml", specifier = "==6.0.12.20260518" }] [[package]] name = "agent-framework-devui" @@ -522,7 +525,7 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "types-python-dateutil", specifier = "==2.9.0.20260402" }] +dev = [{ name = "types-python-dateutil", specifier = "==2.9.0.20260518" }] [[package]] name = "agent-framework-foundry" @@ -606,7 +609,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0b2,<=1.0.0b2" }, ] [[package]] @@ -697,16 +700,16 @@ provides-extras = ["gaia", "lightning", "tau2", "math"] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = "==1.20.0" }, - { name = "poethepoet", specifier = "==0.42.1" }, - { name = "prek", specifier = "==0.3.9" }, + { name = "poethepoet", specifier = "==0.46.0" }, + { name = "prek", specifier = "==0.4.3" }, { name = "pyright", specifier = "==1.1.408" }, { name = "pytest", specifier = "==9.0.3" }, { name = "rich", specifier = ">=13.7.1,<15.0.0" }, - { name = "ruff", specifier = "==0.15.8" }, + { name = "ruff", specifier = "==0.15.15" }, { name = "tau2", git = "https://github.com/sierra-research/tau2-bench?rev=5ba9e3e56db57c5e4114bf7f901291f09b2c5619" }, { name = "tomli", specifier = "==2.4.1" }, { name = "tomli-w", specifier = "==1.2.0" }, - { name = "uv", specifier = "==0.11.6" }, + { name = "uv", specifier = "==0.11.17" }, ] [[package]] @@ -1351,7 +1354,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry" -version = "1.8.7" +version = "1.8.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1368,14 +1371,14 @@ dependencies = [ { name = "opentelemetry-resource-detector-azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/42/ea67bebb400a7561b1ad1dd59d06b67e880daf8081ec0d41d3b0ce8fcc26/azure_monitor_opentelemetry-1.8.7.tar.gz", hash = "sha256:d0a430c69451f8fa09362769d2d65471713989fb78e4ad0f50832b597921efbb", size = 76970, upload-time = "2026-03-19T21:43:57.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9e/3e63aa6cf8a46d06090b6f0046da6a59c470ffaf9968430867fa4a3c2eac/azure_monitor_opentelemetry-1.8.8.tar.gz", hash = "sha256:c6478cac82939230e9af1004b0a147e39b9046a564f3811d65241797f2f9d41d", size = 77532, upload-time = "2026-05-14T16:21:44.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/22/245a4f75a834430759a6fab9c5ab10e18719786ae684cf234c7bb6a693d1/azure_monitor_opentelemetry-1.8.7-py3-none-any.whl", hash = "sha256:0d3a228a183d76cf22698a3eed6e836d1cf57608b8ee879c634609b26f384eb2", size = 41268, upload-time = "2026-03-19T21:43:58.188Z" }, + { url = "https://files.pythonhosted.org/packages/ca/31/eb0cbb6771b222fddc520c30a7a8005e8537470131910c089ece37c82655/azure_monitor_opentelemetry-1.8.8-py3-none-any.whl", hash = "sha256:8c0d3095785ca8297d727181bef8dc0341f318fae750ad63af47722bbc56096f", size = 41371, upload-time = "2026-05-14T16:21:45.949Z" }, ] [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b51" +version = "1.0.0b52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1385,9 +1388,9 @@ dependencies = [ { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/a4/a6cd2d389bc1009300bcd57c9e2ace4b7e7ae1e5dc0bda415ee803629cf2/azure_monitor_opentelemetry_exporter-1.0.0b51.tar.gz", hash = "sha256:a6171c34326bcd6216938bb40d715c15f1f22984ac1986fc97231336d8ac4c3c", size = 319837, upload-time = "2026-04-06T21:45:46.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/7e/bfc03436b88c48f5adc21a3ebbf4392b6b7fbbfe33ef3b1e88d07ba9f380/azure_monitor_opentelemetry_exporter-1.0.0b52.tar.gz", hash = "sha256:7eac679fca32dee9e426df65f2a538161db4514fc322fc66107f7826567d86e1", size = 326179, upload-time = "2026-05-11T22:47:02.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/1a/6b0b7a6181b42709103a65a676c89fd5055cb1d1b281ebe10c49254a170f/azure_monitor_opentelemetry_exporter-1.0.0b51-py2.py3-none-any.whl", hash = "sha256:6572cac11f96e3b18ae1187cb35cf3b40d0004655dae8048896c41c765bea530", size = 242104, upload-time = "2026-04-06T21:45:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e8/d13e6a74c98ecc3011bce9ab09fc2e75aec48ab46288f72be57c2fa21460/azure_monitor_opentelemetry_exporter-1.0.0b52-py2.py3-none-any.whl", hash = "sha256:a38c503e5e2cc0ec8a4bf336b23cce23488719f5361a45cdd01a514080f0e7fc", size = 244751, upload-time = "2026-05-11T22:47:04.304Z" }, ] [[package]] @@ -1798,15 +1801,18 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.15' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.15' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -3073,11 +3079,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, ] [[package]] @@ -3445,87 +3451,87 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, - { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, - { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, - { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, - { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, - { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, - { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] [[package]] @@ -3786,7 +3792,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.27.0" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3804,9 +3810,9 @@ dependencies = [ { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" }, ] [package.optional-dependencies] @@ -4292,15 +4298,18 @@ name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.15' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.15' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -4963,15 +4972,18 @@ name = "pandas" version = "3.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.15' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.15' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -5150,11 +5162,11 @@ wheels = [ [[package]] name = "pip" -version = "26.0.1" +version = "26.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, ] [[package]] @@ -5181,16 +5193,16 @@ wheels = [ [[package]] name = "poethepoet" -version = "0.42.1" +version = "0.46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel", 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'" }, { name = "tomli", 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/05/9b/e717572686bbf23e17483389c1bf3a381ca2427c84c7e0af0cdc0f23fccc/poethepoet-0.42.1.tar.gz", hash = "sha256:205747e276062c2aaba8afd8a98838f8a3a0237b7ab94715fab8d82718aac14f", size = 93209, upload-time = "2026-02-26T22:57:50.883Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/f5/d501fcb67e450fd3fae9db06050420c0c6043758cfa8c30ba40278211265/poethepoet-0.46.0.tar.gz", hash = "sha256:daf8469031879ef59ef0b34fdba83574d65e41eb9186e20cd0f7c89ce479b030", size = 117276, upload-time = "2026-05-15T15:52:02.548Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/68/75fa0a5ef39718ea6ba7ab6a3d031fa93640e57585580cec85539540bb65/poethepoet-0.42.1-py3-none-any.whl", hash = "sha256:d8d1345a5ca521be9255e7c13bc2c4c8698ed5e5ac5e9e94890d239fcd423d0a", size = 119967, upload-time = "2026-02-26T22:57:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/af/01/a9d6ea30351919298d3dc237172ae6a60ce0224ecaaee289b671ab979c13/poethepoet-0.46.0-py3-none-any.whl", hash = "sha256:dc6d770a14792d124abac9066c5a707876027d1878ac9ca26cf57e9b2a96dc89", size = 150581, upload-time = "2026-05-15T15:52:01.118Z" }, ] [[package]] @@ -5265,26 +5277,26 @@ wheels = [ [[package]] name = "prek" -version = "0.3.9" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/ff/5b7a2a9c4fa3dd2ffc8b13a9ec22aa550deda5b39ab273f8e02863b12642/prek-0.3.9.tar.gz", hash = "sha256:f82b92d81f42f1f90a47f5fbbf492373e25ef1f790080215b2722dd6da66510e", size = 423801, upload-time = "2026-04-13T12:30:38.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/3b/a0ae60bbd4c4735f20aeddfbd3c50fb669cd8e99c078a3ed75a6a4a5c6d7/prek-0.4.3.tar.gz", hash = "sha256:e486307ea649e7300b3535fac52fe0ba0b80aebe23143b662659d16e6a7c8b47", size = 461800, upload-time = "2026-05-27T03:18:58.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/08/c11a6b7834b461223763b6b1552f32c9199393685d52d555de621e900ee7/prek-0.3.9-py3-none-linux_armv6l.whl", hash = "sha256:3ed793d51bfaa27bddb64d525d7acb77a7c8644f549412d82252e3eb0b88aad8", size = 5337784, upload-time = "2026-04-13T12:30:46.044Z" }, - { url = "https://files.pythonhosted.org/packages/15/d9/974b02832a645c6411069c713e3191ce807f9962006da108e4727efd2fa1/prek-0.3.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:399c58400c0bd0b82a93a3c09dc1bfd88d8d0cfb242d414d2ed247187b06ead1", size = 5713864, upload-time = "2026-04-13T12:30:27.007Z" }, - { url = "https://files.pythonhosted.org/packages/40/e1/4ed14bef15eb30039a75177b0807ac007095a5a110284706ccf900a8d512/prek-0.3.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ea1ffb124e92f081b8e2ca5b5a623a733efb3be0c5b1f4b7ffe2ee17d1f20c", size = 5290437, upload-time = "2026-04-13T12:30:30.658Z" }, - { url = "https://files.pythonhosted.org/packages/67/80/d5c3015e9da161dede566bfeef41f098f92470613157daa4f7377ab08d58/prek-0.3.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aaf639f95b7301639298311d8d44aad0d0b4864e9736083ad3c71ce9765d37ab", size = 5536208, upload-time = "2026-04-13T12:30:47.964Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/8cdc5eb1018437d7828740defd322e7a96459c02fc8961160c4120325313/prek-0.3.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff104863b187fa443ea8451ca55d51e2c6e94f99f00d88784b5c3c4c623f1ebe", size = 5251785, upload-time = "2026-04-13T12:30:39.78Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e2/a5fc35a0fd3167224a000ca1b6235ecbdea0ac77e24af5979a75b0e6b5a4/prek-0.3.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039ecaf87c63a3e67cca645ebd5bc5eb6aafa6c9d929e9a27b2921e7849d7ef9", size = 5668548, upload-time = "2026-04-13T12:30:24.914Z" }, - { url = "https://files.pythonhosted.org/packages/09/e8/a189ee79f401c259f66f8af587f899d4d5bfb04e0ca371bfd01e49871007/prek-0.3.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bde2a3d045705095983c7f78ba04f72a7565fe1c2b4e85f5628502a254754ff", size = 6660927, upload-time = "2026-04-13T12:30:44.495Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5a/54117316e98ff62a14911ad1488a3a0945530242a2ce3e92f7a40b6ccc02/prek-0.3.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0960a21543563e2c8e19aaad176cc8423a87aac3c914d0f313030d7a9244a", size = 5932244, upload-time = "2026-04-13T12:30:49.532Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/e88d4361f59be7adeeb3a8a3819d69d286d86fe6f7606840af6734362675/prek-0.3.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb5d5171d7523271909246ee306b4dc3d5b63752e7dd7c7e8a8908fc9490d1", size = 5542139, upload-time = "2026-04-13T12:30:41.266Z" }, - { url = "https://files.pythonhosted.org/packages/11/1f/204837115087bb8d063bda754a7fe975428c5d5b6548c30dd749f8ab85d4/prek-0.3.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82b791bd36c1430c84d3ae7220a85152babc7eaf00f70adcb961bd594e756ba3", size = 5392519, upload-time = "2026-04-13T12:30:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/bd/00/de57b5795e670b6d38e7eda6d9ac6fd6d757ca22f725e5054b042104cd53/prek-0.3.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:6eac6d2f736b041118f053a1487abed468a70dd85a8688eaf87bb42d3dcecf20", size = 5222780, upload-time = "2026-04-13T12:30:36.576Z" }, - { url = "https://files.pythonhosted.org/packages/f5/14/0bc055c305d92980b151f2ec00c14d28fe94c6d51180ca07fded28771cbf/prek-0.3.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5517e46e761367a3759b3168eabc120840ffbca9dfbc53187167298a98f87dc4", size = 5524310, upload-time = "2026-04-13T12:30:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d1/eebc2b69be0de36cd84adbe0a0710f4deb468a90e30525be027d6db02d54/prek-0.3.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:92024778cf78683ca32687bb249ab6a7d5c33887b5ee1d1a9f6d0c14228f4cf3", size = 6043751, upload-time = "2026-04-13T12:30:29.101Z" }, - { url = "https://files.pythonhosted.org/packages/46/cb/be98c04e702cbc0b0328cd745ff4634ace69ad5a84461bde36f88a7be873/prek-0.3.9-py3-none-win32.whl", hash = "sha256:7f89c55e5f480f5d073769e319924ad69d4bf9f98c5cb46a83082e26e634c958", size = 5045940, upload-time = "2026-04-13T12:30:42.882Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b6/b51771d69f6282e34edeb73f23d956da34f2cabbb5ba16ba175cc0a056f9/prek-0.3.9-py3-none-win_amd64.whl", hash = "sha256:7722f3372eaa83b147e70a43cb7b9fe2128c13d0c78d8a1cdbf2a8ec2ee071eb", size = 5435204, upload-time = "2026-04-13T12:30:51.482Z" }, - { url = "https://files.pythonhosted.org/packages/30/8a/f8a87c15b095460eccd67c8d89a086b7a37aac8d363f89544b8ce6ec653d/prek-0.3.9-py3-none-win_arm64.whl", hash = "sha256:0bced6278d6cc8a4b46048979e36bc9da034611dc8facd77ab123177b833a929", size = 5279552, upload-time = "2026-04-13T12:30:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/df/be/980a0512f7eec3469dd40574f4e35d9ce7b67b358fea58888d13a0625b0d/prek-0.4.3-py3-none-linux_armv6l.whl", hash = "sha256:c67109de8d9766c2afd6e7e64feb9e1a0d3eceb3b4123280c28344660c1a97cd", size = 5541730, upload-time = "2026-05-27T03:19:09.119Z" }, + { url = "https://files.pythonhosted.org/packages/ef/55/937d707cc01d311e5c856c7019bc7db2c5e1835728396bb1ea32a7ecfdfd/prek-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b43a85f5ddf7827a75491e79ca068a49c5e4efde8dbac844ecb89622a78458e4", size = 5906762, upload-time = "2026-05-27T03:19:21.651Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6a/9a99ac481eb148dba55652df88b029ab6c1f90384bd51996026cdab2dafb/prek-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e99ee90a7b6e84dabef891ff7521eb59dae38953467bdb482f004ea522d3a64c", size = 5461541, upload-time = "2026-05-27T03:18:55.984Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8d/9056b02a100cc18b101fc05ecc82635889f5f8cb1cce5d70b027e517a6d9/prek-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f514ec0d95cd4578d74d4601058bd259f5baf91c937f2aaae942d4b070b8077f", size = 5720501, upload-time = "2026-05-27T03:18:52.424Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ea/efbe4523e53022d94272ddfdd3a198ace7de004dd8830a69318085a10393/prek-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03a4ac3c3023a76faa52ad7775720599b10241930be8902c471085b22572b4b0", size = 5452412, upload-time = "2026-05-27T03:19:17.801Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/8a48b2c6a5117110d688c2d8ca2526264ad9f0d3baed4587038ee85e4c2d/prek-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40173425ab82bf0a7267d672b3e3aae9dd425eaee3a3641c6a5f040da3ff95e4", size = 5849515, upload-time = "2026-05-27T03:19:05.545Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/ccce7a1b6c6b610a22b54092d523ea7d35709e42864dace3734c05dd5f98/prek-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b8d99ee3277f8f3a3453a953120ee5c6c52f7ad89e459a25425cf62135f47b1", size = 6743978, upload-time = "2026-05-27T03:19:07.445Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f8/7a441d780c42e858ad677c82bb54eb3f01b424b710a8db5b9a8782305326/prek-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08595fe96d24c1fe13486b00d55ce73a7b37040a16e82365942606594c67a6b", size = 6108774, upload-time = "2026-05-27T03:19:03.565Z" }, + { url = "https://files.pythonhosted.org/packages/bf/38/fbb1afe14c7536109c68a1d9ca602f152f1929972d006c517c3b92140192/prek-0.4.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8607d636ef9232675507d97d252e1dcca5628bff79cb069fa945fff09d7bbb43", size = 5723165, upload-time = "2026-05-27T03:18:59.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/edafebce2bbd85f9e9de2781c225d690eb2b9897a06b224f5c24658fe398/prek-0.4.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:89484765304a779780f83489eb3aed5de5366f47fce7713fa5a917ebc281baa0", size = 5560557, upload-time = "2026-05-27T03:18:49.59Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2e/a85a40458ac50c452cae2ddd2eed0b70107fd2b4074d7a5003088ac508f1/prek-0.4.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:2d2b0c12e3d1c6d90646f9faa2d4c66f9861f3c6e577d7dbd25e733ed095ac56", size = 5417874, upload-time = "2026-05-27T03:19:01.681Z" }, + { url = "https://files.pythonhosted.org/packages/4c/be/106fb026646e1da65da6d2a5f3cfbda817e68a72429645351b7033c0b2b5/prek-0.4.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ca6802eaf191acb6166e9e013dd277ea193ba27c1dca896ab7debf6dca758b6d", size = 5710013, upload-time = "2026-05-27T03:18:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/edfff5f7d9b6c9e5860dfe05c9488e1b96de990b652db2e379d45af8ad2e/prek-0.4.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a46862d81078d2c8caa286c392f965ed72fb72eb1fed171910ba54fe8d546ed0", size = 6230160, upload-time = "2026-05-27T03:19:15.58Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/70adf26d5da0b7a66d8e284a661feddd5e8c69784b82084f40485fa321e4/prek-0.4.3-py3-none-win32.whl", hash = "sha256:f78e343584cfff106fc3c361109b87949ad8028dc5aa667e0fccd26db8170d7d", size = 5226844, upload-time = "2026-05-27T03:19:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/9f21aca8ccee6978db831dbf36c2e17461692c75dd291c9b3d170e39a82a/prek-0.4.3-py3-none-win_amd64.whl", hash = "sha256:798d04437d30d6b4e6c1d520fe6ca800c340c9246f0dc8900d8b365df54b71b6", size = 5616068, upload-time = "2026-05-27T03:19:19.653Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ef/cad8f9c66bcc199e22d1ad82a50032067a4c8b4182306d3472ff99f64aa3/prek-0.4.3-py3-none-win_arm64.whl", hash = "sha256:70d9da5fc14ef41565ff7ba9f476fb53166bf719a954339b2e9f42ed494a2f71", size = 5448057, upload-time = "2026-05-27T03:19:11.118Z" }, ] [[package]] @@ -5888,16 +5900,16 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-asyncio-runner", 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 = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or 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/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] @@ -6477,27 +6489,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.8" +version = "0.15.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, ] [[package]] @@ -6566,15 +6578,18 @@ name = "scikit-learn" version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.15' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.15' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -6691,15 +6706,18 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.15' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version >= '3.15' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -7298,20 +7316,20 @@ wheels = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20260402" +version = "2.9.0.20260518" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/30/c5d9efbff5422b20c9551dc5af237d1ab0c3d33729a9b3239a876ca47dd4/types_python_dateutil-2.9.0.20260402.tar.gz", hash = "sha256:a980142b9966713acb382c467e35c5cc4208a2f91b10b8d785a0ae6765df6c0b", size = 16941, upload-time = "2026-04-02T04:18:35.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/e8/c01bdf0d7c3659428c091fbd693177093639565bcbc86bc20098e6d37cc6/types_python_dateutil-2.9.0.20260518.tar.gz", hash = "sha256:51f02dc03b61c7f6a07df45797d4dfe8a1aa47f0b7db9ad89f6fd3a1a70e1b51", size = 17082, upload-time = "2026-05-18T06:05:24.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/d7/fe753bf8329c8c3c1addcba1d2bf716c33898216757abb24f8b80f82d040/types_python_dateutil-2.9.0.20260402-py3-none-any.whl", hash = "sha256:7827e6a9c93587cc18e766944254d1351a2396262e4abe1510cbbd7601c5e01f", size = 18436, upload-time = "2026-04-02T04:18:34.806Z" }, + { url = "https://files.pythonhosted.org/packages/36/22/169273273ca34e9ab0ae2f387ba72ed7e09faaaf834da01d6b89c2bea71a/types_python_dateutil-2.9.0.20260518-py3-none-any.whl", hash = "sha256:d6a9c5bd0de61460c8fdef8ab2b400f956a1a1075cce08d4e2b4434e478c50b8", size = 18431, upload-time = "2026-05-18T06:05:23.641Z" }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250915" +version = "6.0.12.20260518" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, ] [[package]] @@ -7379,28 +7397,28 @@ wheels = [ [[package]] name = "uv" -version = "0.11.6" +version = "0.11.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/8e/ec34c19d0f254fcbcc5c1ce8c7f06e47e0f69a7e1a0269c1d59cb0b0f279/uv-0.11.17.tar.gz", hash = "sha256:1d1be74deec997db1dda05a7e67541c904d65cbfd72e455d3c0a2a1e4bf2cddf", size = 4203607, upload-time = "2026-05-28T20:39:47.707Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" }, - { url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" }, - { url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" }, - { url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" }, - { url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" }, - { url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" }, - { url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" }, - { url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" }, - { url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" }, - { url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" }, - { url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" }, - { url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" }, + { url = "https://files.pythonhosted.org/packages/15/2e/e6d42f9d39009eee976f1e5dfd31d3d1943e6e593ad7b191cf11e9744a36/uv-0.11.17-py3-none-linux_armv6l.whl", hash = "sha256:8426bfe315564d414cbc5ba5467595dc6348965e19acec742914f47da3ff269f", size = 23551216, upload-time = "2026-05-28T20:39:05.395Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ee/d72bcc60f3585653a4b768425854d737d98d65c1765547d25c2999547ea9/uv-0.11.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6d1a033cc68cabb4141d6c1e3b66ffc6e970b98ba42e210f33270251e0bd8697", size = 22997377, upload-time = "2026-05-28T20:39:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/58/34/1bc69798d9ae998fbc42c61b02883f2ba00d04bdd858e589604d01846287/uv-0.11.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58c07ffc272c847d29cd98ca5082fa4304a645f87c718ec900e3cca9026bd096", size = 21630197, upload-time = "2026-05-28T20:39:28.935Z" }, + { url = "https://files.pythonhosted.org/packages/6b/93/1be48ec6a8933d9a77d0ce5240ed63f68869f68517ccf5d62268ed03f3e8/uv-0.11.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:036d6e2940afe8b79637530b01b9241d8cfd174b07f1179a1ebbd42409c38ca3", size = 23414940, upload-time = "2026-05-28T20:39:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/00/31/b7488ff49d80090ea9d05d67a4d381a1b4479502e9853e654caa1c1c678e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:283186700c3e65a4644a73a917232da7d3e4a94d25ea0377a44f5b263fa49577", size = 23096330, upload-time = "2026-05-28T20:39:01.284Z" }, + { url = "https://files.pythonhosted.org/packages/fe/95/42b6137c5de06278d229c7eef2f314df2a738cd799795bbb44dace21bd6e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2e44dfbfc7778d0d90edc6738f237c91e5e37e4e3cfe94c8a312cec56a41485", size = 23101906, upload-time = "2026-05-28T20:39:17.149Z" }, + { url = "https://files.pythonhosted.org/packages/17/7c/0ca03b2d19965db6d5dfe0c8cf96a3d0b424503c8cbc3cd2ffdc5869a15d/uv-0.11.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a817eeb3026f27a53d3f4b7855a5105f6787dd192140e201eda4d2b9a11b72e", size = 24444409, upload-time = "2026-05-28T20:39:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fb/179f55a3b19d47c30ec1f41b9b964da74dfa7053ff310a70a9c4d8cb998d/uv-0.11.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf8f5ad959583dcd2c4ae445c754a97c05700246ff89259f3fd285c9c20f4c00", size = 25540153, upload-time = "2026-05-28T20:39:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/f7/29/592f42012765c43ae45c112110e214bca7b0cfc08c4c1b52e1dfa47dedd5/uv-0.11.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce16892a45134d20165c1ceababe06f3e9ce6a58902db1eff812c8c93626823f", size = 24665906, upload-time = "2026-05-28T20:39:41.254Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/b75808766f895248553c6370968509cd4f726e6943e310a8f7a171036ad0/uv-0.11.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da839e5a491c9a701d7d327a199cafc76ac27a03ac84fd2a8d4bf32c3af2448", size = 24863325, upload-time = "2026-05-28T20:39:51.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6a/6f27ee69e97f480104bb8ec335f04c2a12add98edfcc4844a68e9538b6e2/uv-0.11.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ec004b3c9bf9cb7756067ad1bd0bf64eb843e6fa2edbfbb3135ee152c14cea91", size = 23521674, upload-time = "2026-05-28T20:38:55.869Z" }, + { url = "https://files.pythonhosted.org/packages/df/11/1344aca7c710f794750f74de0e552a54ab24193ecc01fa3b3ae22ff822a1/uv-0.11.17-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:659227cac719b618cc91e02be9e274ad5bd72d74fa278123e6373537e9f28216", size = 24224725, upload-time = "2026-05-28T20:39:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/7b11550c1453ea13b81e549c83523e6ab6ed3231d09b2fd6b9eb19acceaf/uv-0.11.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e301d844eed9401f0f0351de12c55f1306ca05372acb0f28d35717c8ba663a22", size = 24301643, upload-time = "2026-05-28T20:39:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/1a/36/8f683bc60547b8f93d0e752a8574d13fad776999cb978482b360c053ca22/uv-0.11.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f0bf483c0d9fa14283992d56061b498b9d3d4adebd285af8744dc33f64dadfba", size = 23786049, upload-time = "2026-05-28T20:39:20.999Z" }, + { url = "https://files.pythonhosted.org/packages/10/dc/7a495db39c2970de4fa375c337dbd617b16780911f88f0511f8fe7f6747c/uv-0.11.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2ccd5487a4a192bc832ea04c867a26883757db8fdfe88bed85d8129c82f9e505", size = 25049786, upload-time = "2026-05-28T20:40:03.292Z" }, + { url = "https://files.pythonhosted.org/packages/37/dd/74eff72d749eaf7e19f489878e21a368a7fef58d26ea0c63ec044ecd78b1/uv-0.11.17-py3-none-win32.whl", hash = "sha256:12b701fa32c5be3691759a73956e4462f30fa7b0dfa52ec66cb305bbb6ea4129", size = 22479213, upload-time = "2026-05-28T20:39:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/8af4a92b99a8a4823297c26df727fe957267e03e1196e3caa803c3f6ccb2/uv-0.11.17-py3-none-win_amd64.whl", hash = "sha256:44ec1fe3af839f87370dcf0400c0cab917cc1ce697d563e860fc7d9ed72655e7", size = 25083161, upload-time = "2026-05-28T20:40:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/00/76/a689077832d585d29d87f9cd0d65eca1af58abd29a4eab004d0a8a858b9c/uv-0.11.17-py3-none-win_arm64.whl", hash = "sha256:37c915bfcf86f99c1c5be7c9ed21e0d80624067ba47bc8916a3cb0530bc94d27", size = 23544936, upload-time = "2026-05-28T20:39:37.137Z" }, ] [[package]] From b298113d15cf5c7e8a7299991cea98ef0400dbbf Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Mon, 1 Jun 2026 11:43:45 -0700 Subject: [PATCH 29/61] .NET - Fix missing id on function_call_output in Foundry Hosting (#6246) * Fix missing id on function_call_output in Foundry Hosting The Foundry storage layer was rejecting responses with "ID cannot be null or empty (Parameter 'id')" because function_call_output items emitted by OutputConverter had no id on the wire. OutputItemFunctionToolCallOutput's public ctor only sets CallId and Output; Id is read-only and only the SDK's internal ctor populates it. OutputItemBuilder.ApplyAutoStamps fills ResponseId and AgentReference but not Id, so the itemId passed to AddOutputItem(itemId) was used only for event sequencing and the serialized item went out with id=null. Switch to stream.OutputItemFunctionCallOutput(callId, output), the SDK convenience method that uses the internal ctor and stamps the id. Add a regression test asserting the added/done events carry a non-empty matching Id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: free disk space and relocate NuGet cache on ubuntu runners The ubuntu-latest dotnet-build/test jobs were hitting No space left on device because the runner image only ships ~14 GB free on /. The full multi-TFM build plus the dotnet pack + console-app install-check exhausts that easily. Add a reusable composite action .github/actions/free-runner-disk-space that runs on Linux runners only and: * removes pre-installed toolchains we never use here (Android SDK, GHC/Haskell, CodeQL, PyPy, Ruby, Go, boost, vcpkg, etc.), prunes docker images, and disables swap (reclaims ~25-30 GB on /) * relocates the NuGet package cache to /mnt/nuget via NUGET_PACKAGES env, since /mnt has ~75 GB free on hosted runners Wire the action into the four ubuntu-touching jobs in dotnet-build-and-test.yml (dotnet-build, dotnet-test, dotnet-foundry-hosted-it, dotnet-test-functions). The action self-guards with runner.os == 'Linux' so the matrix legs that run on windows are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <25218250+alliscode@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../actions/free-runner-disk-space/action.yml | 64 +++++++++++++++++++ .github/workflows/dotnet-build-and-test.yml | 12 ++++ .../OutputConverter.cs | 17 +++-- .../OutputConverterTests.cs | 29 +++++++++ 4 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 .github/actions/free-runner-disk-space/action.yml diff --git a/.github/actions/free-runner-disk-space/action.yml b/.github/actions/free-runner-disk-space/action.yml new file mode 100644 index 0000000000..c534b1c2d8 --- /dev/null +++ b/.github/actions/free-runner-disk-space/action.yml @@ -0,0 +1,64 @@ +name: Free runner disk space +description: | + Reclaims disk space on GitHub-hosted Ubuntu runners by removing + pre-installed toolchains we do not use (Android SDK, GHC/Haskell, + CodeQL bundle), Docker images, and swap. Also relocates the + NuGet package cache to /mnt (which has ~75 GB free vs ~14 GB + on /). No-op on non-Linux runners. + +runs: + using: composite + steps: + - name: Free disk space (Linux only) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + echo "::group::Disk usage before cleanup" + df -h / + echo "::endgroup::" + + # Remove pre-installed toolchains we never use on this repo's + # dotnet/python jobs. These reclaim ~25-30 GB on ubuntu-latest. + sudo rm -rf \ + /usr/local/lib/android \ + /usr/share/dotnet/sdk/NuGetFallbackFolder \ + /opt/ghc \ + /usr/local/.ghcup \ + /opt/hostedtoolcache/CodeQL \ + /opt/hostedtoolcache/PyPy \ + /opt/hostedtoolcache/Ruby \ + /opt/hostedtoolcache/go \ + /usr/local/share/boost \ + /usr/local/share/powershell \ + /usr/local/share/chromium \ + /usr/local/share/vcpkg \ + /usr/local/lib/heroku \ + "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/PyPy" \ + "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/Ruby" \ + "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/go" || true + + # Drop docker images shipped on the runner; jobs that need + # docker pull what they need fresh. + if command -v docker >/dev/null 2>&1; then + sudo docker image prune --all --force >/dev/null 2>&1 || true + fi + + # Disable swap to free its backing file. + sudo swapoff -a || true + sudo rm -f /mnt/swapfile /swapfile || true + + echo "::group::Disk usage after cleanup" + df -h / + echo "::endgroup::" + + - name: Relocate NuGet package cache to /mnt (Linux only) + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo mkdir -p /mnt/nuget + sudo chown -R "$USER":"$USER" /mnt/nuget + echo "NUGET_PACKAGES=/mnt/nuget" >> "$GITHUB_ENV" + echo "Relocated NuGet package cache to /mnt/nuget" + df -h /mnt || true diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 8fe1fbf176..6582240a5c 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -121,6 +121,9 @@ jobs: python declarative-agents + - name: Free runner disk space + uses: ./.github/actions/free-runner-disk-space + - name: Setup dotnet uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: @@ -191,6 +194,9 @@ jobs: python declarative-agents + - name: Free runner disk space + uses: ./.github/actions/free-runner-disk-space + # Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened) - name: Start Azure Cosmos DB Emulator if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }} @@ -365,6 +371,9 @@ jobs: dotnet python + - name: Free runner disk space + uses: ./.github/actions/free-runner-disk-space + - name: Setup dotnet uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: @@ -452,6 +461,9 @@ jobs: python declarative-agents + - name: Free runner disk space + uses: ./.github/actions/free-runner-disk-space + - name: Setup dotnet uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs index fe16edeb3b..1006f0fdb4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs @@ -281,14 +281,19 @@ internal static class OutputConverter var outputText = EncodeFunctionResultAsJsonStringPayload(functionResult.Result); - var itemId = GenerateItemId("fc"); - var outputItem = new OutputItemFunctionToolCallOutput( + // Use the SDK's convenience method so the OutputItemFunctionToolCallOutput + // is constructed with a populated Id. The public OutputItemFunctionToolCallOutput + // ctor only sets CallId/Output (Id is read-only), and AddOutputItem+EmitAdded + // does not auto-stamp Id — only ResponseId/AgentReference. Without this, the + // serialized item arrives at the Foundry storage layer with id=null and is + // rejected with "ID cannot be null or empty (Parameter 'id')". + foreach (var evt in stream.OutputItemFunctionCallOutput( functionResult.CallId, - BinaryData.FromString(outputText)); + BinaryData.FromString(outputText))) + { + yield return evt; + } - var outputBuilder = stream.AddOutputItem(itemId); - yield return outputBuilder.EmitAdded(outputItem); - yield return outputBuilder.EmitDone(outputItem); break; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs index 4103517a10..f8cc4402ca 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs @@ -704,6 +704,35 @@ public class OutputConverterTests Assert.Equal("[{\"id\":1}]", inner); } + // K-06e: Regression — the OutputItemFunctionToolCallOutput must have a populated Id + // and a matching wire id on the added/done events. The Foundry storage layer extracts + // a partition id from this field and throws "ID cannot be null or empty (Parameter 'id')" + // when it is missing. + [Fact] + public async Task ConvertUpdatesToEventsAsync_FunctionResult_OutputItemHasIdAsync() + { + var (stream, _) = CreateTestStream(); + var update = new AgentResponseUpdate { Contents = [new FunctionResultContent("call_1", "sunny")] }; + + var events = new List(); + await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream)) + { + events.Add(evt); + } + + var added = Assert.Single(events.OfType()); + var done = Assert.Single(events.OfType()); + + var addedOutput = Assert.IsType(added.Item); + var doneOutput = Assert.IsType(done.Item); + + Assert.False(string.IsNullOrEmpty(addedOutput.Id)); + Assert.False(string.IsNullOrEmpty(doneOutput.Id)); + Assert.Equal(addedOutput.Id, doneOutput.Id); + Assert.Equal("call_1", addedOutput.CallId); + Assert.Equal("call_1", doneOutput.CallId); + } + // L-01 [Fact] public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflowActionItemAsync() From 03e14ca18703a822f8d1b209a031d65aa882d9be Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Mon, 1 Jun 2026 14:27:29 -0700 Subject: [PATCH 30/61] .NET: Update hosted agents (#6243) * Updating to latest Foundry hosting packages. * Re-applying .gitignore. * Adding empty line at end of .gitignore --------- Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com> --- .gitignore | 1 + SUPPORT.md | 34 +++++++++---------- .../01_SingleAgent/local.settings.json | 10 ------ .../local.settings.json | 10 ------ .../local.settings.json | 10 ------ .../local.settings.json | 10 ------ .../local.settings.json | 10 ------ .../06_LongRunningTools/local.settings.json | 10 ------ .../07_AgentAsMcpTool/local.settings.json | 10 ------ .../08_ReliableStreaming/local.settings.json | 12 ------- .../01_SequentialWorkflow/local.settings.json | 10 ------ .../02_ConcurrentWorkflow/local.settings.json | 10 ------ .../03_WorkflowHITL/local.settings.json | 10 ------ .../04_WorkflowMcpTool/local.settings.json | 8 ----- .../05_WorkflowAndAgents/local.settings.json | 10 ------ .../keycloak/setup-redirect-uris.sh | 0 .../Microsoft.Agents.AI.Foundry.csproj | 2 ++ .../scripts/it-bootstrap-agents.ps1 | 12 +++---- .../Properties/launchSettings.json | 12 ------- .../Properties/launchSettings.json | 12 ------- .../Properties/launchSettings.json | 12 ------- 21 files changed, 26 insertions(+), 189 deletions(-) delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json delete mode 100644 dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json mode change 100755 => 100644 dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/setup-redirect-uris.sh delete mode 100644 dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 07eee848e2..9cb714813a 100644 --- a/.gitignore +++ b/.gitignore @@ -248,3 +248,4 @@ dotnet/filtered-*.slnx .omx/ **/issues/ +.test_* diff --git a/SUPPORT.md b/SUPPORT.md index a95ac5c597..1ed5d443f4 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,17 +1,17 @@ -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please create a GitHub issue. - -AI Support team will support Microsoft Agent Framework issues for customers under a **Unified support agreement when the issue arises from usage of Azure AI services** (Foundry Models, Foundry Agents etc.) in conjunction with the SDK. Conversely, if customer has any other / non unified support agreement and/or Agent Framework SDK is used in a way **not involving an Azure service**, it is treated as a purely open-source tool – Microsoft’s support organization will not handle it, and users should use GitHub or forums for assistance - -For Copilot Studio SDK implementation issues, customers should use GitHub Issues for assistance, as outlined above. Conversely, for prerequisites managed within the Copilot Studio portal, customers can rely on the standard Microsoft Copilot Studio support channels. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +For help and questions about using this project, please create a GitHub issue. + +AI Support team will support Microsoft Agent Framework issues for customers under a **Unified support agreement when the issue arises from usage of Azure AI services** (Foundry Models, Foundry Agents etc.) in conjunction with the SDK. Conversely, if customer has any other / non unified support agreement and/or Agent Framework SDK is used in a way **not involving an Azure service**, it is treated as a purely open-source tool – Microsoft’s support organization will not handle it, and users should use GitHub or forums for assistance + +For Copilot Studio SDK implementation issues, customers should use GitHub Issues for assistance, as outlined above. Conversely, for prerequisites managed within the Copilot Studio portal, customers can rely on the standard Microsoft Copilot Studio support channels. + +## Microsoft Support Policy + +Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/local.settings.json b/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/local.settings.json deleted file mode 100644 index 71e7ff8dac..0000000000 --- a/dotnet/samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/local.settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "", - "REDIS_CONNECTION_STRING": "localhost:6379", - "REDIS_STREAM_TTL_MINUTES": "10" - } -} diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json deleted file mode 100644 index fcb6658e92..0000000000 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/local.settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" - } -} diff --git a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json b/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json deleted file mode 100644 index 5f6d7d3340..0000000000 --- a/dotnet/samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/local.settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None", - "AZURE_OPENAI_ENDPOINT": "", - "AZURE_OPENAI_DEPLOYMENT_NAME": "" - } -} diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/setup-redirect-uris.sh b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/setup-redirect-uris.sh old mode 100755 new mode 100644 diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj index 413a8a397f..d2c529e317 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj @@ -24,11 +24,13 @@ + + diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1 b/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1 index 07a276b9f0..1719fa9ffb 100644 --- a/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1 +++ b/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1 @@ -6,11 +6,11 @@ .DESCRIPTION The IT fixture targets stable, scenario-keyed agent names (e.g. it-happy-path) and only manages versions on each test run. The agent itself must already exist AND its managed - identity must hold the Azure AI User role on the project scope, otherwise inbound + identity must hold the Foundry User role on the project scope, otherwise inbound inference calls fail with HTTP 500 PermissionDenied. This script idempotently creates each scenario agent (with a placeholder version) and - grants Azure AI User on the project to its managed identity. Re-run it safely; existing + grants Foundry User on the project to its managed identity. Re-run it safely; existing agents and role assignments are left in place. .PARAMETER ProjectEndpoint @@ -135,20 +135,20 @@ foreach ($scenario in $Scenarios) { -Body $patchBody | Out-Null } - # 3. Grant Azure AI User on the project scope to the agent MI (idempotent). + # 3. Grant Foundry User on the project scope to the agent MI (idempotent). $existing = az role assignment list --assignee $principalId --scope $projectScope ` - --query "[?roleDefinitionName=='Azure AI User']" 2>$null | ConvertFrom-Json + --query "[?roleDefinitionName=='Foundry User']" 2>$null | ConvertFrom-Json if ($existing) { Write-Host " role already assigned" } else { - Write-Host " granting Azure AI User..." + Write-Host " granting Foundry User..." $maxAttempts = 12 $granted = $false for ($i = 1; $i -le $maxAttempts; $i++) { $output = az role assignment create ` --assignee-object-id $principalId ` --assignee-principal-type ServicePrincipal ` - --role 'Azure AI User' ` + --role 'Foundry User' ` --scope $projectScope 2>&1 if ($LASTEXITCODE -eq 0) { $granted = $true diff --git a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json deleted file mode 100644 index 783215ce29..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 deleted file mode 100644 index 6b8f8d04a4..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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.OpenAI.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Properties/launchSettings.json deleted file mode 100644 index 099bd7018e..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Microsoft.Agents.AI.Hosting.OpenAI.UnitTests": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:60491;http://localhost:60492" - } - } -} \ No newline at end of file From f36096ce1a0258d62810561df5aeedd22e2448c0 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Tue, 2 Jun 2026 06:41:52 +0900 Subject: [PATCH 31/61] Python: Fix core observability unsafe serialization of function-call arguments containing dataclass/framework objects (#6026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: safely serialize function-call arguments in core observability Apply make_json_safe() to content.arguments in _to_otel_part() before building the otel message dict, so that dataclass/framework payloads (e.g. workflow request_info events) do not cause a TypeError when _capture_messages() calls json.dumps(). Lift make_json_safe() into agent_framework._serialization (no new external deps — dataclasses/datetime only) so the core observability path can use it without a dependency on the ag-ui adapter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(core): safely serialize workflow request_info payloads in observability (#5733) - Add make_json_safe() helper to recursively convert non-serializable objects - Use make_json_safe() in _to_otel_part() for function_call arguments - Fix CustomPayload test class to use @dataclass (resolves B903 lint error) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(serialization): guard callability and normalize dict keys in make_json_safe (#5733) - Use callable(getattr(obj, method, None)) instead of hasattr() so that non-callable attributes named model_dump/to_dict/dict do not raise TypeError at runtime. - Wrap each call in try/except TypeError to handle callables with mandatory arguments gracefully. - Convert dict keys to str() so that non-string keys (e.g. datetime, int) cannot cause json.dumps to raise TypeError. - Add regression tests for both scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address observability serialization review feedback --------- Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core/agent_framework/_serialization.py | 45 ++++++++ .../core/agent_framework/_workflows/_agent.py | 3 +- .../agent_framework/_workflows/_functional.py | 3 +- .../core/tests/core/test_observability.py | 102 ++++++++++++++++++ .../workflow/test_functional_workflow.py | 32 ++++++ .../tests/workflow/test_workflow_agent.py | 30 ++++++ 6 files changed, 213 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index ccd28e7f76..d9fdd618f7 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -7,6 +7,8 @@ import json import logging import re from collections.abc import Mapping, MutableMapping +from dataclasses import asdict, is_dataclass +from datetime import date, datetime from typing import Any, ClassVar, Protocol, TypeVar, runtime_checkable logger = logging.getLogger("agent_framework") @@ -614,3 +616,46 @@ class SerializationMixin: # Fallback and default # Convert class name to snake_case return _CAMEL_TO_SNAKE_PATTERN.sub("_", cls.__name__).lower() + + +def make_json_safe(obj: Any) -> Any: + """Recursively convert an object to a JSON-serializable form. + + Handles dataclasses, Pydantic models, objects with ``to_dict``/``dict``/``__dict__``, + datetimes, lists, dicts, and primitives. Falls back to ``str()`` for any remaining + non-serializable value so that ``json.dumps`` never raises a ``TypeError``. + + Args: + obj: Object to make JSON safe. + + Returns: + A JSON-serializable version of the object. + """ + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + if isinstance(obj, (datetime, date)): + return obj.isoformat() + if is_dataclass(obj) and not isinstance(obj, type): + return make_json_safe(asdict(obj)) # type: ignore[arg-type] + if callable(getattr(obj, "model_dump", None)): + try: + return make_json_safe(obj.model_dump()) # type: ignore[no-any-return] + except TypeError: + pass + if callable(getattr(obj, "to_dict", None)): + try: + return make_json_safe(obj.to_dict()) # type: ignore[no-any-return] + except TypeError: + pass + if callable(getattr(obj, "dict", None)): + try: + return make_json_safe(obj.dict()) # type: ignore[no-any-return] + except TypeError: + pass + if isinstance(obj, dict): + return {str(key): make_json_safe(value) for key, value in obj.items()} # type: ignore[misc] + if isinstance(obj, (list, tuple)): + return [make_json_safe(item) for item in obj] # type: ignore[misc] + if hasattr(obj, "__dict__"): + return {key: make_json_safe(value) for key, value in vars(obj).items()} # type: ignore[misc] + return str(obj) diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 2d9b37e1f5..7b3bdbb911 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -12,6 +12,7 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, overload from .._agents import BaseAgent +from .._serialization import make_json_safe from .._sessions import ( AgentSession, ContextProvider, @@ -61,7 +62,7 @@ class WorkflowAgent(BaseAgent): data: Any def to_dict(self) -> dict[str, Any]: - return {"request_id": self.request_id, "data": self.data} + return {"request_id": self.request_id, "data": make_json_safe(self.data)} def to_json(self) -> str: return json.dumps(self.to_dict()) diff --git a/python/packages/core/agent_framework/_workflows/_functional.py b/python/packages/core/agent_framework/_workflows/_functional.py index 5746c2161c..73c0815862 100644 --- a/python/packages/core/agent_framework/_workflows/_functional.py +++ b/python/packages/core/agent_framework/_workflows/_functional.py @@ -47,6 +47,7 @@ from copy import deepcopy from typing import Any, Generic, Literal, TypeVar, overload from .._feature_stage import ExperimentalFeature, experimental +from .._serialization import make_json_safe from .._types import AgentResponse, AgentResponseUpdate, ResponseStream from ..observability import OtelAttr, capture_exception, create_workflow_span from ._checkpoint import CheckpointStorage, WorkflowCheckpoint @@ -1515,7 +1516,7 @@ class FunctionalWorkflowAgent: function_call = Content.from_function_call( call_id=request_id, name=self.REQUEST_INFO_FUNCTION_NAME, - arguments={"request_id": request_id, "data": event.data}, + arguments={"request_id": request_id, "data": make_json_safe(event.data)}, ) return Content.from_function_approval_request( id=request_id, diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 4b48226e63..372cb8a7dd 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -1691,6 +1691,65 @@ def test_to_otel_part_function_call(): } +def test_to_otel_part_function_call_reuses_prepared_arguments(): + """Test _to_otel_part does not re-serialize function-call arguments in the observability hot path.""" + from agent_framework import Content + from agent_framework.observability import _to_otel_part + + arguments = {"payload": object()} + content = Content(type="function_call", call_id="call_789", name="handoff", arguments=arguments) + result = _to_otel_part(content) + + assert result is not None + assert result["arguments"] is arguments + + +def test_make_json_safe_non_callable_method_attribute(): + """Test make_json_safe handles objects where model_dump/to_dict/dict are non-callable attributes.""" + from agent_framework._serialization import make_json_safe + + class ObjWithNonCallableModelDump: + model_dump = 42 # not callable + + obj = ObjWithNonCallableModelDump() + result = make_json_safe(obj) + assert result == {} + + +def test_make_json_safe_callable_method_type_error_falls_through(): + """Test make_json_safe falls through when serializer-like methods require arguments.""" + from agent_framework._serialization import make_json_safe + + class ObjWithRequiredArgModelDump: + def __init__(self) -> None: + self.value = "fallback" + + def model_dump(self, required: str) -> dict[str, str]: + return {"required": required} + + obj = ObjWithRequiredArgModelDump() + result = make_json_safe(obj) + assert result == {"value": "fallback"} + + +def test_make_json_safe_dict_with_non_string_keys(): + """Test make_json_safe converts non-primitive dict keys to strings.""" + import json + from datetime import datetime + + from agent_framework._serialization import make_json_safe + + dt_key = datetime(2024, 1, 1) + obj = {dt_key: "value", 42: "num_value", "str_key": "normal"} + result = make_json_safe(obj) + # json.dumps must not raise TypeError + serialized = json.dumps(result) + parsed = json.loads(serialized) + assert parsed[str(dt_key)] == "value" + assert parsed["42"] == "num_value" + assert parsed["str_key"] == "normal" + + def test_to_otel_part_function_result(): """Test _to_otel_part with function_result content.""" from agent_framework import Content @@ -3019,6 +3078,49 @@ async def test_system_instructions_preserves_non_ascii_characters(span_exporter: assert [msg.get("role") for msg in input_messages] == ["user"] +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +def test_capture_messages_with_prepared_request_info_function_call_arguments(span_exporter: InMemorySpanExporter): + """Test _capture_messages handles request-info function-call arguments prepared at Content creation.""" + import dataclasses + import json + + from opentelemetry import trace + + from agent_framework import WorkflowAgent + + @dataclasses.dataclass + class HandoffRequest: + target_agent: str + reason: str + + arguments = WorkflowAgent.RequestInfoFunctionArgs( + request_id="call_dc", + data=HandoffRequest(target_agent="helper", reason="overflow"), + ).to_dict() + msg = Message( + role="assistant", + contents=[ + Content( + type="function_call", + call_id="call_dc", + name="request_info", + arguments=arguments, + ) + ], + ) + span_exporter.clear() + tracer = trace.get_tracer("test") + with tracer.start_as_current_span("test_span") as span: + _capture_messages(span=span, provider_name="test_provider", messages=[msg]) + + spans = span_exporter.get_finished_spans() + span = spans[0] + input_messages = json.loads(span.attributes[OtelAttr.INPUT_MESSAGES]) + tool_part = input_messages[0]["parts"][0] + assert tool_part["type"] == "tool_call" + assert tool_part["arguments"]["data"] == {"target_agent": "helper", "reason": "overflow"} + + def test_capture_messages_keeps_framework_instructions_out_of_logs_and_span_messages( span_exporter: InMemorySpanExporter, ): diff --git a/python/packages/core/tests/workflow/test_functional_workflow.py b/python/packages/core/tests/workflow/test_functional_workflow.py index 6502a0e353..d52c5497f9 100644 --- a/python/packages/core/tests/workflow/test_functional_workflow.py +++ b/python/packages/core/tests/workflow/test_functional_workflow.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio +import json import logging from collections.abc import Iterator from contextlib import contextmanager @@ -1642,6 +1643,37 @@ class TestFunctionalWorkflowAgentHITL: break assert approval_found, "expected FunctionApprovalRequestContent in agent response" + async def test_request_info_dataclass_arguments_are_serialized_for_agent(self): + @dataclass + class HandoffRequest: + target_agent: str + reason: str + + @workflow + async def wf(x: str, ctx: RunContext) -> str: + answer = await ctx.request_info( + HandoffRequest(target_agent=x, reason="overflow"), + response_type=str, + request_id="rid-1", + ) + return f"got:{answer}" + + agent = wf.as_agent() + response = await agent.run("helper") + + function_call_arguments = None + for message in response.messages: + for content in message.contents: + if getattr(content, "type", None) == "function_approval_request" and content.function_call is not None: + function_call_arguments = content.function_call.arguments + break + + assert function_call_arguments == { + "request_id": "rid-1", + "data": {"target_agent": "helper", "reason": "overflow"}, + } + assert json.loads(json.dumps(function_call_arguments)) == function_call_arguments + async def test_resume_via_agent_responses_kwarg(self): @workflow async def wf(x: str, ctx: RunContext) -> str: diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 3dcdd26c86..c473fdaaf8 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -1,7 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. +import json import uuid from collections.abc import Awaitable, Sequence +from dataclasses import dataclass from typing import Any, Literal, overload import pytest @@ -23,6 +25,7 @@ from agent_framework import ( WorkflowAgent, WorkflowBuilder, WorkflowContext, + WorkflowEvent, executor, handler, response_handler, @@ -293,6 +296,33 @@ class TestWorkflowAgent: # Verify cleanup - pending requests should be cleared after function response handling assert len(agent.pending_requests) == 0 + def test_request_info_dataclass_arguments_are_serialized_when_content_is_created(self) -> None: + """Test WorkflowAgent prepares request_info arguments before observability captures messages.""" + + @dataclass + class HandoffRequest: + target_agent: str + reason: str + + executor = SimpleExecutor(id="executor1", response_text="Response") + workflow = WorkflowBuilder(start_executor=executor).build() + agent = WorkflowAgent(workflow=workflow, name="Request Test Agent") + event = WorkflowEvent.request_info( + request_id="request_123", + source_executor_id="executor1", + request_data=HandoffRequest(target_agent="helper", reason="overflow"), + response_type=str, + ) + + function_call, approval_request = agent._process_request_info_event(event) # pyright: ignore[reportPrivateUsage] + + assert function_call.arguments == { + "request_id": "request_123", + "data": {"target_agent": "helper", "reason": "overflow"}, + } + assert approval_request.function_call is function_call + assert json.loads(json.dumps(function_call.arguments)) == function_call.arguments + def test_workflow_as_agent_method(self) -> None: """Test that Workflow.as_agent() creates a properly configured WorkflowAgent.""" # Create a simple workflow From e0d0ad16a06b404ef7da38e31b2bcaecf2ac6efe Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Mon, 1 Jun 2026 16:01:56 -0700 Subject: [PATCH 32/61] Python: feat(evals): Foundry Adaptive Evals integration (rubric-generation) (#6101) * Python: feat(evals): RubricScore type + EvalScoreResult.dimensions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: feat(foundry-evals): RubricDimension + GeneratedEvaluatorRef + accept in evaluators= Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: feat(evals): parse rubric_scores from output items + assertion helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: feat(evals): BaseAgent.as_eval_source / Workflow.as_eval_source Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: feat(foundry-evals): EvalGenerationSource + generate_rubric helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: feat(foundry-evals): YAML config loader + sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix(evals): address PR review feedback Addresses 4 Copilot review comments on PR #6101: 1. assert_dimension_score_at_least: drop the (not evaluator or found_any) guard so require_applicable=True correctly raises when the named evaluator produces no entries for the dimension. Adds TestRubricAssertions covering the regression. 2. GeneratedEvaluatorRef docstring: reword to describe actual behaviour (pinning recommended, not required) so it matches the dataclass default and FoundryEvals warning path. 3. _poll_generation_job: switch from asyncio.get_event_loop() to get_running_loop() and bound the per-iteration sleep by remaining time, matching _poll_eval_run. 4. generate_rubric: type category as Literal['quality','safety'] and validate at the entry point with a ValueError; drop the silent 'invalid -> quality' rewrite in _generation_job_to_ref. Adds a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: feat(foundry-evals): hosted-agent-aware rubric generation * Auto-detect hosted Foundry agents in agent_as_eval_source: when the agent's chat_client exposes a string agent_name (the convention used by RawFoundryAgentChatClient for PromptAgents/HostedAgents), emit a type='agent' EvalGenerationSource so the service fetches instructions and tools from the agent registry instead of relying on the local wrapper (which holds neither for hosted agents). * Add hosted_agent_version kwarg and a new agent_version field on EvalGenerationSource so PromptAgent runs can pin to a specific hosted version for reproducible rubric generation. * Add force_prompt_source escape hatch to bypass auto-detection and always emit a rendered prompt dossier - useful when the local wrapper carries overrides the service-side agent doesnt see. * Fix _to_sdk_source for dataset sources: SDK ctor takes name=/version=, not dataset_name=/dataset_version=. The mismatch would raise TypeError against the real azure-ai-projects 2.3.0a* SDK; only unmocked integration paths were affected. Tests cover: auto-detection happy path, versionless hosted agent, explicit hosted_agent_version forwarding, force_prompt_source override, non-string chat_client attrs (MagicMock test doubles) not mis-detected, agent_version forwarded through _to_sdk_source, and the corrected dataset SDK kwarg names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry-evals): accept canonical dimension_scores key per docs The published Foundry rubric-evaluator output (Microsoft Learn 'Rubric evaluators' reference) places per-dimension breakdowns under properties.dimension_scores, not properties.rubric_scores. The parser now tries dimension_scores first and falls back to rubric_scores for preview-build compatibility, and tolerates non-list payloads (e.g. MagicMock auto-attrs) by trying the next candidate when parsing yields zero entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(foundry-evals): add manual create_rubric_evaluator Adds FoundryEvals.create_rubric_evaluator as the agent-framework surface over project_client.beta.evaluators.create_version. This is the manual counterpart to generate_rubric: callers supply RubricDimension instances (authored locally, ported from another framework, or hand-tuned) and we POST a RubricBasedEvaluatorDefinition. The service auto-attaches the non-editable residual dimension (general_quality for quality, general_policy_compliance for safety). Per the Microsoft Learn 'Rubric evaluators' reference, the auto-generation path (create_generation_job) is primarily a portal/UI feature; external SDK clients with rich local agent context are better served by manual create_version. This keeps generate_rubric for users who want to round-trip through a Foundry-registered agent. Validation up front: weight must be in [1,10], ids unique, descriptions non-empty, pass_threshold in [0,1]. The returned GeneratedEvaluatorRef is identical in shape to one obtained from generate_rubric, so downstream evaluators= lists work unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * samples(foundry-evals): manual rubric sample + namespace re-exports Adds evaluate_with_manual_rubric_sample.py demonstrating the end-to-end dev scenario for FoundryEvals.create_rubric_evaluator: hand-author a list of RubricDimension, register via create_rubric_evaluator, then use the pinned GeneratedEvaluatorRef alongside built-in evaluators in an agent regression run. Also re-exports RubricDimension, GeneratedEvaluatorRef, build_sources, and load_evals_config from agent_framework.foundry (both the lazy runtime shim and the type stub) so the rubric samples can import everything from a single namespace; the auto-generate sample was previously broken because the shim was missing build_sources / load_evals_config. Updates the foundry-evals README with a chooser entry for the two rubric paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(foundry-evals): remove rubric creation flows; keep consumption only Reframes agent-framework as a pure consumer of Foundry rubric evaluators: scoring against rubrics that already exist (authored in the Foundry portal or via the dedicated SDK / REST surface) instead of creating them from the SDK. Removed creation surface area: - FoundryEvals.generate_rubric (auto-generate path) and create_rubric_evaluator (manual path), plus all _GenerationSdkTypes / _ManualRubricSdkTypes / _to_sdk_dimensions / _coalesce_generation_sources / _to_sdk_source / _poll_generation_job / _generation_job_to_ref / _evaluator_version_to_ref / _get_beta_evaluators / _import_*_sdk_types helpers. - EvalGenerationSource (the input source discriminator), RubricDimension (the input dimension type), agent_as_eval_source / workflow_as_eval_source / _detect_hosted_foundry_agent helpers, and the YAML-config loader (_evals_config.py with RubricGenerationSpec / RubricSourceSpec / parse_evals_config / load_evals_config / build_sources). - BaseAgent.as_eval_source / Workflow.as_eval_source plus the _render_agent_dossier / _render_workflow_dossier helpers in core. These existed only to feed the now-removed generation pipeline. - Samples evaluate_with_generated_rubric_sample.py, evaluate_with_manual_rubric_sample.py, and evaluators.yaml. Replaced with a short README section showing how to reference an existing rubric evaluator via GeneratedEvaluatorRef. Kept (consumption surface): - GeneratedEvaluatorRef, slimmed to (name, version, display_name). Still accepted alongside built-in evaluator strings in FoundryEvals(evaluators=[...]). Versionless refs still warn. - RubricScore on EvalScoreResult.dimensions plus EvalResults.assert_dimension_score_at_least for per-dimension CI gates. - _parse_dimension_entries / _extract_rubric_scores output parsing (both canonical dimension_scores and the legacy rubric_scores key). Tests: 160/160 foundry unit tests and 71/71 core local-eval tests pass; pyright is clean across changed files. The pre-existing tests/core/test_telemetry.py::test_detect_hosted_fallback_import_error failure is unrelated and reproduces on the prior commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * samples(foundry-evals): add evaluate_with_rubric_sample Adds a runnable end-to-end sample showing how to consume a pre-existing rubric evaluator created in Foundry: reference it with GeneratedEvaluatorRef(name, version), mix it with built-in evaluators in FoundryEvals, and gate CI with assert_dimension_score_at_least on a specific dimension. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry-evals): satisfy mypy on _fetch_output_items mypy infers OutputItemListResponse.sample as dict[str, object] | None while pyright correctly infers the typed Sample model. Cast to Any so both type checkers accept the attribute access pattern, rename the local to avoid shadowing the inner-loop sample binding, and drop the now-stale pyright suppressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(foundry-evals): drop unpublished rubric-evaluators learn.microsoft.com link The Adaptive Evals authoring docs are not yet published on Microsoft Learn, so the link 404s. Keep the descriptive text without the broken hyperlink; we can re-add it once the docs ship. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(foundry-evals): hoist repeated local imports to module top Per code review feedback (eavanvalkenburg): the test file repeated 'from agent_framework_foundry._foundry_evals import ...' inside 22 test bodies and 'from agent_framework_foundry import GeneratedEvaluatorRef' inside 8 more. Move all of them to the existing top-level imports; the symbols are the same across tests and the local imports were redundant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/__init__.py | 2 + .../core/agent_framework/_evaluation.py | 176 ++++++++++++ .../core/agent_framework/foundry/__init__.py | 1 + .../core/agent_framework/foundry/__init__.pyi | 2 + .../core/tests/core/test_local_eval.py | 107 ++++++- .../agent_framework_foundry/__init__.py | 2 + .../agent_framework_foundry/_foundry_evals.py | 272 ++++++++++++++++-- .../foundry/tests/test_foundry_evals.py | 267 ++++++++++++++++- .../evaluation/foundry_evals/.env.example | 9 + .../evaluation/foundry_evals/README.md | 29 ++ .../evaluate_with_rubric_sample.py | 138 +++++++++ 11 files changed, 951 insertions(+), 54 deletions(-) create mode 100644 python/samples/05-end-to-end/evaluation/foundry_evals/evaluate_with_rubric_sample.py diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index cc517d993e..bd8b66cbb1 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -71,6 +71,7 @@ from ._evaluation import ( Evaluator, ExpectedToolCall, LocalEvaluator, + RubricScore, evaluate_agent, evaluate_workflow, evaluator, @@ -460,6 +461,7 @@ __all__ = [ "ResponseStream", "Role", "RoleLiteral", + "RubricScore", "RunContext", "Runner", "RunnerContext", diff --git a/python/packages/core/agent_framework/_evaluation.py b/python/packages/core/agent_framework/_evaluation.py index 64fab0eacb..52bdf90d0f 100644 --- a/python/packages/core/agent_framework/_evaluation.py +++ b/python/packages/core/agent_framework/_evaluation.py @@ -311,12 +311,15 @@ class EvalScoreResult: score: Numeric score from the evaluator. passed: Whether the item passed this evaluator's threshold. sample: Optional raw evaluator output (rationale, metadata). + dimensions: Per-dimension scores when this evaluator is a rubric + evaluator. ``None`` for non-rubric (e.g. built-in) evaluators. """ name: str score: float passed: bool | None = None sample: dict[str, Any] | None = None + dimensions: list[RubricScore] | None = None @experimental(feature_id=ExperimentalFeature.EVALS) @@ -496,6 +499,179 @@ class EvalResults: detail += f" Errored items: {', '.join(summaries)}." raise EvalNotPassedError(detail) + def assert_score_at_least( + self, + min_score: float, + *, + evaluator: str | None = None, + msg: str | None = None, + ) -> None: + """Assert every item's score (optionally filtered by evaluator) is ``>= min_score``. + + Designed for CI gates on generated rubric evaluators (e.g. + ``results.assert_score_at_least(0.80)``). Includes any + sub-results from workflow evaluations. + + Args: + min_score: Minimum acceptable score (inclusive). + evaluator: When set, only check scores from the evaluator + whose ``EvalScoreResult.name`` matches. + msg: Optional custom failure message. + + Raises: + EvalNotPassedError: When any matching score is below the threshold. + """ + offenders: list[str] = [] + + def _check(results: EvalResults) -> None: + for item in results.items: + for score in item.scores: + if evaluator is not None and score.name != evaluator: + continue + if score.score < min_score: + offenders.append(f"{item.item_id}/{score.name}={score.score:.3f}") + for sub in results.sub_results.values(): + _check(sub) + + _check(self) + if offenders: + detail = msg or ( + f"{len(offenders)} score(s) below threshold {min_score}" + f"{' for ' + evaluator if evaluator else ''}: {', '.join(offenders[:5])}" + + (f" (+{len(offenders) - 5} more)" if len(offenders) > 5 else "") + ) + raise EvalNotPassedError(detail) + + def assert_dimension_score_at_least( + self, + dimension_id: str, + min_score: float, + *, + evaluator: str | None = None, + require_applicable: bool = False, + msg: str | None = None, + ) -> None: + """Assert every item's score for a rubric *dimension* is ``>= min_score``. + + Walks ``EvalScoreResult.dimensions`` looking for the named + dimension across all items (and sub-results). Non-applicable + dimensions are skipped by default; pass + ``require_applicable=True`` to fail when no applicable score is + produced. + + Args: + dimension_id: Dimension id (matches the rubric definition). + min_score: Minimum acceptable dimension score (inclusive). + evaluator: When set, only consider scores from the evaluator + whose ``EvalScoreResult.name`` matches. + require_applicable: When ``True``, missing or non-applicable + dimension scores raise. Defaults to ``False`` (skip). + msg: Optional custom failure message. + + Raises: + EvalNotPassedError: When the dimension fails the threshold. + """ + offenders: list[str] = [] + missing_items: list[str] = [] + + def _check(results: EvalResults) -> None: + for item in results.items: + found_applicable = False + for score in item.scores: + if evaluator is not None and score.name != evaluator: + continue + if not score.dimensions: + continue + for rs in score.dimensions: + if rs.id != dimension_id: + continue + if not rs.applicable: + continue + found_applicable = True + if rs.score is None or rs.score < min_score: + offenders.append( + f"{item.item_id}/{score.name}/{dimension_id}=" + f"{rs.score if rs.score is not None else 'None'}" + ) + if require_applicable and not found_applicable: + missing_items.append(item.item_id) + for sub in results.sub_results.values(): + _check(sub) + + _check(self) + problems: list[str] = [] + if offenders: + problems.append( + f"{len(offenders)} dimension score(s) for '{dimension_id}' below {min_score}: " + f"{', '.join(offenders[:5])}" + (f" (+{len(offenders) - 5} more)" if len(offenders) > 5 else "") + ) + if missing_items: + problems.append( + f"Dimension '{dimension_id}' not applicable on {len(missing_items)} item(s): " + f"{', '.join(missing_items[:5])}" + ) + if problems: + raise EvalNotPassedError(msg or "; ".join(problems)) + + def assert_no_failed_items(self, msg: str | None = None) -> None: + """Assert no item ended in ``fail`` or ``error`` status. + + Includes any sub-results from workflow evaluations. + + Args: + msg: Optional custom failure message. + + Raises: + EvalNotPassedError: When any item failed or errored. + """ + bad: list[str] = [] + + def _check(results: EvalResults) -> None: + for item in results.items: + if item.is_failed or item.is_error: + bad.append(f"{item.item_id}:{item.status}") + for sub in results.sub_results.values(): + _check(sub) + + _check(self) + if bad: + detail = msg or ( + f"{len(bad)} item(s) failed or errored: {', '.join(bad[:5])}" + + (f" (+{len(bad) - 5} more)" if len(bad) > 5 else "") + ) + raise EvalNotPassedError(detail) + + +# endregion + +# region Generated rubric evaluators + + +@experimental(feature_id=ExperimentalFeature.EVALS) +@dataclass(frozen=True) +class RubricScore: + """A single dimension's score from a rubric-based evaluator run. + + Rubric evaluators emit one ``RubricScore`` per dimension per item. + Attached to :class:`EvalScoreResult` as a typed view of the raw + ``properties.rubric_scores`` payload returned by providers such as + Foundry's generated rubric evaluators. + + Attributes: + id: Dimension id (matches the rubric definition). + score: Numeric score, or ``None`` when the dimension was marked + non-applicable for this item. + applicable: Whether the dimension applied to this item. + weight: Dimension weight (mirrors the rubric definition). + reason: Short rationale produced by the evaluator. + """ + + id: str + score: int | None + applicable: bool + weight: int + reason: str + # endregion diff --git a/python/packages/core/agent_framework/foundry/__init__.py b/python/packages/core/agent_framework/foundry/__init__.py index 103bdca8f8..06bd4df17e 100644 --- a/python/packages/core/agent_framework/foundry/__init__.py +++ b/python/packages/core/agent_framework/foundry/__init__.py @@ -34,6 +34,7 @@ _IMPORTS: dict[str, tuple[str, str]] = { "FoundryLocalChatOptions": ("agent_framework_foundry_local", "agent-framework-foundry-local"), "FoundryLocalClient": ("agent_framework_foundry_local", "agent-framework-foundry-local"), "FoundryLocalSettings": ("agent_framework_foundry_local", "agent-framework-foundry-local"), + "GeneratedEvaluatorRef": ("agent_framework_foundry", "agent-framework-foundry"), "RawAnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"), "RawFoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"), "RawFoundryAgentChatClient": ("agent_framework_foundry", "agent-framework-foundry"), diff --git a/python/packages/core/agent_framework/foundry/__init__.pyi b/python/packages/core/agent_framework/foundry/__init__.pyi index 73c3ffe589..08a7fc1b88 100644 --- a/python/packages/core/agent_framework/foundry/__init__.pyi +++ b/python/packages/core/agent_framework/foundry/__init__.pyi @@ -20,6 +20,7 @@ from agent_framework_foundry import ( FoundryEmbeddingSettings, FoundryEvals, FoundryMemoryProvider, + GeneratedEvaluatorRef, RawFoundryAgent, RawFoundryAgentChatClient, RawFoundryChatClient, @@ -52,6 +53,7 @@ __all__ = [ "FoundryLocalClient", "FoundryLocalSettings", "FoundryMemoryProvider", + "GeneratedEvaluatorRef", "RawAnthropicFoundryClient", "RawFoundryAgent", "RawFoundryAgentChatClient", diff --git a/python/packages/core/tests/core/test_local_eval.py b/python/packages/core/tests/core/test_local_eval.py index 96b0e1a391..e60fb35d51 100644 --- a/python/packages/core/tests/core/test_local_eval.py +++ b/python/packages/core/tests/core/test_local_eval.py @@ -11,8 +11,13 @@ import pytest from agent_framework._evaluation import ( CheckResult, EvalItem, + EvalItemResult, + EvalNotPassedError, + EvalResults, + EvalScoreResult, ExpectedToolCall, LocalEvaluator, + RubricScore, _coerce_result, evaluator, keyword_check, @@ -1010,19 +1015,101 @@ class TestAllPassedSubResults: # --------------------------------------------------------------------------- -# r5 review: _build_overall_item with empty outputs +# Rubric assertions (EvalResults.assert_*) # --------------------------------------------------------------------------- -class TestBuildOverallItemEmpty: - """Test _build_overall_item returns None for empty workflow outputs.""" +def _rubric_results(*scores_per_item: list[EvalScoreResult]) -> EvalResults: + items = [ + EvalItemResult(item_id=f"item-{i}", status="pass", scores=scores) for i, scores in enumerate(scores_per_item) + ] + return EvalResults( + provider="test", + eval_id="ev1", + run_id="run1", + result_counts={"passed": len(items), "failed": 0, "errored": 0, "total": len(items)}, + items=items, + ) - def test_returns_none_for_empty_outputs(self): - from unittest.mock import MagicMock - from agent_framework._evaluation import _build_overall_item +class TestRubricAssertions: + """Tests for EvalResults.assert_dimension_score_at_least.""" - mock_result = MagicMock() - mock_result.get_outputs.return_value = [] - item = _build_overall_item("Hello", mock_result) - assert item is None + def test_dimension_at_or_above_threshold_passes(self) -> None: + results = _rubric_results( + [ + EvalScoreResult( + name="policy", + score=0.9, + dimensions=[RubricScore(id="clarity", score=4, applicable=True, weight=1, reason="")], + ) + ], + ) + # Should not raise. + results.assert_dimension_score_at_least("clarity", 3) + + def test_dimension_below_threshold_raises(self) -> None: + results = _rubric_results( + [ + EvalScoreResult( + name="policy", + score=0.5, + dimensions=[RubricScore(id="clarity", score=2, applicable=True, weight=1, reason="")], + ) + ], + ) + with pytest.raises(EvalNotPassedError): + results.assert_dimension_score_at_least("clarity", 3) + + def test_non_applicable_skipped_by_default(self) -> None: + results = _rubric_results( + [ + EvalScoreResult( + name="policy", + score=1.0, + dimensions=[RubricScore(id="clarity", score=None, applicable=False, weight=1, reason="n/a")], + ) + ], + ) + # No applicable scores; default behaviour is to skip silently. + results.assert_dimension_score_at_least("clarity", 3) + + def test_require_applicable_raises_when_dimension_absent(self) -> None: + results = _rubric_results( + [EvalScoreResult(name="policy", score=1.0, dimensions=[])], + ) + with pytest.raises(EvalNotPassedError, match="not applicable"): + results.assert_dimension_score_at_least("clarity", 3, require_applicable=True) + + def test_require_applicable_raises_when_filtered_evaluator_missing(self) -> None: + # Regression: previously the (not evaluator or found_any) guard caused + # this case to silently pass even with require_applicable=True. + results = _rubric_results( + [ + EvalScoreResult( + name="other", + score=0.9, + dimensions=[RubricScore(id="clarity", score=4, applicable=True, weight=1, reason="")], + ) + ], + ) + with pytest.raises(EvalNotPassedError, match="not applicable"): + results.assert_dimension_score_at_least("clarity", 3, evaluator="policy", require_applicable=True) + + def test_evaluator_filter_isolates_offenders(self) -> None: + results = _rubric_results( + [ + EvalScoreResult( + name="other", + score=0.1, + dimensions=[RubricScore(id="clarity", score=1, applicable=True, weight=1, reason="")], + ), + EvalScoreResult( + name="policy", + score=0.9, + dimensions=[RubricScore(id="clarity", score=4, applicable=True, weight=1, reason="")], + ), + ], + ) + # The low-scoring "other" evaluator is filtered out; "policy" passes. + results.assert_dimension_score_at_least("clarity", 3, evaluator="policy") diff --git a/python/packages/foundry/agent_framework_foundry/__init__.py b/python/packages/foundry/agent_framework_foundry/__init__.py index e6422e72c8..1ee0fc56dd 100644 --- a/python/packages/foundry/agent_framework_foundry/__init__.py +++ b/python/packages/foundry/agent_framework_foundry/__init__.py @@ -12,6 +12,7 @@ from ._embedding_client import ( ) from ._foundry_evals import ( FoundryEvals, + GeneratedEvaluatorRef, evaluate_foundry_target, evaluate_traces, ) @@ -33,6 +34,7 @@ __all__ = [ "FoundryEmbeddingSettings", "FoundryEvals", "FoundryMemoryProvider", + "GeneratedEvaluatorRef", "RawFoundryAgent", "RawFoundryAgentChatClient", "RawFoundryChatClient", diff --git a/python/packages/foundry/agent_framework_foundry/_foundry_evals.py b/python/packages/foundry/agent_framework_foundry/_foundry_evals.py index eef58b0a04..8059c2ce99 100644 --- a/python/packages/foundry/agent_framework_foundry/_foundry_evals.py +++ b/python/packages/foundry/agent_framework_foundry/_foundry_evals.py @@ -28,8 +28,9 @@ from __future__ import annotations import asyncio import logging -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any +from collections.abc import Iterable, Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, cast from agent_framework._evaluation import ( AgentEvalConverter, @@ -39,6 +40,7 @@ from agent_framework._evaluation import ( EvalItemResult, EvalResults, EvalScoreResult, + RubricScore, ) from agent_framework._feature_stage import ExperimentalFeature, experimental from openai import AsyncOpenAI @@ -51,6 +53,54 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) + +# region Generated rubric evaluator references + + +@experimental(feature_id=ExperimentalFeature.EVALS) +@dataclass(frozen=True) +class GeneratedEvaluatorRef: + """A reference to a rubric evaluator that already exists in Foundry. + + Pass instances of this class to :class:`FoundryEvals` to score items + with a pre-existing rubric evaluator (manually authored or + auto-generated through the Foundry portal). agent-framework is a + consumer here: it does not create or modify the evaluator definition; + it only references the persisted version by name. + + Pinning ``version`` is strongly recommended so evaluation runs are + reproducible. ``version=None`` resolves to whichever version is + current at execution time; :class:`FoundryEvals` emits a warning when + a versionless reference is used. CI gates should always pass a + concrete version. + + Attributes: + name: Evaluator name as stored in the Foundry project (for + example ``"reservation-policy-rubric"``). Distinct from + built-in evaluators such as ``"builtin.relevance"``. + version: Pinned evaluator version. ``None`` means "latest" — + this is discouraged for CI/repro and :class:`FoundryEvals` + will emit a warning when used. + display_name: Optional human-readable name used in result + summaries. Defaults to ``name`` when unset. + """ + + name: str + version: str | None = None + display_name: str | None = None + + @classmethod + def latest(cls, name: str, *, display_name: str | None = None) -> GeneratedEvaluatorRef: + """Construct a versionless reference (resolves to the latest version at run time). + + Discouraged for reproducible runs. Prefer the constructor with + an explicit ``version`` so CI and replay evaluations stay stable + when the evaluator is updated in Foundry. + """ + return cls(name=name, version=None, display_name=display_name) + + +# endregion # Agent evaluators that accept query/response as conversation arrays. # Maintained manually — check https://learn.microsoft.com/en-us/azure/ai-studio/how-to/develop/evaluate-sdk # for the latest evaluator list. These are the evaluators that need conversation-format input. @@ -166,7 +216,7 @@ def _resolve_evaluator(name: str) -> str: def _build_testing_criteria( - evaluators: Sequence[str], + evaluators: Sequence[str | GeneratedEvaluatorRef], model: str, *, include_data_mapping: bool = False, @@ -175,7 +225,9 @@ def _build_testing_criteria( """Build ``testing_criteria`` for ``evals.create()``. Args: - evaluators: Evaluator names. + evaluators: Evaluator names (built-in shorts / fully-qualified + ``builtin.*`` names) or :class:`GeneratedEvaluatorRef` + instances for generated rubric evaluators. model: Model deployment for the LLM judge. include_data_mapping: Whether to include field-level data mapping (required for the JSONL data source, not needed for response-based). @@ -183,7 +235,38 @@ def _build_testing_criteria( definitions. """ criteria: list[dict[str, Any]] = [] - for name in evaluators: + for entry_spec in evaluators: + if isinstance(entry_spec, GeneratedEvaluatorRef): + short = entry_spec.display_name or entry_spec.name + ref_entry: dict[str, Any] = { + "type": "azure_ai_evaluator", + "name": short, + "evaluator_name": entry_spec.name, + "initialization_parameters": {"deployment_name": model}, + } + if entry_spec.version is not None: + ref_entry["evaluator_version"] = entry_spec.version + else: + logger.warning( + "GeneratedEvaluatorRef '%s' has no pinned version; the eval run " + "will resolve to whichever version is current at execution time. " + "Pin the version for reproducible runs.", + entry_spec.name, + ) + if include_data_mapping: + # Rubric evaluators accept conversation arrays like agent + # evaluators, plus tool_definitions when items are tool-aware. + ref_mapping: dict[str, str] = { + "query": "{{item.query_messages}}", + "response": "{{item.response_messages}}", + } + if include_tool_definitions: + ref_mapping["tool_definitions"] = "{{item.tool_definitions}}" + ref_entry["data_mapping"] = ref_mapping + criteria.append(ref_entry) + continue + + name = entry_spec qualified = _resolve_evaluator(name) short = name if not name.startswith("builtin.") else name.split(".")[-1] @@ -247,9 +330,9 @@ def _build_item_schema( def _resolve_default_evaluators( - evaluators: Sequence[str] | None, + evaluators: Sequence[str | GeneratedEvaluatorRef] | None, items: Sequence[EvalItem | dict[str, Any]] | None = None, -) -> list[str]: +) -> list[str | GeneratedEvaluatorRef]: """Resolve evaluators, applying defaults when ``None``. Defaults to relevance + coherence + task_adherence. Automatically adds @@ -258,7 +341,7 @@ def _resolve_default_evaluators( if evaluators is not None: return list(evaluators) - result = list(_DEFAULT_EVALUATORS) + result: list[str | GeneratedEvaluatorRef] = list(_DEFAULT_EVALUATORS) if items is not None: has_tools = any((item.tools if isinstance(item, EvalItem) else item.get("tool_definitions")) for item in items) if has_tools: @@ -267,14 +350,24 @@ def _resolve_default_evaluators( def _filter_tool_evaluators( - evaluators: list[str], + evaluators: list[str | GeneratedEvaluatorRef], items: Sequence[EvalItem | dict[str, Any]], -) -> list[str]: - """Remove tool evaluators if no items have tool definitions.""" +) -> list[str | GeneratedEvaluatorRef]: + """Remove tool evaluators if no items have tool definitions. + + Generated rubric evaluators are tool-aware but not tool-required; they + are preserved regardless of whether items carry tool definitions. + """ has_tools = any((item.tools if isinstance(item, EvalItem) else item.get("tool_definitions")) for item in items) if has_tools: return evaluators - filtered = [e for e in evaluators if _resolve_evaluator(e) not in _TOOL_EVALUATORS] + + def _is_tool_only(spec: str | GeneratedEvaluatorRef) -> bool: + if isinstance(spec, GeneratedEvaluatorRef): + return False + return _resolve_evaluator(spec) in _TOOL_EVALUATORS + + filtered = [e for e in evaluators if not _is_tool_only(e)] if not filtered: raise ValueError( f"All requested evaluators {evaluators} require tool definitions, " @@ -282,7 +375,7 @@ def _filter_tool_evaluators( "or choose evaluators that do not require tools." ) if len(filtered) < len(evaluators): - removed = [e for e in evaluators if _resolve_evaluator(e) in _TOOL_EVALUATORS] + removed = [e for e in evaluators if _is_tool_only(e)] logger.info("Removed tool evaluators %s (no items have tools)", removed) return filtered @@ -354,6 +447,114 @@ def _extract_per_evaluator(run: RunRetrieveResponse) -> dict[str, dict[str, int] return per_eval +_RUBRIC_DIMENSION_KEYS: tuple[str, ...] = ("dimension_scores", "rubric_scores") +"""Property keys that may carry per-dimension rubric breakdowns. + +The published Foundry rubric-evaluator output format uses +``properties.dimension_scores`` (see the Microsoft Learn "Rubric +evaluators" reference). Earlier preview builds and some SDK shapes +used ``rubric_scores``; we accept both for defensive forward/backward +compatibility. +""" + + +def _parse_dimension_entries(raw: Any) -> list[RubricScore]: + """Parse a raw list-like payload into ``RubricScore`` instances. + + Returns an empty list when ``raw`` is falsy, not iterable, or + contains no well-formed entries. + """ + if not raw: + return [] + try: + raw_iter: Iterable[Any] = iter(raw) + except TypeError: + return [] + + parsed: list[RubricScore] = [] + for raw_entry in raw_iter: + entry: Any = raw_entry + try: + rid: Any + score_val: Any + applicable: Any + weight: Any + reason: Any + if isinstance(entry, dict): + entry_any = cast("dict[str, Any]", entry) + rid = entry_any.get("id") + score_val = entry_any.get("score") + applicable = entry_any.get("applicable") + weight = entry_any.get("weight") + reason = entry_any.get("reason", "") + else: + rid = getattr(entry, "id", None) + score_val = getattr(entry, "score", None) + applicable = getattr(entry, "applicable", None) + weight = getattr(entry, "weight", None) + reason = getattr(entry, "reason", "") or "" + if rid is None or weight is None or applicable is None: + continue + parsed.append( + RubricScore( + id=str(rid), + score=int(score_val) if isinstance(score_val, (int, float)) else None, + applicable=bool(applicable), + weight=int(weight), + reason=str(reason) if reason is not None else "", + ) + ) + except (TypeError, ValueError): + logger.debug("Skipping malformed rubric dimension entry: %s", cast("Any", entry), exc_info=True) + return parsed + + +def _extract_rubric_scores(sample: Any) -> list[RubricScore] | None: + """Extract typed ``RubricScore`` instances from an evaluator's raw sample payload. + + Foundry rubric evaluators include a per-dimension breakdown under + ``properties.dimension_scores`` on each result (preview builds used + ``rubric_scores``; both keys are accepted, with the canonical + ``dimension_scores`` taking priority). The exact location may + vary across SDK versions, so this helper accepts a few shapes: + + * The SDK ``sample`` object exposes + ``properties.dimension_scores`` / ``properties.rubric_scores``. + * The ``sample`` is a dict containing the same under + ``properties.``. + * The ``sample`` is a dict with ``dimension_scores`` / + ``rubric_scores`` at the top level. + + Returns ``None`` when no rubric scores are present (i.e. the + evaluator was not a rubric evaluator). + """ + if sample is None: + return None + + containers: list[Any] = [] + properties: Any = getattr(sample, "properties", None) + if properties is not None: + containers.append(properties) + if isinstance(sample, dict): + sample_any = cast("dict[str, Any]", sample) + props_dict: Any = sample_any.get("properties") + if props_dict is not None and props_dict is not properties: + containers.append(props_dict) + containers.append(sample_any) + + for container in containers: + for key in _RUBRIC_DIMENSION_KEYS: + raw: Any = None + if isinstance(container, dict): + raw = cast("dict[str, Any]", container).get(key) + elif hasattr(container, key): + raw = getattr(container, key, None) + parsed = _parse_dimension_entries(raw) + if parsed: + return parsed + return None + + async def _fetch_output_items( client: AsyncOpenAI, eval_id: str, @@ -377,12 +578,15 @@ async def _fetch_output_items( # Extract per-evaluator scores scores: list[EvalScoreResult] = [] for r in oi.results or []: + sample = r.sample + dimensions = _extract_rubric_scores(sample) scores.append( EvalScoreResult( name=r.name, score=r.score, passed=r.passed, - sample=r.sample, + sample=sample, + dimensions=dimensions, ) ) @@ -394,15 +598,18 @@ async def _fetch_output_items( output_text: str | None = None response_id: str | None = None - sample = oi.sample - if sample is not None: # pyright: ignore[reportUnnecessaryComparison] - err = sample.error - if err is not None and (err.code or err.message): # pyright: ignore[reportUnnecessaryComparison] + # mypy infers oi.sample as dict[str, object] | None, but the + # OpenAI SDK actually returns a typed Sample model. Cast to Any so + # both type checkers accept the attribute access pattern. + oi_sample: Any = oi.sample + if oi_sample is not None: + err = oi_sample.error + if err is not None and (err.code or err.message): error_code = err.code or None error_message = err.message or None - usage = sample.usage - if usage is not None and usage.total_tokens: # pyright: ignore[reportUnnecessaryComparison] + usage = oi_sample.usage + if usage is not None and usage.total_tokens: token_usage = { "prompt_tokens": usage.prompt_tokens, "completion_tokens": usage.completion_tokens, @@ -411,13 +618,13 @@ async def _fetch_output_items( } # Extract input/output text - if sample.input: - parts = [si.content for si in sample.input if si.role == "user"] + if oi_sample.input: + parts = [si.content for si in oi_sample.input if si.role == "user"] if parts: input_text = " ".join(parts) - if sample.output: - parts = [so.content or "" for so in sample.output if so.role == "assistant"] + if oi_sample.output: + parts = [so.content or "" for so in oi_sample.output if so.role == "assistant"] if parts: output_text = " ".join(parts) @@ -472,7 +679,7 @@ async def _evaluate_via_responses_impl( *, client: AsyncOpenAI, response_ids: Sequence[str], - evaluators: list[str], + evaluators: list[str | GeneratedEvaluatorRef], model: str, eval_name: str, poll_interval: float, @@ -573,8 +780,11 @@ class FoundryEvals: (from ``azure.ai.projects.aio``). Provide this or *client*. model: Model deployment name for the evaluator LLM judge. Resolved from ``client.model`` when omitted. - evaluators: Evaluator names (e.g. ``["relevance", "tool_call_accuracy"]``). - When ``None`` (default), uses smart defaults based on item data. + evaluators: Evaluator specifications. Entries may be built-in + short names (e.g. ``"relevance"``), fully-qualified + ``"builtin.*"`` names, or :class:`GeneratedEvaluatorRef` + instances for previously generated rubric evaluators. When + ``None`` (default), uses smart defaults based on item data. conversation_split: How to split multi-turn conversations into query/response halves. Defaults to ``LAST_TURN``. Pass a ``ConversationSplit`` enum value or a custom callable — see @@ -623,7 +833,7 @@ class FoundryEvals: client: FoundryChatClient | None = None, project_client: AIProjectClient | None = None, model: str | None = None, - evaluators: Sequence[str] | None = None, + evaluators: Sequence[str | GeneratedEvaluatorRef] | None = None, conversation_split: ConversationSplitter = ConversationSplit.LAST_TURN, poll_interval: float = 5.0, timeout: float = 180.0, @@ -642,7 +852,9 @@ class FoundryEvals: "Model is required. Pass model= explicitly or use a FoundryChatClient that has a model configured." ) self._model = resolved_model - self._evaluators = list(evaluators) if evaluators is not None else None + self._evaluators: list[str | GeneratedEvaluatorRef] | None = ( + list(evaluators) if evaluators is not None else None + ) self._conversation_split = conversation_split self._poll_interval = poll_interval self._timeout = timeout @@ -678,7 +890,7 @@ class FoundryEvals: async def _evaluate_via_dataset( self, items: Sequence[EvalItem], - evaluators: list[str], + evaluators: list[str | GeneratedEvaluatorRef], eval_name: str, ) -> EvalResults: """Evaluate using JSONL dataset upload path.""" diff --git a/python/packages/foundry/tests/test_foundry_evals.py b/python/packages/foundry/tests/test_foundry_evals.py index a5d9f2e864..8734650aaf 100644 --- a/python/packages/foundry/tests/test_foundry_evals.py +++ b/python/packages/foundry/tests/test_foundry_evals.py @@ -25,16 +25,25 @@ from agent_framework._evaluation import ( from agent_framework._workflows._workflow import WorkflowRunResult from openai import AsyncOpenAI +from agent_framework_foundry import GeneratedEvaluatorRef from agent_framework_foundry._foundry_evals import ( + _AGENT_EVALUATORS, + _BUILTIN_EVALUATORS, + _TOOL_EVALUATORS, FoundryEvals, _build_item_schema, _build_testing_criteria, _extract_per_evaluator, _extract_result_counts, + _extract_rubric_scores, + _fetch_output_items, _filter_tool_evaluators, + _poll_eval_run, _resolve_default_evaluators, _resolve_evaluator, _resolve_openai_client, + evaluate_foundry_target, + evaluate_traces, ) @@ -806,6 +815,67 @@ class TestBuildTestingCriteria: for c in criteria: assert "tool_definitions" in c["data_mapping"], f"{c['name']} missing tool_definitions" + def test_generated_evaluator_ref_pinned_version(self) -> None: + + ref = GeneratedEvaluatorRef(name="my-rubric", version="1") + criteria = _build_testing_criteria([ref], "gpt-4o", include_data_mapping=True) + + assert len(criteria) == 1 + c = criteria[0] + assert c["type"] == "azure_ai_evaluator" + assert c["evaluator_name"] == "my-rubric" + assert c["evaluator_version"] == "1" + assert c["name"] == "my-rubric" + assert c["initialization_parameters"] == {"deployment_name": "gpt-4o"} + assert c["data_mapping"] == { + "query": "{{item.query_messages}}", + "response": "{{item.response_messages}}", + } + + def test_generated_evaluator_ref_display_name_used_as_short(self) -> None: + + ref = GeneratedEvaluatorRef(name="my-rubric", version="2", display_name="My Rubric") + criteria = _build_testing_criteria([ref], "gpt-4o") + + assert criteria[0]["name"] == "My Rubric" + assert criteria[0]["evaluator_name"] == "my-rubric" + + def test_generated_evaluator_ref_tool_definitions_added(self) -> None: + + ref = GeneratedEvaluatorRef(name="my-rubric", version="1") + criteria = _build_testing_criteria( + [ref], + "gpt-4o", + include_data_mapping=True, + include_tool_definitions=True, + ) + + assert criteria[0]["data_mapping"]["tool_definitions"] == "{{item.tool_definitions}}" + + def test_generated_evaluator_ref_unpinned_warns(self, caplog: pytest.LogCaptureFixture) -> None: + import logging + + ref = GeneratedEvaluatorRef.latest("my-rubric") + with caplog.at_level(logging.WARNING, logger="agent_framework_foundry._foundry_evals"): + criteria = _build_testing_criteria([ref], "gpt-4o") + + assert "evaluator_version" not in criteria[0] + assert any("no pinned version" in r.message for r in caplog.records) + + def test_generated_evaluator_ref_mixed_with_builtins(self) -> None: + + ref = GeneratedEvaluatorRef(name="my-rubric", version="1") + criteria = _build_testing_criteria( + ["relevance", ref, "task_adherence"], + "gpt-4o", + include_data_mapping=True, + ) + + assert [c["name"] for c in criteria] == ["relevance", "my-rubric", "task_adherence"] + assert criteria[0]["evaluator_name"] == "builtin.relevance" + assert criteria[1]["evaluator_name"] == "my-rubric" + assert criteria[2]["evaluator_name"] == "builtin.task_adherence" + # --------------------------------------------------------------------------- # _build_item_schema @@ -1263,6 +1333,29 @@ class TestFilterToolEvaluators: items, ) + def test_preserves_generated_ref_when_no_tools(self) -> None: + + ref = GeneratedEvaluatorRef(name="rubric", version="1") + items = [ + EvalItem(conversation=[Message("user", ["q"]), Message("assistant", ["r"])]), + ] + result = _filter_tool_evaluators( + ["relevance", ref, "tool_call_accuracy"], + items, + ) + assert "relevance" in result + assert ref in result + assert "tool_call_accuracy" not in result + + def test_generated_ref_alone_does_not_raise(self) -> None: + + ref = GeneratedEvaluatorRef(name="rubric", version="1") + items = [ + EvalItem(conversation=[Message("user", ["q"]), Message("assistant", ["r"])]), + ] + result = _filter_tool_evaluators([ref], items) + assert result == [ref] + # --------------------------------------------------------------------------- # EvalResults @@ -2267,7 +2360,6 @@ class TestEvalResultsWithItems: class TestFetchOutputItems: async def test_fetches_and_converts_output_items(self) -> None: - from agent_framework_foundry._foundry_evals import _fetch_output_items # Build mock output items matching the OpenAI SDK schema mock_result = MagicMock() @@ -2329,7 +2421,6 @@ class TestFetchOutputItems: assert item.error_code is None async def test_handles_errored_item(self) -> None: - from agent_framework_foundry._foundry_evals import _fetch_output_items mock_error = MagicMock() mock_error.code = "QueryExtractionError" @@ -2361,7 +2452,6 @@ class TestFetchOutputItems: assert len(item.scores) == 0 async def test_handles_api_failure_gracefully(self) -> None: - from agent_framework_foundry._foundry_evals import _fetch_output_items mock_client = MagicMock() mock_client.evals.runs.output_items.list = AsyncMock(side_effect=TypeError("API error")) @@ -2369,6 +2459,166 @@ class TestFetchOutputItems: items = await _fetch_output_items(mock_client, "eval_1", "run_1") assert items == [] + async def test_extracts_rubric_scores_from_dict_sample(self) -> None: + + mock_result = MagicMock() + mock_result.name = "my-rubric" + mock_result.score = 0.85 + mock_result.passed = True + mock_result.sample = { + "properties": { + "rubric_scores": [ + {"id": "policy", "score": 4, "applicable": True, "weight": 1, "reason": "ok"}, + {"id": "safety", "score": None, "applicable": False, "weight": 1, "reason": "n/a"}, + ] + } + } + + mock_oi = MagicMock() + mock_oi.id = "oi_1" + mock_oi.status = "pass" + mock_oi.results = [mock_result] + mock_oi.sample = None + mock_oi.datasource_item = {} + + mock_client = MagicMock() + mock_client.evals.runs.output_items.list = AsyncMock(return_value=_AsyncPage([mock_oi])) + + items = await _fetch_output_items(mock_client, "eval_1", "run_1") + + assert len(items) == 1 + scores = items[0].scores + assert len(scores) == 1 + assert scores[0].dimensions is not None + assert len(scores[0].dimensions) == 2 + policy = next(d for d in scores[0].dimensions if d.id == "policy") + assert policy.score == 4 + assert policy.applicable is True + assert policy.weight == 1 + assert policy.reason == "ok" + safety = next(d for d in scores[0].dimensions if d.id == "safety") + assert safety.score is None + assert safety.applicable is False + + async def test_no_rubric_scores_when_absent(self) -> None: + + mock_result = MagicMock() + mock_result.name = "relevance" + mock_result.score = 0.85 + mock_result.passed = True + mock_result.sample = None + + mock_oi = MagicMock() + mock_oi.id = "oi_2" + mock_oi.status = "pass" + mock_oi.results = [mock_result] + mock_oi.sample = None + mock_oi.datasource_item = {} + + mock_client = MagicMock() + mock_client.evals.runs.output_items.list = AsyncMock(return_value=_AsyncPage([mock_oi])) + + items = await _fetch_output_items(mock_client, "eval_1", "run_1") + + assert items[0].scores[0].dimensions is None + + +class TestExtractRubricScores: + def test_handles_attribute_style_properties(self) -> None: + + rs = MagicMock() + rs.id = "policy" + rs.score = 5 + rs.applicable = True + rs.weight = 2 + rs.reason = "ok" + + sample = MagicMock() + sample.properties = MagicMock() + sample.properties.rubric_scores = [rs] + + result = _extract_rubric_scores(sample) + assert result is not None + assert result[0].id == "policy" + assert result[0].score == 5 + assert result[0].weight == 2 + + def test_top_level_rubric_scores_in_dict(self) -> None: + + sample = {"rubric_scores": [{"id": "a", "score": 3, "applicable": True, "weight": 1, "reason": "r"}]} + result = _extract_rubric_scores(sample) + assert result is not None + assert result[0].id == "a" + + def test_returns_none_when_missing(self) -> None: + + assert _extract_rubric_scores(None) is None + assert _extract_rubric_scores({}) is None + assert _extract_rubric_scores({"properties": {}}) is None + + def test_skips_malformed_entries(self) -> None: + + sample = { + "properties": { + "rubric_scores": [ + {"id": "good", "score": 3, "applicable": True, "weight": 1, "reason": "ok"}, + {"id": "bad-no-weight", "score": 2, "applicable": True, "reason": "x"}, + ] + } + } + result = _extract_rubric_scores(sample) + assert result is not None + assert len(result) == 1 + assert result[0].id == "good" + + def test_canonical_dimension_scores_key_from_docs(self) -> None: + """Per the Microsoft Learn docs, runtime output uses ``properties.dimension_scores``.""" + + sample = { + "properties": { + "dimension_scores": [ + { + "id": "intent_recognition", + "score": 5, + "applicable": True, + "weight": 9, + "reason": "Identified correctly.", + }, + { + "id": "general_quality", + "score": 4, + "applicable": True, + "weight": 5, + "reason": "Strong overall.", + }, + ] + } + } + result = _extract_rubric_scores(sample) + assert result is not None + assert [r.id for r in result] == ["intent_recognition", "general_quality"] + assert [r.score for r in result] == [5, 4] + assert [r.weight for r in result] == [9, 5] + + def test_dimension_scores_via_attribute(self) -> None: + """Canonical key also resolves when properties exposes ``dimension_scores`` as an attr.""" + + rs = MagicMock() + rs.id = "policy_enforcement" + rs.score = 1 + rs.applicable = True + rs.weight = 5 + rs.reason = "violated" + + sample = MagicMock() + sample.properties = MagicMock(spec=["dimension_scores"]) + sample.properties.dimension_scores = [rs] + + result = _extract_rubric_scores(sample) + assert result is not None + assert result[0].id == "policy_enforcement" + assert result[0].score == 1 + # --------------------------------------------------------------------------- # _poll_eval_run — timeout / failed / canceled paths @@ -2378,7 +2628,6 @@ class TestFetchOutputItems: class TestPollEvalRun: async def test_timeout_returns_timeout_status(self) -> None: """Poll timeout returns EvalResults with status='timeout'.""" - from agent_framework_foundry._foundry_evals import _poll_eval_run mock_client = MagicMock() mock_pending = MagicMock() @@ -2392,7 +2641,6 @@ class TestPollEvalRun: async def test_failed_run_returns_error(self) -> None: """Failed run returns EvalResults with error message.""" - from agent_framework_foundry._foundry_evals import _poll_eval_run mock_client = MagicMock() mock_failed = MagicMock() @@ -2410,7 +2658,6 @@ class TestPollEvalRun: async def test_canceled_run_returns_canceled_status(self) -> None: """Canceled run returns EvalResults with status='canceled'.""" - from agent_framework_foundry._foundry_evals import _poll_eval_run mock_client = MagicMock() mock_canceled = MagicMock() @@ -2435,7 +2682,6 @@ class TestPollEvalRun: class TestEvaluateTraces: async def test_raises_without_required_args(self) -> None: """Raises ValueError when no response_ids, trace_ids, or agent_id given.""" - from agent_framework_foundry._foundry_evals import evaluate_traces mock_client = MagicMock() with pytest.raises(ValueError, match="Provide at least one of"): @@ -2446,7 +2692,6 @@ class TestEvaluateTraces: async def test_response_ids_path(self) -> None: """evaluate_traces with response_ids uses the responses API path.""" - from agent_framework_foundry._foundry_evals import evaluate_traces mock_client = MagicMock() @@ -2494,7 +2739,6 @@ class TestEvaluateTraces: async def test_trace_ids_path(self) -> None: """evaluate_traces with trace_ids builds azure_ai_traces data source.""" - from agent_framework_foundry._foundry_evals import evaluate_traces mock_client = MagicMock() @@ -2534,7 +2778,6 @@ class TestEvaluateTraces: class TestEvaluateFoundryTarget: async def test_happy_path(self) -> None: """evaluate_foundry_target creates eval + run and polls to completion.""" - from agent_framework_foundry._foundry_evals import evaluate_foundry_target mock_client = MagicMock() @@ -2670,13 +2913,11 @@ class TestEvaluatorSetConsistency: """Verify that _AGENT_EVALUATORS and _TOOL_EVALUATORS are subsets of _BUILTIN_EVALUATORS.""" def test_agent_evaluators_subset(self): - from agent_framework_foundry._foundry_evals import _AGENT_EVALUATORS, _BUILTIN_EVALUATORS diff = _AGENT_EVALUATORS - set(_BUILTIN_EVALUATORS.values()) assert not diff, f"_AGENT_EVALUATORS has names not in _BUILTIN_EVALUATORS: {diff}" def test_tool_evaluators_subset(self): - from agent_framework_foundry._foundry_evals import _BUILTIN_EVALUATORS, _TOOL_EVALUATORS diff = _TOOL_EVALUATORS - set(_BUILTIN_EVALUATORS.values()) assert not diff, f"_TOOL_EVALUATORS has names not in _BUILTIN_EVALUATORS: {diff}" @@ -2690,7 +2931,6 @@ class TestEvaluatorSetConsistency: class TestEvaluateTracesAgentId: async def test_agent_id_only_path(self) -> None: """evaluate_traces with agent_id only builds azure_ai_traces data source.""" - from agent_framework_foundry._foundry_evals import evaluate_traces mock_client = MagicMock() @@ -2748,7 +2988,6 @@ class TestFilterToolEvaluatorsRaises: class TestEvaluateFoundryTargetValidation: async def test_target_without_type_raises(self) -> None: """target dict without 'type' key raises ValueError.""" - from agent_framework_foundry._foundry_evals import evaluate_foundry_target mock_client = MagicMock() with pytest.raises(ValueError, match="'type' key"): diff --git a/python/samples/05-end-to-end/evaluation/foundry_evals/.env.example b/python/samples/05-end-to-end/evaluation/foundry_evals/.env.example index b6a8af233e..388350edea 100644 --- a/python/samples/05-end-to-end/evaluation/foundry_evals/.env.example +++ b/python/samples/05-end-to-end/evaluation/foundry_evals/.env.example @@ -1,3 +1,12 @@ FOUNDRY_PROJECT_ENDPOINT="" FOUNDRY_MODEL="" +# Only needed for evaluate_with_rubric_sample.py — connects to the +# pre-existing Foundry agent that the rubric evaluator was created against. +FOUNDRY_AGENT_NAME="" +FOUNDRY_AGENT_VERSION="" + +# Only needed for evaluate_with_rubric_sample.py — references a rubric +# evaluator you created in Foundry. Pin the version for reproducible runs. +FOUNDRY_RUBRIC_NAME="" +FOUNDRY_RUBRIC_VERSION="" \ No newline at end of file diff --git a/python/samples/05-end-to-end/evaluation/foundry_evals/README.md b/python/samples/05-end-to-end/evaluation/foundry_evals/README.md index 81412a7f0e..e30ce6aa46 100644 --- a/python/samples/05-end-to-end/evaluation/foundry_evals/README.md +++ b/python/samples/05-end-to-end/evaluation/foundry_evals/README.md @@ -35,6 +35,34 @@ Evaluate what already happened — zero changes to agent code: uv run samples/05-end-to-end/evaluation/foundry_evals/evaluate_traces_sample.py ``` +### Referencing a rubric evaluator created in Foundry + +Foundry users can create rubric evaluators in the Foundry portal (or +through the dedicated SDK / REST surface). Once an evaluator exists, +agent-framework consumes it like any other evaluator: pass a +`GeneratedEvaluatorRef(name=..., version=...)` in the `evaluators=` +list and pin the version for reproducible runs. + +```python +from agent_framework.foundry import FoundryEvals, GeneratedEvaluatorRef + +evals = FoundryEvals( + evaluators=[ + GeneratedEvaluatorRef(name="reservation-policy-rubric", version="3"), + "relevance", + "coherence", + ], +) +``` + +Quality gates on rubric output use the standard `EvalResults` helpers, +including `assert_dimension_score_at_least(...)` for per-dimension +thresholds. + +See [`evaluate_with_rubric_sample.py`](./evaluate_with_rubric_sample.py) +for a runnable end-to-end example that combines a rubric evaluator with +built-in evaluators and gates a per-dimension threshold. + ## Setup Create a `.env` file with configuration as in the `.env.example` file in this folder. @@ -44,3 +72,4 @@ Create a `.env` file with configuration as in the `.env.example` file in this fo - **"I want to test my agent during development"** → `evaluate_agent_sample.py`, Pattern 1 - **"I want to evaluate past agent runs"** → `evaluate_traces_sample.py` - **"I want to inspect/modify eval data before submitting"** → `evaluate_agent_sample.py`, Pattern 2 +- **"I want to score against a custom rubric I created in Foundry"** → `evaluate_with_rubric_sample.py` diff --git a/python/samples/05-end-to-end/evaluation/foundry_evals/evaluate_with_rubric_sample.py b/python/samples/05-end-to-end/evaluation/foundry_evals/evaluate_with_rubric_sample.py new file mode 100644 index 0000000000..06ec5c9bdd --- /dev/null +++ b/python/samples/05-end-to-end/evaluation/foundry_evals/evaluate_with_rubric_sample.py @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Evaluate a Foundry agent against a rubric evaluator that was created in Foundry. + +Rubric evaluators are LLM-as-judge evaluators with custom scoring dimensions +that you define for your domain. agent-framework consumes pre-existing rubric +evaluators — they are authored in the Foundry portal (or via the dedicated +SDK / REST surface) and referenced here by name and version. + +See: https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-evaluators/rubric-evaluators + +This sample demonstrates: +1. Connecting to a pre-existing Foundry agent (PromptAgent or HostedAgent). +2. Referencing a pre-existing rubric evaluator by ``name`` and ``version``. +3. Mixing the rubric with built-in Foundry evaluators in one run. +4. Asserting per-dimension thresholds with + ``EvalResults.assert_dimension_score_at_least(...)`` for CI quality gates. + +Starting condition / prerequisites: +- An Azure AI Foundry project with a deployed model. +- A registered Foundry agent (PromptAgent or HostedAgent) in that project. + This is the agent the rubric is meant to evaluate. +- A rubric evaluator already created in the Foundry portal against that + agent. Creating rubrics through the portal currently requires picking a + Foundry agent as the generation context, so this prerequisite is implied + by having a rubric at all. +- Set the following in .env (see ``.env.example``): + - ``FOUNDRY_PROJECT_ENDPOINT`` + - ``FOUNDRY_AGENT_NAME`` and ``FOUNDRY_AGENT_VERSION`` for the agent + - ``FOUNDRY_RUBRIC_NAME`` and ``FOUNDRY_RUBRIC_VERSION`` for the rubric + - ``FOUNDRY_MODEL`` for the rubric judge model +""" + +import asyncio +import os + +from agent_framework import EvalNotPassedError, evaluate_agent +from agent_framework.foundry import FoundryAgent, FoundryChatClient, FoundryEvals, GeneratedEvaluatorRef +from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +load_dotenv(override=True) + + +async def main() -> None: + # 1. Connect to the existing Foundry agent that the rubric was created + # against. PromptAgents and HostedAgents are both supported. + credential = AzureCliCredential() + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + + agent = FoundryAgent( + project_endpoint=project_endpoint, + agent_name=os.environ["FOUNDRY_AGENT_NAME"], + agent_version=os.environ.get("FOUNDRY_AGENT_VERSION"), + credential=credential, + ) + + # 2. Reference the pre-existing rubric evaluator by name + version. + # Always pin a version for reproducible CI runs; versionless refs + # resolve to "latest" and emit a warning at evaluation time. + rubric_name = os.environ["FOUNDRY_RUBRIC_NAME"] + rubric_version = os.environ["FOUNDRY_RUBRIC_VERSION"] + rubric = GeneratedEvaluatorRef(name=rubric_name, version=rubric_version) + + # 3. Mix the rubric with built-in evaluators in a single FoundryEvals + # config. FoundryEvals talks to Foundry over the project endpoint, so + # we hand it a FoundryChatClient configured with the same credential. + eval_client = FoundryChatClient( + project_endpoint=project_endpoint, + model=os.environ["FOUNDRY_MODEL"], + credential=credential, + ) + evals = FoundryEvals( + client=eval_client, + evaluators=[ + rubric, + FoundryEvals.RELEVANCE, + FoundryEvals.COHERENCE, + ], + ) + + # ========================================================================= + # Run evaluation + # ========================================================================= + print("=" * 60) + print(f"Evaluating '{agent.name}' with rubric '{rubric_name}' (version {rubric_version})") + print("=" * 60) + + results = await evaluate_agent( + agent=agent, + queries=[ + "What's the weather like in Seattle?", + "Should I bring an umbrella to London tomorrow?", + ], + evaluators=evals, + ) + + for r in results: + print(f"Status: {r.status}") + print(f"Results: {r.passed}/{r.total} passed") + print(f"Portal: {r.report_url}") + if r.all_passed: + print("[PASS] All passed") + else: + print(f"[FAIL] {r.failed} failed") + + # ========================================================================= + # Per-dimension quality gate + # ========================================================================= + # Rubric evaluators emit per-dimension scores (1–5) on top of the overall + # weighted score. Use assert_dimension_score_at_least to gate CI on a + # specific dimension — e.g., never ship if a critical dimension drops + # below 3. + # + # The dimension_id must match an id defined on your rubric in Foundry. + # ``general_quality`` is used here because it's the conventional + # ``always_applicable: true`` dimension in the Foundry docs' example + # rubric — swap it for whatever dimension id(s) your rubric actually + # defines. + print() + print("=" * 60) + print("Per-dimension quality gate") + print("=" * 60) + + for r in results: + try: + r.assert_dimension_score_at_least( + "general_quality", + min_score=3.0, + evaluator=rubric_name, + ) + print(f"[PASS] {r.provider}: general_quality >= 3 on every item") + except EvalNotPassedError as exc: + print(f"[FAIL] {r.provider}: dimension gate tripped: {exc}") + + +if __name__ == "__main__": + asyncio.run(main()) From 5d98beddf52dd8e3d3323d2108c9fe3461ab0c04 Mon Sep 17 00:00:00 2001 From: Thota Sai Karthik Date: Tue, 2 Jun 2026 05:00:19 +0530 Subject: [PATCH 33/61] Python: feat(bedrock): implement native structured output support via Converse API (#6052) * feat(bedrock): add structured output support via Converse API (Fixes #5966) * fix(bedrock): improve unsupported model exception handling and schema parsing * refactor(bedrock): use generic traversal for strict schema enforcement * address Copilot review comments on structured output * refine bedrock structured output: guard additionalProperties, TypeError check, docs + test * fix(bedrock): widen response_format to Mapping and add missing test coverage --- .../agent_framework_bedrock/_chat_client.py | 156 ++++++- .../tests/test_bedrock_structured_output.py | 382 ++++++++++++++++++ 2 files changed, 526 insertions(+), 12 deletions(-) create mode 100644 python/packages/bedrock/tests/test_bedrock_structured_output.py diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index ebf8909d52..cb8545f9a3 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio +import copy import json import logging import sys @@ -36,6 +37,7 @@ from agent_framework.observability import ChatTelemetryLayer from boto3.session import Session as Boto3Session from botocore.client import BaseClient from botocore.config import Config as BotoConfig +from botocore.exceptions import ClientError from pydantic import BaseModel if sys.version_info >= (3, 13): @@ -115,13 +117,20 @@ class BedrockChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], t translates to ``toolConfig.tools``. tool_choice: How the model should use tools, translates to ``toolConfig.toolChoice``. + response_format: Structured output format. Accepts a Pydantic BaseModel + subclass or an OpenAI-style dict schema + (``{"json_schema": {"name": ..., "schema": ...}}``). + When provided, the Converse API request includes + ``outputConfig.textFormat`` with the schema serialized as a JSON + string. ``ChatResponse.value`` will be populated with the parsed + model instance. Only supported on models that support + ``outputConfig.textFormat``. Unsupported models raise a ValueError. # Options not supported in Bedrock Converse API: seed: Not supported. frequency_penalty: Not supported. presence_penalty: Not supported. allow_multiple_tool_calls: Not supported (models handle parallel calls automatically). - response_format: Not directly supported (use model-specific prompting). user: Not supported. store: Not supported. logit_bias: Not supported. @@ -161,9 +170,6 @@ class BedrockChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], t allow_multiple_tool_calls: None # type: ignore[misc] """Not supported. Bedrock models handle parallel tool calls automatically.""" - response_format: None # type: ignore[misc] - """Not directly supported. Use model-specific prompting for JSON output.""" - user: None # type: ignore[misc] """Not supported in Bedrock Converse API.""" @@ -324,10 +330,28 @@ class BedrockChatClient( return Boto3Session(**session_kwargs) def _invoke_converse(self, request: Mapping[str, Any]) -> dict[str, Any]: - response = self._bedrock_client.converse(**request) - if not isinstance(response, Mapping): - raise ChatClientInvalidResponseException("Bedrock converse response must be a mapping.") - return response + try: + response = self._bedrock_client.converse(**request) + if not isinstance(response, Mapping): + raise ChatClientInvalidResponseException("Bedrock converse response must be a mapping.") + return response + except ClientError as e: + error_details = e.response.get("Error", {}) + error_code = error_details.get("Code", "") + error_message = error_details.get("Message", "") + # "outputConfig" in error_message catches cases where Bedrock explicitly + # rejects the outputConfig field (unsupported model). Other ValidationExceptions + # (e.g. malformed schema shape, invalid property values) will not mention + # "outputConfig" and will bubble up as raw ClientError without being misdiagnosed. + if error_code == "ValidationException" and ( + "outputconfig" in error_message.lower() or "outputconfig" in str(e).lower() + ): + raise ValueError( + f"Model '{self.model}' does not support structured output via outputConfig.textFormat. " + "Check the model's Bedrock Converse outputConfig/textFormat support. " + f"AWS error Code: {error_code}. AWS error Message: {error_message}" + ) from e + raise @override def _inner_get_response( @@ -344,7 +368,7 @@ class BedrockChatClient( # Streaming mode - simulate streaming by yielding a single update async def _stream() -> AsyncIterable[ChatResponseUpdate]: response = await asyncio.to_thread(self._invoke_converse, request) - parsed_response = self._process_converse_response(response) + parsed_response = self._process_converse_response(response, options) contents = list(parsed_response.messages[0].contents if parsed_response.messages else []) if parsed_response.usage_details: contents.append(Content.from_usage(usage_details=parsed_response.usage_details)) # type: ignore[arg-type] @@ -360,12 +384,12 @@ class BedrockChatClient( raw_representation=parsed_response.raw_representation, ) - return self._build_response_stream(_stream()) + return self._build_response_stream(_stream(), response_format=options.get("response_format")) # Non-streaming mode async def _get_response() -> ChatResponse: raw_response = await asyncio.to_thread(self._invoke_converse, request) - return self._process_converse_response(raw_response) + return self._process_converse_response(raw_response, options) return _get_response() @@ -430,6 +454,9 @@ class BedrockChatClient( if tool_config: run_options["toolConfig"] = tool_config + if output_config := self._prepare_output_config(options.get("response_format")): + run_options["outputConfig"] = output_config + return run_options def _prepare_bedrock_messages( @@ -628,7 +655,9 @@ class BedrockChatClient( def _generate_tool_call_id() -> str: return f"tool-call-{uuid4().hex}" - def _process_converse_response(self, response: dict[str, Any]) -> ChatResponse: + def _process_converse_response( + self, response: dict[str, Any], options: Mapping[str, Any] | None = None + ) -> ChatResponse: """Convert Bedrock Converse API response to ChatResponse.""" output = response.get("output") or {} message = output.get("message") or {} @@ -646,6 +675,7 @@ class BedrockChatClient( usage_details=usage_details, model=model, finish_reason=finish_reason, + response_format=options.get("response_format") if options else None, raw_representation=response, ) @@ -728,6 +758,108 @@ class BedrockChatClient( return None return FINISH_REASON_MAP.get(reason.lower()) + def _prepare_output_config(self, response_format: Any | None) -> dict[str, Any] | None: + """Convert response_format into the AWS Bedrock outputConfig wire format. + + Args: + response_format: A Pydantic model class or a dict schema, or None. + + Returns: + A dict for the Converse API ``outputConfig`` parameter, or None if + response_format is not set. + """ + if response_format is None: + return None + + if isinstance(response_format, Mapping): + if "json_schema" in response_format: + # Shape A — OpenAI-style wrapper + json_schema_config = response_format["json_schema"] + schema_src = json_schema_config.get("schema", {}) + name = json_schema_config.get("name", "output_schema") + elif "schema" in response_format: + # Shape B — inner shape directly {"name": ..., "schema": ...} + schema_src = response_format["schema"] + name = response_format.get("name", "output_schema") + else: + # Shape C — assume entire dict is the raw schema + logger.warning( + "response_format dict has no 'json_schema' or 'schema' key; " + "treating entire dict as raw JSON schema." + ) + schema_src = dict(response_format) + name = "output_schema" + + if isinstance(schema_src, str): + schema_src = json.loads(schema_src) + schema = copy.deepcopy(schema_src) + else: + if not isinstance(response_format, type) or not issubclass(response_format, BaseModel): + raise TypeError( + "response_format must be None, a dict JSON schema, " + "or a Pydantic BaseModel subclass." + ) + # response_format is a Pydantic model class + schema = response_format.model_json_schema() + name = response_format.__name__ + + self._set_additional_properties_false(schema) + + json_schema: dict[str, Any] = { + "name": name, + "schema": json.dumps(schema), + } + + description = getattr(response_format, "__doc__", None) if not isinstance(response_format, Mapping) else None + if description and isinstance(description, str) and description.strip(): + json_schema["description"] = description.strip() + + return { + "textFormat": { + "type": "json_schema", + "structure": { + "jsonSchema": json_schema + }, + } + } + + def _set_additional_properties_false(self, schema: dict[str, Any]) -> None: + """Recursively set additionalProperties: false on all object types in a JSON schema. + + AWS requires strict schema enforcement. This mirrors the approach used by + AnthropicChatClient._prepare_response_format(). + + Args: + schema: The JSON schema dict to modify in-place. + """ + visited: set[int] = set() + + def walk(node: Any) -> None: + if isinstance(node, dict): + node_id = id(node) + if node_id in visited: + return + visited.add(node_id) + if node.get("type") == "object" or ( + "properties" in node and "type" not in node + ): + existing = node.get("additionalProperties") + if existing is None or existing is True: + node["additionalProperties"] = False + for value in node.values(): + if isinstance(value, (dict, list)): + walk(value) + elif isinstance(node, list): + node_id = id(node) + if node_id in visited: + return + visited.add(node_id) + for item in node: + if isinstance(item, (dict, list)): + walk(item) + + walk(schema) + def service_url(self) -> str: """Returns the service URL for the Bedrock runtime in the configured AWS region. diff --git a/python/packages/bedrock/tests/test_bedrock_structured_output.py b/python/packages/bedrock/tests/test_bedrock_structured_output.py new file mode 100644 index 0000000000..8df04b5e75 --- /dev/null +++ b/python/packages/bedrock/tests/test_bedrock_structured_output.py @@ -0,0 +1,382 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import copy +import json +from typing import Any +from unittest.mock import patch + +import pytest +from agent_framework import Content, Message +from botocore.exceptions import ClientError +from pydantic import BaseModel + +from agent_framework_bedrock import BedrockChatClient + +# region Test models + + +class WeatherReport(BaseModel): + city: str + temperature: float + summary: str + + +class NestedAddress(BaseModel): + street: str + city: str + zip_code: str + + +class Person(BaseModel): + name: str + age: int + address: NestedAddress + + +# endregion + + +# region Helpers + + +class _StubBedrockRuntime: + """Stub that records calls and returns a canned response.""" + + def __init__(self, response_text: str = "Bedrock says hi") -> None: + self.calls: list[dict[str, Any]] = [] + self._response_text = response_text + + def converse(self, **kwargs: Any) -> dict[str, Any]: + self.calls.append(kwargs) + return { + "modelId": kwargs["modelId"], + "responseId": "resp-structured", + "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30}, + "output": { + "completionReason": "end_turn", + "message": { + "id": "msg-structured", + "role": "assistant", + "content": [{"text": self._response_text}], + }, + }, + } + + +def _make_client(response_text: str = "Bedrock says hi") -> tuple[BedrockChatClient, _StubBedrockRuntime]: + stub = _StubBedrockRuntime(response_text) + client = BedrockChatClient( + model="us.anthropic.claude-haiku-4-5-v1:0", + region="us-east-1", + client=stub, + ) + return client, stub + + +def _user_messages() -> list[Message]: + return [Message(role="user", contents=[Content.from_text(text="Give me a weather report")])] + + +# endregion + + +# region Tests + + +def test_prepare_output_config_correct_wire_shape() -> None: + """_prepare_output_config(WeatherReport) must produce the correct + textFormat → structure → jsonSchema shape with type: 'json_schema'.""" + client, _ = _make_client() + + output_config = client._prepare_output_config(WeatherReport) + + assert output_config is not None + text_format = output_config["textFormat"] + assert text_format["type"] == "json_schema" + assert "structure" in text_format + json_schema = text_format["structure"]["jsonSchema"] + assert json_schema["name"] == "WeatherReport" + assert "schema" in json_schema + + +def test_prepare_output_config_schema_is_json_string() -> None: + """The schema value inside jsonSchema must be a JSON string, not a dict.""" + client, _ = _make_client() + + output_config = client._prepare_output_config(WeatherReport) + + assert output_config is not None + schema_value = output_config["textFormat"]["structure"]["jsonSchema"]["schema"] + assert isinstance(schema_value, str), f"Expected str, got {type(schema_value)}" + # Verify it's valid JSON + parsed = json.loads(schema_value) + assert isinstance(parsed, dict) + assert parsed["type"] == "object" + + +def test_additional_properties_false_set_recursively() -> None: + """additionalProperties: false must be set on all nested object types.""" + client, _ = _make_client() + + output_config = client._prepare_output_config(Person) + + assert output_config is not None + schema_str = output_config["textFormat"]["structure"]["jsonSchema"]["schema"] + schema = json.loads(schema_str) + + # Top-level object + assert schema.get("additionalProperties") is False + + # Check $defs for NestedAddress + defs = schema.get("$defs", {}) + assert "NestedAddress" in defs, "Expected NestedAddress to be present in $defs" + assert defs["NestedAddress"].get("additionalProperties") is False, ( + "Expected additionalProperties=False on nested NestedAddress schema" + ) + + +def test_no_output_config_when_response_format_none() -> None: + """When response_format is None, no outputConfig key should appear in the request.""" + client, stub = _make_client() + messages = _user_messages() + + request = client._prepare_options(messages, {"max_tokens": 100}) + + assert "outputConfig" not in request, ( + f"outputConfig should not be present when response_format is None, got: {request.get('outputConfig')}" + ) + + +async def test_chat_response_value_populated() -> None: + """After a mocked response with response_format, .value should be a populated Pydantic model.""" + json_response = json.dumps({"city": "Seattle", "temperature": 72.5, "summary": "Sunny and warm"}) + client, stub = _make_client(response_text=json_response) + messages = _user_messages() + + response = await client.get_response( + messages=messages, + options={"max_tokens": 100, "response_format": WeatherReport}, + ) + + assert response.text == json_response + assert response.value is not None + assert isinstance(response.value, WeatherReport) + assert response.value.city == "Seattle" + assert response.value.temperature == 72.5 + assert response.value.summary == "Sunny and warm" + + # Verify outputConfig was sent to the API + assert len(stub.calls) == 1 + api_request = stub.calls[0] + assert "outputConfig" in api_request + assert api_request["outputConfig"]["textFormat"]["type"] == "json_schema" + + +def test_dict_schema_response_format() -> None: + """_prepare_output_config should work when response_format is a dict, not just a Pydantic class.""" + client, _ = _make_client() + + dict_schema = { + "json_schema": { + "name": "weather_output", + "schema": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "temp": {"type": "number"}, + }, + }, + } + } + + output_config = client._prepare_output_config(dict_schema) + + assert output_config is not None + json_schema = output_config["textFormat"]["structure"]["jsonSchema"] + assert json_schema["name"] == "weather_output" + schema_parsed = json.loads(json_schema["schema"]) + assert schema_parsed["type"] == "object" + assert "city" in schema_parsed["properties"] + + +def test_prepare_output_config_none_returns_none() -> None: + """_prepare_output_config(None) must return None.""" + client, _ = _make_client() + + result = client._prepare_output_config(None) + + assert result is None + + +async def test_chat_response_value_populated_streaming() -> None: + """In streaming mode, .value should also be populated on the final response.""" + json_response = json.dumps({"city": "Portland", "temperature": 68.0, "summary": "Cloudy"}) + client, stub = _make_client(response_text=json_response) + messages = _user_messages() + + stream = client.get_response( + messages=messages, + stream=True, + options={"max_tokens": 100, "response_format": WeatherReport}, + ) + + # Consume stream and get final response + async for _ in stream: + pass + response = await stream.get_final_response() + + assert response.value is not None + assert isinstance(response.value, WeatherReport) + assert response.value.city == "Portland" + + # Verify outputConfig was sent + assert len(stub.calls) == 1 + assert "outputConfig" in stub.calls[0] + + +async def test_unsupported_model_validation_exception() -> None: + """When a model doesn't support outputConfig, a clear error should be raised.""" + class _FailingStubBedrockRuntime: + def converse(self, **kwargs: Any) -> dict[str, Any]: + # Simulate botocore ClientError for ValidationException + error_response = {"Error": {"Code": "ValidationException", "Message": "Invalid field outputConfig"}} + raise ClientError(error_response, "Converse") + + client = BedrockChatClient( + model="us.anthropic.claude-v2", + region="us-east-1", + client=_FailingStubBedrockRuntime(), + ) + + with pytest.raises(ValueError) as exc: + await client.get_response( + messages=_user_messages(), + options={"response_format": WeatherReport}, + ) + + assert "does not support structured output via outputConfig.textFormat" in str(exc.value) + assert "Check the model's Bedrock Converse outputConfig/textFormat support." in str(exc.value) + + +def test_invalid_response_format_type_raises() -> None: + """Non-dict, non-BaseModel response_format should raise TypeError.""" + client, _ = _make_client() + with pytest.raises(TypeError, match="Pydantic BaseModel subclass"): + client._prepare_output_config("not_a_valid_format") + + +def test_mapping_response_format_accepted() -> None: + """A non-dict Mapping response_format must be accepted and produce + correct outputConfig, not raise TypeError.""" + from collections.abc import MutableMapping + + class _WrappedMapping(MutableMapping): + def __init__(self, data): + self._data = dict(data) + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + client, _ = _make_client() + mapping_format = _WrappedMapping({ + "json_schema": { + "name": "test_output", + "schema": { + "type": "object", + "properties": {"result": {"type": "string"}}, + }, + } + }) + + output_config = client._prepare_output_config(mapping_format) + + assert output_config is not None + json_schema = output_config["textFormat"]["structure"]["jsonSchema"] + assert json_schema["name"] == "test_output" + schema = json.loads(json_schema["schema"]) + assert schema.get("additionalProperties") is False + + +def test_shape_b_dict_schema_wire_format() -> None: + """Dict response_format in Shape B (inner shape directly) should + produce correct outputConfig.""" + client, _ = _make_client() + + response_format = { + "name": "weather_output", + "schema": { + "type": "object", + "properties": { + "city": {"type": "string"}, + "temperature": {"type": "number"}, + }, + }, + } + + output_config = client._prepare_output_config(response_format) + + assert output_config is not None + text_format = output_config["textFormat"] + assert text_format["type"] == "json_schema" + json_schema = text_format["structure"]["jsonSchema"] + assert json_schema["name"] == "weather_output" + schema = json.loads(json_schema["schema"]) + assert schema.get("additionalProperties") is False + + +def test_dict_schema_not_mutated() -> None: + """Caller's dict schema must not be mutated by _prepare_output_config.""" + client, _ = _make_client() + original_schema = { + "json_schema": { + "name": "test", + "schema": { + "type": "object", + "properties": {"a": {"type": "string"}}, + }, + } + } + snapshot = copy.deepcopy(original_schema) + client._prepare_output_config(original_schema) + assert original_schema == snapshot, "Original dict schema was mutated" + + +async def test_non_outputconfig_validation_exception_propagates() -> None: + """ValidationException unrelated to outputConfig must propagate + as raw ClientError, not be caught and reclassified.""" + client, _ = _make_client() + error_response = { + "Error": { + "Code": "ValidationException", + "Message": "Invalid message format", + } + } + with ( + patch.object( + client, + "_bedrock_client", + **{"converse.side_effect": ClientError(error_response, "Converse")}, + ), + pytest.raises(ClientError), + ): + await client.get_response( + messages=_user_messages(), + options={"max_tokens": 100}, + ) + + +# endregion From c83a944e85e7615d5c12adc772310e649667f637 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:09:36 +0900 Subject: [PATCH 34/61] Fix open pr count check (#6255) --- .github/scripts/pr_limit_moderation.js | 21 +++--- .github/tests/test_pr_limit_moderation.js | 84 +++++++++++++++-------- 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/.github/scripts/pr_limit_moderation.js b/.github/scripts/pr_limit_moderation.js index 489a06ea78..450813d22f 100644 --- a/.github/scripts/pr_limit_moderation.js +++ b/.github/scripts/pr_limit_moderation.js @@ -63,19 +63,22 @@ function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) } async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber }) { - const query = `repo:${owner}/${repo} is:pr is:open author:${author}`; - const response = await github.rest.search.issuesAndPullRequests({ - q: query, + const openPullRequests = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: 'open', per_page: 100, }); - const indexedPrNumbers = response.data.items.map((item) => item.number); - const currentPrIsIndexed = indexedPrNumbers.includes(pullRequestNumber); - if (currentPrIsIndexed || response.data.total_count >= 100) { - return response.data.total_count; - } + const authorOpenPullRequestNumbers = openPullRequests + .filter((pullRequest) => pullRequest.user?.login === author) + .map((pullRequest) => pullRequest.number); + const currentPrIsOpen = authorOpenPullRequestNumbers.includes(pullRequestNumber); + const existingOpenPrCount = currentPrIsOpen + ? authorOpenPullRequestNumbers.length - 1 + : authorOpenPullRequestNumbers.length; - return response.data.total_count + 1; + return existingOpenPrCount + 1; } async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) { diff --git a/.github/tests/test_pr_limit_moderation.js b/.github/tests/test_pr_limit_moderation.js index 67192c8327..d3dccb0cce 100644 --- a/.github/tests/test_pr_limit_moderation.js +++ b/.github/tests/test_pr_limit_moderation.js @@ -44,23 +44,20 @@ function createCore() { }; } -function createGithub({ totalCount, itemNumbers, labelExists = true }) { +function createGithub({ + itemNumbers, + labelExists = true, + pullRequests = createPullRequestPage({ numbers: itemNumbers }), +}) { const calls = []; return { calls, + async paginate(method, params) { + calls.push({ api: 'paginate', method, params }); + return pullRequests; + }, rest: { - search: { - async issuesAndPullRequests(params) { - calls.push({ api: 'search.issuesAndPullRequests', params }); - return { - data: { - total_count: totalCount, - items: itemNumbers.map((number) => ({ number })), - }, - }; - }, - }, issues: { async getLabel(params) { calls.push({ api: 'issues.getLabel', params }); @@ -85,6 +82,10 @@ function createGithub({ totalCount, itemNumbers, labelExists = true }) { }, }, pulls: { + async list(params) { + calls.push({ api: 'pulls.list', params }); + return { data: pullRequests }; + }, async update(params) { calls.push({ api: 'pulls.update', params }); return { data: { state: params.state } }; @@ -94,6 +95,15 @@ function createGithub({ totalCount, itemNumbers, labelExists = true }) { }; } +function createPullRequestPage({ author = 'community-user', numbers }) { + return numbers.map((number) => ({ + number, + user: { + login: author, + }, + })); +} + // --------------------------------------------------------------------------- // PR limit enforcement @@ -102,7 +112,6 @@ function createGithub({ totalCount, itemNumbers, labelExists = true }) { describe('PR limit enforcement', () => { it('does not close the PR when the author is at the open PR limit', async () => { const github = createGithub({ - totalCount: 10, itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 123], }); @@ -119,14 +128,13 @@ describe('PR limit enforcement', () => { assert.equal(result.openPrCount, 10); assert.deepEqual( github.calls.map((call) => call.api), - ['search.issuesAndPullRequests'], + ['paginate'], ); }); - it('counts the new PR when search has not indexed it yet', async () => { + it('counts the new PR when the pull list includes it', async () => { const github = createGithub({ - totalCount: 10, - itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + itemNumbers: [123, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], }); const result = await enforcePrLimit({ @@ -143,7 +151,7 @@ describe('PR limit enforcement', () => { assert.deepEqual( github.calls.map((call) => call.api), [ - 'search.issuesAndPullRequests', + 'paginate', 'issues.getLabel', 'issues.addLabels', 'issues.createComment', @@ -152,9 +160,31 @@ describe('PR limit enforcement', () => { ); }); + it('counts the current PR on top of existing open PRs', async () => { + const github = createGithub({ + itemNumbers: [123, ...Array.from({ length: 24 }, (_, index) => index + 1)], + pullRequests: createPullRequestPage({ + numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)], + }), + }); + + const result = await enforcePrLimit({ + github, + context: createContext(), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, true); + assert.equal(result.openPrCount, 26); + const comment = github.calls.find((call) => call.api === 'issues.createComment').params.body; + assert.match(comment, /This PR would put you at 26 open pull requests/); + }); + it('creates the label when it does not already exist', async () => { const github = createGithub({ - totalCount: 11, itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], labelExists: false, }); @@ -172,7 +202,7 @@ describe('PR limit enforcement', () => { assert.deepEqual( github.calls.map((call) => call.api), [ - 'search.issuesAndPullRequests', + 'paginate', 'issues.getLabel', 'issues.createLabel', 'issues.addLabels', @@ -188,7 +218,6 @@ describe('PR limit enforcement', () => { it('tolerates a 422 race when creating the label', async () => { const github = createGithub({ - totalCount: 11, itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], labelExists: false, }); @@ -212,7 +241,7 @@ describe('PR limit enforcement', () => { assert.deepEqual( github.calls.map((call) => call.api), [ - 'search.issuesAndPullRequests', + 'paginate', 'issues.getLabel', 'issues.createLabel', 'issues.addLabels', @@ -224,8 +253,11 @@ describe('PR limit enforcement', () => { it('uses a diplomatic close message with the configured limit', async () => { const github = createGithub({ - totalCount: 11, itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], + pullRequests: createPullRequestPage({ + author: 'octo-contributor', + numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], + }), }); await enforcePrLimit({ @@ -246,7 +278,6 @@ describe('PR limit enforcement', () => { it('does not close an exempt PR when it is reopened', async () => { const github = createGithub({ - totalCount: 11, itemNumbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 123], }); @@ -265,10 +296,9 @@ describe('PR limit enforcement', () => { assert.deepEqual(github.calls, []); }); - it('does not over-count when the current PR is not on the first search page', async () => { + it('counts the current PR when the author has more than one page of open PRs', async () => { const github = createGithub({ - totalCount: 101, - itemNumbers: Array.from({ length: 100 }, (_, index) => index + 1), + itemNumbers: [123, ...Array.from({ length: 100 }, (_, index) => index + 1)], }); const result = await enforcePrLimit({ From 05ebb966cf7555aa3690a9fae11188b83f68daf1 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:48:42 +0800 Subject: [PATCH 35/61] fix: skip orphan anthropic thinking signatures (#5784) --- .../agent_framework_anthropic/_chat_client.py | 9 ++++ .../anthropic/tests/test_anthropic_client.py | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 98c181f152..c90b061b4f 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -803,6 +803,15 @@ class RawAnthropicClient( } a_content.append(mcp_result) case "text_reasoning": + if content.text is None: + if ( + content.protected_data + and a_content + and a_content[-1].get("type") == "thinking" + and "signature" not in a_content[-1] + ): + a_content[-1]["signature"] = content.protected_data + continue thinking_block: dict[str, Any] = {"type": "thinking", "thinking": content.text} if content.protected_data: thinking_block["signature"] = content.protected_data diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 0cfec3423c..abad158b8c 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -485,6 +485,48 @@ def test_prepare_message_for_anthropic_text_reasoning_with_signature( assert result["content"][0]["signature"] == "sig_abc123" +def test_prepare_message_for_anthropic_attaches_signature_only_reasoning( + mock_anthropic_client: MagicMock, +) -> None: + client = create_test_anthropic_client(mock_anthropic_client) + message = Message( + role="assistant", + contents=[ + Content.from_text_reasoning(text="Let me think about this..."), + Content.from_text_reasoning(text=None, protected_data="sig_abc123"), + ], + ) + + result = client._prepare_message_for_anthropic(message) + + assert result["content"] == [ + {"type": "thinking", "thinking": "Let me think about this...", "signature": "sig_abc123"} + ] + + +def test_prepare_message_for_anthropic_skips_orphan_signature_only_reasoning( + mock_anthropic_client: MagicMock, +) -> None: + client = create_test_anthropic_client(mock_anthropic_client) + message = Message( + role="assistant", + contents=[ + Content.from_text_reasoning(text=None, protected_data="sig_abc123"), + Content.from_function_call( + call_id="call_123", + name="get_weather", + arguments={"location": "San Francisco"}, + ), + ], + ) + + result = client._prepare_message_for_anthropic(message) + + assert len(result["content"]) == 1 + assert result["content"][0]["type"] == "tool_use" + assert result["content"][0]["id"] == "call_123" + + def test_prepare_message_for_anthropic_mcp_server_tool_call( mock_anthropic_client: MagicMock, ) -> None: From 043208241a4f54769cfe499b1b24e0c3ea720fcb Mon Sep 17 00:00:00 2001 From: Hameed Kunkanoor <41198503+Hameedkunkanoor@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:00:36 +0530 Subject: [PATCH 36/61] Python: Persist hosted MCP call/results as canonical mcp_call output (#6070) * Persist hosted MCP call/results as canonical mcp_call output - Preserve hosted MCP call/result pairs as canonical mcp_call output items - Coalesce MCP call + result in non-streaming conversion path - Keep call-id alignment for MCP tool call tracking and output mapping - Update tests and package metadata * Fix missing Mapping import in hosted responses adapter * Fix pyright unknown type in MCP output stringification * Fix typing for MCP output sequence iteration * Improve MCP output robustness and avoid eager flattening * Bump foundry_hosting to b7 and update responses dependency to b7 * Restore foundry_hosting package version to 1.0.0a260521 * Refactor hosted MCP output parsing --- .../_responses.py | 171 ++++++++++++--- .../packages/foundry_hosting/pyproject.toml | 2 +- .../foundry_hosting/tests/test_responses.py | 197 ++++++++++++++++++ 3 files changed, 338 insertions(+), 32 deletions(-) diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py index 8940365930..6dc9bbd8c6 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py @@ -9,7 +9,7 @@ import logging import os import tempfile import threading -from collections.abc import AsyncIterable, AsyncIterator, Generator, Sequence +from collections.abc import AsyncIterable, AsyncIterator, Generator, Mapping, Sequence from contextlib import AbstractAsyncContextManager, AsyncExitStack, suppress from dataclasses import asdict, is_dataclass from pathlib import Path @@ -472,14 +472,12 @@ class ResponsesHostServer(ResponsesAgentServerHost): # Run the agent in non-streaming mode response = await self._agent.run(stream=False, **run_kwargs) # type: ignore[reportUnknownMemberType] - for message in response.messages: - for content in message.contents: - async for item in _to_outputs( - response_event_stream, - content, - approval_storage=self._approval_storage, - ): - yield item + async for item in _to_outputs_for_messages( + response_event_stream, + response.messages, + approval_storage=self._approval_storage, + ): + yield item yield response_event_stream.emit_completed() else: if tracker is None: # pragma: no cover - defensive, set above @@ -620,10 +618,8 @@ class ResponsesHostServer(ResponsesAgentServerHost): checkpoint_storage=write_storage, ) - for message in response.messages: - for content in message.contents: - async for item in _to_outputs(response_event_stream, content): - yield item + async for item in _to_outputs_for_messages(response_event_stream, response.messages): + yield item await self._delete_not_latest_checkpoints(write_storage, self._agent.workflow.name) yield response_event_stream.emit_completed() @@ -729,7 +725,7 @@ class _OutputItemTracker: yield self._fc_builder.emit_arguments_delta(args_str) elif content.type == "mcp_server_tool_call" and content.tool_name: - key = f"{content.server_name or 'default'}::{content.tool_name}" + key = content.call_id or f"{content.server_name or 'default'}::{content.tool_name}" if self._active_type != "mcp_server_tool_call" or self._active_id != key: yield from self._close() yield from self._open_mcp_call(content) @@ -738,6 +734,24 @@ class _OutputItemTracker: if self._mcp_builder is not None: yield self._mcp_builder.emit_arguments_delta(args_str) + elif ( + content.type == "mcp_server_tool_result" + and self._active_type == "mcp_server_tool_call" + and self._mcp_builder is not None + and content.call_id is not None + and content.call_id == self._mcp_builder.item_id + ): + accumulated = "".join(self._accumulated) + yield self._mcp_builder.emit_arguments_done(accumulated) + yield self._mcp_builder.emit_completed() + yield self._mcp_builder.emit_done(output=_stringify_mcp_output(content.output)) + self._mcp_builder = None + self._active_type = None + self._active_id = None + self._accumulated.clear() + self.needs_async = False + return + else: yield from self._close() self.needs_async = True @@ -777,9 +791,10 @@ class _OutputItemTracker: self._mcp_builder = self._stream.add_output_item_mcp_call( server_label=content.server_name or "default", name=content.tool_name or "", + item_id=content.call_id, ) self._active_type = "mcp_server_tool_call" - self._active_id = f"{content.server_name or 'default'}::{content.tool_name}" + self._active_id = content.call_id or f"{content.server_name or 'default'}::{content.tool_name}" yield self._mcp_builder.emit_added() def _close(self) -> Generator[ResponseStreamEvent]: @@ -927,16 +942,19 @@ async def _item_to_message(item: Item, *, approval_storage: ApprovalStorage | No if item.type == "mcp_call": mcp = cast(ItemMcpToolCall, item) + contents = [ + Content.from_mcp_server_tool_call( + mcp.id, + mcp.name, + server_name=mcp.server_label, + arguments=mcp.arguments, + ) + ] + if getattr(mcp, "output", None) is not None: + contents.append(Content.from_mcp_server_tool_result(call_id=mcp.id, output=mcp.output)) return Message( role="assistant", - contents=[ - Content.from_mcp_server_tool_call( - mcp.id, - mcp.name, - server_name=mcp.server_label, - arguments=mcp.arguments, - ) - ], + contents=contents, ) if item.type == "mcp_approval_request": @@ -1197,16 +1215,19 @@ async def _output_item_to_message(item: OutputItem, *, approval_storage: Approva if item.type == "mcp_call": mcp = cast(OutputItemMcpToolCall, item) + contents = [ + Content.from_mcp_server_tool_call( + mcp.id, + mcp.name, + server_name=mcp.server_label, + arguments=mcp.arguments, + ) + ] + if getattr(mcp, "output", None) is not None: + contents.append(Content.from_mcp_server_tool_result(call_id=mcp.id, output=mcp.output)) return Message( role="assistant", - contents=[ - Content.from_mcp_server_tool_call( - mcp.id, - mcp.name, - server_name=mcp.server_label, - arguments=mcp.arguments, - ) - ], + contents=contents, ) if item.type == "mcp_approval_request": @@ -1583,6 +1604,7 @@ async def _to_outputs( mcp_call = stream.add_output_item_mcp_call( server_label=content.server_name or "default", name=content.tool_name or "", + item_id=content.call_id, ) yield mcp_call.emit_added() async for event in mcp_call.aarguments(_arguments_to_str(content.arguments)): @@ -1657,4 +1679,91 @@ async def _to_outputs( logger.warning(f"Content type '{content.type}' is not supported yet. This is usually safe to ignore.") +def _stringify_mcp_output(output: Any) -> str: + """Convert hosted MCP output payloads into the string shape expected by mcp_call.output.""" + if output is None: + return "" + if isinstance(output, str): + return output + if isinstance(output, Mapping): + text = cast(Any, output).get("text") + if isinstance(text, str): + return text + return json.dumps(output, default=str) + if isinstance(output, Sequence) and not isinstance(output, (str, bytes, bytearray)): + parts: list[str] = [] + entries = cast(Sequence[object], output) + for entry in entries: + if isinstance(entry, Content) and entry.type == "text": + parts.append(entry.text or "") + continue + parts.append(_stringify_mcp_output(entry)) + return "".join(parts) + return str(output) + + +def _emit_completed_mcp_call( + stream: ResponseEventStream, + call_content: Content, + *, + arguments: str, + output: str, +) -> Generator[ResponseStreamEvent]: + """Emit a single completed MCP call item carrying both arguments and output.""" + mcp_call = stream.add_output_item_mcp_call( + server_label=call_content.server_name or "default", + name=call_content.tool_name or "", + item_id=call_content.call_id, + ) + yield mcp_call.emit_added() + yield mcp_call.emit_arguments_done(arguments) + yield mcp_call.emit_completed() + yield mcp_call.emit_done(output=output) + + +async def _to_outputs_for_messages( + stream: ResponseEventStream, + messages: Sequence[Message], + *, + approval_storage: ApprovalStorage | None = None, +) -> AsyncIterator[ResponseStreamEvent]: + """Convert messages to output events with hosted-MCP call/result coalescing. + + Parse once in message/content order and emit either: + - a single canonical completed ``mcp_call`` when adjacent hosted MCP + call/result content are encountered, or + - standard output items for all other content types. + """ + pending_mcp_call: Content | None = None + + for message in messages: + for content in message.contents: + if pending_mcp_call is not None: + if content.type == "mcp_server_tool_result" and content.call_id == pending_mcp_call.call_id: + for event in _emit_completed_mcp_call( + stream, + pending_mcp_call, + arguments=_arguments_to_str(pending_mcp_call.arguments), + output=_stringify_mcp_output(content.output), + ): + yield event + pending_mcp_call = None + continue + + async for event in _to_outputs(stream, pending_mcp_call, approval_storage=approval_storage): + yield event + pending_mcp_call = None + + if content.type == "mcp_server_tool_call" and content.call_id: + pending_mcp_call = content + continue + + async for event in _to_outputs(stream, content, approval_storage=approval_storage): + yield event + + if pending_mcp_call is not None: + async for event in _to_outputs(stream, pending_mcp_call, approval_storage=approval_storage): + yield event + + # endregion diff --git a/python/packages/foundry_hosting/pyproject.toml b/python/packages/foundry_hosting/pyproject.toml index f2d9686b8a..e9f9949a33 100644 --- a/python/packages/foundry_hosting/pyproject.toml +++ b/python/packages/foundry_hosting/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ dependencies = [ "agent-framework-core>=1.7.0,<2", "azure-ai-agentserver-core>=2.0.0b3,<3", - "azure-ai-agentserver-responses>=1.0.0b5,<2", + "azure-ai-agentserver-responses>=1.0.0b7,<2", "azure-ai-agentserver-invocations>=1.0.0b3,<2", ] diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index 9358549a86..0bfff345a7 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -260,6 +260,50 @@ class TestNonStreaming: assert "function_call_output" in types assert "message" in types + async def test_hosted_mcp_call_and_result_persist_as_single_mcp_call(self) -> None: + agent = _make_agent( + response=AgentResponse( + messages=[ + Message( + role="assistant", + contents=[ + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ) + ], + ), + Message( + role="tool", + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_abc123", + output=[Content.from_text(text="found 10 cats")], + ) + ], + ), + Message(role="assistant", contents=[Content.from_text("I found 10 cats!")]), + ] + ) + ) + server = _make_server(agent) + resp = await _post(server, stream=False) + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "completed" + + types = [item["type"] for item in body["output"]] + assert "mcp_call" in types + assert "custom_tool_call_output" not in types + + mcp_items = [item for item in body["output"] if item["type"] == "mcp_call"] + assert len(mcp_items) == 1 + assert mcp_items[0]["id"] == "mcp_abc123" + assert mcp_items[0]["output"] == "found 10 cats" + async def test_reasoning_content(self) -> None: agent = _make_agent( response=AgentResponse( @@ -617,6 +661,53 @@ class TestStreaming: assert "response.output_item.added" in types assert "response.output_item.done" in types + async def test_mcp_tool_call_and_result_streaming_emit_single_completed_mcp_call(self) -> None: + agent = _make_agent( + stream_updates=[ + AgentResponseUpdate( + contents=[ + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q":', + ) + ], + role="assistant", + ), + AgentResponseUpdate( + contents=[ + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments=' "cats"}', + ) + ], + role="assistant", + ), + AgentResponseUpdate( + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_abc123", + output=[Content.from_text(text="found 10 cats")], + ) + ], + role="tool", + ), + ] + ) + server = _make_server(agent) + resp = await _post(server, stream=True) + + assert resp.status_code == 200 + events = _parse_sse_events(resp.text) + done_events = [e for e in events if e["event"] == "response.output_item.done"] + assert len(done_events) == 1 + assert done_events[0]["data"]["item"]["type"] == "mcp_call" + assert done_events[0]["data"]["item"]["id"] == "mcp_abc123" + assert done_events[0]["data"]["item"]["output"] == "found 10 cats" + # endregion @@ -720,6 +811,24 @@ class TestOutputItemToMessage: assert msg.contents[0].server_name == "my_server" assert msg.contents[0].tool_name == "search" + async def test_mcp_call_with_output_reconstructs_mcp_result_content(self) -> None: + from azure.ai.agentserver.responses.models import OutputItemMcpToolCall + + item = OutputItemMcpToolCall({ + "type": "mcp_call", + "id": "mcp-1", + "server_label": "my_server", + "name": "search", + "arguments": '{"q": "test"}', + "output": "found 10 cats", + }) + msg = await _output_item_to_message(item) + assert msg.role == "assistant" + assert len(msg.contents) == 2 + assert msg.contents[0].type == "mcp_server_tool_call" + assert msg.contents[1].type == "mcp_server_tool_result" + assert msg.contents[1].output == "found 10 cats" + async def test_mcp_approval_request(self) -> None: from azure.ai.agentserver.responses.models import OutputItemMcpApprovalRequest @@ -1189,6 +1298,25 @@ class TestItemToMessage: assert msg.contents[0].server_name == "my_server" assert msg.contents[0].tool_name == "search" + async def test_mcp_call_with_output_reconstructs_mcp_result_content(self) -> None: + from azure.ai.agentserver.responses.models import ItemMcpToolCall + + item = ItemMcpToolCall({ + "type": "mcp_call", + "id": "mcp-1", + "server_label": "my_server", + "name": "search", + "arguments": '{"q": "test"}', + "output": "found 10 cats", + }) + msg = await _item_to_message(item) + assert msg is not None + assert msg.role == "assistant" + assert len(msg.contents) == 2 + assert msg.contents[0].type == "mcp_server_tool_call" + assert msg.contents[1].type == "mcp_server_tool_result" + assert msg.contents[1].output == "found 10 cats" + async def test_mcp_approval_request(self) -> None: from azure.ai.agentserver.responses.models import ItemMcpApprovalRequest @@ -1937,6 +2065,75 @@ class TestMultiTurnMixedContent: assert len(fc_contents) >= 1 assert fc_contents[0].name == "search" + async def test_hosted_mcp_call_round_trip_does_not_orphan_function_call_output(self) -> None: + """Turn 1 produces hosted MCP call + result, turn 2 must replay both without orphaning output.""" + agent = _make_multi_response_agent([ + AgentResponse( + messages=[ + Message( + role="assistant", + contents=[ + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ) + ], + ), + Message( + role="tool", + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_abc123", + output=[Content.from_text(text="found 10 cats")], + ) + ], + ), + Message(role="assistant", contents=[Content.from_text("I found 10 cats!")]), + ] + ), + AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Here are more details")])]), + ]) + server = _make_server(agent) + + resp1 = await _post(server, input_text="Search for cats", stream=False) + assert resp1.status_code == 200 + response_id = resp1.json()["id"] + + types1 = [item["type"] for item in resp1.json()["output"]] + assert "mcp_call" in types1 + assert "custom_tool_call_output" not in types1 + + resp2 = await _post_json( + server, + { + "model": "test-model", + "input": "Tell me more", + "stream": False, + "previous_response_id": response_id, + }, + ) + assert resp2.status_code == 200 + assert resp2.json()["status"] == "completed" + + second_call_messages = agent.run.call_args_list[1].kwargs["messages"] + mcp_call_contents = [ + c for m in second_call_messages for c in m.contents if c.type == "mcp_server_tool_call" + ] + mcp_result_contents = [ + c for m in second_call_messages for c in m.contents if c.type == "mcp_server_tool_result" + ] + function_result_contents = [ + c for m in second_call_messages for c in m.contents if c.type == "function_result" + ] + + assert len(mcp_call_contents) >= 1 + assert len(mcp_result_contents) >= 1 + assert all((c.call_id or "") != "mcp_abc123" for c in function_result_contents) + assert any((c.call_id or "") == "mcp_abc123" for c in mcp_call_contents) + assert any((c.call_id or "") == "mcp_abc123" for c in mcp_result_contents) + async def test_multi_turn_reasoning_in_history(self) -> None: """Turn 1 produces reasoning + text, turn 2 sees them in history.""" agent = _make_multi_response_agent([ From cdc4809b8a503ec86d78a90051e5cc5d053ebf21 Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:43:08 -0700 Subject: [PATCH 37/61] ci: harden Python test coverage workflow (#5982) Improve input handling and token management in the Python test coverage workflows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/python-test-coverage-report.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-test-coverage-report.yml b/.github/workflows/python-test-coverage-report.yml index f03967e72a..5637b5a2fb 100644 --- a/.github/workflows/python-test-coverage-report.yml +++ b/.github/workflows/python-test-coverage-report.yml @@ -8,6 +8,7 @@ on: permissions: contents: read + actions: read pull-requests: write jobs: @@ -23,7 +24,7 @@ jobs: - name: Download coverage report uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + github-token: ${{ github.token }} run-id: ${{ github.event.workflow_run.id }} path: ./python merge-multiple: true @@ -38,9 +39,9 @@ jobs: echo "PR number file 'pr_number' is missing or empty" exit 1 fi - PR_NUMBER=$(head -1 pr_number | tr -dc '0-9') - if [ -z "$PR_NUMBER" ]; then - echo "PR number file 'pr_number' does not contain a valid PR number" + PR_NUMBER=$(cat pr_number) + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "::error::PR number file contains invalid content" exit 1 fi echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" @@ -48,7 +49,7 @@ jobs: id: coverageComment uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0 with: - github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} + github-token: ${{ github.token }} issue-number: ${{ env.PR_NUMBER }} pytest-xml-coverage-path: python/python-coverage.xml title: "Python Test Coverage Report" From 0cf48923cd6793f9da042c6b8b819f632a65957e Mon Sep 17 00:00:00 2001 From: semenshi-m Date: Tue, 2 Jun 2026 09:41:21 +0100 Subject: [PATCH 38/61] .NET: Add Hosted-ToolboxMcpSkills sample (#6175) * .NET: Add Hosted-ToolboxMcpSkills sample Adds a hosted Foundry Responses sample that discovers MCP-based skills from a Foundry Toolbox and makes them available to the agent via AgentSkillsProvider. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align README and Program.cs default model to gpt-5 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify MCP skills provider log to avoid implying eager discovery Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop redundant skills provider configured log Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Foundry Toolbox Skills tag to manifest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify BearerTokenHandler by deriving from HttpClientHandler Removes the need for an explicit InnerHandler. Enables CheckCertificateRevocationList to satisfy CA5399. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 3 + .../Hosted-ToolboxMcpSkills/.env.example | 6 + .../Hosted-ToolboxMcpSkills/Dockerfile | 26 +++++ .../Dockerfile.contributor | 18 +++ .../HostedToolboxMcpSkills.csproj | 36 ++++++ .../Hosted-ToolboxMcpSkills/Program.cs | 109 ++++++++++++++++++ .../Hosted-ToolboxMcpSkills/README.md | 103 +++++++++++++++++ .../agent.manifest.yaml | 43 +++++++ .../Hosted-ToolboxMcpSkills/agent.yaml | 14 +++ 9 files changed, 358 insertions(+) create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index fbf09d442a..e395627bc9 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -344,6 +344,9 @@ + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example new file mode 100644 index 0000000000..5c312b3f8e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/.env.example @@ -0,0 +1,6 @@ +AZURE_AI_PROJECT_ENDPOINT= +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5 +FOUNDRY_TOOLBOX_NAME= +AZURE_BEARER_TOKEN=DefaultAzureCredential diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile new file mode 100644 index 0000000000..d04bf72711 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile @@ -0,0 +1,26 @@ +# Dockerfile for end-users consuming the Agent Framework via NuGet packages. +# +# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container, +# which only succeeds when the project references its dependencies via PackageReference (see the +# commented-out section in HostedToolboxMcpSkills.csproj). Contributors building from the +# agent-framework repository source must use Dockerfile.contributor instead because +# ProjectReference dependencies live outside this folder and cannot be restored from inside +# this build context. +# +# Use the official .NET 10.0 ASP.NET runtime as a parent image +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore +RUN dotnet publish -c Release -o /app/publish + +# Final stage +FROM base AS final +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 8088 +ENV ASPNETCORE_URLS=http://+:8088 +ENTRYPOINT ["dotnet", "HostedToolboxMcpSkills.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor new file mode 100644 index 0000000000..a01cc1e38c --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Dockerfile.contributor @@ -0,0 +1,18 @@ +# Dockerfile for contributors building from the agent-framework repository source. +# +# This project uses ProjectReference to the local source, which means a standard +# multi-stage Docker build cannot resolve dependencies outside this folder. +# Pre-publish the app targeting the container runtime and copy the output: +# +# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +# docker build -f Dockerfile.contributor -t hosted-toolbox-mcp-skills . +# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-toolbox-mcp-skills -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-toolbox-mcp-skills +# +# For end-users consuming the NuGet package (not ProjectReference), use the standard +# Dockerfile which performs a full dotnet restore + publish inside the container. +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final +WORKDIR /app +COPY out/ . +EXPOSE 8088 +ENV ASPNETCORE_URLS=http://+:8088 +ENTRYPOINT ["dotnet", "HostedToolboxMcpSkills.dll"] diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj new file mode 100644 index 0000000000..d4c4155baf --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj @@ -0,0 +1,36 @@ + + + + net10.0 + enable + enable + false + HostedToolboxMcpSkills + HostedToolboxMcpSkills + $(NoWarn); + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs new file mode 100644 index 0000000000..f8ca3f4991 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/Program.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Hosted Toolbox MCP Skills Agent +// +// Demonstrates how to host an agent that discovers MCP-based skills from a +// Foundry Toolbox MCP endpoint and injects them as AIContextProviders using +// AgentSkillsProviderBuilder.UseMcpSkills(). +// +// Required environment variables: +// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint +// FOUNDRY_TOOLBOX_NAME - Name of the Foundry Toolbox to connect to +// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-5) + +using System.Net.Http.Headers; +using Azure.AI.Projects; +using Azure.Core; +using Azure.Identity; +using DotNetEnv; +using Hosted_Shared_Contributor_Setup; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry.Hosting; +using ModelContextProtocol.Client; + +// Load .env file if present (for local development) +Env.TraversePath().Load(); + +var projectEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5"; +var toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_NAME") + ?? throw new InvalidOperationException("FOUNDRY_TOOLBOX_NAME is not set."); + +// Build the Toolbox MCP URL from the project endpoint and toolbox name. +var toolboxMcpServerUrl = $"{projectEndpoint.TrimEnd('/')}/toolboxes/{toolboxName}/mcp?api-version=v1"; + +// Use a chained credential: try a temporary dev token first (for local Docker debugging), +// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production). +TokenCredential credential = new ChainedTokenCredential( + new DevTemporaryTokenCredential(), + new DefaultAzureCredential()); + +// ── Connect to the Foundry Toolbox MCP endpoint ───────────────────────────── +// Create an HttpClient that attaches a fresh Foundry bearer token to every request. +using var httpClient = new HttpClient(new BearerTokenHandler(credential, "https://ai.azure.com/.default") { CheckCertificateRevocationList = true }); + +Console.WriteLine($"Connecting to Foundry Toolbox '{toolboxName}' MCP server..."); + +await using var mcpClient = await McpClient.CreateAsync( + new HttpClientTransport( + new HttpClientTransportOptions + { + Endpoint = new Uri(toolboxMcpServerUrl), + Name = toolboxName, + TransportMode = HttpTransportMode.StreamableHttp, + AdditionalHeaders = new Dictionary + { + ["Foundry-Features"] = "Toolboxes=V1Preview", + }, + }, + httpClient)); + +// ── Configure MCP-based skills provider ────────────────────────────────────── +var skillsProvider = new AgentSkillsProviderBuilder() + .UseMcpSkills(mcpClient) + .Build(); + +// ── Create the agent ───────────────────────────────────────────────────────── +AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential) + .AsAIAgent(new ChatClientAgentOptions + { + Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-mcp-skills", + Description = "Hosted agent with MCP skills discovered from a Foundry Toolbox", + ChatOptions = new() + { + ModelId = deployment, + Instructions = "You are a helpful assistant.", + }, + AIContextProviders = [skillsProvider], + }); + +// ── Build the host ─────────────────────────────────────────────────────────── +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFoundryResponses(agent); +builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production. + +var app = builder.Build(); +app.MapFoundryResponses(); + +// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses +// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint). +// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path. +app.MapDevTemporaryLocalAgentEndpoint(); + +app.Run(); + +// --------------------------------------------------------------------------- +// HttpClientHandler: attaches a fresh Foundry bearer token to every request +// --------------------------------------------------------------------------- +internal sealed class BearerTokenHandler(TokenCredential credential, string scope) : HttpClientHandler +{ + private readonly TokenRequestContext _tokenContext = new([scope]); + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + AccessToken token = await credential.GetTokenAsync(this._tokenContext, cancellationToken).ConfigureAwait(false); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md new file mode 100644 index 0000000000..0c6b5ba60e --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md @@ -0,0 +1,103 @@ +# Hosted-ToolboxMcpSkills + +A hosted agent that discovers **MCP-based skills from a Foundry Toolbox** and makes them available to the agent using `AgentSkillsProviderBuilder.UseMcpSkills(mcpClient)`. + +The `AgentSkillsProvider` is attached to the agent as a context provider and implements the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern. When the agent is prompted, it discovers available skills in the Foundry Toolbox via the provider: + +1. **Advertise** - skill names and descriptions are injected into the system prompt so the agent knows what is available. +2. **Load** - when the agent decides a skill is relevant, it retrieves the full skill body with detailed instructions via the provider. +3. **Read resources** - if a skill includes supplementary content (reference documents, assets), the agent reads them on demand via the provider. + +This way the full skill body and resources are only loaded when the agent actually needs them, reducing token usage. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- An Azure AI Foundry project with a deployed model (e.g., `gpt-5`) +- A Foundry Toolbox already configured with skills provisioned +- Azure CLI logged in (`az login`) + +## Configuration + +Copy the template and fill in your values: + +```bash +cp .env.example .env +``` + +Edit `.env` and set your Azure AI Foundry project endpoint and toolbox name: + +```env +AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/ +ASPNETCORE_URLS=http://+:8088 +ASPNETCORE_ENVIRONMENT=Development +AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5 +FOUNDRY_TOOLBOX_NAME=my-toolbox +``` + +> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference. + +## Running directly (contributors) + +This project uses `ProjectReference` to build against the local Agent Framework source. + +```bash +cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills +dotnet run +``` + +The agent will start on `http://localhost:8088`. + +### Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "What skills do you have available?" +``` + +## Running with Docker + +Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output. + +### 1. Publish for the container runtime (Linux Alpine) + +```bash +dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out +``` + +### 2. Build the Docker image + +```bash +docker build -f Dockerfile.contributor -t hosted-toolbox-mcp-skills . +``` + +### 3. Run the container + +Generate a bearer token on your host and pass it to the container: + +```bash +# Generate token (expires in ~1 hour) +export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) + +# Run with token +docker run --rm -p 8088:8088 \ + -e AGENT_NAME=hosted-toolbox-mcp-skills \ + -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \ + --env-file .env \ + hosted-toolbox-mcp-skills +``` + +> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration. + +### 4. Test it + +Using the Azure Developer CLI: + +```bash +azd ai agent invoke --local "What skills do you have available?" +``` + +## NuGet package users + +If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedToolboxMcpSkills.csproj` for the `PackageReference` alternative. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml new file mode 100644 index 0000000000..2887336252 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.manifest.yaml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: hosted-toolbox-mcp-skills +displayName: "Hosted Toolbox MCP Skills Agent" + +description: > + A hosted agent that discovers MCP-based skills from a Foundry Toolbox + and makes them available to the agent via the agent skills provider. + +metadata: + tags: + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - Agent Framework + - MCP + - Model Context Protocol + - Agent Skills + - Foundry Toolbox + - Foundry Toolbox Skills + +template: + name: hosted-toolbox-mcp-skills + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + resources: + cpu: "0.25" + memory: 0.5Gi + environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + - name: FOUNDRY_TOOLBOX_NAME + value: "{{FOUNDRY_TOOLBOX_NAME}}" +parameters: + properties: + - name: FOUNDRY_TOOLBOX_NAME + secret: false + description: Name of the Foundry Toolbox to connect to for MCP skill discovery +resources: + - kind: model + id: gpt-5 + name: AZURE_AI_MODEL_DEPLOYMENT_NAME diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml new file mode 100644 index 0000000000..5f53abb2e2 --- /dev/null +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/agent.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: hosted-toolbox-mcp-skills +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: FOUNDRY_TOOLBOX_NAME + value: ${FOUNDRY_TOOLBOX_NAME} From a5f355e04a4cc13da8ce8ddc13430d343decfa8d Mon Sep 17 00:00:00 2001 From: Dineshsuriya D <43177361+droideronline@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:29:50 +0530 Subject: [PATCH 39/61] Python: Fix OTLP HTTP base-endpoint losing /v1/{signal} auto-append (#5913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Python: Fix OTLP HTTP base-endpoint losing /v1/{signal} auto-append Per the OTel spec, OTEL_EXPORTER_OTLP_ENDPOINT is a *base* URL for HTTP — the SDK auto-appends /v1/traces, /v1/metrics, /v1/logs when it reads the env var directly. Signal-specific endpoint env vars are *full* URLs used verbatim. _get_exporters_from_env read the base endpoint and forwarded it as the constructor ``endpoint=`` argument, which the SDK always treats as a full signal URL. As a result, with OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 and HTTP protocol, the exporter sent to http://localhost:4318 instead of http://localhost:4318/v1/traces (and likewise for metrics/logs). Replicate the spec's auto-append here when falling back to the base endpoint under HTTP. gRPC behavior is unchanged. * Python: Fix mypy type errors in OTLP endpoint assignment Pre-declare traces_endpoint, metrics_endpoint, logs_endpoint as str | None before the if/else block. Mypy inferred str from the if-branch f-string assignments and then rejected the str | None expressions in the else-branch as incompatible. --- .../core/agent_framework/observability.py | 28 ++++- .../core/tests/core/test_observability.py | 109 ++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index d7734f2457..1654799d87 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -498,14 +498,34 @@ def _get_exporters_from_env( # Get base endpoint base_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") - # Get signal-specific endpoints (these override base endpoint) - traces_endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or base_endpoint - metrics_endpoint = os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") or base_endpoint - logs_endpoint = os.getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") or base_endpoint + # Get signal-specific endpoints (these override base endpoint and are used verbatim) + traces_endpoint_specific = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") + metrics_endpoint_specific = os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") + logs_endpoint_specific = os.getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") # Get protocol (default is grpc) protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc").lower() + # Per the OTel spec, OTEL_EXPORTER_OTLP_ENDPOINT is a *base* URL for HTTP — the SDK + # auto-appends /v1/{traces,metrics,logs} when it reads the env var directly. The + # signal-specific endpoint env vars are *full* URLs used verbatim. Because we read + # the env vars here and forward them as the ``endpoint=`` constructor argument + # (which the SDK always treats as a full URL), we must replicate the auto-append + # ourselves for HTTP when falling back to the base endpoint. For gRPC, the base + # endpoint is used as-is. + traces_endpoint: str | None + metrics_endpoint: str | None + logs_endpoint: str | None + if protocol in ("http/protobuf", "http") and base_endpoint: + base_for_http = base_endpoint.rstrip("/") + traces_endpoint = traces_endpoint_specific or f"{base_for_http}/v1/traces" + metrics_endpoint = metrics_endpoint_specific or f"{base_for_http}/v1/metrics" + logs_endpoint = logs_endpoint_specific or f"{base_for_http}/v1/logs" + else: + traces_endpoint = traces_endpoint_specific or base_endpoint + metrics_endpoint = metrics_endpoint_specific or base_endpoint + logs_endpoint = logs_endpoint_specific or base_endpoint + # Get base headers base_headers_str = os.getenv("OTEL_EXPORTER_OTLP_HEADERS", "") base_headers = _parse_headers(base_headers_str) diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 372cb8a7dd..94d4ee3bad 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -761,6 +761,115 @@ def test_get_exporters_from_env_missing_grpc_dependency(monkeypatch): _get_exporters_from_env() +# region Test OTLP endpoint computation (base-URL auto-append for HTTP) + + +def test_get_exporters_from_env_http_base_endpoint_appends_signal_paths(monkeypatch): + """OTEL_EXPORTER_OTLP_ENDPOINT is a base URL for HTTP; SDK auto-appends + /v1/{traces,metrics,logs}. Because we read the env var and forward it as the + constructor ``endpoint=`` arg (which the SDK treats as a full URL), we must + replicate the auto-append ourselves. + """ + from unittest.mock import patch + + from agent_framework import observability + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + for key in ( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ): + monkeypatch.delenv(key, raising=False) + + with patch.object(observability, "_create_otlp_exporters", return_value=[]) as create: + observability._get_exporters_from_env() + + kwargs = create.call_args.kwargs + assert kwargs["protocol"] == "http/protobuf" + assert kwargs["traces_endpoint"] == "http://localhost:4318/v1/traces" + assert kwargs["metrics_endpoint"] == "http://localhost:4318/v1/metrics" + assert kwargs["logs_endpoint"] == "http://localhost:4318/v1/logs" + + +def test_get_exporters_from_env_http_base_endpoint_trailing_slash(monkeypatch): + """A trailing slash on the base endpoint should not produce a doubled slash.""" + from unittest.mock import patch + + from agent_framework import observability + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + for key in ( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ): + monkeypatch.delenv(key, raising=False) + + with patch.object(observability, "_create_otlp_exporters", return_value=[]) as create: + observability._get_exporters_from_env() + + kwargs = create.call_args.kwargs + assert kwargs["traces_endpoint"] == "http://localhost:4318/v1/traces" + assert kwargs["metrics_endpoint"] == "http://localhost:4318/v1/metrics" + assert kwargs["logs_endpoint"] == "http://localhost:4318/v1/logs" + + +def test_get_exporters_from_env_http_signal_specific_used_verbatim(monkeypatch): + """Signal-specific endpoint env vars are full URLs and must be used verbatim, + even when a base endpoint is also set. + """ + from unittest.mock import patch + + from agent_framework import observability + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://traces.example.com/custom/path") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + for key in ( + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ): + monkeypatch.delenv(key, raising=False) + + with patch.object(observability, "_create_otlp_exporters", return_value=[]) as create: + observability._get_exporters_from_env() + + kwargs = create.call_args.kwargs + # Signal-specific is verbatim — no path appended + assert kwargs["traces_endpoint"] == "http://traces.example.com/custom/path" + # Others fall back to base, with path appended + assert kwargs["metrics_endpoint"] == "http://localhost:4318/v1/metrics" + assert kwargs["logs_endpoint"] == "http://localhost:4318/v1/logs" + + +def test_get_exporters_from_env_grpc_base_endpoint_unchanged(monkeypatch): + """For gRPC, the base endpoint applies to all signals as-is (no path append).""" + from unittest.mock import patch + + from agent_framework import observability + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + for key in ( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ): + monkeypatch.delenv(key, raising=False) + + with patch.object(observability, "_create_otlp_exporters", return_value=[]) as create: + observability._get_exporters_from_env() + + kwargs = create.call_args.kwargs + assert kwargs["protocol"] == "grpc" + assert kwargs["traces_endpoint"] == "http://localhost:4317" + assert kwargs["metrics_endpoint"] == "http://localhost:4317" + assert kwargs["logs_endpoint"] == "http://localhost:4317" + + # region Test create_resource From 6de4c24fdda68bb991bf76c017618b89c6197d87 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:10:02 -0700 Subject: [PATCH 40/61] .NET: Promote Workflows.Declarative packages to stable versions (#6254) * Promote Workflows.Declarative packages to stable versions * Address PR feedback: enable package validation on GA declarative packages Both Workflows.Declarative and Workflows.Declarative.Mcp set IsReleased=true but were disabling package validation, bypassing the repo's GA convention (see dotnet/nuget/nuget-package.props which auto-enables validation when IsReleased=true). Re-enable validation by removing the local EnablePackageValidation=false overrides and pointing PackageValidationBaselineVersion at 1.8.0-rc1 (the latest published version of each package). This catches accidental breaking changes between RC and the first GA. Future GAs should bump the baseline to the previous GA version. Verified locally: dotnet build -c Release on both projects runs RunPackageValidation -> APICompat ran successfully without finding any breaking changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update statement for the baseline validation. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rosoft.Agents.AI.Workflows.Declarative.Foundry.csproj | 2 +- .../Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj | 8 +++++--- .../Microsoft.Agents.AI.Workflows.Declarative.csproj | 9 ++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj index 407593536e..63c2bd30ae 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj @@ -1,7 +1,7 @@  - true + $(NoWarn);MEAI001;OPENAI001 diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj index bca32e93fc..00865e2fa6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj @@ -1,7 +1,7 @@ - true + true $(NoWarn);MEAI001;OPENAI001 @@ -13,9 +13,11 @@ - + - false + 1.8.0-rc1 diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj index 5f8cd37505..145dbe243b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj @@ -1,7 +1,7 @@  - true + true $(NoWarn);MEAI001;OPENAI001 @@ -13,6 +13,13 @@ + + + 1.8.0-rc1 + + Microsoft Agent Framework Declarative Workflows From fa8cfb75673eb2f8f3a9870b2d95e1a435ac3dc3 Mon Sep 17 00:00:00 2001 From: Benke Qu <89610947+benke520@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:30:04 -0700 Subject: [PATCH 41/61] Python: Fix FoundryAgent stripping model from PromptAgent requests (#5526) * Fix FoundryAgent stripping model from PromptAgent requests Move run_options.pop('model', None) inside the _uses_foundry_agent_session() conditional so that model is only stripped for hosted agent sessions (where the server manages the model) and preserved for PromptAgent requests that require it in the Responses API call. Fixes #5525 * test: add coverage for resp_* continuation preserving model Adds test_raw_foundry_agent_chat_client_prepare_options_preserves_model_for_resp_continuation to explicitly verify that HostedAgent v1 / v2-no-session paths (where conversation_id starts with resp_) preserve model and previous_response_id without triggering the hosted-session gate. --------- Co-authored-by: Benke Qu Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> --- .../foundry/agent_framework_foundry/_agent.py | 2 +- .../tests/foundry/test_foundry_agent.py | 74 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 1e1157d05a..433380580d 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -351,6 +351,7 @@ class RawFoundryAgentChatClient( # type: ignore[misc] if _uses_foundry_agent_session(conversation_id): run_options.pop("previous_response_id", None) run_options.pop("conversation", None) + run_options.pop("model", None) extra_body["agent_session_id"] = conversation_id # Non-preview Prompt/Hosted Agent calls need agent_reference in the request body to # tell the Responses API which Foundry agent (and version) is in use, since ``model`` @@ -366,7 +367,6 @@ class RawFoundryAgentChatClient( # type: ignore[misc] # Strip tools from request body - Foundry API rejects requests with both # agent endpoint and tools present. FunctionTools are invoked client-side # by the function invocation layer, not sent to the service. - run_options.pop("model", None) if not self.allow_preview: run_options.pop("tools", None) run_options.pop("tool_choice", None) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 44bc744f64..2cbf491c16 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -203,7 +203,7 @@ async def test_raw_foundry_agent_chat_client_prepare_options_accepts_function_to async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields() -> None: - """Test that _prepare_options strips model and tool-loop fields from run_options.""" + """Test that _prepare_options strips tool-loop fields but preserves model for non-session requests.""" mock_project = MagicMock() mock_openai = MagicMock() @@ -235,16 +235,49 @@ async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_ options={"tools": [my_func]}, ) - assert "model" not in result + # model is preserved for non-session (PromptAgent) requests + assert result["model"] == "gpt-4.1" assert "tools" not in result assert "tool_choice" not in result assert "parallel_tool_calls" not in result # agent_reference is required so the Responses API can resolve model server-side; see #5582. assert result == { + "model": "gpt-4.1", "extra_body": {"agent_reference": {"name": "test-agent", "type": "agent_reference"}}, } +async def test_raw_foundry_agent_chat_client_prepare_options_strips_model_for_hosted_session() -> None: + """Test that model is stripped when using a hosted agent session (not a PromptAgent).""" + + mock_project = MagicMock() + mock_openai = MagicMock() + mock_project.get_openai_client.return_value = mock_openai + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "gpt-4.1", + "previous_response_id": "resp_abc", + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"conversation_id": "agent-session-123"}, + ) + + assert "model" not in result + assert "previous_response_id" not in result + assert result["extra_body"]["agent_session_id"] == "agent-session-123" + assert result["extra_body"]["agent_reference"] == {"name": "test-agent", "type": "agent_reference"} + + async def test_raw_foundry_agent_chat_client_prepare_options_injects_agent_reference_first_turn() -> None: """First-turn (no conversation_id) Prompt Agent calls must carry agent_reference in extra_body. @@ -272,7 +305,6 @@ async def test_raw_foundry_agent_chat_client_prepare_options_injects_agent_refer options={}, ) - assert "model" not in result assert result["extra_body"] == { "agent_reference": {"name": "test-agent", "type": "agent_reference", "version": "2"}, } @@ -333,7 +365,8 @@ async def test_raw_foundry_agent_chat_client_prepare_options_skips_agent_referen options={}, ) - assert "model" not in result + # model is preserved for non-session requests (platform tolerates it for hosted agents) + assert result["model"] == "gpt-4.1" # No extra_body at all is the cleanest signal — agent_reference must not be injected here. assert "extra_body" not in result @@ -363,6 +396,39 @@ async def test_raw_foundry_agent_chat_client_prepare_options_respects_caller_age assert result["extra_body"]["agent_reference"] == caller_reference +async def test_raw_foundry_agent_chat_client_prepare_options_preserves_model_for_resp_continuation() -> None: + """Test that model is preserved when conversation_id is a resp_* continuation (HostedAgent v1 / v2-no-session).""" + + mock_project = MagicMock() + mock_openai = MagicMock() + mock_project.get_openai_client.return_value = mock_openai + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "gpt-4.1", + "previous_response_id": "resp_abc123", + }, + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"conversation_id": "resp_abc123"}, + ) + + # model preserved — resp_* is standard Responses API continuity, not a hosted session + assert result["model"] == "gpt-4.1" + # previous_response_id preserved — not stripped outside hosted session path + assert result["previous_response_id"] == "resp_abc123" + # no agent_session_id injected + assert "extra_body" not in result or "agent_session_id" not in result.get("extra_body", {}) + + async def test_raw_foundry_agent_chat_client_prepare_options_maps_agent_session_id_to_extra_body() -> None: """Test that service_session_id is forwarded as agent_session_id for hosted sessions.""" From 6086a743023c7b8a0e8ec8dde92316e42d634c09 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:30:05 -0700 Subject: [PATCH 42/61] Python: Promote agent-framework-declarative package to RC (#6256) * Promote agent-framework-declarative package to RC * Update missed package status file. --- python/PACKAGE_STATUS.md | 9 ++++++++- .../packages/core/agent_framework/_feature_stage.py | 1 + python/packages/declarative/README.md | 12 ++++++++++++ .../agent_framework_declarative/__init__.py | 13 +++++++++++++ .../agent_framework_declarative/_loader.py | 8 ++++++++ python/packages/declarative/pyproject.toml | 5 +++-- python/samples/03-workflows/declarative/README.md | 1 - python/uv.lock | 4 ++-- 8 files changed, 47 insertions(+), 6 deletions(-) diff --git a/python/PACKAGE_STATUS.md b/python/PACKAGE_STATUS.md index 736a18bf1b..959fcf8008 100644 --- a/python/PACKAGE_STATUS.md +++ b/python/PACKAGE_STATUS.md @@ -27,7 +27,7 @@ Status is grouped into these buckets: | `agent-framework-claude` | `python/packages/claude` | `beta` | | `agent-framework-copilotstudio` | `python/packages/copilotstudio` | `beta` | | `agent-framework-core` | `python/packages/core` | `released` | -| `agent-framework-declarative` | `python/packages/declarative` | `beta` | +| `agent-framework-declarative` | `python/packages/declarative` | `rc` | | `agent-framework-devui` | `python/packages/devui` | `beta` | | `agent-framework-durabletask` | `python/packages/durabletask` | `beta` | | `agent-framework-foundry` | `python/packages/foundry` | `released` | @@ -58,6 +58,13 @@ listed below. ### Experimental features +#### `DECLARATIVE_AGENTS` + +- `agent-framework-declarative`: declarative agent loading APIs from + `agent_framework_declarative`, including `AgentFactory`, + `DeclarativeLoaderError`, `ProviderLookupError`, and `ProviderTypeMapping` + from `agent_framework_declarative/_loader.py` + #### `EVALS` - `agent-framework-core`: exported evaluation APIs from `agent_framework`, including diff --git a/python/packages/core/agent_framework/_feature_stage.py b/python/packages/core/agent_framework/_feature_stage.py index 55d4ac1096..dfa9cb6343 100644 --- a/python/packages/core/agent_framework/_feature_stage.py +++ b/python/packages/core/agent_framework/_feature_stage.py @@ -50,6 +50,7 @@ class ExperimentalFeature(str, Enum): on enum membership or attribute presence over time. """ + DECLARATIVE_AGENTS = "DECLARATIVE_AGENTS" EVALS = "EVALS" FILE_HISTORY = "FILE_HISTORY" FIDES = "FIDES" diff --git a/python/packages/declarative/README.md b/python/packages/declarative/README.md index b4a97f049a..02c1a35472 100644 --- a/python/packages/declarative/README.md +++ b/python/packages/declarative/README.md @@ -6,6 +6,18 @@ Please install this package via pip: pip install agent-framework-declarative --pre ``` +## Release stage + +This package ships at two different stability levels: + +- **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the + `_workflows` surface) are at **release-candidate** stability and may receive only + minor refinements before GA. +- **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path: + `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are + **experimental** and may change or be removed in future versions without notice. + Using any of these symbols emits an `ExperimentalWarning` on first use. + ## Declarative features The declarative packages provides support for building agents based on a declarative yaml specification. diff --git a/python/packages/declarative/agent_framework_declarative/__init__.py b/python/packages/declarative/agent_framework_declarative/__init__.py index 84bc404d5d..fd864a7068 100644 --- a/python/packages/declarative/agent_framework_declarative/__init__.py +++ b/python/packages/declarative/agent_framework_declarative/__init__.py @@ -1,5 +1,18 @@ # Copyright (c) Microsoft. All rights reserved. +"""Declarative specification support for Microsoft Agent Framework. + +Release stage: + +* The declarative-workflows surface (``WorkflowFactory``, executors, handlers, + etc.) is at release-candidate stability. +* The declarative-agents surface (``AgentFactory`` and the YAML agent + loading/parsing path: ``DeclarativeLoaderError``, ``ProviderLookupError``, + ``ProviderTypeMapping``) is *experimental* and may change or be removed in + future versions without notice. Using these symbols emits an + ``ExperimentalWarning`` on first use. +""" + from importlib import metadata from ._loader import AgentFactory, DeclarativeLoaderError, ProviderLookupError, ProviderTypeMapping diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index a9c534ee2d..4507d0112d 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -15,6 +15,10 @@ from agent_framework import ( from agent_framework import ( FunctionTool as AFFunctionTool, ) +from agent_framework._feature_stage import ( # type: ignore[reportPrivateUsage] + ExperimentalFeature, + experimental, +) from agent_framework.exceptions import AgentException from dotenv import load_dotenv @@ -43,6 +47,7 @@ else: from typing_extensions import TypedDict # type: ignore # pragma: no cover +@experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS) class ProviderTypeMapping(TypedDict, total=True): package: str name: str @@ -118,18 +123,21 @@ PROVIDER_TYPE_OBJECT_MAPPING: dict[str, ProviderTypeMapping] = { } +@experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS) class DeclarativeLoaderError(AgentException): """Exception raised for errors in the declarative loader.""" pass +@experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS) class ProviderLookupError(DeclarativeLoaderError): """Exception raised for errors in provider type lookup.""" pass +@experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS) class AgentFactory: """Factory for creating Agent instances from declarative YAML definitions. diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index 0efa733daa..25f7315ebc 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260528" +version = "1.0.0rc1" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -49,7 +49,8 @@ addopts = "-ra -q -r fEX" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" filterwarnings = [ - "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*" + "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*", + "ignore::agent_framework._feature_stage.ExperimentalWarning", ] timeout = 120 markers = [ diff --git a/python/samples/03-workflows/declarative/README.md b/python/samples/03-workflows/declarative/README.md index 1c48ef1e7a..9a24b741e7 100644 --- a/python/samples/03-workflows/declarative/README.md +++ b/python/samples/03-workflows/declarative/README.md @@ -64,7 +64,6 @@ actions: ### Agent Invocation - `InvokeAzureAgent` - Call an Azure AI agent -- `InvokePromptAgent` - Call a local prompt agent ### Tool Invocation - `InvokeFunctionTool` - Call a registered Python function diff --git a/python/uv.lock b/python/uv.lock index 5e4ae35369..1f816411e3 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -437,7 +437,7 @@ provides-extras = ["all"] [[package]] name = "agent-framework-declarative" -version = "1.0.0b260528" +version = "1.0.0rc1" source = { editable = "packages/declarative" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -609,7 +609,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0b2,<=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, ] [[package]] From 49a6e433a332f5952cad275998fa954572810c11 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 3 Jun 2026 11:01:07 +0200 Subject: [PATCH 43/61] Python: progressive tool exposure via FunctionInvocationContext (#6233) * Python: progressive tool exposure via FunctionInvocationContext Add first-class progressive tool exposure to the Python core function-calling loop. Tools can now add or remove real FunctionTool schemas at runtime via the injected FunctionInvocationContext, taking effect on the next iteration of the loop. - FunctionInvocationContext gains a live `tools` list plus experimental `add_tools()` / `remove_tools()` helpers (feature: PROGRESSIVE_TOOLS). - The function-calling loop establishes a run-local, normalized tools list and threads it into the context at both invocation paths so mutations propagate. - Add a sample (dynamic_tool_exposure.py) and a tools samples README, including a note that CodeAct providers (Monty/Hyperlight) use their own provider-level tool management instead. Supersedes #3877. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate non-negative input in dynamic_tool_exposure sample tools Address review feedback: factorial and fibonacci now return an error message for negative n instead of producing incorrect results. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make add_tools atomic and surface swallowed function errors Address review feedback on progressive tool exposure: - add_tools now validates the full batch against a throwaway copy before committing, so a duplicate-name clash partway through a sequence leaves the live tool list unchanged (all-or-nothing). - _auto_invoke_function now logs a warning (with traceback) when a tool raises, so contract errors such as a duplicate-name ValueError from add_tools are debuggable without enabling include_detailed_errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Avoid retaining tracebacks when logging swallowed function errors Logging with exc_info=exc fed the exception traceback to the logging machinery, whose frame references created reference cycles collected lazily by the cyclic GC. On Windows that could drop a hyperlight WasmSandbox on a non-owning thread ("unsendable, dropped on another thread"), crashing the xdist worker. Log a pre-formatted message with the exception repr instead, so no traceback object is retained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * added missing decorator --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/core/AGENTS.md | 2 +- .../core/agent_framework/_feature_stage.py | 1 + .../core/agent_framework/_middleware.py | 113 +++++ .../packages/core/agent_framework/_tools.py | 24 + .../core/test_function_invocation_logic.py | 422 ++++++++++++++++++ python/samples/02-agents/tools/README.md | 75 ++++ .../02-agents/tools/dynamic_tool_exposure.py | 79 ++++ python/uv.lock | 111 ++++- 8 files changed, 817 insertions(+), 10 deletions(-) create mode 100644 python/samples/02-agents/tools/README.md create mode 100644 python/samples/02-agents/tools/dynamic_tool_exposure.py diff --git a/python/packages/core/AGENTS.md b/python/packages/core/AGENTS.md index ffb6b3e2c5..a0f4c81b56 100644 --- a/python/packages/core/AGENTS.md +++ b/python/packages/core/AGENTS.md @@ -56,7 +56,7 @@ agent_framework/ - **`AgentMiddleware`** - Intercepts agent `run()` calls - **`ChatMiddleware`** - Intercepts chat client `get_response()` calls - **`FunctionMiddleware`** - Intercepts function/tool invocations -- **`AgentContext`** / **`ChatContext`** / **`FunctionInvocationContext`** - Context objects passed through middleware +- **`AgentContext`** / **`ChatContext`** / **`FunctionInvocationContext`** - Context objects passed through middleware. A tool can declare a `FunctionInvocationContext` parameter to receive it; `context.tools` is the live, mutable tools list for the run, and `context.add_tools(...)` / `context.remove_tools(...)` enable progressive tool exposure (changes apply on the next function-calling iteration). ### Sessions (`_sessions.py`) diff --git a/python/packages/core/agent_framework/_feature_stage.py b/python/packages/core/agent_framework/_feature_stage.py index dfa9cb6343..455c61bb7e 100644 --- a/python/packages/core/agent_framework/_feature_stage.py +++ b/python/packages/core/agent_framework/_feature_stage.py @@ -58,6 +58,7 @@ class ExperimentalFeature(str, Enum): FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS" FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS" HARNESS = "HARNESS" + PROGRESSIVE_TOOLS = "PROGRESSIVE_TOOLS" SKILLS = "SKILLS" TO_PROMPT_AGENT = "TO_PROMPT_AGENT" diff --git a/python/packages/core/agent_framework/_middleware.py b/python/packages/core/agent_framework/_middleware.py index 4f90030368..2eddb08a07 100644 --- a/python/packages/core/agent_framework/_middleware.py +++ b/python/packages/core/agent_framework/_middleware.py @@ -11,6 +11,7 @@ from enum import Enum from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, cast, overload from ._clients import SupportsChatGetResponse +from ._feature_stage import ExperimentalFeature, experimental from ._types import ( AgentResponse, AgentResponseUpdate, @@ -214,6 +215,12 @@ class FunctionInvocationContext: result: Function execution result. Can be observed after calling ``call_next()`` to see the actual execution result or can be set to override the execution result. kwargs: Additional runtime keyword arguments forwarded to the function invocation. + tools: The live, mutable list of tools available to the model for the current + agent run, or ``None`` when the function is invoked outside of a + function-calling loop (for example via ``FunctionTool.invoke`` directly). + Tools can add or remove tools during execution using :meth:`add_tools` + and :meth:`remove_tools` (progressive tool exposure). Mutations take + effect on the **next** model iteration, not the in-flight batch. Examples: .. code-block:: python @@ -232,6 +239,18 @@ class FunctionInvocationContext: # Continue execution await call_next() + + Progressive tool exposure from inside a tool: + + .. code-block:: python + + from agent_framework import FunctionInvocationContext, tool + + + @tool(approval_mode="never_require") + def load_math_tools(ctx: FunctionInvocationContext) -> str: + ctx.add_tools([factorial, fibonacci]) + return "Math tools are now available." """ def __init__( @@ -242,6 +261,7 @@ class FunctionInvocationContext: metadata: Mapping[str, Any] | None = None, result: Any = None, kwargs: Mapping[str, Any] | None = None, + tools: list[ToolTypes] | None = None, ) -> None: """Initialize the FunctionInvocationContext. @@ -252,6 +272,9 @@ class FunctionInvocationContext: metadata: Metadata dictionary for sharing data between function middleware. result: Function execution result. kwargs: Additional runtime keyword arguments forwarded to the function invocation. + tools: The live, mutable list of tools for the current agent run. When provided, + this is the same list object the model sees on the next iteration, so + appending or removing tools changes the model's available tools. """ self.function = function self.arguments = arguments @@ -259,6 +282,96 @@ class FunctionInvocationContext: self.metadata: dict[str, Any] = dict(metadata) if metadata is not None else {} self.result = result self.kwargs: dict[str, Any] = dict(kwargs) if kwargs is not None else {} + self.tools = tools + + @experimental(feature_id=ExperimentalFeature.PROGRESSIVE_TOOLS) + def add_tools( + self, + tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]], + ) -> None: + """Add one or more tools to the current agent run (progressive tool exposure). + + Callable inputs are converted to :class:`FunctionTool`, and tool collections are + flattened, using the same normalization as the rest of the framework. Added tools + become available to the model on the **next** iteration of the function-calling + loop; they do not affect tool calls already requested in the in-flight batch. + + Adding a tool whose name already exists is a no-op when it is the same object, and + raises ``ValueError`` when it is a different object with a duplicate name. + + Args: + tools: A single tool/callable or a sequence of tools/callables to add. + + Raises: + RuntimeError: If the context has no live tools list (for example when the + function is invoked outside of a function-calling loop). + ValueError: If a different tool with a duplicate name is added. + """ + from ._tools import _append_unique_tools, normalize_tools # type: ignore[reportPrivateUsage] + + if self.tools is None: + raise RuntimeError( + "Cannot add tools: this FunctionInvocationContext is not bound to a live " + "agent run. add_tools is only available for functions invoked within an " + "agent's function-calling loop." + ) + # Validate the whole batch against a throwaway copy first, so a duplicate-name + # clash partway through the batch raises before the live tool list is mutated + # (all-or-nothing semantics). + merged = _append_unique_tools(list(self.tools), normalize_tools(tools)) + self.tools[:] = merged + + @experimental(feature_id=ExperimentalFeature.PROGRESSIVE_TOOLS) + def remove_tools( + self, + tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | str | Sequence[str], + ) -> None: + """Remove one or more tools from the current agent run (progressive tool exposure). + + Tools may be specified by name, by tool object, or by the original callable. Names + that are not currently present are ignored. Removals take effect on the **next** + iteration of the function-calling loop; tool calls already requested in the + in-flight batch still execute. + + Args: + tools: A tool name, tool/callable, or a sequence of any of these to remove. + + Raises: + RuntimeError: If the context has no live tools list (for example when the + function is invoked outside of a function-calling loop). + """ + from ._tools import _get_tool_name, normalize_tools # type: ignore[reportPrivateUsage] + + if self.tools is None: + raise RuntimeError( + "Cannot remove tools: this FunctionInvocationContext is not bound to a live " + "agent run. remove_tools is only available for functions invoked within an " + "agent's function-calling loop." + ) + + names_to_remove: set[str] = set() + raw_items: list[Any] + if isinstance(tools, str): + raw_items = [tools] + elif isinstance(tools, Sequence) and not isinstance(tools, (bytes, bytearray)): + raw_items = list(cast("Sequence[Any]", tools)) + else: + raw_items = [tools] + for item in raw_items: + if isinstance(item, str): + names_to_remove.add(item) + continue + for normalized in normalize_tools(item): + if name := _get_tool_name(normalized): # type: ignore[reportPrivateUsage] + names_to_remove.add(name) + + if not names_to_remove: + return + self.tools[:] = [ + tool + for tool in self.tools + if _get_tool_name(tool) not in names_to_remove # type: ignore[reportPrivateUsage] + ] class ChatContext: diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 93722a8987..5237cf62ba 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1418,6 +1418,7 @@ async def _auto_invoke_function( sequence_index: int | None = None, request_index: int | None = None, middleware_pipeline: FunctionMiddlewarePipeline | None = None, + live_tools: list[ToolTypes] | None = None, ) -> Content: """Invoke a function call requested by the agent, applying middleware that is defined. @@ -1432,6 +1433,8 @@ async def _auto_invoke_function( sequence_index: The index of the function call in the sequence. request_index: The index of the request iteration. middleware_pipeline: Optional middleware pipeline to apply during execution. + live_tools: The live, mutable tools list for the current agent run, exposed on + the FunctionInvocationContext so tools can add/remove tools at runtime. Returns: The function result content. @@ -1523,6 +1526,7 @@ async def _auto_invoke_function( arguments=args, session=invocation_session, kwargs=runtime_kwargs.copy(), + tools=live_tools, ) function_result = await tool.invoke( arguments=args, @@ -1537,6 +1541,10 @@ async def _auto_invoke_function( except UserInputRequiredException: raise except Exception as exc: + logger.warning( + f"Function '{tool.name}' raised an exception; returning an error result to the " + f"model. Set include_detailed_errors=True for the full detail. Exception: {exc!r}" + ) message = "Error: Function failed." if config.get("include_detailed_errors", False): message = f"{message} Exception: {exc}" @@ -1552,6 +1560,7 @@ async def _auto_invoke_function( arguments=args, session=invocation_session, kwargs=runtime_kwargs.copy(), + tools=live_tools, ) call_id = function_call_content.call_id @@ -1608,6 +1617,10 @@ async def _auto_invoke_function( except UserInputRequiredException: raise except Exception as exc: + logger.warning( + f"Function '{tool.name}' raised an exception; returning an error result to the " + f"model. Set include_detailed_errors=True for the full detail. Exception: {exc!r}" + ) message = "Error: Function failed." if config.get("include_detailed_errors", False): message = f"{message} Exception: {exc}" @@ -1659,6 +1672,9 @@ async def _try_execute_function_calls( from ._types import Content tool_map = _get_tool_map(tools) + # The live tools list (when tools is the run-local list) is exposed on the + # FunctionInvocationContext so tools can add/remove tools during the run. + live_tools: list[ToolTypes] | None = cast("list[ToolTypes]", tools) if isinstance(tools, list) else None approval_tools = [tool_name for tool_name, tool in tool_map.items() if tool.approval_mode == "always_require"] logger.debug( "_try_execute_function_calls: tool_map keys=%s, approval_tools=%s", @@ -1733,6 +1749,7 @@ async def _try_execute_function_calls( request_index=attempt_idx, middleware_pipeline=middleware_pipeline, config=config, + live_tools=live_tools, ) return (result, False) except MiddlewareTermination as exc: @@ -2371,6 +2388,13 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): function_invocation_kwargs=function_invocation_kwargs, client_kwargs=filtered_kwargs, ) + # Establish a single, run-local mutable tools list so that tools can add or remove + # tools during the run (progressive tool exposure). A fresh list is created via + # normalize_tools so the caller's original tools container is never mutated, while + # the same list object is shared with the model (options["tools"]) and the tool map + # rebuilt on every loop iteration. + if mutable_options.get("tools"): + mutable_options["tools"] = normalize_tools(mutable_options["tools"]) if not stream: async def _get_response() -> ChatResponse[Any]: diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py index 3d20a26080..f96ca99ad5 100644 --- a/python/packages/core/tests/core/test_function_invocation_logic.py +++ b/python/packages/core/tests/core/test_function_invocation_logic.py @@ -3975,3 +3975,425 @@ async def test_user_input_request_empty_contents_returns_fallback(chat_client_ba ] assert len(function_results) >= 1 assert any("user input" in (fr.result or "").lower() for fr in function_results) + + +# region Progressive tool exposure (FunctionInvocationContext.add_tools / remove_tools) + + +def _pte_function_call_response(call_id: str, name: str, arguments: str = "{}") -> ChatResponse: + return ChatResponse( + messages=Message( + role="assistant", + contents=[Content.from_function_call(call_id=call_id, name=name, arguments=arguments)], + ) + ) + + +def _pte_text_response(text: str = "done") -> ChatResponse: + return ChatResponse(messages=Message(role="assistant", contents=[text])) + + +@tool(name="factorial", approval_mode="never_require") +def _pte_factorial(n: int) -> int: + """Compute the factorial of n.""" + result = 1 + for value in range(2, n + 1): + result *= value + return result + + +async def test_context_exposes_live_tools(chat_client_base: SupportsChatGetResponse): + from agent_framework import FunctionTool + + seen_names: list[str] = [] + + @tool(name="inspect_tools", approval_mode="never_require") + def inspect_tools(ctx: FunctionInvocationContext) -> str: + assert ctx.tools is not None + seen_names.extend(t.name for t in ctx.tools if isinstance(t, FunctionTool)) + return "inspected" + + chat_client_base.run_responses = [ + _pte_function_call_response("1", "inspect_tools"), + _pte_text_response(), + ] + await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": [inspect_tools]}, + ) + assert "inspect_tools" in seen_names + + +async def test_add_tools_available_next_iteration(chat_client_base: SupportsChatGetResponse): + exec_counter = 0 + + @tool(name="factorial", approval_mode="never_require") + def factorial(n: int) -> int: + nonlocal exec_counter + exec_counter += 1 + return 120 + + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(factorial) + return "math tools loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_math"), + _pte_function_call_response("2", "factorial", '{"n": 5}'), + _pte_text_response(), + ] + response = await chat_client_base.get_response( + [Message(role="user", contents=["compute 5!"])], + options={"tool_choice": "auto", "tools": [load_math]}, + ) + assert exec_counter == 1 + assert response.messages[-1].text == "done" + + +async def test_add_tools_model_sees_added_tools_in_options(chat_client_base: SupportsChatGetResponse): + from agent_framework import FunctionTool + + recorded: list[list[str]] = [] + client_cls = type(chat_client_base) + original = client_cls._get_non_streaming_response + + async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse: + tools = options.get("tools") or [] + recorded.append([t.name for t in tools if isinstance(t, FunctionTool)]) + return await original(self, messages=messages, options=options, **kwargs) + + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(_pte_factorial) + return "loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_math"), + _pte_function_call_response("2", "factorial", '{"n": 5}'), + _pte_text_response(), + ] + monkey = pytest.MonkeyPatch() + monkey.setattr(client_cls, "_get_non_streaming_response", recording) + try: + await chat_client_base.get_response( + [Message(role="user", contents=["compute 5!"])], + options={"tool_choice": "auto", "tools": [load_math]}, + ) + finally: + monkey.undo() + + assert recorded[0] == ["load_math"] + assert "factorial" in recorded[1] + + +async def test_remove_tools_next_iteration(chat_client_base: SupportsChatGetResponse): + from agent_framework import FunctionTool + + recorded: list[list[str]] = [] + client_cls = type(chat_client_base) + original = client_cls._get_non_streaming_response + + async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse: + tools = options.get("tools") or [] + recorded.append([t.name for t in tools if isinstance(t, FunctionTool)]) + return await original(self, messages=messages, options=options, **kwargs) + + @tool(name="get_weather", approval_mode="never_require") + def get_weather(location: str) -> str: + return "sunny" + + @tool(name="drop_weather", approval_mode="never_require") + def drop_weather(ctx: FunctionInvocationContext) -> str: + ctx.remove_tools("get_weather") + return "removed" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "drop_weather"), + _pte_text_response(), + ] + monkey = pytest.MonkeyPatch() + monkey.setattr(client_cls, "_get_non_streaming_response", recording) + try: + await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": [get_weather, drop_weather]}, + ) + finally: + monkey.undo() + + assert set(recorded[0]) == {"get_weather", "drop_weather"} + assert "get_weather" not in recorded[1] + + +async def test_add_tools_does_not_mutate_caller_tools_list(chat_client_base: SupportsChatGetResponse): + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(_pte_factorial) + return "loaded" + + original_tools: list[Any] = [load_math] + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_math"), + _pte_text_response(), + ] + await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": original_tools}, + ) + assert original_tools == [load_math] + + +async def test_add_tools_persists_across_iterations(chat_client_base: SupportsChatGetResponse): + from agent_framework import FunctionTool + + recorded: list[list[str]] = [] + client_cls = type(chat_client_base) + original = client_cls._get_non_streaming_response + + async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse: + tools = options.get("tools") or [] + recorded.append([t.name for t in tools if isinstance(t, FunctionTool)]) + return await original(self, messages=messages, options=options, **kwargs) + + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(_pte_factorial) + return "loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 4 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_math"), + _pte_function_call_response("2", "factorial", '{"n": 5}'), + _pte_function_call_response("3", "factorial", '{"n": 3}'), + _pte_text_response(), + ] + monkey = pytest.MonkeyPatch() + monkey.setattr(client_cls, "_get_non_streaming_response", recording) + try: + await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": [load_math]}, + ) + finally: + monkey.undo() + + assert "factorial" in recorded[1] + assert "factorial" in recorded[2] + + +async def test_add_tools_through_function_middleware(chat_client_base: SupportsChatGetResponse): + exec_counter = 0 + + class PassthroughMiddleware(FunctionMiddleware): + async def process(self, context: FunctionInvocationContext, call_next: Any) -> None: + await call_next() + + @tool(name="factorial", approval_mode="never_require") + def factorial(n: int) -> int: + nonlocal exec_counter + exec_counter += 1 + return 120 + + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(factorial) + return "loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_math"), + _pte_function_call_response("2", "factorial", '{"n": 5}'), + _pte_text_response(), + ] + await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": [load_math]}, + middleware=[PassthroughMiddleware()], + ) + assert exec_counter == 1 + + +async def test_add_tools_with_approval_required_tool(chat_client_base: SupportsChatGetResponse): + @tool(name="secure_tool", approval_mode="always_require") + def secure_tool(value: str) -> str: + return f"secure: {value}" + + @tool(name="load_secure", approval_mode="never_require") + def load_secure(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(secure_tool) + return "loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_secure"), + _pte_function_call_response("2", "secure_tool", '{"value": "x"}'), + _pte_text_response(), + ] + response = await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": [load_secure]}, + ) + assert any(item.type == "function_approval_request" for msg in response.messages for item in msg.contents) + + +async def test_add_tools_accepts_plain_callable(chat_client_base: SupportsChatGetResponse): + exec_counter = 0 + + def plain_factorial(n: int) -> int: + """Compute factorial.""" + nonlocal exec_counter + exec_counter += 1 + return 120 + + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(plain_factorial) + return "loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.run_responses = [ + _pte_function_call_response("1", "load_math"), + _pte_function_call_response("2", "plain_factorial", '{"n": 5}'), + _pte_text_response(), + ] + await chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + options={"tool_choice": "auto", "tools": [load_math]}, + ) + assert exec_counter == 1 + + +async def test_add_tools_streaming(chat_client_base: SupportsChatGetResponse): + exec_counter = 0 + + @tool(name="factorial", approval_mode="never_require") + def factorial(n: int) -> int: + nonlocal exec_counter + exec_counter += 1 + return 120 + + @tool(name="load_math", approval_mode="never_require") + def load_math(ctx: FunctionInvocationContext) -> str: + ctx.add_tools(factorial) + return "loaded" + + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.streaming_responses = [ + [ + ChatResponseUpdate( + contents=[Content.from_function_call(call_id="1", name="load_math", arguments="{}")], + role="assistant", + ) + ], + [ + ChatResponseUpdate( + contents=[Content.from_function_call(call_id="2", name="factorial", arguments='{"n": 5}')], + role="assistant", + ) + ], + [ChatResponseUpdate(contents=[Content.from_text("done")], role="assistant", finish_reason="stop")], + ] + async for _ in chat_client_base.get_response( + [Message(role="user", contents=["hi"])], + stream=True, + options={"tool_choice": "auto", "tools": [load_math]}, + ): + pass + assert exec_counter == 1 + + +def test_add_tools_duplicate_same_object_is_noop(): + @tool(name="dup", approval_mode="never_require") + def dup(x: int) -> int: + return x + + ctx = FunctionInvocationContext(function=dup, arguments={}, tools=[dup]) + ctx.add_tools(dup) + assert ctx.tools is not None + assert len(ctx.tools) == 1 + + +def test_add_tools_duplicate_name_different_object_raises(): + @tool(name="dup", approval_mode="never_require") + def dup_a(x: int) -> int: + return x + + @tool(name="dup", approval_mode="never_require") + def dup_b(x: int) -> int: + return x + + ctx = FunctionInvocationContext(function=dup_a, arguments={}, tools=[dup_a]) + with pytest.raises(ValueError): + ctx.add_tools(dup_b) + + +def test_add_tools_batch_with_duplicate_is_atomic(): + """A duplicate-name clash partway through a batch must leave the live list unchanged.""" + + @tool(name="existing", approval_mode="never_require") + def existing(x: int) -> int: + return x + + @tool(name="fresh", approval_mode="never_require") + def fresh(x: int) -> int: + return x + + @tool(name="existing", approval_mode="never_require") + def clashing(x: int) -> int: + return x + + ctx = FunctionInvocationContext(function=existing, arguments={}, tools=[existing]) + with pytest.raises(ValueError): + ctx.add_tools([fresh, clashing]) + assert ctx.tools is not None + # The valid "fresh" tool must not have been committed before the clash raised. + assert ctx.tools == [existing] + + +def test_remove_tools_by_name_and_object(): + @tool(name="a", approval_mode="never_require") + def a(x: int) -> int: + return x + + @tool(name="b", approval_mode="never_require") + def b(x: int) -> int: + return x + + ctx = FunctionInvocationContext(function=a, arguments={}, tools=[a, b]) + ctx.remove_tools("a") + assert ctx.tools is not None + assert [t.name for t in ctx.tools] == ["b"] + ctx.remove_tools(b) + assert ctx.tools == [] + + +def test_remove_tools_unknown_name_is_noop(): + @tool(name="a", approval_mode="never_require") + def a(x: int) -> int: + return x + + ctx = FunctionInvocationContext(function=a, arguments={}, tools=[a]) + ctx.remove_tools("nonexistent") + assert ctx.tools is not None + assert [t.name for t in ctx.tools] == ["a"] + + +def test_progressive_tools_helpers_raise_without_live_tools(): + @tool(name="a", approval_mode="never_require") + def a(x: int) -> int: + return x + + ctx = FunctionInvocationContext(function=a, arguments={}) + assert ctx.tools is None + with pytest.raises(RuntimeError): + ctx.add_tools(a) + with pytest.raises(RuntimeError): + ctx.remove_tools("a") + + +# endregion diff --git a/python/samples/02-agents/tools/README.md b/python/samples/02-agents/tools/README.md new file mode 100644 index 0000000000..ad07d86ecd --- /dev/null +++ b/python/samples/02-agents/tools/README.md @@ -0,0 +1,75 @@ +# Tools + +Samples that show how to define, configure, and control function tools for an +agent — from basic declarations to approvals, invocation limits, session +injection, and dynamic (progressive) tool exposure. + +## Function tools + +| File | Demonstrates | +|------|--------------| +| [`function_tool_with_explicit_schema.py`](function_tool_with_explicit_schema.py) | Defining a tool with an explicit JSON schema. | +| [`function_tool_declaration_only.py`](function_tool_declaration_only.py) | A declaration-only tool (schema without a local implementation). | +| [`function_tool_with_kwargs.py`](function_tool_with_kwargs.py) | Passing extra keyword arguments into a tool. | +| [`function_tool_from_dict_with_dependency_injection.py`](function_tool_from_dict_with_dependency_injection.py) | Dependency injection into a tool defined from a dict. | +| [`function_tool_with_session_injection.py`](function_tool_with_session_injection.py) | Injecting the session into a tool. | +| [`tool_in_class.py`](tool_in_class.py) | Using a method on a class as a tool. | +| [`agent_as_tool_with_session_propagation.py`](agent_as_tool_with_session_propagation.py) | Exposing an agent as a tool with session propagation. | + +## Approvals & invocation control + +| File | Demonstrates | +|------|--------------| +| [`function_tool_with_approval.py`](function_tool_with_approval.py) | Requiring human approval before a tool runs. | +| [`function_tool_with_approval_and_sessions.py`](function_tool_with_approval_and_sessions.py) | Tool approvals combined with sessions. | +| [`function_invocation_configuration.py`](function_invocation_configuration.py) | Configuring function-invocation settings (e.g. max iterations). | +| [`control_total_tool_executions.py`](control_total_tool_executions.py) | All the ways to cap how many times tools run. | +| [`function_tool_with_max_invocations.py`](function_tool_with_max_invocations.py) | Limiting the number of invocations per tool. | +| [`function_tool_with_max_exceptions.py`](function_tool_with_max_exceptions.py) | Limiting the number of exceptions a tool may raise. | +| [`function_tool_recover_from_failures.py`](function_tool_recover_from_failures.py) | Returning errors so the agent can recover from tool failures. | + +## Progressive tool exposure (dynamic loading) + +| File | Demonstrates | +|------|--------------| +| [`dynamic_tool_exposure.py`](dynamic_tool_exposure.py) | A "loader" tool that adds more tools at runtime via `FunctionInvocationContext`. | + +Frontloading a model with hundreds of tools hurts tool-selection accuracy, +bloats context, and raises cost. Instead, start with a small set of loader +tools and let the model pull in more on demand. Inside a tool, the injected +`ctx: FunctionInvocationContext` exposes a live `ctx.tools` list plus +`ctx.add_tools(...)` / `ctx.remove_tools(...)` helpers. Tools added or removed +take effect on the **next iteration** of the function-calling loop. + +> [!NOTE] +> Progressive tool exposure applies to the standard function-calling loop. It +> does **not** apply to CodeAct providers (`agent-framework-monty`, +> `agent-framework-hyperlight`). In CodeAct the model only sees a single +> `execute_code` tool, and host tools are exposed *inside the sandbox* as typed +> Python functions rather than as model tool-schemas. Host tools there are +> invoked without a `FunctionInvocationContext`, so `ctx.add_tools()` is not +> available; the helpers fail fast with a clear `RuntimeError` instead of +> silently doing nothing. To change a CodeAct agent's tool set, use the +> provider's own `add_tools` / `remove_tool` / `clear_tools` methods (applied +> between runs). The recommended provider-driven path for Monty and Hyperlight +> is shown in [`../context_providers/code_act/`](../context_providers/code_act/) +> ([`code_act.py`](../context_providers/code_act/code_act.py) for Hyperlight, +> [`monty_code_act.py`](../context_providers/code_act/monty_code_act.py) for +> Monty). + +## Local shell & code interpreters + +| Path | Demonstrates | +|------|--------------| +| [`local_shell_with_allowlist.py`](local_shell_with_allowlist.py) | `LocalShellTool` restricted by a strict command allow-list. | +| [`local_shell_with_environment_provider.py`](local_shell_with_environment_provider.py) | `LocalShellTool` wired with a `ShellEnvironmentProvider`. | +| [`local_code_interpreter/`](local_code_interpreter/) | Hyperlight-backed sandboxed code interpreter (standalone tool — *extra* pattern). | +| [`monty_code_interpreter/`](monty_code_interpreter/) | Monty-backed sandboxed code interpreter (standalone tool — *extra* pattern). | + +> [!TIP] +> The `local_code_interpreter/` and `monty_code_interpreter/` samples show the +> standalone-tool wiring and are provided as *extra* reference. For most +> Monty/Hyperlight use cases the **recommended** path is the provider-driven +> CodeAct setup in +> [`../context_providers/code_act/`](../context_providers/code_act/), which adds +> dynamic tool / capability management. diff --git a/python/samples/02-agents/tools/dynamic_tool_exposure.py b/python/samples/02-agents/tools/dynamic_tool_exposure.py new file mode 100644 index 0000000000..2886399486 --- /dev/null +++ b/python/samples/02-agents/tools/dynamic_tool_exposure.py @@ -0,0 +1,79 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Annotated + +from agent_framework import Agent, FunctionInvocationContext, tool +from agent_framework.openai import OpenAIChatClient +from dotenv import load_dotenv +from pydantic import Field + +# Load environment variables from .env file +load_dotenv() + +""" +Dynamic Tool Exposure (Progressive Tool Loading) Example + +This example demonstrates "progressive tool exposure": a tool that adds more tools to +the agent at runtime, in the same run, via ``FunctionInvocationContext``. + +Frontloading a model with hundreds of tools hurts tool-selection accuracy, bloats +context, and raises cost. Instead, you can start with a small set of "loader" tools and +let the model pull in additional tools on demand. Tools added with ``ctx.add_tools(...)`` +(or removed with ``ctx.remove_tools(...)``) become available to the model on the next +iteration of the function-calling loop. +""" + + +# These math tools are not registered on the agent up front. They are added on demand by +# the ``load_math_tools`` tool below, and only then become callable by the model. +@tool(approval_mode="never_require") +def factorial(n: Annotated[int, Field(description="A non-negative integer.")]) -> str: + """Compute the factorial of n.""" + if n < 0: + return "Error: n must be a non-negative integer." + result = 1 + for value in range(2, n + 1): + result *= value + return f"{n}! = {result}" + + +@tool(approval_mode="never_require") +def fibonacci(n: Annotated[int, Field(description="The 0-based index in the Fibonacci sequence.")]) -> str: + """Compute the n-th Fibonacci number.""" + if n < 0: + return "Error: n must be a non-negative integer." + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return f"fib({n}) = {a}" + + +# The only tool the agent starts with. When called, it exposes the math tools above so the +# model can use them on the next turn. Note the ``ctx`` parameter is injected by the +# framework and is not visible to the model. +@tool(approval_mode="never_require") +def load_math_tools(ctx: FunctionInvocationContext) -> str: + """Load additional math tools (factorial, fibonacci) so they can be used.""" + ctx.add_tools([factorial, fibonacci]) + return "Loaded math tools: factorial, fibonacci. You can now call them." + + +async def main() -> None: + agent = Agent( + client=OpenAIChatClient(), + name="MathAgent", + instructions=( + "You are a math assistant. If you need math capabilities that are not yet " + "available, call load_math_tools first, then use the newly available tools." + ), + tools=[load_math_tools], + ) + + # The agent starts with only ``load_math_tools``. To answer the question it must first + # load the math tools, then call ``factorial`` on the next iteration. + print(f"Agent: {await agent.run('What is 5 factorial?')}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/uv.lock b/python/uv.lock index 1f816411e3..676413c1cc 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -562,7 +562,7 @@ requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, { name = "azure-ai-agentserver-core", specifier = ">=2.0.0b3,<3" }, { name = "azure-ai-agentserver-invocations", specifier = ">=1.0.0b3,<2" }, - { name = "azure-ai-agentserver-responses", specifier = ">=1.0.0b5,<2" }, + { name = "azure-ai-agentserver-responses", specifier = ">=1.0.0b7,<2" }, ] [[package]] @@ -1171,19 +1171,18 @@ wheels = [ [[package]] name = "azure-ai-agentserver-core" -version = "2.0.0b3" +version = "2.0.0b5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "hypercorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "microsoft-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/29/1a9606d5252b02d77070a1b633dd0c26fe65a0f4a0fb0cfdaa751e2ed458/azure_ai_agentserver_core-2.0.0b3.tar.gz", hash = "sha256:e295b19a65d53c513929f52f0862bbb815cc9e9fc29d2a2825452f3136260123", size = 42573, upload-time = "2026-04-23T04:13:16.717Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/06/7c88b6506d26ee625a967cef762e6a155ed7ab8812f3f1e45ec1a950b8ae/azure_ai_agentserver_core-2.0.0b5.tar.gz", hash = "sha256:f03dc737351e5d847e9fc18c5b78b261436de368f1317a0c29957cc2179c37d1", size = 46273, upload-time = "2026-05-25T12:48:01.739Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9b/1fc87c05b55821f33c46c5e8a3b97a573aa2fc4bff387e75cca1a87800b4/azure_ai_agentserver_core-2.0.0b3-py3-none-any.whl", hash = "sha256:5ef921eb9fd9c0f15682fe930320fae50dccfa915d7518f9a16d99014bbcb3cb", size = 29127, upload-time = "2026-04-23T04:13:17.976Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/a43a269601512793b220c36dc0864b44d806b969dbfe14f1ecc3b5f5202b/azure_ai_agentserver_core-2.0.0b5-py3-none-any.whl", hash = "sha256:0d00c298892e2ff466b32235d5d9c55b57054f0e8fcedb0726eacd7684e1aa89", size = 31521, upload-time = "2026-05-25T12:48:03.072Z" }, ] [[package]] @@ -1200,7 +1199,7 @@ wheels = [ [[package]] name = "azure-ai-agentserver-responses" -version = "1.0.0b5" +version = "1.0.0b7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1208,9 +1207,9 @@ dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/27/3ecb7fe704ff8764199bfbe4cc1e584a520a9affe042470d9d50b6e1e73a/azure_ai_agentserver_responses-1.0.0b5.tar.gz", hash = "sha256:0b627b810359c792ea7b6fa6782abaf6df32d9bc9e5a569ad722afcffd0ce8d9", size = 410908, upload-time = "2026-04-23T04:31:15.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/53/febb6f3453f5dc1e0b6dc47d4e5198b64605d1f83c847255946f74bc300e/azure_ai_agentserver_responses-1.0.0b7.tar.gz", hash = "sha256:2f67cdfc0219cb0ab86800dadb1cfdb40ab4aa0413dae7ffa5ea4ea84eec3eb0", size = 419032, upload-time = "2026-05-25T12:48:38.81Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/91/1e5c0d7ce95ca8b022e69e4ca6b23e413fc2d57f0191429c4633e02213d2/azure_ai_agentserver_responses-1.0.0b5-py3-none-any.whl", hash = "sha256:4c2a6ab56e71eeb330aa52b7cb2cc71b8ec6b5bbe0e7dc84310f2c7fbda393a3", size = 268362, upload-time = "2026-04-23T04:31:17.014Z" }, + { url = "https://files.pythonhosted.org/packages/b3/94/48825357e009f7db3b6b5d0a9344a7ab3304e32f06f50328b2393e3b06cb/azure_ai_agentserver_responses-1.0.0b7-py3-none-any.whl", hash = "sha256:efb5271f24a297bacde9769359308e54e870f66ad4d3b4826ae97a77e40e94d4", size = 268063, upload-time = "2026-05-25T12:48:40.817Z" }, ] [[package]] @@ -3887,6 +3886,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/1b/543ddaa2daf8593911a02a07a6a78366d4a6a0053ec86a557c19fa97b60e/microsoft_agents_hosting_core-0.3.1-py3-none-any.whl", hash = "sha256:a4b41556b15321b74f539c5a0a89f70955459b7ec57e9e4b24e61bba27f1cbbc", size = 94573, upload-time = "2025-09-09T23:19:53.855Z" }, ] +[[package]] +name = "microsoft-opentelemetry" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-core-tracing-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-flask", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-logging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-openai-agents-v2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-openai-v2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-psycopg2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-urllib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-resource-detector-azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-util-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyjwt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/cf/74885d07d38e225b84b63a8a2720de846e518fe4c7e89457f4c150a9c7d5/microsoft_opentelemetry-1.3.2.tar.gz", hash = "sha256:d36f31731740170624b53f370358a9700f503bb4f9bd25c7f81c0c88c66f511c", size = 178031, upload-time = "2026-05-29T22:05:53.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/8d/6960be61c8fe236fef730b0cae1d97a1898f62355b2d6679ef46abe1e4be/microsoft_opentelemetry-1.3.2-py3-none-any.whl", hash = "sha256:65292474ce7efee115f671457188e92edc4a8d432fad163e49e504155be66ae5", size = 198419, upload-time = "2026-05-29T22:05:54.849Z" }, +] + [[package]] name = "mistralai" version = "2.4.2" @@ -4633,6 +4667,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/41/619f3530324a58491f2d20f216a10dd7393629b29db4610dda642a27f4ed/opentelemetry_instrumentation_flask-0.61b0-py3-none-any.whl", hash = "sha256:e8ce474d7ce543bfbbb3e93f8a6f8263348af9d7b45502f387420cf3afa71253", size = 15996, upload-time = "2026-03-04T14:19:31.304Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/2a/e2becd55e33c29d1d9ef76e2579040ed1951cb33bacba259f6aff2fdd2a6/opentelemetry_instrumentation_httpx-0.61b0.tar.gz", hash = "sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd", size = 24104, upload-time = "2026-03-04T14:20:34.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/88/dde310dce56e2d85cf1a09507f5888544955309edc4b8d22971d6d3d1417/opentelemetry_instrumentation_httpx-0.61b0-py3-none-any.whl", hash = "sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e", size = 17198, upload-time = "2026-03-04T14:19:33.585Z" }, +] + [[package]] name = "opentelemetry-instrumentation-logging" version = "0.61b0" @@ -4646,6 +4696,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/0e/2137db5239cc5e564495549a4d11488a7af9b48fc76520a0eea20e69ddae/opentelemetry_instrumentation_logging-0.61b0-py3-none-any.whl", hash = "sha256:6d87e5ded6a0128d775d41511f8380910a1b610671081d16efb05ac3711c0074", size = 17076, upload-time = "2026-03-04T14:19:36.765Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-openai-agents-v2" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-util-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/15/b6a303454d2800d772cdebc490c1d598d06d0e541619db80195eb9ea85c6/opentelemetry_instrumentation_openai_agents_v2-0.1.0.tar.gz", hash = "sha256:1033f4b261ce07f65d197ac0e9c499302c805eae987a6cc4e7f99bb279363477", size = 22423, upload-time = "2025-10-15T19:04:59.912Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/0a/b6f47734e1d7f936cbc52ef8e673d3e08d9c3c8a13d9549c03f978758076/opentelemetry_instrumentation_openai_agents_v2-0.1.0-py3-none-any.whl", hash = "sha256:e4e3dfba32bd6eeee0624eca9be54341ab7cc4f7a3bb895354f2f9d6f7afe2f3", size = 25002, upload-time = "2025-10-15T19:04:58.562Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-openai-v2" +version = "2.3b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/4e/21f8cd16ccb471dd217ed85eb817796a10c4f2718ae2c91e752a57180cf0/opentelemetry_instrumentation_openai_v2-2.3b0.tar.gz", hash = "sha256:5de9d70cc9536eea1fe48ea016e0c5f25735fa9a13709076a64b20657fadb6ba", size = 170838, upload-time = "2025-12-24T13:20:58.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/02/7ff0a9282520592772a356dd39d1559f3726610ccc3854a2f598b756c66f/opentelemetry_instrumentation_openai_v2-2.3b0-py3-none-any.whl", hash = "sha256:c6aca87be0da0289ea1d8167fea4b0f227ea5ef0e90496e2822121e47340d36a", size = 18053, upload-time = "2025-12-24T13:20:57.233Z" }, +] + [[package]] name = "opentelemetry-instrumentation-psycopg2" version = "0.61b0" @@ -4772,6 +4851,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, ] +[[package]] +name = "opentelemetry-util-genai" +version = "0.3b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/d8/4dd2fb622d26ec45b10ef63eb87fd512f5d7467c7bd35ce390629bd6dff8/opentelemetry_util_genai-0.3b0.tar.gz", hash = "sha256:83e127789a9ad615b8ca65f05fc36955a67ce257b06142bfd46159a3b7ed73d3", size = 31800, upload-time = "2026-02-20T16:16:14.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/e5/fada54909e445d7b4007f8b96221d571999efeab9446f3127cc1cebe5e07/opentelemetry_util_genai-0.3b0-py3-none-any.whl", hash = "sha256:ebc2b01bcb891ddc7218452470d189d3321cd742653299ff8e7de45debcfb986", size = 28426, upload-time = "2026-02-20T16:16:12.027Z" }, +] + [[package]] name = "opentelemetry-util-http" version = "0.61b0" From 90a3e5de47cf48dfdef25c00c49cf56e0c80df8e Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:09:39 +0100 Subject: [PATCH 44/61] .NET: Add ILoggerFactory and IServiceProvider to HarnessAgent constructor (#6273) * Add ILoggerFactory and IServiceProvider to HarnessAgent constructor Add optional ILoggerFactory and IServiceProvider parameters to the HarnessAgent constructor and AsHarnessAgent extension method, passing them to all downstream components that accept them: - FunctionInvokingChatClient (via UseFunctionInvocation) - CompactionProvider - AgentSkillsProvider - ChatClientAgent (via BuildAIAgent) - AIAgentBuilder.Build() Closes #6103 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve tests to verify ILoggerFactory and IServiceProvider propagation - Add test verifying ILoggerFactory.CreateLogger() is called by downstream components (CompactionProvider, AgentSkillsProvider) - Add test verifying IServiceProvider is queried during pipeline build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ChatClientHarnessExtensions.cs | 14 +- .../HarnessAgent.cs | 37 +++-- .../HarnessAgentTests.cs | 128 ++++++++++++++++++ 3 files changed, 164 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs index 1a55624f3d..be66e9e635 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.AI; +using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; @@ -32,11 +34,19 @@ public static class ChatClientHarnessExtensions /// additional context providers, and chat history provider. /// When , the agent uses built-in default settings. /// + /// + /// Optional logger factory for creating loggers used by the agent and its components. + /// + /// + /// Optional service provider for resolving dependencies required by AI functions and other agent components. + /// /// A new instance. public static HarnessAgent AsHarnessAgent( this IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, - HarnessAgentOptions? options = null) => - new(chatClient, maxContextWindowTokens, maxOutputTokens, options); + HarnessAgentOptions? options = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) => + new(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services); } diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs index ef1af05513..139f47db3c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs @@ -10,6 +10,7 @@ using Microsoft.Agents.AI.Compaction; using Microsoft.Agents.AI.Tools.Shell; #endif using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -105,6 +106,12 @@ public sealed class HarnessAgent : DelegatingAIAgent /// additional context providers, and chat history provider. /// When , the agent uses built-in default settings. /// + /// + /// Optional logger factory for creating loggers used by the agent and its components. + /// + /// + /// Optional service provider for resolving dependencies required by AI functions and other agent components. + /// /// /// is . /// @@ -112,18 +119,20 @@ public sealed class HarnessAgent : DelegatingAIAgent /// is not positive, or /// is negative or greater than or equal to . /// - public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null) + public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) : base(BuildAgent( Throw.IfNull(chatClient), maxContextWindowTokens, maxOutputTokens, - options)) + options, + loggerFactory, + services)) { } - private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options) + private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services) { - ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options); + ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services); AIAgentBuilder builder = innerAgent.AsBuilder(); @@ -137,10 +146,10 @@ public sealed class HarnessAgent : DelegatingAIAgent builder.UseOpenTelemetry(sourceName: options?.OpenTelemetrySourceName); } - return builder.Build(); + return builder.Build(services); } - private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options) + private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services) { var compactionStrategy = new ContextWindowCompactionStrategy( maxContextWindowTokens: maxContextWindowTokens, @@ -165,13 +174,13 @@ public sealed class HarnessAgent : DelegatingAIAgent ChatOptions chatOptions = BuildChatOptions(options, instructions, maxOutputTokens); - var compactionProvider = new CompactionProvider(compactionStrategy); + var compactionProvider = new CompactionProvider(compactionStrategy, loggerFactory: loggerFactory); - IEnumerable contextProviders = BuildContextProviders(options); + IEnumerable contextProviders = BuildContextProviders(options, loggerFactory); return chatClient .AsBuilder() - .UseFunctionInvocation(configure: options?.MaximumIterationsPerRequest is int maxIterations + .UseFunctionInvocation(loggerFactory, configure: options?.MaximumIterationsPerRequest is int maxIterations ? ficc => ficc.MaximumIterationsPerRequest = maxIterations : null) .UseMessageInjection() @@ -189,7 +198,9 @@ public sealed class HarnessAgent : DelegatingAIAgent RequirePerServiceCallChatHistoryPersistence = true, WarnOnChatHistoryProviderConflict = false, ThrowOnChatHistoryProviderConflict = false, - }); + }, + loggerFactory, + services); } private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string instructions, int maxOutputTokens) @@ -215,7 +226,7 @@ public sealed class HarnessAgent : DelegatingAIAgent return result; } - private static List BuildContextProviders(HarnessAgentOptions? options) + private static List BuildContextProviders(HarnessAgentOptions? options, ILoggerFactory? loggerFactory) { var providers = new List(); @@ -255,8 +266,8 @@ public sealed class HarnessAgent : DelegatingAIAgent if (options?.DisableAgentSkillsProvider is not true) { AgentSkillsProvider skillsProvider = options?.AgentSkillsSource is AgentSkillsSource source - ? new AgentSkillsProvider(source) - : new AgentSkillsProvider(Directory.GetCurrentDirectory()); + ? new AgentSkillsProvider(source, loggerFactory: loggerFactory) + : new AgentSkillsProvider(Directory.GetCurrentDirectory(), loggerFactory: loggerFactory); providers.Add(skillsProvider); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index 4f08209bd8..2711fa1458 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Tools.Shell; #endif using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using Moq; namespace Microsoft.Agents.AI.UnitTests; @@ -1460,4 +1461,131 @@ public class HarnessAgentTests #endregion #endif + + #region LoggerFactory and ServiceProvider + + /// + /// Verify that the constructor succeeds when loggerFactory is provided. + /// + [Fact] + public void Constructor_SucceedsWithLoggerFactory() + { + // Arrange + var chatClient = new Mock().Object; + var loggerFactory = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that the constructor succeeds when serviceProvider is provided. + /// + [Fact] + public void Constructor_SucceedsWithServiceProvider() + { + // Arrange + var chatClient = new Mock().Object; + var services = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: services); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that the constructor succeeds when both loggerFactory and serviceProvider are provided. + /// + [Fact] + public void Constructor_SucceedsWithLoggerFactoryAndServiceProvider() + { + // Arrange + var chatClient = new Mock().Object; + var loggerFactory = new Mock().Object; + var services = new Mock().Object; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that AsHarnessAgent extension method accepts loggerFactory and serviceProvider. + /// + [Fact] + public void AsHarnessAgent_SucceedsWithLoggerFactoryAndServiceProvider() + { + // Arrange + var chatClient = new Mock().Object; + var loggerFactory = new Mock().Object; + var services = new Mock().Object; + + // Act + var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that ILoggerFactory is threaded to downstream components by confirming CreateLogger is called. + /// + [Fact] + public void Constructor_LoggerFactoryIsUsedByDownstreamComponents() + { + // Arrange + var chatClient = new Mock().Object; + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(lf => lf.CreateLogger(It.IsAny())) + .Returns(new Mock().Object); + + // Act — use options that leave CompactionProvider and AgentSkillsProvider enabled + var options = new HarnessAgentOptions + { + DisableToolApproval = true, + DisableOpenTelemetry = true, + DisableFileMemory = true, + DisableFileAccess = true, + DisableWebSearch = true, + DisableTodoProvider = true, + DisableAgentModeProvider = true, + }; + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options, mockLoggerFactory.Object); + + // Assert — CreateLogger should have been called by one or more downstream components + Assert.NotNull(agent); + mockLoggerFactory.Verify(lf => lf.CreateLogger(It.IsAny()), Times.AtLeastOnce()); + } + + /// + /// Verify that IServiceProvider is propagated through the agent pipeline by confirming + /// it is queried during agent construction. + /// + [Fact] + public void Constructor_ServiceProviderIsQueriedDuringBuild() + { + // Arrange + var chatClient = new Mock().Object; + var mockServices = new Mock(); + mockServices + .Setup(sp => sp.GetService(It.IsAny())) + .Returns(null!); + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: mockServices.Object); + + // Assert — the service provider should have been queried during pipeline construction + Assert.NotNull(agent); + mockServices.Verify(sp => sp.GetService(It.IsAny()), Times.AtLeastOnce()); + } + + #endregion } From a9824289168c5af4acc5bfc7bd227206eb942fdc Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:45:58 +0100 Subject: [PATCH 45/61] .NET: Bug fixes for AGUI hosting and workflows (#6311) * Add mcp tool execution fix * Apply IsolationKeyScopedAgentSessionStore to MapAGUI by default if not yet set and improve comments in samples * Address PR comments * Fix formatting --- .../Step01_GettingStarted/Server/Program.cs | 5 + .../Server/Server.csproj | 1 + .../Step02_BackendTools/Server/Program.cs | 5 + .../Step02_BackendTools/Server/Server.csproj | 1 + .../Step03_FrontendTools/Server/Program.cs | 5 + .../Step03_FrontendTools/Server/Server.csproj | 1 + .../AGUI/Step04_HumanInLoop/Server/Program.cs | 5 + .../Step04_HumanInLoop/Server/Server.csproj | 1 + .../Step05_StateManagement/Server/Program.cs | 5 + .../Server/Server.csproj | 1 + .../AGUIDojoServer/AGUIDojoServer.csproj | 1 + .../AGUIDojoServer/Program.cs | 5 + .../AGUIClientServer/AGUIServer/Program.cs | 5 +- .../Server/AGUIWebChatServer.csproj | 1 + .../AGUIWebChat/Server/Program.cs | 5 + .../AGUIEndpointRouteBuilderExtensions.cs | 11 +- .../ObjectModel/InvokeMcpToolExecutor.cs | 69 +++- .../ObjectModel/InvokeMcpToolExecutorTest.cs | 354 ++++++++++++++++++ 18 files changed, 472 insertions(+), 9 deletions(-) diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs index 2c7333015d..0981ece789 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs @@ -10,6 +10,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUI(); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj index 01c8663a7b..a551fed512 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj @@ -14,6 +14,7 @@ + diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs index 33a32410e2..53b680c861 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs @@ -16,6 +16,11 @@ builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default)); builder.Services.AddAGUI(); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj index 01c8663a7b..a551fed512 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj @@ -14,6 +14,7 @@ + diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs index 2c7333015d..0981ece789 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs @@ -10,6 +10,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUI(); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj index 01c8663a7b..a551fed512 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj @@ -14,6 +14,7 @@ + diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs index edfcd03219..88967acb99 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -27,6 +27,11 @@ builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default)); builder.Services.AddAGUI(); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); app.UseHttpLogging(); diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj index 01c8663a7b..a551fed512 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj @@ -14,6 +14,7 @@ + diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs index 1965cf55f7..67a6889fb1 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs @@ -17,6 +17,11 @@ builder.Services.AddAGUI(); // Configure to listen on port 8888 builder.WebHost.UseUrls("http://localhost:8888"); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj index 01c8663a7b..a551fed512 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj @@ -14,6 +14,7 @@ + diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj index 96a72d1109..03e2493623 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj @@ -15,6 +15,7 @@ + diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs index e3b0020362..3f0032d4da 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs @@ -19,6 +19,11 @@ builder.Services.AddHttpClient().AddLogging(); builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIDojoServerSerializerContext.Default)); builder.Services.AddAGUI(); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); app.UseHttpLogging(); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index e3b97d34e1..575924255a 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -49,8 +49,9 @@ var agent = new AzureOpenAIClient( AGUIServerSerializerContext.Default.Options) ]); -// When running in production, make sure to use an SessionIsolationKeyProvider, e.g. ClaimsIdentity-based -// if using Claims-based Identity for Authentication/Authorization +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: // builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); // Register the agent with the host and configure it to use an in-memory session store diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj index e798d23506..8d44079173 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj @@ -14,6 +14,7 @@ + diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs index 185b7d6bbf..06a138b8c3 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs @@ -12,6 +12,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUI(); +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: +// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); + WebApplication app = builder.Build(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs index 85fd00fb8b..0d4c390bbb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs @@ -103,7 +103,16 @@ public static class AGUIEndpointRouteBuilderExtensions ArgumentNullException.ThrowIfNull(aiAgent); var agentSessionStore = endpoints.ServiceProvider.GetKeyedService(aiAgent.Name); - var hostAgent = new AIHostAgent(aiAgent, agentSessionStore ?? new NoopAgentSessionStore()); + + // Ensure that we have an IsolationKeyScopedAgentSessionStore registered. + var isolationKeyProvider = endpoints.ServiceProvider.GetService(); + if (agentSessionStore?.GetService() is null) + { + agentSessionStore ??= new NoopAgentSessionStore(); + agentSessionStore = new IsolationKeyScopedAgentSessionStore(agentSessionStore, isolationKeyProvider, new() { Strict = isolationKeyProvider != null }); + } + + var hostAgent = new AIHostAgent(aiAgent, agentSessionStore); return endpoints.MapPost(pattern, async ([FromBody] RunAgentInput? input, HttpContext context, CancellationToken cancellationToken) => { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs index c4c490551a..27079104a6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeMcpToolExecutor.cs @@ -27,6 +27,14 @@ internal sealed class InvokeMcpToolExecutor( WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { + private const string ApprovalSnapshotStateKey = nameof(_approvalSnapshot); + + /// + /// Snapshot of evaluated parameters at approval-request time. + /// Used to prevent TOCTOU attacks where state mutates during the approval window. + /// + private ApprovalSnapshot? _approvalSnapshot; + /// /// Step identifiers for the MCP tool invocation workflow. /// @@ -75,6 +83,10 @@ internal sealed class InvokeMcpToolExecutor( if (requireApproval) { + // Snapshot the evaluated parameters to prevent TOCTOU attacks. + // If state mutates during the approval window, the approved values are used on resume. + this._approvalSnapshot = new ApprovalSnapshot(serverUrl, serverLabel, toolName, arguments, connectionName); + // Create tool call content for approval request. // Transport headers (e.g. Authorization) are intentionally excluded from the // approval event: they must not cross into the externally-surfaced approval request. @@ -137,13 +149,14 @@ internal sealed class InvokeMcpToolExecutor( return; } - // Approved - now invoke the tool - string serverUrl = this.GetServerUrl(); - string? serverLabel = this.GetServerLabel(); - string toolName = this.GetToolName(); - Dictionary? arguments = this.GetArguments(); + // Approved - use the snapshot from approval-request time to prevent TOCTOU attacks. + // Headers are re-evaluated (they may contain auth secrets that should not be persisted). + string serverUrl = this._approvalSnapshot?.ServerUrl ?? this.GetServerUrl(); + string? serverLabel = this._approvalSnapshot?.ServerLabel ?? this.GetServerLabel(); + string toolName = this._approvalSnapshot?.ToolName ?? this.GetToolName(); + Dictionary? arguments = this._approvalSnapshot?.Arguments ?? this.GetArguments(); Dictionary? headers = this.GetHeaders(); - string? connectionName = this.GetConnectionName(); + string? connectionName = this._approvalSnapshot?.ConnectionName ?? this.GetConnectionName(); McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync( serverUrl, @@ -162,9 +175,33 @@ internal sealed class InvokeMcpToolExecutor( /// public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken) { + // Clear the approval snapshot after successful completion. + this._approvalSnapshot = null; + await ClearSnapshotStateAsync(context, cancellationToken).ConfigureAwait(false); + await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false); } + /// + /// + /// Persists the approval snapshot to workflow state so it survives checkpoint/restore cycles. + /// + protected override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + await context.QueueStateUpdateAsync(ApprovalSnapshotStateKey, this._approvalSnapshot, null, cancellationToken).ConfigureAwait(false); + await base.OnCheckpointingAsync(context, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// Restores the approval snapshot from workflow state after a checkpoint restore. + /// + protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) + { + await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false); + this._approvalSnapshot = await context.ReadStateAsync(ApprovalSnapshotStateKey, null, cancellationToken).ConfigureAwait(false); + } + private async ValueTask ProcessResultAsync(IWorkflowContext context, McpServerToolResultContent resultContent, CancellationToken cancellationToken) { bool autoSend = this.GetAutoSendValue(); @@ -365,4 +402,24 @@ internal sealed class InvokeMcpToolExecutor( return result; } + + /// + /// Clears the persisted approval snapshot state after a successful tool invocation. + /// + private static async ValueTask ClearSnapshotStateAsync(IWorkflowContext context, CancellationToken cancellationToken) + { + await context.QueueStateUpdateAsync(ApprovalSnapshotStateKey, null, null, cancellationToken).ConfigureAwait(false); + } + + /// + /// Stores the evaluated parameters at approval-request time so that + /// uses the values the user reviewed, + /// even if mutates during the approval window. + /// + internal sealed record ApprovalSnapshot( + string ServerUrl, + string? ServerLabel, + string ToolName, + Dictionary? Arguments, + string? ConnectionName); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs index b8d936dab9..0f1ce950ff 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Events; @@ -11,7 +12,9 @@ using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.PowerFx.Types; using Moq; +using ApprovalSnapshot = Microsoft.Agents.AI.Workflows.Declarative.ObjectModel.InvokeMcpToolExecutor.ApprovalSnapshot; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; @@ -842,6 +845,313 @@ public sealed class InvokeMcpToolExecutorTest(ITestOutputHelper output) : Workfl #endregion + #region Approval Snapshot Security Tests + + /// + /// Verifies that mutating the tool name variable after approval does not change + /// which tool is actually invoked. The originally-approved tool name must be used. + /// + [Fact] + public async Task InvokeMcpToolCaptureResponseUsesApprovedToolNameNotMutatedAsync() + { + // Arrange + const string ApprovedToolName = "safe_readonly_query"; + const string MutatedToolName = "dangerous_admin_tool"; + + this.State.Set("TargetTool", FormulaValue.New(ApprovedToolName)); + this.State.InitializeSystem(); + this.State.Bind(); + + InvokeMcpTool model = this.CreateModelWithVariableToolName( + displayName: nameof(InvokeMcpToolCaptureResponseUsesApprovedToolNameNotMutatedAsync), + serverUrl: TestServerUrl, + variableName: "TargetTool"); + + string? capturedToolName = null; + Mock mockProvider = new(); + mockProvider.Setup(provider => provider.InvokeToolAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny?>(), + It.IsAny?>(), + It.IsAny(), + It.IsAny())) + .Callback?, IDictionary?, string?, CancellationToken>( + (_, _, toolName, _, _, _, _) => capturedToolName = toolName) + .ReturnsAsync(new McpServerToolResultContent("capture-call-id") + { + Outputs = [new TextContent("result")] + }); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + // Act - trigger ExecuteAsync to store the approval snapshot + Mock mockContext = CreateMockWorkflowContext(); + await action.HandleAsync(new ActionExecutorResult(action.Id), mockContext.Object, CancellationToken.None); + + // Simulate parallel branch mutating state during the approval window + this.State.Set("TargetTool", FormulaValue.New(MutatedToolName)); + this.State.Bind(); + + // User clicks approve (they saw "safe_readonly_query" in the approval UI) + McpServerToolCallContent toolCall = new(action.Id, ApprovedToolName, TestServerUrl); + ToolApprovalRequestContent approvalRequest = new(action.Id, toolCall); + ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved: true); + ExternalInputResponse response = new(new ChatMessage(ChatRole.User, [approvalResponse])); + + // Resume after approval + await action.CaptureResponseAsync(mockContext.Object, response, CancellationToken.None); + + // Assert - the originally-approved tool name must be used, not the mutated one + Assert.NotNull(capturedToolName); + Assert.Equal(ApprovedToolName, capturedToolName); + } + + /// + /// Verifies that mutating an argument variable after approval does not change + /// the arguments actually passed to the MCP tool. The originally-approved arguments must be used. + /// + [Fact] + public async Task InvokeMcpToolCaptureResponseUsesApprovedArgumentsNotMutatedAsync() + { + // Arrange + const string ApprovedQuery = "SELECT * FROM users LIMIT 10"; + const string MutatedQuery = "DROP TABLE users CASCADE; --"; + + this.State.Set("SqlQuery", FormulaValue.New(ApprovedQuery)); + this.State.InitializeSystem(); + this.State.Bind(); + + InvokeMcpTool model = this.CreateModelWithVariableArgument( + displayName: nameof(InvokeMcpToolCaptureResponseUsesApprovedArgumentsNotMutatedAsync), + serverUrl: TestServerUrl, + toolName: TestToolName, + argumentKey: "query", + variableName: "SqlQuery"); + + IDictionary? capturedArguments = null; + Mock mockProvider = new(); + mockProvider.Setup(provider => provider.InvokeToolAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny?>(), + It.IsAny?>(), + It.IsAny(), + It.IsAny())) + .Callback?, IDictionary?, string?, CancellationToken>( + (_, _, _, arguments, _, _, _) => capturedArguments = arguments) + .ReturnsAsync(new McpServerToolResultContent("capture-call-id") + { + Outputs = [new TextContent("result")] + }); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + // Act - trigger ExecuteAsync to store the approval snapshot + Mock mockContext = CreateMockWorkflowContext(); + await action.HandleAsync(new ActionExecutorResult(action.Id), mockContext.Object, CancellationToken.None); + + // Simulate parallel branch mutating state during the approval window + this.State.Set("SqlQuery", FormulaValue.New(MutatedQuery)); + this.State.Bind(); + + // User clicks approve + McpServerToolCallContent toolCall = new(action.Id, TestToolName, TestServerUrl); + ToolApprovalRequestContent approvalRequest = new(action.Id, toolCall); + ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved: true); + ExternalInputResponse response = new(new ChatMessage(ChatRole.User, [approvalResponse])); + + // Resume after approval + await action.CaptureResponseAsync(mockContext.Object, response, CancellationToken.None); + + // Assert - the originally-approved argument must be used, not the mutated one + Assert.NotNull(capturedArguments); + Assert.Equal(ApprovedQuery, capturedArguments["query"]?.ToString()); + } + + /// + /// Verifies that mutating the server URL variable after approval does not redirect + /// the MCP tool call to a different server. The originally-approved server URL must be used. + /// + [Fact] + public async Task InvokeMcpToolCaptureResponseUsesApprovedServerUrlNotMutatedAsync() + { + // Arrange + const string ApprovedServerUrl = "https://internal-mcp.corp"; + const string MutatedServerUrl = "https://attacker.evil/steal"; + + this.State.Set("McpEndpoint", FormulaValue.New(ApprovedServerUrl)); + this.State.InitializeSystem(); + this.State.Bind(); + + InvokeMcpTool model = this.CreateModelWithVariableServerUrl( + displayName: nameof(InvokeMcpToolCaptureResponseUsesApprovedServerUrlNotMutatedAsync), + variableName: "McpEndpoint", + toolName: TestToolName); + + string? capturedServerUrl = null; + Mock mockProvider = new(); + mockProvider.Setup(provider => provider.InvokeToolAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny?>(), + It.IsAny?>(), + It.IsAny(), + It.IsAny())) + .Callback?, IDictionary?, string?, CancellationToken>( + (serverUrl, _, _, _, _, _, _) => capturedServerUrl = serverUrl) + .ReturnsAsync(new McpServerToolResultContent("capture-call-id") + { + Outputs = [new TextContent("result")] + }); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + // Act - trigger ExecuteAsync to store the approval snapshot + Mock mockContext = CreateMockWorkflowContext(); + await action.HandleAsync(new ActionExecutorResult(action.Id), mockContext.Object, CancellationToken.None); + + // Simulate parallel branch mutating state during the approval window + this.State.Set("McpEndpoint", FormulaValue.New(MutatedServerUrl)); + this.State.Bind(); + + // User clicks approve + McpServerToolCallContent toolCall = new(action.Id, TestToolName, ApprovedServerUrl); + ToolApprovalRequestContent approvalRequest = new(action.Id, toolCall); + ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved: true); + ExternalInputResponse response = new(new ChatMessage(ChatRole.User, [approvalResponse])); + + // Resume after approval + await action.CaptureResponseAsync(mockContext.Object, response, CancellationToken.None); + + // Assert - the originally-approved server URL must be used, not the mutated one + Assert.NotNull(capturedServerUrl); + Assert.Equal(ApprovedServerUrl, capturedServerUrl); + } + + /// + /// Verifies that the approval snapshot survives a checkpoint/restore cycle. + /// After restore, the originally-approved tool name must still be used even if state was mutated. + /// + [Fact] + public async Task InvokeMcpToolCaptureResponseUsesSnapshotAfterCheckpointRestoreAsync() + { + // Arrange + const string ApprovedToolName = "safe_readonly_query"; + const string MutatedToolName = "dangerous_admin_tool"; + + this.State.Set("TargetTool", FormulaValue.New(ApprovedToolName)); + this.State.InitializeSystem(); + this.State.Bind(); + + InvokeMcpTool model = this.CreateModelWithVariableToolName( + displayName: nameof(InvokeMcpToolCaptureResponseUsesSnapshotAfterCheckpointRestoreAsync), + serverUrl: TestServerUrl, + variableName: "TargetTool"); + + string? capturedToolName = null; + Mock mockProvider = new(); + mockProvider.Setup(provider => provider.InvokeToolAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny?>(), + It.IsAny?>(), + It.IsAny(), + It.IsAny())) + .Callback?, IDictionary?, string?, CancellationToken>( + (_, _, toolName, _, _, _, _) => capturedToolName = toolName) + .ReturnsAsync(new McpServerToolResultContent("capture-call-id") + { + Outputs = [new TextContent("result")] + }); + MockAgentProvider mockAgentProvider = new(); + InvokeMcpToolExecutor action = new(model, mockProvider.Object, mockAgentProvider.Object, this.State); + + // Act - trigger ExecuteAsync to store the approval snapshot + Mock mockContext = CreateMockWorkflowContextWithStateStore(); + await action.HandleAsync(new ActionExecutorResult(action.Id), mockContext.Object, CancellationToken.None); + + // Simulate checkpoint: persist to state store + await InvokeProtectedMethodAsync(action, "OnCheckpointingAsync", mockContext.Object, CancellationToken.None); + + // Simulate restore on a "new" executor instance by clearing the in-memory field via reflection + // (In production, a new executor instance would be created with _approvalSnapshot == null) + typeof(InvokeMcpToolExecutor) + .GetField("_approvalSnapshot", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(action, null); + + // Restore from state store + await InvokeProtectedMethodAsync(action, "OnCheckpointRestoredAsync", mockContext.Object, CancellationToken.None); + + // Mutate state after restore (simulating parallel branch) + this.State.Set("TargetTool", FormulaValue.New(MutatedToolName)); + this.State.Bind(); + + // User clicks approve + McpServerToolCallContent toolCall = new(action.Id, ApprovedToolName, TestServerUrl); + ToolApprovalRequestContent approvalRequest = new(action.Id, toolCall); + ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved: true); + ExternalInputResponse response = new(new ChatMessage(ChatRole.User, [approvalResponse])); + + // Resume after approval + await action.CaptureResponseAsync(mockContext.Object, response, CancellationToken.None); + + // Assert - the originally-approved tool name must be used, not the mutated one + Assert.NotNull(capturedToolName); + Assert.Equal(ApprovedToolName, capturedToolName); + } + + private static Mock CreateMockWorkflowContext() + { + Mock mockContext = new(); + mockContext.Setup(c => c.AddEventAsync(It.IsAny(), It.IsAny())) + .Returns(default(ValueTask)); + mockContext.Setup(c => c.QueueStateUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(default(ValueTask)); + mockContext.Setup(c => c.SendMessageAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(default(ValueTask)); + return mockContext; + } + + /// + /// Creates a mock workflow context that actually stores state values (for checkpoint/restore tests). + /// + private static Mock CreateMockWorkflowContextWithStateStore() + { + Dictionary stateStore = new(); + Mock mockContext = new(); + mockContext.Setup(c => c.AddEventAsync(It.IsAny(), It.IsAny())) + .Returns(default(ValueTask)); + mockContext.Setup(c => c.QueueStateUpdateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((key, value, _, _) => stateStore[key] = value) + .Returns(default(ValueTask)); + mockContext.Setup(c => c.SendMessageAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(default(ValueTask)); + mockContext.Setup(c => c.ReadStateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((key, _, _) => + new ValueTask(stateStore.TryGetValue(key, out object? val) ? val as ApprovalSnapshot : null)); + mockContext.Setup(c => c.ReadStateKeysAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HashSet()); + return mockContext; + } + + /// + /// Invokes a protected method on an executor via reflection (for testing checkpoint hooks). + /// + private static async ValueTask InvokeProtectedMethodAsync(InvokeMcpToolExecutor action, string methodName, IWorkflowContext context, CancellationToken cancellationToken) + { + MethodInfo method = typeof(InvokeMcpToolExecutor) + .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)!; + ValueTask result = (ValueTask)method.Invoke(action, [context, cancellationToken])!; + await result.ConfigureAwait(false); + } + + #endregion + #region CompleteAsync Tests [Fact] @@ -951,6 +1261,50 @@ public sealed class InvokeMcpToolExecutorTest(ITestOutputHelper output) : Workfl return AssignParent(builder); } + private InvokeMcpTool CreateModelWithVariableToolName(string displayName, string serverUrl, string variableName) + { + InvokeMcpTool.Builder builder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + ServerUrl = new StringExpression.Builder(StringExpression.Literal(serverUrl)), + ToolName = new StringExpression.Builder( + StringExpression.Variable(PropertyPath.TopicVariable(variableName))), + RequireApproval = new BoolExpression.Builder(BoolExpression.Literal(true)), + }; + return AssignParent(builder); + } + + private InvokeMcpTool CreateModelWithVariableArgument( + string displayName, string serverUrl, string toolName, string argumentKey, string variableName) + { + InvokeMcpTool.Builder builder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + ServerUrl = new StringExpression.Builder(StringExpression.Literal(serverUrl)), + ToolName = new StringExpression.Builder(StringExpression.Literal(toolName)), + RequireApproval = new BoolExpression.Builder(BoolExpression.Literal(true)), + }; + builder.Arguments.Add(argumentKey, + ValueExpression.Variable(PropertyPath.TopicVariable(variableName))); + return AssignParent(builder); + } + + private InvokeMcpTool CreateModelWithVariableServerUrl(string displayName, string variableName, string toolName) + { + InvokeMcpTool.Builder builder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + ServerUrl = new StringExpression.Builder( + StringExpression.Variable(PropertyPath.TopicVariable(variableName))), + ToolName = new StringExpression.Builder(StringExpression.Literal(toolName)), + RequireApproval = new BoolExpression.Builder(BoolExpression.Literal(true)), + }; + return AssignParent(builder); + } + #endregion #region Mock MCP Tool Provider From c6951c21f69af4c9a5d5cbe107b36fca9f648907 Mon Sep 17 00:00:00 2001 From: semenshi-m Date: Wed, 3 Jun 2026 19:09:50 +0100 Subject: [PATCH 46/61] Python: Add MCP-based skills discovery (McpSkillsSource) (#6169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add MCP-based skills discovery (McpSkill, McpSkillsSource, McpSkillResource) Implement Agent Skills discovery over MCP following the SEP-2640 convention: - McpSkillsSource: reads skill://index.json to discover skills served by an MCP server - McpSkill: lazily fetches SKILL.md content via resources/read on demand - McpSkillResource: wraps MCP resource results (text and binary) - Path traversal protection in get_resource for defense in depth - Samples for Foundry Toolbox and standalone MCP skills server - Comprehensive unit tests (514 lines) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments: rename to MCP* convention, fix error handling and samples - Rename McpSkill/McpSkillResource/McpSkillsSource to MCPSkill/MCPSkillResource/MCPSkillsSource - Add data-URI prefix stripping for blob resource decoding - Let non-McpError exceptions propagate from get_resource() - Fix contradictory test comment - Use interactive input() in mcp_based_skill sample - Remove misleading sample output block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore debug logging for McpError in get_resource() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use AzureCliCredential in Foundry toolbox skills sample for consistency Replace DefaultAzureCredential with AzureCliCredential to match the credential convention used in all other samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use MCPStreamableHTTPTool in MCP skills sample Replace raw mcp library imports (ClientSession, streamable_http_client) with the framework's MCPStreamableHTTPTool to keep MCP server connections consistent regardless of whether skills are enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Branch on McpError.error.code so only not-found errors return empty Previously _try_read_index() and get_resource() swallowed every McpError as 'no skills available', making auth failures, server crashes, and connection drops indistinguishable from a server that simply has no skills. Now only two codes are treated as not-found: - -32002 (MCP-spec Resource not found) - -32601 (METHOD_NOT_FOUND — server lacks resources/read) All other McpError codes and non-McpError exceptions propagate with a warning log, surfacing real failures visibly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add tests for non-McpError and non-not-found error propagation in MCP skills Cover the re-raise branch in MCPSkill.get_resource for plain ConnectionError/TimeoutError, the generic McpError (code 0) propagation on get_resource, and TimeoutError propagation in _try_read_index. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Use MCPStreamableHTTPTool in MCP skills sample" This reverts commit f31ed0ded914e094f3ac5d811997b2cefc55836b. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Introduce MCP_SKILLS experimental feature for MCP skill classes Add a separate MCP_SKILLS feature ID to ExperimentalFeature enum and use it for MCPSkillResource, MCPSkill, and MCPSkillsSource, since their promotion timeline is partly outside of our control. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/__init__.py | 6 + .../core/agent_framework/_feature_stage.py | 1 + .../packages/core/agent_framework/_skills.py | 444 ++++++++++++ .../core/tests/core/test_mcp_skills.py | 667 ++++++++++++++++++ .../02-agents/providers/foundry/README.md | 1 + ...foundry_chat_client_with_toolbox_skills.py | 87 +++ python/samples/02-agents/skills/README.md | 1 + .../skills/mcp_based_skill/README.md | 51 ++ .../skills/mcp_based_skill/mcp_based_skill.py | 75 ++ 9 files changed, 1333 insertions(+) create mode 100644 python/packages/core/tests/core/test_mcp_skills.py create mode 100644 python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py create mode 100644 python/samples/02-agents/skills/mcp_based_skill/README.md create mode 100644 python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index bd8b66cbb1..d65a926e92 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -168,6 +168,9 @@ from ._skills import ( InlineSkillResource, InlineSkillScript, InMemorySkillsSource, + MCPSkill, + MCPSkillResource, + MCPSkillsSource, Skill, SkillFrontmatter, SkillResource, @@ -444,6 +447,9 @@ __all__ = [ "MCPStdioTool", "MCPStreamableHTTPTool", "MCPWebsocketTool", + "MCPSkill", + "MCPSkillResource", + "MCPSkillsSource", "MemoryContextProvider", "MemoryFileStore", "MemoryIndexEntry", diff --git a/python/packages/core/agent_framework/_feature_stage.py b/python/packages/core/agent_framework/_feature_stage.py index 455c61bb7e..28e026c864 100644 --- a/python/packages/core/agent_framework/_feature_stage.py +++ b/python/packages/core/agent_framework/_feature_stage.py @@ -58,6 +58,7 @@ class ExperimentalFeature(str, Enum): FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS" FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS" HARNESS = "HARNESS" + MCP_SKILLS = "MCP_SKILLS" PROGRESSIVE_TOOLS = "PROGRESSIVE_TOOLS" SKILLS = "SKILLS" TO_PROMPT_AGENT = "TO_PROMPT_AGENT" diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 5e313f20d9..97afe66cea 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -44,6 +44,7 @@ Only use skills from trusted sources. from __future__ import annotations import asyncio +import base64 import inspect import json import logging @@ -60,6 +61,10 @@ from ._sessions import ContextProvider from ._tools import FunctionTool if TYPE_CHECKING: + from mcp.client.session import ClientSession + from mcp.types import ReadResourceResult + from pydantic import AnyUrl + from ._agents import SupportsAgentRun from ._sessions import AgentSession, SessionContext @@ -3285,4 +3290,443 @@ class AggregatingSkillsSource(SkillsSource): return result +# region MCP Skills + + +def _mcp_any_url(uri: str) -> AnyUrl: + """Convert a string URI to a :class:`pydantic.AnyUrl` for MCP client calls.""" + from pydantic import AnyUrl as _AnyUrl + + return _AnyUrl(uri) + + +def _is_mcp_resource_not_found(ex: Exception) -> bool: + """Return ``True`` when *ex* is an :class:`McpError` indicating a missing resource. + + Two codes are treated as "not found": + + * ``-32002`` — the MCP-spec "Resource not found" code returned by a + compliant server when the URI does not exist. Not exported as a + constant from ``mcp.types`` but defined by the resources subprotocol. + * ``METHOD_NOT_FOUND`` (``-32601``) — the server does not implement + ``resources/read`` at all, which for the skills source is functionally + equivalent to "no skills available." + + All other codes — ``INVALID_PARAMS``, ``INTERNAL_ERROR``, ``PARSE_ERROR``, + ``CONNECTION_CLOSED``, auth rejections, and generic handler errors + (code ``0``) — are treated as real failures so that a misconfigured + token or crashing server is not silently mistaken for "the server has no + skills." + """ + from mcp.shared.exceptions import McpError as _McpError + + if not isinstance(ex, _McpError): + return False + from mcp.types import METHOD_NOT_FOUND as _METHOD_NOT_FOUND + + return ex.error.code in {-32002, _METHOD_NOT_FOUND} + + +def _mcp_join_text(result: ReadResourceResult) -> str: + """Join all :class:`TextResourceContents` items in a result into a single string.""" + from mcp.types import TextResourceContents as _TextResourceContents + + return "\n".join(c.text for c in result.contents if isinstance(c, _TextResourceContents)) + + +class _McpSkillIndexEntry: # noqa: B903 + """A single entry in the ``skill://index.json`` discovery document. + + All fields are optional to support lenient deserialization; callers + validate required fields before use. + """ + + def __init__( + self, + *, + name: str | None = None, + type: str | None = None, + description: str | None = None, + url: str | None = None, + digest: str | None = None, + ) -> None: + self.name = name + self.type = type + self.description = description + self.url = url + self.digest = digest + + +class _McpSkillIndex: + """DTO for the ``skill://index.json`` discovery document. + + Represents the Agent Skills Discovery v0.2.0 schema as bound to MCP + by SEP-2640. + """ + + def __init__( + self, + *, + schema: str | None = None, + skills: list[_McpSkillIndexEntry] | None = None, + ) -> None: + self.schema = schema + self.skills: list[_McpSkillIndexEntry] = skills if skills is not None else [] + + +def _parse_mcp_skill_index(text: str) -> _McpSkillIndex: + """Parse a JSON string into a :class:`_McpSkillIndex`. + + Args: + text: Raw JSON text from ``skill://index.json``. + + Returns: + A populated :class:`_McpSkillIndex` instance. + + Raises: + json.JSONDecodeError: If the text is not valid JSON. + ValueError: If the top-level value is not a JSON object. + """ + raw: dict[str, Any] = json.loads(text) + + if not isinstance(raw, dict): + raise ValueError("skill://index.json must be a JSON object") + + entries: list[_McpSkillIndexEntry] = [] + + raw_skills: list[Any] = raw.get("skills") or [] + + for item in raw_skills: + if isinstance(item, dict): + d = cast(dict[str, Any], item) + + entries.append( + _McpSkillIndexEntry( + name=d.get("name"), + type=d.get("type"), + description=d.get("description"), + url=d.get("url"), + digest=d.get("digest"), + ) + ) + + return _McpSkillIndex(schema=raw.get("$schema"), skills=entries) + + +@experimental(feature_id=ExperimentalFeature.MCP_SKILLS) +class MCPSkillResource(SkillResource): + """A :class:`SkillResource` backed by content fetched from an MCP server. + + The :class:`~mcp.types.ReadResourceResult` is fetched eagerly by + :meth:`MCPSkill.get_resource` at construction time; :meth:`read` + extracts text or binary content from the result. + """ + + def __init__(self, *, name: str, result: ReadResourceResult) -> None: + """Initialize an MCPSkillResource. + + Args: + name: The resource name (e.g. a relative path or identifier). + result: The result returned by the MCP server's ``resources/read`` request. + """ + super().__init__(name=name) + self._result = result + + async def read(self, **kwargs: Any) -> Any: + """Read the resource content. + + Returns: + A ``bytes`` object when the resource contains binary content, + a ``str`` when it contains text, or ``None`` when the server + returned no content blocks. + """ + from mcp.types import BlobResourceContents, TextResourceContents + + for content in self._result.contents: + if isinstance(content, BlobResourceContents): + blob = content.blob + # Strip data-URI prefix if present (some MCP servers send + # full data URIs instead of raw base64). + if blob.startswith("data:"): + blob = blob.split(",", 1)[-1] + return base64.b64decode(blob) + + text = "\n".join(c.text for c in self._result.contents if isinstance(c, TextResourceContents)) + return text if text else None + + +@experimental(feature_id=ExperimentalFeature.MCP_SKILLS) +class MCPSkill(Skill): + """A :class:`Skill` discovered from an MCP server exposing the Agent Skills convention. + + The skill is constructed from ``skill://index.json`` discovery metadata; + :meth:`get_content` fetches the full ``SKILL.md`` content from the MCP + server on demand via ``resources/read``. + + Per SEP-2640, resources referenced inside SKILL.md are fetched on demand + via the originating MCP server: :meth:`get_resource` resolves a relative + resource name against the skill's root URI, issues a ``resources/read`` + request, and returns an :class:`MCPSkillResource` with pre-fetched content. + """ + + _SKILL_MD_SUFFIX: Final[str] = "SKILL.md" + + def __init__( + self, + frontmatter: SkillFrontmatter, + skill_md_uri: str, + client: ClientSession, + ) -> None: + """Initialize an MCPSkill. + + Args: + frontmatter: The parsed frontmatter metadata for this skill. + skill_md_uri: The full MCP resource URI of the ``SKILL.md`` resource + (e.g. ``skill://unit-converter/SKILL.md``). The skill's root URI + is derived by stripping the trailing ``SKILL.md`` segment. + client: The MCP client session used to fetch resources on demand. + """ + self._frontmatter = frontmatter + self._skill_md_uri = skill_md_uri + self._skill_root_uri = self._compute_skill_root_uri(skill_md_uri) + self._client = client + self._content: str | None = None + + @property + def frontmatter(self) -> SkillFrontmatter: + """The L1 discovery metadata for this skill.""" + return self._frontmatter + + async def get_content(self) -> str: + """Get the full SKILL.md content from the MCP server. + + Fetches the content via ``resources/read`` on the first call and + caches the result for subsequent calls. + + Returns: + The SKILL.md content string. + + Raises: + ValueError: If the MCP server returned no text content for the + SKILL.md resource. + """ + if self._content is not None: + return self._content + + result = await self._client.read_resource(_mcp_any_url(self._skill_md_uri)) + text = _mcp_join_text(result) + if not text: + raise ValueError( + f"The MCP server returned no text content for SKILL.md resource '{self._skill_md_uri}'." + ) + self._content = text + return text + + async def get_resource(self, name: str) -> SkillResource | None: + """Get a sibling resource by name from the MCP server. + + Resolves *name* as a relative path against the skill's root URI, + issues a ``resources/read`` request to the MCP server, and returns + an :class:`MCPSkillResource` with the pre-fetched content. + + Args: + name: The resource name (e.g. ``references/checklist.md``). + + Returns: + An :class:`MCPSkillResource`, or ``None`` when the name is empty + or the resource does not exist on the server. + """ + if not name or not name.strip(): + return None + + normalized = self._validate_resource_name(name) + if normalized is None: + return None + + uri = self._skill_root_uri + normalized + try: + result = await self._client.read_resource(_mcp_any_url(uri)) + except Exception as ex: + if _is_mcp_resource_not_found(ex): + logger.debug("MCP resource '%s' not available: %s", uri, ex) + return None + raise + + return MCPSkillResource(name=name, result=result) + + @staticmethod + def _validate_resource_name(name: str) -> str | None: + """Validate a resource name and return the normalized form. + + Defense in depth: refuses names that could escape the skill root + (absolute paths, embedded URI schemes, parent-traversal segments). + The MCP server is the authority on URI resolution, but rejecting + obviously unsafe shapes client-side avoids leaking escape attempts + upstream. + + Args: + name: The raw resource name to validate. + + Returns: + The normalized name with backslashes replaced by forward slashes, + or ``None`` if the name is unsafe. + """ + normalized = name.replace("\\", "/") + if ( + normalized.startswith("/") + or "://" in normalized + or any(seg == ".." for seg in normalized.split("/")) + ): + logger.debug("Rejecting resource name with unsafe path components: %r", name) + return None + return normalized + + @staticmethod + def _compute_skill_root_uri(skill_md_uri: str) -> str: + """Strip the trailing ``SKILL.md`` from the URI to produce the skill root. + + If the URI doesn't end with ``SKILL.md``, ensures it ends with a + trailing slash. + """ + if skill_md_uri.endswith(MCPSkill._SKILL_MD_SUFFIX): + return skill_md_uri[: -len(MCPSkill._SKILL_MD_SUFFIX)] + if skill_md_uri.endswith("/"): + return skill_md_uri + return skill_md_uri + "/" + + +@experimental(feature_id=ExperimentalFeature.MCP_SKILLS) +class MCPSkillsSource(SkillsSource): + """A :class:`SkillsSource` that discovers Agent Skills served over MCP. + + Discovery follows the SEP-2640 recommended approach: the source reads + the well-known ``skill://index.json`` resource and constructs one + :class:`MCPSkill` per ``skill-md`` entry directly from the entry's + ``name``, ``description``, and ``url`` fields. + + The referenced ``SKILL.md`` resource is **not** read during discovery; + the host fetches its body on demand via ``resources/read`` when the + skill content is needed. + + Only index entries of type ``skill-md`` are supported; entries of any + other type are silently skipped. + + If ``skill://index.json`` is absent, unreadable, empty, or fails to + parse, this source returns an empty list. + + Examples: + .. code-block:: python + + from mcp.client.session import ClientSession + + source = MCPSkillsSource(client=session) + skills = await source.get_skills() + """ + + _INDEX_URI: Final[str] = "skill://index.json" + _SKILL_MD_TYPE: Final[str] = "skill-md" + + def __init__(self, client: ClientSession) -> None: + """Initialize an MCPSkillsSource. + + Args: + client: An MCP client session connected to a server that + exposes Agent Skills resources. + """ + self._client = client + + async def get_skills(self) -> list[Skill]: + """Discover and return skills from the MCP server. + + Reads ``skill://index.json``, parses it, and creates an + :class:`MCPSkill` for each valid ``skill-md`` entry. + + Returns: + A list of discovered :class:`MCPSkill` instances. + """ + index = await self._try_read_index() + if index is None: + return [] + + skills: list[Skill] = [] + for entry in index.skills: + result = self._try_create_skill(entry) + if result is not None: + skills.append(result) + logger.info("Loaded MCP skill: %s", result.frontmatter.name) + else: + logger.debug( + "Skipping skill index entry '%s'", + entry.name or "(unnamed)", + ) + + logger.info("Successfully loaded %d skills from MCP server", len(skills)) + return skills + + async def _try_read_index(self) -> _McpSkillIndex | None: + """Attempt to read and parse ``skill://index.json`` from the MCP server. + + Returns: + A parsed :class:`_McpSkillIndex`, or ``None`` if the index is + absent, empty, or malformed. + """ + try: + result = await self._client.read_resource(_mcp_any_url(self._INDEX_URI)) + except Exception as ex: + if _is_mcp_resource_not_found(ex): + logger.debug("No skill://index.json resource available on MCP server: %s", ex) + return None + logger.warning("Failed to read skill://index.json from MCP server.", exc_info=True) + raise + + index_text = _mcp_join_text(result) + if not index_text: + logger.debug("skill://index.json on MCP server returned empty/non-text contents") + return None + + try: + return _parse_mcp_skill_index(index_text) + except (json.JSONDecodeError, ValueError): + logger.warning("Failed to parse skill://index.json JSON document.", exc_info=True) + return None + + def _try_create_skill(self, entry: _McpSkillIndexEntry) -> MCPSkill | None: + """Attempt to create an :class:`MCPSkill` from an index entry. + + Args: + entry: A single entry from the skill index. + + Returns: + An :class:`MCPSkill` if the entry is valid, or ``None`` if the + entry should be skipped. + """ + if entry.type != self._SKILL_MD_TYPE: + logger.debug( + "Skipping entry '%s': unsupported type '%s'", + entry.name or "(unnamed)", + entry.type or "(none)", + ) + return None + + if not entry.name or not entry.name.strip(): + logger.debug("Skipping entry: missing required 'name' field") + return None + + if not entry.description or not entry.description.strip(): + logger.debug("Skipping entry '%s': missing required 'description' field", entry.name) + return None + + if not entry.url or not entry.url.strip(): + logger.debug("Skipping entry '%s': missing required 'url' field", entry.name) + return None + + try: + fm = SkillFrontmatter(name=entry.name, description=entry.description) + except ValueError as ex: + logger.debug("Skipping entry '%s': invalid metadata: %s", entry.name, ex) + return None + + return MCPSkill(frontmatter=fm, skill_md_uri=entry.url, client=self._client) + + # endregion diff --git a/python/packages/core/tests/core/test_mcp_skills.py b/python/packages/core/tests/core/test_mcp_skills.py new file mode 100644 index 0000000000..3e7c67662a --- /dev/null +++ b/python/packages/core/tests/core/test_mcp_skills.py @@ -0,0 +1,667 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for MCP-based skills (MCPSkillsSource, MCPSkill, MCPSkillResource).""" + +from __future__ import annotations + +import base64 +import json +from unittest.mock import AsyncMock + +import pytest +from mcp.shared.exceptions import McpError +from mcp.types import ( + BlobResourceContents, + ErrorData, + ReadResourceResult, + TextResourceContents, +) +from pydantic import AnyUrl + +from agent_framework import MCPSkill, MCPSkillResource, MCPSkillsSource +from agent_framework._skills import _parse_mcp_skill_index + +# --------------------------------------------------------------------------- +# Fixtures & helpers +# --------------------------------------------------------------------------- + +SAMPLE_SKILL_MD = """\ +--- +name: unit-converter +description: Convert between common units. +--- +# Unit Converter + +Body content here. +""" + +SAMPLE_SKILL_INDEX = json.dumps( + { + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "name": "unit-converter", + "type": "skill-md", + "description": "Convert between common units.", + "url": "skill://unit-converter/SKILL.md", + } + ], + } +) + + +def _make_text_result(text: str, uri: str = "skill://test") -> ReadResourceResult: + """Create a ReadResourceResult with a single TextResourceContents.""" + return ReadResourceResult( + contents=[TextResourceContents(uri=AnyUrl(uri), text=text, mimeType="text/markdown")] + ) + + +def _make_blob_result( + data: bytes, + uri: str = "skill://test", + mime_type: str = "application/octet-stream", +) -> ReadResourceResult: + """Create a ReadResourceResult with a single BlobResourceContents.""" + return ReadResourceResult( + contents=[BlobResourceContents(uri=AnyUrl(uri), blob=base64.b64encode(data).decode(), mimeType=mime_type)] + ) + + +def _make_empty_result() -> ReadResourceResult: + """Create a ReadResourceResult with no contents.""" + return ReadResourceResult(contents=[]) + + +def _make_client(**read_resource_responses: ReadResourceResult) -> AsyncMock: + """Create a mock ClientSession whose read_resource returns different results per URI. + + Args: + **read_resource_responses: Mapping of URI string to ReadResourceResult. + Any URI not in this mapping raises McpError with the MCP-spec + "Resource not found" code (-32002). + """ + client = AsyncMock() + + async def _read_resource(uri: AnyUrl) -> ReadResourceResult: + uri_str = str(uri) + if uri_str in read_resource_responses: + return read_resource_responses[uri_str] + raise McpError(error=ErrorData(code=-32002, message=f"Resource not found: {uri_str}")) + + client.read_resource = AsyncMock(side_effect=_read_resource) + return client + + +# --------------------------------------------------------------------------- +# _parse_mcp_skill_index tests +# --------------------------------------------------------------------------- + + +class TestParseMCPSkillIndex: + """Tests for the _parse_mcp_skill_index helper.""" + + def test_parses_valid_index(self) -> None: + index = _parse_mcp_skill_index(SAMPLE_SKILL_INDEX) + assert index.schema == "https://schemas.agentskills.io/discovery/0.2.0/schema.json" + assert len(index.skills) == 1 + assert index.skills[0].name == "unit-converter" + assert index.skills[0].type == "skill-md" + assert index.skills[0].url == "skill://unit-converter/SKILL.md" + + def test_parses_empty_skills_array(self) -> None: + index = _parse_mcp_skill_index('{"$schema": "test", "skills": []}') + assert index.skills == [] + + def test_parses_missing_skills_key(self) -> None: + index = _parse_mcp_skill_index('{"$schema": "test"}') + assert index.skills == [] + + def test_raises_on_non_object(self) -> None: + with pytest.raises(ValueError, match="must be a JSON object"): + _parse_mcp_skill_index("[]") + + def test_raises_on_invalid_json(self) -> None: + with pytest.raises(json.JSONDecodeError): + _parse_mcp_skill_index("not json") + + def test_skips_non_dict_entries(self) -> None: + index = _parse_mcp_skill_index('{"skills": ["not-a-dict", {"name": "ok", "type": "skill-md"}]}') + assert len(index.skills) == 1 + assert index.skills[0].name == "ok" + + +# --------------------------------------------------------------------------- +# MCPSkillResource tests +# --------------------------------------------------------------------------- + + +class TestMCPSkillResource: + """Tests for MCPSkillResource.""" + + @pytest.mark.asyncio + async def test_read_text_content(self) -> None: + result = _make_text_result("hello world") + resource = MCPSkillResource(name="test.md", result=result) + content = await resource.read() + assert content == "hello world" + + @pytest.mark.asyncio + async def test_read_binary_content(self) -> None: + data = bytes([0x01, 0x02, 0x03, 0x04]) + result = _make_blob_result(data) + resource = MCPSkillResource(name="icon.bin", result=result) + content = await resource.read() + assert content == data + + @pytest.mark.asyncio + async def test_read_empty_returns_none(self) -> None: + result = _make_empty_result() + resource = MCPSkillResource(name="empty", result=result) + content = await resource.read() + assert content is None + + @pytest.mark.asyncio + async def test_read_multiple_text_contents_joined(self) -> None: + result = ReadResourceResult( + contents=[ + TextResourceContents(uri=AnyUrl("skill://a"), text="line1", mimeType="text/plain"), + TextResourceContents(uri=AnyUrl("skill://b"), text="line2", mimeType="text/plain"), + ] + ) + resource = MCPSkillResource(name="multi", result=result) + content = await resource.read() + assert content == "line1\nline2" + + @pytest.mark.asyncio + async def test_binary_takes_precedence_over_text(self) -> None: + data = b"\xff\xfe" + result = ReadResourceResult( + contents=[ + TextResourceContents(uri=AnyUrl("skill://a"), text="text", mimeType="text/plain"), + BlobResourceContents( + uri=AnyUrl("skill://b"), + blob=base64.b64encode(data).decode(), + mimeType="application/octet-stream", + ), + ] + ) + resource = MCPSkillResource(name="mixed", result=result) + content = await resource.read() + # The implementation iterates all contents checking for BlobResourceContents + # first, so when both text and binary are present, binary is returned. + assert content == data + + +# --------------------------------------------------------------------------- +# MCPSkill tests +# --------------------------------------------------------------------------- + + +class TestMCPSkill: + """Tests for MCPSkill.""" + + @pytest.mark.asyncio + async def test_get_content_fetches_and_caches(self) -> None: + client = _make_client(**{"skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD)}) + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="unit-converter", description="Convert between common units.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) + + content1 = await skill.get_content() + content2 = await skill.get_content() + + assert "Body content here." in content1 + assert content1 == content2 + # Only one MCP call should be made (cached) + assert client.read_resource.call_count == 1 + + @pytest.mark.asyncio + async def test_get_content_raises_on_empty(self) -> None: + client = _make_client(**{"skill://empty/SKILL.md": _make_empty_result()}) + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="empty-skill", description="Empty skill.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://empty/SKILL.md", client=client) + + with pytest.raises(ValueError, match="no text content"): + await skill.get_content() + + @pytest.mark.asyncio + async def test_get_resource_text(self) -> None: + client = _make_client( + **{ + "skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD), + "skill://unit-converter/references/checklist.md": _make_text_result("- check thing 1\n- check thing 2"), + } + ) + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="unit-converter", description="Convert between common units.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) + + resource = await skill.get_resource("references/checklist.md") + assert resource is not None + content = await resource.read() + assert content == "- check thing 1\n- check thing 2" + + @pytest.mark.asyncio + async def test_get_resource_binary(self) -> None: + data = bytes([0x01, 0x02, 0x03, 0x04]) + client = _make_client( + **{ + "skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD), + "skill://unit-converter/assets/icon.bin": _make_blob_result(data), + } + ) + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="unit-converter", description="Convert between common units.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) + + resource = await skill.get_resource("assets/icon.bin") + assert resource is not None + content = await resource.read() + assert content == data + + @pytest.mark.asyncio + async def test_get_resource_unknown_returns_none(self) -> None: + client = _make_client(**{"skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD)}) + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="unit-converter", description="Convert between common units.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) + + resource = await skill.get_resource("references/does-not-exist.md") + assert resource is None + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "name", + [ + "../escape.md", + "references/../../escape.md", + "..", + "..\\escape.md", + "/etc/passwd", + "http://attacker.example.com/payload", + ], + ) + async def test_get_resource_path_traversal_returns_none(self, name: str) -> None: + # Register a permissive mock that would happily return content for any URI, + # so the test fails unless the client-side validation rejects the name + # before issuing the read. + client = AsyncMock() + client.read_resource = AsyncMock(return_value=_make_text_result("should never be returned")) + + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="unit-converter", description="Convert between common units.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) + + resource = await skill.get_resource(name) + assert resource is None + client.read_resource.assert_not_called() + + @pytest.mark.asyncio + async def test_get_resource_empty_name_returns_none(self) -> None: + client = _make_client() + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + + assert await skill.get_resource("") is None + assert await skill.get_resource(" ") is None + + @pytest.mark.asyncio + async def test_get_script_returns_none(self) -> None: + client = _make_client() + from agent_framework import SkillFrontmatter + + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + + assert await skill.get_script("anything") is None + + def test_compute_skill_root_uri_strips_suffix(self) -> None: + assert MCPSkill._compute_skill_root_uri("skill://unit-converter/SKILL.md") == "skill://unit-converter/" + + def test_compute_skill_root_uri_trailing_slash(self) -> None: + assert MCPSkill._compute_skill_root_uri("skill://unit-converter/") == "skill://unit-converter/" + + def test_compute_skill_root_uri_no_suffix_adds_slash(self) -> None: + assert MCPSkill._compute_skill_root_uri("skill://unit-converter") == "skill://unit-converter/" + + +# --------------------------------------------------------------------------- +# MCPSkillsSource tests +# --------------------------------------------------------------------------- + + +class TestMCPSkillsSource: + """Tests for MCPSkillsSource.""" + + @pytest.mark.asyncio + async def test_index_based_discovery_returns_skill(self) -> None: + client = _make_client( + **{ + "skill://index.json": _make_text_result(SAMPLE_SKILL_INDEX, uri="skill://index.json"), + "skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD), + } + ) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + + assert len(skills) == 1 + assert skills[0].frontmatter.name == "unit-converter" + assert skills[0].frontmatter.description == "Convert between common units." + + # Content is fetched on demand, not during discovery + content = await skills[0].get_content() + assert "Body content here." in content + + @pytest.mark.asyncio + async def test_no_index_returns_empty(self) -> None: + client = _make_client() # No resources at all + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_does_not_read_skill_md_during_discovery(self) -> None: + # Index points to a skill, but SKILL.md is not registered on the server. + # Discovery should succeed because it only reads the index. + client = _make_client( + **{"skill://index.json": _make_text_result(SAMPLE_SKILL_INDEX, uri="skill://index.json")} + ) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + + assert len(skills) == 1 + assert skills[0].frontmatter.name == "unit-converter" + + @pytest.mark.asyncio + async def test_invalid_name_is_skipped(self) -> None: + index_json = json.dumps( + { + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "name": "UnitConverter", # Invalid: uppercase + "type": "skill-md", + "description": "Convert between common units.", + "url": "skill://UnitConverter/SKILL.md", + } + ], + } + ) + client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_missing_required_fields_is_skipped(self) -> None: + index_json = json.dumps( + { + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "name": "unit-converter", + "type": "skill-md", + # Missing description and url + } + ], + } + ) + client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_unsupported_type_is_skipped(self) -> None: + index_json = json.dumps( + { + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "name": "some-skill", + "type": "archive", + "description": "Packaged skill.", + "url": "skill://some-skill.tar.gz", + } + ], + } + ) + client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_template_type_is_skipped(self) -> None: + index_json = json.dumps( + { + "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", + "skills": [ + { + "type": "mcp-resource-template", + "description": "Per-product documentation skill", + "url": "skill://docs/{product}/SKILL.md", + } + ], + } + ) + client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_empty_index_returns_empty(self) -> None: + client = _make_client( + **{"skill://index.json": _make_text_result('{"skills": []}', uri="skill://index.json")} + ) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_malformed_index_json_returns_empty(self) -> None: + client = _make_client( + **{"skill://index.json": _make_text_result("not valid json", uri="skill://index.json")} + ) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_sibling_text_resource(self) -> None: + client = _make_client( + **{ + "skill://index.json": _make_text_result(SAMPLE_SKILL_INDEX, uri="skill://index.json"), + "skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD), + "skill://unit-converter/references/checklist.md": _make_text_result("- check thing 1\n- check thing 2"), + } + ) + source = MCPSkillsSource(client=client) + skill = (await source.get_skills())[0] + resource = await skill.get_resource("references/checklist.md") + assert resource is not None + content = await resource.read() + assert content == "- check thing 1\n- check thing 2" + + @pytest.mark.asyncio + async def test_sibling_binary_resource(self) -> None: + data = bytes([0x01, 0x02, 0x03, 0x04]) + client = _make_client( + **{ + "skill://index.json": _make_text_result(SAMPLE_SKILL_INDEX, uri="skill://index.json"), + "skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD), + "skill://unit-converter/assets/icon.bin": _make_blob_result(data), + } + ) + source = MCPSkillsSource(client=client) + skill = (await source.get_skills())[0] + resource = await skill.get_resource("assets/icon.bin") + assert resource is not None + content = await resource.read() + assert content == data + + +# --------------------------------------------------------------------------- +# McpError code branching tests +# --------------------------------------------------------------------------- + + +class TestMCPSkillsSourceErrorCodeBranching: + """Tests that MCPSkillsSource and MCPSkill branch on McpError.error.code. + + Only "not found" codes (RESOURCE_NOT_FOUND -32002, METHOD_NOT_FOUND -32601) + should be silently swallowed as "no skills available." Other McpError codes + and non-McpError exceptions must propagate so that auth failures, server + crashes, and connection drops are visible. + """ + + @pytest.mark.asyncio + async def test_index_method_not_found_returns_empty(self) -> None: + """METHOD_NOT_FOUND (-32601) -> server doesn't support resources/read.""" + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=McpError(error=ErrorData(code=-32601, message="Method not found"))) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_index_resource_not_found_returns_empty(self) -> None: + """MCP-spec "Resource not found" (-32002) -> server has no index.""" + client = AsyncMock() + client.read_resource = AsyncMock( + side_effect=McpError(error=ErrorData(code=-32002, message="Resource not found")) + ) + source = MCPSkillsSource(client=client) + skills = await source.get_skills() + assert skills == [] + + @pytest.mark.asyncio + async def test_index_invalid_params_propagates(self) -> None: + """INVALID_PARAMS (-32602) is a real bug, must propagate (not "not found").""" + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=McpError(error=ErrorData(code=-32602, message="Invalid params"))) + source = MCPSkillsSource(client=client) + with pytest.raises(McpError): + await source.get_skills() + + @pytest.mark.asyncio + async def test_index_internal_error_propagates(self) -> None: + """INTERNAL_ERROR (-32603) must propagate, not silently return empty.""" + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=McpError(error=ErrorData(code=-32603, message="Internal error"))) + source = MCPSkillsSource(client=client) + with pytest.raises(McpError): + await source.get_skills() + + @pytest.mark.asyncio + async def test_index_connection_closed_propagates(self) -> None: + """CONNECTION_CLOSED (-32000) must propagate.""" + client = AsyncMock() + client.read_resource = AsyncMock( + side_effect=McpError(error=ErrorData(code=-32000, message="Connection closed")) + ) + source = MCPSkillsSource(client=client) + with pytest.raises(McpError): + await source.get_skills() + + @pytest.mark.asyncio + async def test_index_generic_error_code_propagates(self) -> None: + """Generic handler error (code 0) must propagate.""" + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=McpError(error=ErrorData(code=0, message="Some handler error"))) + source = MCPSkillsSource(client=client) + with pytest.raises(McpError): + await source.get_skills() + + @pytest.mark.asyncio + async def test_index_non_mcp_error_propagates(self) -> None: + """Non-McpError exceptions (connection drop, timeout) must propagate.""" + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=ConnectionError("connection lost")) + source = MCPSkillsSource(client=client) + with pytest.raises(ConnectionError): + await source.get_skills() + + @pytest.mark.asyncio + async def test_get_resource_internal_error_propagates(self) -> None: + """McpError with INTERNAL_ERROR on get_resource must propagate.""" + from agent_framework import SkillFrontmatter + + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=McpError(error=ErrorData(code=-32603, message="Server crashed"))) + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + with pytest.raises(McpError): + await skill.get_resource("references/file.md") + + @pytest.mark.asyncio + async def test_get_resource_not_found_returns_none(self) -> None: + """McpError with RESOURCE_NOT_FOUND (-32002) on get_resource returns None.""" + from agent_framework import SkillFrontmatter + + client = AsyncMock() + client.read_resource = AsyncMock( + side_effect=McpError(error=ErrorData(code=-32002, message="Resource not found")) + ) + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + result = await skill.get_resource("references/file.md") + assert result is None + + @pytest.mark.asyncio + async def test_get_resource_connection_error_propagates(self) -> None: + """A plain ConnectionError on get_resource must propagate, not return None.""" + from agent_framework import SkillFrontmatter + + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=ConnectionError("connection lost")) + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + with pytest.raises(ConnectionError): + await skill.get_resource("references/file.md") + + @pytest.mark.asyncio + async def test_get_resource_timeout_error_propagates(self) -> None: + """A TimeoutError on get_resource must propagate, not return None.""" + from agent_framework import SkillFrontmatter + + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=TimeoutError("read timed out")) + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + with pytest.raises(TimeoutError): + await skill.get_resource("references/file.md") + + @pytest.mark.asyncio + async def test_get_resource_generic_mcp_error_propagates(self) -> None: + """McpError with a generic code (0) on get_resource must propagate.""" + from agent_framework import SkillFrontmatter + + client = AsyncMock() + client.read_resource = AsyncMock( + side_effect=McpError(error=ErrorData(code=0, message="Handler error")) + ) + fm = SkillFrontmatter(name="test-skill", description="Test.") + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + with pytest.raises(McpError): + await skill.get_resource("references/file.md") + + @pytest.mark.asyncio + async def test_index_timeout_error_propagates(self) -> None: + """A TimeoutError reading skill://index.json must propagate.""" + client = AsyncMock() + client.read_resource = AsyncMock(side_effect=TimeoutError("read timed out")) + source = MCPSkillsSource(client=client) + with pytest.raises(TimeoutError): + await source.get_skills() diff --git a/python/samples/02-agents/providers/foundry/README.md b/python/samples/02-agents/providers/foundry/README.md index 598b849620..1a3c06ffda 100644 --- a/python/samples/02-agents/providers/foundry/README.md +++ b/python/samples/02-agents/providers/foundry/README.md @@ -27,6 +27,7 @@ This folder contains Azure AI Foundry and Foundry Local samples for Agent Framew | [`foundry_chat_client_with_local_mcp.py`](foundry_chat_client_with_local_mcp.py) | Foundry Chat Client with local MCP | | [`foundry_chat_client_with_session.py`](foundry_chat_client_with_session.py) | Foundry Chat Client with session management | | [`foundry_chat_client_with_toolbox.py`](foundry_chat_client_with_toolbox.py) | Foundry Chat Client connected to a toolbox via its MCP endpoint using `MCPStreamableHTTPTool` | +| [`foundry_chat_client_with_toolbox_skills.py`](foundry_chat_client_with_toolbox_skills.py) | Foundry Chat Client that discovers MCP-based skills from a Foundry Toolbox endpoint via `MCPSkillsSource` (uses an Azure AD bearer token and the toolbox preview header) | ## FoundryLocalClient Samples diff --git a/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py b/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py new file mode 100644 index 0000000000..73b9dd1e89 --- /dev/null +++ b/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from collections.abc import Generator + +import httpx +from agent_framework import Agent, MCPSkillsSource, SkillsProvider +from agent_framework.foundry import FoundryChatClient +from azure.core.credentials import TokenCredential +from azure.identity import AzureCliCredential, get_bearer_token_provider +from dotenv import load_dotenv +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamable_http_client + +# Load environment variables from .env file +load_dotenv() + +""" +Foundry Chat Client with Toolbox-Hosted Skills + +Discover Agent Skills served by a Microsoft Foundry Toolbox MCP endpoint +and inject them into a ``FoundryChatClient`` agent via ``MCPSkillsSource``. +The toolbox's discovery document (``skill://index.json``) is read once at +startup; SKILL.md bodies are fetched on demand as the agent uses them. + +Prerequisites: +- A Microsoft Foundry project with a toolbox that exposes + ``skill://index.json`` with ``skill-md`` entries +- FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL environment variables set +- FOUNDRY_TOOLBOX_MCP_SERVER_URL: the toolbox's MCP endpoint URL, e.g. + ``https://.services.ai.azure.com/api/projects//toolboxes//mcp?api-version=v1`` +- Azure CLI authentication (``az login``) +""" + + +class _BearerAuth(httpx.Auth): + """Attach a fresh Foundry bearer token to every request.""" + + def __init__(self, credential: TokenCredential) -> None: + self._get_token = get_bearer_token_provider(credential, "https://ai.azure.com/.default") + + def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]: + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request + + +async def main() -> None: + """Example showing toolbox-hosted MCP skills for a Foundry Chat Client agent.""" + credential = AzureCliCredential() + + # HTTP client that signs every request with a fresh Foundry bearer token + # and advertises the toolbox preview feature flag, plus the MCP streamable + # HTTP transport that uses it. + async with ( + httpx.AsyncClient( + auth=_BearerAuth(credential), + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + timeout=httpx.Timeout(30.0, read=300.0), + follow_redirects=True, + ) as http_client, + streamable_http_client( + url=os.environ["FOUNDRY_TOOLBOX_MCP_SERVER_URL"], + http_client=http_client, + ) as (read, write, _), + ClientSession(read, write) as session, + ): + await session.initialize() + + # Discover skills served by the toolbox and inject them as a context provider. + skills_provider = SkillsProvider(MCPSkillsSource(client=session)) + + async with Agent( + client=FoundryChatClient(credential=credential), + name="ToolboxMCPSkillsAgent", + instructions="You are a helpful assistant. Use available skills to answer the user.", + context_providers=[skills_provider], + ) as agent: + query = input("User: ").strip() # noqa: ASYNC250 + if not query: + return + response = await agent.run(query) + print(f"Assistant: {response.text}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/skills/README.md b/python/samples/02-agents/skills/README.md index 6e9bb08202..53a2167531 100644 --- a/python/samples/02-agents/skills/README.md +++ b/python/samples/02-agents/skills/README.md @@ -12,6 +12,7 @@ Start with file-based or code-defined skills, then explore combining them and ad | [**code_defined_skill**](code_defined_skill/) | Define skills entirely in Python code using `Skill`, `@skill.resource`, and `@skill.script` decorators. Uses a code-defined unit-converter skill. | | [**class_based_skill**](class_based_skill/) | Define skills as Python classes using `ClassSkill` with `@ClassSkill.resource` and `@ClassSkill.script` decorators for auto-discovery. Uses a class-based unit-converter skill. | | [**mixed_skills**](mixed_skills/) | Combine code-defined, class-based, and file-based skills in a single agent. Uses a code-defined volume-converter, a class-based temperature-converter, and a file-based unit-converter. | +| [**mcp_based_skill**](mcp_based_skill/) | Discover skills served over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) via `MCPSkillsSource`. Connects to a remote MCP server that exposes skills as `skill://...` resources following the SEP-2640 convention. | | [**script_approval**](script_approval/) | Require human-in-the-loop approval before executing skill scripts | ## Key Concepts diff --git a/python/samples/02-agents/skills/mcp_based_skill/README.md b/python/samples/02-agents/skills/mcp_based_skill/README.md new file mode 100644 index 0000000000..994fa2d7fb --- /dev/null +++ b/python/samples/02-agents/skills/mcp_based_skill/README.md @@ -0,0 +1,51 @@ +# MCP-Based Agent Skills Sample + +This sample demonstrates how to discover **Agent Skills served over MCP** with an `Agent`. + +## What it demonstrates + +- Connecting to a remote MCP server (over streamable HTTP) that exposes skill + resources following the SEP-2640 convention. +- Building a `SkillsProvider` from an `MCPSkillsSource`, which reads + `skill://index.json` (SEP-2640 canonical discovery) and constructs skills from + the index entries. +- The progressive disclosure pattern across MCP: advertise → load → read + resources, exactly as for filesystem-backed skills. + +## Running the Sample + +### Prerequisites + +- Python 3.10+ +- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model +- Azure CLI authentication (`az login`) +- A running MCP server that hosts SEP-2640 skill resources (see "Providing + an MCP server" below) + +### Setup + +Set the following environment variables (in a `.env` file or your shell): + +```powershell +$env:FOUNDRY_PROJECT_ENDPOINT="https://your-endpoint.services.ai.azure.com/api/projects/your-project" +$env:FOUNDRY_MODEL="gpt-4o-mini" +$env:MCP_SKILLS_SERVER_URL="https://your-mcp-server.example.com/mcp" +``` + +### Run + +```powershell +python mcp_based_skill.py +``` + +## Providing an MCP server + +This sample is a **consumer**: it does not host an MCP server itself. To try +it end-to-end you need an MCP server that exposes the SEP-2640 skill +resources (`skill://index.json` plus per-skill `SKILL.md`). + +- See [`samples/02-agents/mcp/agent_as_mcp_server.py`](../../mcp/agent_as_mcp_server.py) + for an example of hosting an MCP server via the Agent Framework. +- The Model Context Protocol working group maintains reference MCP-skills + servers at + [`modelcontextprotocol/experimental-ext-skills`](https://github.com/modelcontextprotocol/experimental-ext-skills). diff --git a/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py b/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py new file mode 100644 index 0000000000..a85841cd4d --- /dev/null +++ b/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os + +# Uncomment this filter to suppress the experimental Skills warning before +# using the sample's Skills APIs. +# import warnings +# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning) +from agent_framework import Agent, MCPSkillsSource, SkillsProvider +from agent_framework.foundry import FoundryChatClient +from azure.identity import AzureCliCredential +from dotenv import load_dotenv +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamable_http_client + +""" +MCP-Based Agent Skills + +This sample demonstrates how to discover Agent Skills served over the +Model Context Protocol (MCP) using :class:`MCPSkillsSource`. + +The sample connects to a remote MCP server that exposes skill resources +under the ``skill://`` URI scheme: + +* ``skill://index.json`` — discovery document listing all skills +* ``skill:///SKILL.md`` — the skill instructions + +To run, set ``MCP_SKILLS_SERVER_URL`` to the streamable HTTP endpoint of an +MCP server that hosts the skill resources. +""" + + +async def main() -> None: + """Connect to a remote MCP skills server and run the agent.""" + load_dotenv() + + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") + mcp_url = os.environ["MCP_SKILLS_SERVER_URL"] + + print("Discovering MCP-based skills") + print("-" * 60) + + # 1. Connect to the MCP server over streamable HTTP. + async with streamable_http_client(url=mcp_url) as (read, write, _), ClientSession(read, write) as session: + await session.initialize() + + # 2. Build a SkillsProvider that discovers skills over MCP. + # MCPSkillsSource reads skill://index.json and creates one + # MCPSkill per skill-md entry; SKILL.md bodies are fetched + # on demand via resources/read. + skills_provider = SkillsProvider(MCPSkillsSource(client=session)) + + # 3. Run the agent. + client = FoundryChatClient( + project_endpoint=endpoint, + model=deployment, + credential=AzureCliCredential(), + ) + + async with Agent( + client=client, + instructions="You are a helpful assistant. Use available skills to answer the user.", + context_providers=[skills_provider], + ) as agent: + query = input("User: ").strip() # noqa: ASYNC250 + if not query: + return + response = await agent.run(query) + print(f"Agent: {response}\n") + + +if __name__ == "__main__": + asyncio.run(main()) From afa7834e2ec8a93b2224fe7ab184b97fbcaa8c9a Mon Sep 17 00:00:00 2001 From: Ben Thomas Date: Wed, 3 Jun 2026 13:03:21 -0700 Subject: [PATCH 47/61] Updating dotnet package versions for 1.9 release (#6314) Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com> --- dotnet/nuget/nuget-package.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index ed9a52f6c4..28bc03d112 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -1,14 +1,14 @@ - 1.8.0 + 1.9.0 1 - 260528 + 260603 $(VersionPrefix)-rc$(RCNumber) $(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1 $(VersionPrefix)-preview.$(DateSuffix).1 $(VersionPrefix) - 1.8.0 + 1.9.0 Debug;Release;Publish true From ba617fc3b5028605e55dc56050871ef918913f5e Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:31:36 +0900 Subject: [PATCH 48/61] Don't count dependabot prs as part of the limit (#6317) --- .github/scripts/pr_limit_moderation.js | 17 +++++++++++++- .github/tests/test_pr_limit_moderation.js | 27 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/scripts/pr_limit_moderation.js b/.github/scripts/pr_limit_moderation.js index 450813d22f..6cb23e053a 100644 --- a/.github/scripts/pr_limit_moderation.js +++ b/.github/scripts/pr_limit_moderation.js @@ -8,6 +8,7 @@ function getPullRequest(context) { return { author: pullRequest.user.login, + authorType: pullRequest.user.type, labels: pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [], number: pullRequest.number, }; @@ -49,6 +50,10 @@ function hasLabel(labels, labelName) { return labels.some((label) => label.toLowerCase() === labelName.toLowerCase()); } +function isDependabotAuthor({ author, authorType }) { + return authorType === 'Bot' && author.toLowerCase() === 'dependabot[bot]'; +} + function buildLimitMessage({ author, exemptLabelName, maxOpenPrs, openPrCount }) { return [ `Thank you for your contribution, @${author}.`, @@ -83,7 +88,17 @@ async function getOpenPrCount({ github, owner, repo, author, pullRequestNumber } async function enforcePrLimit({ github, context, core, exemptLabelName, maxOpenPrs, labelName }) { const { owner, repo } = context.repo; - const { author, labels, number } = getPullRequest(context); + const { author, authorType, labels, number } = getPullRequest(context); + + if (isDependabotAuthor({ author, authorType })) { + core.info(`Author ${author} is Dependabot; skipping open PR limit enforcement.`); + return { + author, + closed: false, + dependabotExempt: true, + openPrCount: null, + }; + } if (hasLabel(labels, exemptLabelName)) { core.info(`PR #${number} has the ${exemptLabelName} label; skipping open PR limit enforcement.`); diff --git a/.github/tests/test_pr_limit_moderation.js b/.github/tests/test_pr_limit_moderation.js index d3dccb0cce..5f0c8c865a 100644 --- a/.github/tests/test_pr_limit_moderation.js +++ b/.github/tests/test_pr_limit_moderation.js @@ -16,7 +16,7 @@ const { enforcePrLimit } = require('../scripts/pr_limit_moderation.js'); // Helpers // --------------------------------------------------------------------------- -function createContext({ author = 'community-user', labels = [], number = 123 } = {}) { +function createContext({ author = 'community-user', authorType = 'User', labels = [], number = 123 } = {}) { return { repo: { owner: 'microsoft', @@ -28,6 +28,7 @@ function createContext({ author = 'community-user', labels = [], number = 123 } labels: labels.map((name) => ({ name })), user: { login: author, + type: authorType, }, }, }, @@ -296,6 +297,30 @@ describe('PR limit enforcement', () => { assert.deepEqual(github.calls, []); }); + it('does not close Dependabot PRs', async () => { + const github = createGithub({ + itemNumbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)], + pullRequests: createPullRequestPage({ + author: 'dependabot[bot]', + numbers: [123, ...Array.from({ length: 25 }, (_, index) => index + 1)], + }), + }); + + const result = await enforcePrLimit({ + github, + context: createContext({ author: 'dependabot[bot]', authorType: 'Bot' }), + core: createCore(), + exemptLabelName: 'pr-limit-exempt', + maxOpenPrs: 10, + labelName: 'too-many-prs', + }); + + assert.equal(result.closed, false); + assert.equal(result.dependabotExempt, true); + assert.equal(result.openPrCount, null); + assert.deepEqual(github.calls, []); + }); + it('counts the current PR when the author has more than one page of open PRs', async () => { const github = createGithub({ itemNumbers: [123, ...Array.from({ length: 100 }, (_, index) => index + 1)], From c3901a4ddda0c0467472de786b21aad66ff138bf Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:52:50 -0700 Subject: [PATCH 49/61] Fix Observability/WorkflowAsAnAgent sampl (#6316) --- .../WorkflowAsAnAgent/WorkflowHelper.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs b/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs index 54e3eb40f2..4db325b658 100644 --- a/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs +++ b/dotnet/samples/03-workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs @@ -50,12 +50,16 @@ internal static partial class WorkflowHelper /// /// Executor that starts the concurrent processing by sending messages to the agents. /// - private sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor") + [SendsMessage(typeof(List))] + [SendsMessage(typeof(TurnToken))] + private sealed partial class ConcurrentStartExecutor() + : Executor("ConcurrentStartExecutor", declareCrossRunShareable: true), IResettableExecutor { [MessageHandler] - internal ValueTask RouteMessages(List messages, IWorkflowContext context, CancellationToken cancellationToken) + internal ValueTask RouteMessages(IEnumerable messages, IWorkflowContext context, CancellationToken cancellationToken) { - return context.SendMessageAsync(messages, cancellationToken: cancellationToken); + List payload = messages as List ?? messages.ToList(); + return context.SendMessageAsync(payload, cancellationToken: cancellationToken); } [MessageHandler] @@ -63,13 +67,16 @@ internal static partial class WorkflowHelper { return context.SendMessageAsync(token, cancellationToken: cancellationToken); } + + public ValueTask ResetAsync() => default; } /// /// Executor that aggregates the results from the concurrent agents. /// - [YieldsOutput(typeof(List))] - private sealed partial class ConcurrentAggregationExecutor() : Executor>("ConcurrentAggregationExecutor") + [YieldsOutput(typeof(string))] + private sealed partial class ConcurrentAggregationExecutor() : + Executor>("ConcurrentAggregationExecutor"), IResettableExecutor { private readonly List _messages = []; @@ -90,5 +97,11 @@ internal static partial class WorkflowHelper await context.YieldOutputAsync(formattedMessages, cancellationToken); } } + + public ValueTask ResetAsync() + { + this._messages.Clear(); + return default; + } } } From f29bae8fbc9e3057f9d14cd223ddcc5a8fae3fd4 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:42:08 +0800 Subject: [PATCH 50/61] Python: run sync tools off the event loop (#5773) * fix: run sync tools off event loop * chore: silence harness tool marker type check --- .../_harness/_background_agents.py | 4 ++ .../packages/core/agent_framework/_tools.py | 18 ++++++-- python/packages/core/tests/core/test_tools.py | 41 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/python/packages/core/agent_framework/_harness/_background_agents.py b/python/packages/core/agent_framework/_harness/_background_agents.py index c329af4aa9..c5efa1a6fb 100644 --- a/python/packages/core/agent_framework/_harness/_background_agents.py +++ b/python/packages/core/agent_framework/_harness/_background_agents.py @@ -349,6 +349,8 @@ class BackgroundAgentsProvider(ContextProvider): _save_provider_state(session, provider_state, source_id=source_id) return f"Background task {task_id} started on agent '{agent_name}'." + background_agents_start_task._invoke_sync_on_event_loop = True # pyright: ignore[reportPrivateUsage] + @tool(name="background_agents_wait_for_first_completion", approval_mode="never_require") async def background_agents_wait_for_first_completion(task_ids: list[int]) -> str: """Block until the first of the specified background tasks completes. Returns the completed task's ID.""" @@ -471,6 +473,8 @@ class BackgroundAgentsProvider(ContextProvider): _save_provider_state(session, provider_state, source_id=source_id) return f"Task {task_id} continued with new input." + background_agents_continue_task._invoke_sync_on_event_loop = True # pyright: ignore[reportPrivateUsage] + @tool(name="background_agents_clear_completed_task", approval_mode="never_require") def background_agents_clear_completed_task(task_id: int) -> str: """Remove a completed or failed task and release its session to free memory.""" diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 5237cf62ba..7bb54ee2c9 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -292,6 +292,7 @@ class FunctionTool(SerializationMixin): "_cached_parameters", "_input_schema", "_schema_supplied", + "_invoke_sync_on_event_loop", } def __init__( @@ -366,6 +367,7 @@ class FunctionTool(SerializationMixin): self.description = description self.kind = kind self.additional_properties = additional_properties + self._invoke_sync_on_event_loop = False for key, value in kwargs.items(): setattr(self, key, value) @@ -537,6 +539,16 @@ class FunctionTool(SerializationMixin): self.invocation_exception_count += 1 raise + async def _invoke_function(self, call_kwargs: Mapping[str, Any]) -> Any: + """Run sync tools off the event loop during async invocation.""" + func = self.func.func if isinstance(self.func, FunctionTool) else self.func + if inspect.iscoroutinefunction(func) or getattr(self, "_invoke_sync_on_event_loop", False): + res = self.__call__(**call_kwargs) + return await res if inspect.isawaitable(res) else res + + res = await asyncio.to_thread(self.__call__, **call_kwargs) + return await res if inspect.isawaitable(res) else res + @overload async def invoke( self, @@ -679,8 +691,7 @@ class FunctionTool(SerializationMixin): if not OBSERVABILITY_SETTINGS.ENABLED: # type: ignore[name-defined] logger.info(f"Function name: {self.name}") logger.debug(f"Function arguments: {observable_kwargs}") - res = self.__call__(**call_kwargs) - result = await res if inspect.isawaitable(res) else res + result = await self._invoke_function(call_kwargs) if skip_parsing: logger.info(f"Function {self.name} succeeded.") logger.debug(f"Function result: {type(result).__name__}") @@ -730,8 +741,7 @@ class FunctionTool(SerializationMixin): start_time_stamp = perf_counter() end_time_stamp: float | None = None try: - res = self.__call__(**call_kwargs) - result = await res if inspect.isawaitable(res) else res + result = await self._invoke_function(call_kwargs) end_time_stamp = perf_counter() except Exception as exception: end_time_stamp = perf_counter() diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index b3762bf4ef..f44cbc267a 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1,4 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. +import asyncio +import threading from typing import Annotated, Any, Literal, get_args, get_origin from unittest.mock import Mock @@ -1346,6 +1348,45 @@ async def test_invoke_skip_parsing_awaits_async_functions() -> None: assert raw == 42 +async def test_invoke_sync_tool_does_not_block_event_loop() -> None: + release_tool = threading.Event() + tool_thread_ids: list[int] = [] + event_loop_thread_id = threading.get_ident() + + @tool + def wait_for_release() -> str: + tool_thread_ids.append(threading.get_ident()) + return "released" if release_tool.wait(timeout=0.2) else "timed out" + + async def release_soon() -> None: + await asyncio.sleep(0.01) + release_tool.set() + + tool_task = asyncio.create_task(wait_for_release.invoke(skip_parsing=True)) + release_task = asyncio.create_task(release_soon()) + + assert await asyncio.wait_for(tool_task, timeout=1) == "released" + await release_task + assert tool_thread_ids + assert tool_thread_ids[0] != event_loop_thread_id + + +async def test_invoke_sync_tool_can_stay_on_event_loop() -> None: + event_loop_thread_id = threading.get_ident() + tool_thread_ids: list[int] = [] + + @tool + def needs_event_loop() -> str: + tool_thread_ids.append(threading.get_ident()) + asyncio.get_running_loop() + return "ok" + + needs_event_loop._invoke_sync_on_event_loop = True + + assert await needs_event_loop.invoke(skip_parsing=True) == "ok" + assert tool_thread_ids == [event_loop_thread_id] + + async def test_invoke_skip_parsing_bypasses_configured_result_parser() -> None: """The tool's own result_parser is bypassed when skip_parsing=True is requested.""" parser_calls: list[Any] = [] From f970a699d8c60904c17af75e3188495e0a817a94 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 4 Jun 2026 10:37:59 +0200 Subject: [PATCH 51/61] Python: Fix compaction message-id collisions and tool-loop summary persistence (#6299) * Fix compaction message-id collisions and tool-loop summary persistence Fixes two bugs in the compaction strategies: - #5237: incremental group annotation assigned message ids by position within the re-annotated slice, so moving the re-annotation start back to a previous group start restarted ids at 0 and produced collisions (e.g. a user message reusing an assistant message's id), merging groups and causing tool-result compaction to wrongly exclude messages. group_messages/_ensure_message_ids now take an id_offset and guard against existing-id collisions; annotate_message_groups threads the slice start index through as the offset. - #4991: the function-invocation loop copied the message list each iteration, so summaries inserted by compaction landed in a throwaway copy and were lost across tool-loop iterations (only the persistent excluded flags survived). _prepare_messages_for_model_call now compacts the list in place when messages is a list, so inserted summaries persist. Adds regression tests (incremental id uniqueness, existing-id collision avoidance, idempotency, and tool-loop summary persistence including streaming and conversation-id modes). Also adds a summarization.py sample demonstrating SummarizationStrategy directly with a real client, and reworks advanced.py with tool-call groups and a real summarizer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Guard incremental message-id assignment against prefix-id collisions Addresses PR review on #5237: _ensure_message_ids only guarded against collisions within the re-annotated slice. A preexisting (e.g. user-supplied) id in the preserved prefix could still be reassigned in the suffix when the id was numerically out of position, merging groups across the re-annotation boundary again. group_messages/_ensure_message_ids now accept reserved_ids, and annotate_message_groups passes the preserved prefix's ids so auto-assigned suffix ids never collide across the full list. Adds a regression test reproducing the out-of-position prefix-id collision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_clients.py | 9 +- .../core/agent_framework/_compaction.py | 41 +++- .../packages/core/tests/core/test_clients.py | 194 ++++++++++++++++++ .../core/tests/core/test_compaction.py | 96 +++++++++ python/samples/02-agents/compaction/README.md | 6 +- .../samples/02-agents/compaction/advanced.py | 182 ++++++++++++---- .../02-agents/compaction/summarization.py | 159 ++++++++++++++ 7 files changed, 633 insertions(+), 54 deletions(-) create mode 100644 python/samples/02-agents/compaction/summarization.py diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index ddd765e654..fd004003b4 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -380,8 +380,15 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): return prepared_messages from ._compaction import apply_compaction + # Compact the caller's list in place when possible. A compaction operation has + # two halves: exclusion flags (mutated on shared Message objects) and inserted + # summary messages. Operating on the original list keeps both halves on the list + # the function-invocation tool loop reuses across iterations; otherwise inserted + # summaries would be lost on a throwaway copy while exclusions persisted, silently + # dropping older groups (issue #4991). + working_messages = messages if isinstance(messages, list) else prepared_messages return await apply_compaction( - prepared_messages, + working_messages, strategy=compaction_strategy, tokenizer=tokenizer, ) diff --git a/python/packages/core/agent_framework/_compaction.py b/python/packages/core/agent_framework/_compaction.py index 69e35726ea..3644bf4a9c 100644 --- a/python/packages/core/agent_framework/_compaction.py +++ b/python/packages/core/agent_framework/_compaction.py @@ -4,7 +4,7 @@ from __future__ import annotations import json import logging -from collections.abc import Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence from typing import ( TYPE_CHECKING, Any, @@ -92,10 +92,23 @@ def _is_reasoning_only_assistant(message: Message) -> bool: return all(content.type == "text_reasoning" for content in message.contents) -def _ensure_message_ids(messages: list[Message]) -> None: +def _ensure_message_ids( + messages: list[Message], *, id_offset: int = 0, reserved_ids: Iterable[str] | None = None +) -> None: + existing_ids: set[str] = set(reserved_ids) if reserved_ids is not None else set() + existing_ids.update(message.message_id for message in messages if message.message_id) for index, message in enumerate(messages): - if not message.message_id: - message.message_id = f"msg_{index}" + if message.message_id: + continue + candidate = f"msg_{id_offset + index}" + if candidate in existing_ids: + counter = id_offset + len(messages) + candidate = f"msg_{counter}" + while candidate in existing_ids: + counter += 1 + candidate = f"msg_{counter}" + message.message_id = candidate + existing_ids.add(candidate) def _group_id_for(message: Message, group_index: int) -> str: @@ -104,14 +117,27 @@ def _group_id_for(message: Message, group_index: int) -> str: return f"group_index_{group_index}" -def group_messages(messages: list[Message]) -> list[dict[str, Any]]: +def group_messages( + messages: list[Message], *, id_offset: int = 0, reserved_ids: Iterable[str] | None = None +) -> list[dict[str, Any]]: """Compute group spans and metadata for annotation. + Args: + messages: The messages (or a slice of them) to group. + + Keyword Args: + id_offset: Absolute starting index used when auto-assigning ``message_id`` + values, so incremental annotation of a list slice produces ids that + stay unique across the full list. + reserved_ids: Message ids that already exist outside ``messages`` (for + example in a preserved prefix). Auto-assigned ids are guaranteed not + to collide with these, preventing duplicate ids across the full list. + Returns: Ordered list of lightweight span dicts with keys: ``group_id``, ``kind``, ``start_index``, ``end_index``, ``has_reasoning``. """ - _ensure_message_ids(messages) + _ensure_message_ids(messages, id_offset=id_offset, reserved_ids=reserved_ids) spans: list[dict[str, Any]] = [] i = 0 group_index = 0 @@ -439,7 +465,8 @@ def annotate_message_groups( if previous_group_index is not None: group_index_offset = previous_group_index + 1 - spans = group_messages(messages[start_index:]) + reserved_ids = {message.message_id for message in messages[:start_index] if message.message_id} + spans = group_messages(messages[start_index:], id_offset=start_index, reserved_ids=reserved_ids) for span_index, span in enumerate(spans): group_id = str(span["group_id"]) kind = _coerce_group_kind(span["kind"]) diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index 7657993d56..ac110a4a17 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -11,10 +11,14 @@ from agent_framework import ( GROUP_TOKEN_COUNT_KEY, BaseChatClient, ChatResponse, + ChatResponseUpdate, + Content, Message, SlidingWindowStrategy, SupportsChatGetResponse, + ToolResultCompactionStrategy, TruncationStrategy, + tool, ) @@ -258,6 +262,196 @@ async def test_base_client_default_tokenizer_without_strategy_annotates_messages assert captured_token_counts == [[19, 19]] +def _tool_call_response(call_id: str, location: str) -> ChatResponse: + return ChatResponse( + messages=Message( + role="assistant", + contents=[ + Content.from_function_call( + call_id=call_id, + name="lookup_weather", + arguments=f'{{"location": "{location}"}}', + ) + ], + ), + response_id=f"resp_{call_id}", + ) + + +def _is_tool_result_summary(message: Message) -> bool: + text = message.text or "" + return message.role == "assistant" and text.startswith("[Tool results:") + + +async def test_function_loop_persists_inserted_summaries_across_iterations( + chat_client_base: SupportsChatGetResponse, +) -> None: + # Regression test for #4991: compaction inserts summary messages and excludes the + # originals. Across tool-loop iterations the exclusion flags persisted (shared Message + # objects) but the inserted summaries were dropped (they only lived on a throwaway copy), + # so older tool groups were silently lost with no summary representing them. + chat_client_base.function_invocation_configuration["enabled"] = True # type: ignore[attr-defined] + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.compaction_strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=1) # type: ignore[attr-defined] + + @tool(name="lookup_weather", approval_mode="never_require") + def lookup_weather(location: str) -> str: + return f"Weather in {location}: sunny" + + chat_client_base.run_responses = [ # type: ignore[attr-defined] + _tool_call_response("call_1", "London"), + _tool_call_response("call_2", "Paris"), + _tool_call_response("call_3", "Tokyo"), + ] + + captured_inputs: list[list[Message]] = [] + original = chat_client_base._get_non_streaming_response # type: ignore[attr-defined] + + async def _capture( + *, + messages: list[Message], + options: dict[str, Any], + **kwargs: Any, + ) -> ChatResponse: + captured_inputs.append(list(messages)) + return await original(messages=messages, options=options, **kwargs) + + chat_client_base._get_non_streaming_response = _capture # type: ignore[attr-defined,method-assign] + + await chat_client_base.get_response( + [Message(role="user", contents=["What is the weather in London?"])], + options={"tools": [lookup_weather]}, # type: ignore[typeddict-unknown-key] + ) + + # The final model call should represent every compacted tool group with a summary. + # Two older tool groups get collapsed (London, Paris) while the last (Tokyo) is kept. + final_input = captured_inputs[-1] + summaries = [message for message in final_input if _is_tool_result_summary(message)] + summary_text = " ".join(message.text or "" for message in summaries) + + assert len(summaries) == 2, [message.text for message in final_input] + assert "London" in summary_text + assert "Paris" in summary_text + + +def _tool_call_update(call_id: str, location: str) -> list[ChatResponseUpdate]: + return [ + ChatResponseUpdate( + contents=[ + Content.from_function_call( + call_id=call_id, + name="lookup_weather", + arguments=f'{{"location": "{location}"}}', + ) + ], + role="assistant", + finish_reason="stop", + response_id=f"resp_{call_id}", + ) + ] + + +async def test_function_loop_persists_inserted_summaries_across_iterations_streaming( + chat_client_base: SupportsChatGetResponse, +) -> None: + # Streaming counterpart of the #4991 regression test: the summary persistence fix in + # ``_prepare_messages_for_model_call`` must cover the streaming tool loop too. + chat_client_base.function_invocation_configuration["enabled"] = True # type: ignore[attr-defined] + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.compaction_strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=1) # type: ignore[attr-defined] + + @tool(name="lookup_weather", approval_mode="never_require") + def lookup_weather(location: str) -> str: + return f"Weather in {location}: sunny" + + chat_client_base.streaming_responses = [ # type: ignore[attr-defined] + _tool_call_update("call_1", "London"), + _tool_call_update("call_2", "Paris"), + _tool_call_update("call_3", "Tokyo"), + ] + + captured_inputs: list[list[Message]] = [] + original = chat_client_base._get_streaming_response # type: ignore[attr-defined] + + def _capture( + *, + messages: list[Message], + options: dict[str, Any], + **kwargs: Any, + ): + captured_inputs.append(list(messages)) + return original(messages=messages, options=options, **kwargs) + + chat_client_base._get_streaming_response = _capture # type: ignore[attr-defined,method-assign] + + stream = chat_client_base.get_response( + [Message(role="user", contents=["What is the weather in London?"])], + stream=True, + options={"tools": [lookup_weather]}, # type: ignore[typeddict-unknown-key] + ) + async for _ in stream: + pass + + final_input = captured_inputs[-1] + summaries = [message for message in final_input if _is_tool_result_summary(message)] + summary_text = " ".join(message.text or "" for message in summaries) + + assert len(summaries) == 2, [message.text for message in final_input] + assert "London" in summary_text + assert "Paris" in summary_text + + +async def test_function_loop_compaction_conversation_id_mode_does_not_resend_history( + chat_client_base: SupportsChatGetResponse, +) -> None: + # In conversation-id mode the server owns prior context, so the tool loop clears + # ``prepped_messages`` and only sends the latest message. Compaction must not fight that + # by re-inserting summaries or re-sending earlier turns. + chat_client_base.function_invocation_configuration["enabled"] = True # type: ignore[attr-defined] + chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined] + chat_client_base.compaction_strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=1) # type: ignore[attr-defined] + + @tool(name="lookup_weather", approval_mode="never_require") + def lookup_weather(location: str) -> str: + return f"Weather in {location}: sunny" + + def _conversation_tool_call(call_id: str, location: str) -> ChatResponse: + response = _tool_call_response(call_id, location) + response.conversation_id = "conv_1" + return response + + chat_client_base.run_responses = [ # type: ignore[attr-defined] + _conversation_tool_call("call_1", "London"), + _conversation_tool_call("call_2", "Paris"), + _conversation_tool_call("call_3", "Tokyo"), + ] + + captured_inputs: list[list[Message]] = [] + original = chat_client_base._get_non_streaming_response # type: ignore[attr-defined] + + async def _capture( + *, + messages: list[Message], + options: dict[str, Any], + **kwargs: Any, + ) -> ChatResponse: + captured_inputs.append(list(messages)) + return await original(messages=messages, options=options, **kwargs) + + chat_client_base._get_non_streaming_response = _capture # type: ignore[attr-defined,method-assign] + + await chat_client_base.get_response( + [Message(role="user", contents=["What is the weather in London?"])], + options={"tools": [lookup_weather]}, # type: ignore[typeddict-unknown-key] + ) + + # After the conversation id is established the loop only forwards the latest message, + # so subsequent model calls never receive the full history or summary messages. + for sent in captured_inputs[1:]: + assert len(sent) <= 1, [message.text for message in sent] + assert not any(_is_tool_result_summary(message) for message in sent) + + def test_base_client_as_agent_does_not_copy_client_compaction_defaults( chat_client_base: SupportsChatGetResponse, ) -> None: diff --git a/python/packages/core/tests/core/test_compaction.py b/python/packages/core/tests/core/test_compaction.py index 9e9cd0b466..99a90c6c0d 100644 --- a/python/packages/core/tests/core/test_compaction.py +++ b/python/packages/core/tests/core/test_compaction.py @@ -196,6 +196,64 @@ def test_append_compaction_message_annotates_new_message() -> None: assert isinstance(_group_id(messages[1]), str) +def test_incremental_annotation_assigns_unique_message_ids() -> None: + # Regression test for #5237: ``_ensure_message_ids`` assigned ``msg_{index}`` + # using the position within the slice handed to ``group_messages``. Successive + # incremental annotations restart the index at 0, so distinct messages collided + # on the same ``message_id``. + messages: list[Message] = [] + for turn in range(4): + messages.append(Message(role="user", contents=[f"user {turn}"])) + annotate_message_groups(messages) + messages.append(Message(role="assistant", contents=[f"assistant {turn}"])) + annotate_message_groups(messages) + + message_ids = [message.message_id for message in messages] + assert all(message_ids), "every message should receive an id" + assert len(set(message_ids)) == len(message_ids), f"duplicate message ids: {message_ids}" + + +def test_ensure_message_ids_avoids_existing_id_collisions() -> None: + # An auto-generated ``msg_{index}`` must not collide with an id already present + # on another message (user-supplied or assigned by an earlier annotation pass). + messages = [ + Message(role="user", contents=["zero"]), + Message(role="assistant", contents=["one"], message_id="msg_2"), + Message(role="user", contents=["two"]), + ] + annotate_message_groups(messages) + + message_ids = [message.message_id for message in messages] + assert message_ids[1] == "msg_2" + assert len(set(message_ids)) == len(message_ids), f"duplicate message ids: {message_ids}" + + +def test_incremental_annotation_avoids_prefix_id_collision() -> None: + # Regression for the PR review on #5237: when only a suffix is re-annotated, + # an auto-assigned ``msg_{index}`` in the suffix must not collide with a + # preexisting id carried by a message in the *preserved prefix* (a group + # before the one re-annotation pulls back to). Otherwise ``_group_id_for`` + # derives the same group id and merges groups across the boundary. + messages = [ + # Out-of-position, user-supplied id that matches the ``msg_{index}`` the + # suffix pass would assign to the appended message below. This message is + # two groups back, so it stays outside the re-annotated slice. + Message(role="user", contents=["zero"], message_id="msg_2"), + Message(role="user", contents=["one"]), + ] + annotate_message_groups(messages) + assert messages[0].message_id == "msg_2" + assert messages[1].message_id == "msg_1" + + messages.append(Message(role="user", contents=["two"])) + annotate_message_groups(messages, from_index=2) + + message_ids = [message.message_id for message in messages] + assert all(message_ids), "every message should receive an id" + assert len(set(message_ids)) == len(message_ids), f"duplicate message ids: {message_ids}" + assert messages[0].message_id == "msg_2" + + async def test_truncation_strategy_keeps_system_anchor() -> None: messages = [ Message(role="system", contents=["you are helpful"]), @@ -484,6 +542,44 @@ async def test_tool_result_compaction_collapses_old_groups_into_summary() -> Non assert any(m.role == "tool" for m in projected) +async def test_tool_result_compaction_is_idempotent_after_summary_insertion() -> None: + """Re-running compaction after a mid-list summary insertion must not duplicate it. + + Mirrors a subsequent tool-loop iteration (issue #4991): the inserted summary and the + excluded originals now persist on the same list, so a second annotate + compaction pass + over the same groups should be a no-op rather than collapsing the group again. + """ + messages = [ + Message(role="user", contents=["u"]), + _assistant_function_call("call-1"), + _tool_result("call-1", "r1"), + _assistant_function_call("call-2"), + _tool_result("call-2", "r2"), + Message(role="assistant", contents=["done"]), + ] + strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=1) + annotate_message_groups(messages) + assert await strategy(messages) is True + + summaries_after_first = [m for m in messages if (m.text or "").startswith("[Tool results:")] + assert len(summaries_after_first) == 1 + summary = summaries_after_first[0] + summary_group_ids = _group_unknown_value(summary, SUMMARY_OF_GROUP_IDS_KEY) + + # Second pass over the same (now partially compacted) list. + annotate_message_groups(messages) + changed = await strategy(messages) + + assert changed is False + summaries_after_second = [m for m in messages if (m.text or "").startswith("[Tool results:")] + assert len(summaries_after_second) == 1 + assert _group_unknown_value(summaries_after_second[0], SUMMARY_OF_GROUP_IDS_KEY) == summary_group_ids + + # The kept tool-call group stays atomic and included. + projected = included_messages(messages) + assert any(m.role == "tool" for m in projected) + + async def test_tool_result_compaction_zero_collapses_all() -> None: """With keep=0, all tool-call groups are collapsed into summaries.""" messages = [ diff --git a/python/samples/02-agents/compaction/README.md b/python/samples/02-agents/compaction/README.md index ed5c3dab12..42806bd6de 100644 --- a/python/samples/02-agents/compaction/README.md +++ b/python/samples/02-agents/compaction/README.md @@ -5,7 +5,8 @@ This folder demonstrates context compaction patterns introduced by ADR-0019. ## Files - `basics.py` — builds a local message list and applies each built-in strategy one at a time. -- `advanced.py` — composes multiple strategies with `TokenBudgetComposedStrategy`. +- `summarization.py` — runs `SummarizationStrategy` directly with a real summarizing chat client. +- `advanced.py` — composes multiple strategies with `TokenBudgetComposedStrategy`, including a real summarizer and tool-call groups. - `agent_client_overrides.py` — shows client defaults, agent-level overrides, and per-run compaction overrides. - `custom.py` — defines a custom strategy implementing the `CompactionStrategy` protocol. - `tiktoken_tokenizer.py` — shows a `TokenizerProtocol` implementation backed by `tiktoken`. @@ -15,7 +16,8 @@ Run samples with: ```bash uv run samples/02-agents/compaction/basics.py -uv run samples/02-agents/compaction/advanced.py +uv run samples/02-agents/compaction/summarization.py # requires OPENAI_API_KEY +uv run samples/02-agents/compaction/advanced.py # requires OPENAI_API_KEY uv run samples/02-agents/compaction/agent_client_overrides.py uv run samples/02-agents/compaction/custom.py uv run samples/02-agents/compaction/tiktoken_tokenizer.py diff --git a/python/samples/02-agents/compaction/advanced.py b/python/samples/02-agents/compaction/advanced.py index 12482f131a..4e14ceb5c2 100644 --- a/python/samples/02-agents/compaction/advanced.py +++ b/python/samples/02-agents/compaction/advanced.py @@ -1,11 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -from typing import Any +from typing import Any, cast from agent_framework import ( + GROUP_ANNOTATION_KEY, + GROUP_TOKEN_COUNT_KEY, + SUMMARY_OF_MESSAGE_IDS_KEY, CharacterEstimatorTokenizer, - ChatResponse, + Content, Message, SelectiveToolCallCompactionStrategy, SlidingWindowStrategy, @@ -15,36 +18,48 @@ from agent_framework import ( apply_compaction, included_token_count, ) +from agent_framework.openai import OpenAIChatClient +from dotenv import load_dotenv -"""This sample demonstrates composed in-run compaction with a token budget. +load_dotenv() + +"""This sample demonstrates composed in-run compaction under a token budget. + +A long, tool-using conversation is compacted with a single +``TokenBudgetComposedStrategy`` that runs three strategies in order until the +included-token count fits the budget: + +1. ``SelectiveToolCallCompactionStrategy`` — drop older tool-call groups + (assistant ``function_call`` + ``tool`` result messages) that are expensive + and rarely needed verbatim once acted upon. +2. ``SummarizationStrategy`` — use a *real* chat client to summarize the oldest + remaining turns into a single linked summary message. +3. ``SlidingWindowStrategy`` — as a final guard, keep only the most recent + groups if the budget is still exceeded. Key components: -- TokenBudgetComposedStrategy -- Sequential strategy composition -- Summarization with a SupportsChatGetResponse-compatible summarizer client +- TokenBudgetComposedStrategy with ordered, escalating strategies +- A real OpenAIChatClient used as the summarizer (not a stub) +- Tool-call groups in the history so tool-call compaction is meaningful +- Token accounting before/after via a TokenizerProtocol + +Run with: + uv run samples/02-agents/compaction/advanced.py # requires OPENAI_API_KEY """ -class BudgetSummaryClient: - async def get_response( - self, - messages: list[Message], - *, - stream: bool = False, - options: dict[str, Any] | None = None, - **kwargs: Any, - ) -> ChatResponse: - summary_text = f"Budget summary generated from {len(messages)} prompt messages." - return ChatResponse(messages=[Message(role="assistant", contents=[summary_text])]) - - def _build_long_history() -> list[Message]: - history = [Message(role="system", contents=["You are a migration copilot."])] - for i in range(1, 8): + """Build a long, tool-using migration conversation to create token pressure.""" + history: list[Message] = [ + Message(role="system", contents=["You are a migration copilot that plans and executes database migrations."]), + ] + + # A few verbose planning turns to build up token pressure. + for i in range(1, 5): history.append( Message( role="user", - contents=[f"Iteration {i}: capture migration requirements and edge cases."], + contents=[f"Iteration {i}: capture migration requirements, constraints, and edge cases in detail."], ) ) history.append( @@ -52,17 +67,62 @@ def _build_long_history() -> list[Message]: role="assistant", contents=[ ( - f"Iteration {i}: detailed plan with dependencies, rollback guidance, and testing details. " - "This sentence is intentionally long to create token pressure." + f"Iteration {i}: produced a detailed plan covering dependencies, rollback guidance, data " + "backfill, and a full testing matrix. This response is intentionally verbose to add pressure." ) ], ) ) + + # A tool-call group: the assistant inspects the schema via a tool. + history.append( + Message( + role="assistant", + contents=[Content.from_function_call(call_id="call_1", name="inspect_schema", arguments='{"db":"legacy"}')], + ) + ) + history.append( + Message( + role="tool", + contents=[Content.from_function_result(call_id="call_1", result="tables: users, orders, invoices, events")], + ) + ) + history.append(Message(role="assistant", contents=["Schema inspection found four core tables to migrate."])) + + # The most recent turn — this should survive compaction verbatim. + history.append(Message(role="user", contents=["What is the safest order to migrate these tables?"])) + history.append( + Message( + role="assistant", + contents=["Migrate reference tables (users) first, then orders, then invoices, and events last."], + ) + ) return history +def _annotation(message: Message) -> dict[str, Any] | None: + annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY) + return cast("dict[str, Any]", annotation) if isinstance(annotation, dict) else None + + +def _token_count(message: Message) -> int | None: + annotation = _annotation(message) + return annotation.get(GROUP_TOKEN_COUNT_KEY) if annotation else None + + +def _relation(message: Message) -> str: + """Describe how a projected message relates to the original messages.""" + annotation = _annotation(message) + if annotation is None: + return "" + summarizes = annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY) + if summarizes: + return f" <- summary of {summarizes}" + return "" + + async def main() -> None: - # 1. Build synthetic history representing long-running in-run growth. + # 1. Build synthetic history representing long-running, tool-using growth. messages = _build_long_history() # 2. Configure tokenizer and measure token count before compaction. @@ -70,22 +130,35 @@ async def main() -> None: annotate_message_groups(messages, tokenizer=tokenizer) budget_before = included_token_count(messages) - # 3. Configure composed strategy stack. + print("Before compaction message set:") + for msg in messages: + text_preview = msg.text[:80] if msg.text else "" + print(f"- [{msg.role}] {text_preview} ({msg.message_id}, {_token_count(msg)} tokens)") + print() + + # 3. Create a real summarizer client. SummarizationStrategy only requires a + # SupportsChatGetResponse-compatible client. + summarizer = OpenAIChatClient(model="gpt-4o-mini") + + # 4. Configure the composed strategy stack. Strategies run in order and the + # composed strategy stops as soon as the included-token budget is met. + # The budget is set high enough that the generated summary fits within it: + # a tighter budget would trip the composed fallback, which excludes the + # oldest group first (the summary) once the included set exceeds the + # budget. SlidingWindowStrategy remains as a recency safety net for longer + # histories; for this sample summarization alone reaches budget, so the + # window does not need to fire. composed = TokenBudgetComposedStrategy( - token_budget=200, + token_budget=400, tokenizer=tokenizer, strategies=[ SelectiveToolCallCompactionStrategy(keep_last_tool_call_groups=0), - SummarizationStrategy( - client=BudgetSummaryClient(), - target_count=3, - threshold=3, - ), + SummarizationStrategy(client=summarizer, target_count=3, threshold=2), SlidingWindowStrategy(keep_last_groups=4), ], ) - # 4. Apply compaction and inspect the budget result. + # 5. Apply compaction and inspect the budget result. projected = await apply_compaction(messages, strategy=composed, tokenizer=tokenizer) budget_after = included_token_count(messages) @@ -95,23 +168,44 @@ async def main() -> None: print("Projected roles:", [m.role for m in projected]) print("Projected messages with token counts:") for msg in projected: - group = msg.additional_properties.get("_group") - token_count = group.get("token_count") if isinstance(group, dict) else None text_preview = msg.text[:80] if msg.text else "" - print(f"- [{msg.role}] {text_preview} ({token_count} tokens)") + print(f"- [{msg.role}] {text_preview} ({msg.message_id}, {_token_count(msg)} tokens){_relation(msg)}") + + # 6. Surface the model-generated summary, if summarization fired. + for msg in messages: + annotation = _annotation(msg) + if annotation and annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY): + print("\nGenerated summary:") + print(f" {msg.text}") + print(f" summarizes: {annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY)}") if __name__ == "__main__": asyncio.run(main()) """ -Sample output: -Projected messages after compaction: 3 -Included token count before compaction: 793 -Included token count after compaction: 144 -Projected roles: ['system', 'user', 'assistant'] +Sample output (summary text and token counts vary because the summary is generated by the model): + +Before compaction message set: +- [system] You are a migration copilot that plans and executes database migrations. (msg_0, 46 tokens) +- [user] Iteration 1: capture migration requirements, constraints, and edge cases in deta (msg_1, 48 tokens) +- [assistant] Iteration 1: produced a detailed plan covering dependencies, rollback guidance, (msg_2, 73 tokens) +... +- [user] What is the safest order to migrate these tables? (msg_12, 40 tokens) +- [assistant] Migrate reference tables (users) first, then orders, then invoices, and events l (msg_13, 50 tokens) + +Projected messages after compaction: 5 +Included token count before compaction: 757 +Included token count after compaction: 274 +Projected roles: ['system', 'assistant', 'assistant', 'user', 'assistant'] Projected messages with token counts: -- [system] You are a migration copilot. (35 tokens) -- [user] Iteration 7: capture migration requirements and edge cases. (43 tokens) -- [assistant] Iteration 7: detailed plan with dependencies, rollback guidance, and testing det (66 tokens) +- [system] You are a migration copilot that plans and executes database migrations. (msg_0, 46 tokens) +- [assistant] Across four planning turns the user and assistant... (summary_14, 96 tokens) <- summary of [msg_1..8] +- [assistant] Schema inspection found four core tables to migrate. (msg_11, 42 tokens) +- [user] What is the safest order to migrate these tables? (msg_12, 40 tokens) +- [assistant] Migrate reference tables (users) first, then orders, then invoices, and events l (msg_13, 50 tokens) + +Generated summary: + Across four planning turns the user and assistant defined the migration requirements... + summarizes: ['msg_1', 'msg_2', 'msg_3', 'msg_4', 'msg_5', 'msg_6', 'msg_7', 'msg_8'] """ diff --git a/python/samples/02-agents/compaction/summarization.py b/python/samples/02-agents/compaction/summarization.py new file mode 100644 index 0000000000..b05d2374e1 --- /dev/null +++ b/python/samples/02-agents/compaction/summarization.py @@ -0,0 +1,159 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Any, cast + +from agent_framework import ( + GROUP_ANNOTATION_KEY, + SUMMARIZED_BY_SUMMARY_ID_KEY, + SUMMARY_OF_MESSAGE_IDS_KEY, + Message, + SummarizationStrategy, + apply_compaction, +) +from agent_framework.openai import OpenAIChatClient +from dotenv import load_dotenv + +load_dotenv() + +"""This sample demonstrates the SummarizationStrategy directly. + +Unlike SlidingWindow/Truncation strategies that simply drop older groups, +``SummarizationStrategy`` calls a real chat client to *summarize* the oldest +message groups, replaces them with a single linked summary message, and keeps +the most recent turns verbatim. This preserves long-range context (decisions, +goals, unresolved items) while bounding the prompt size. + +Key components: +- SummarizationStrategy with a real OpenAIChatClient summarizer +- ``apply_compaction`` to run the strategy over a message list +- Bidirectional summary trace metadata (summary -> originals, original -> summary) + +Run with: + uv run samples/02-agents/compaction/summarization.py # requires OPENAI_API_KEY +""" + + +def _annotation(message: Message) -> dict[str, Any] | None: + annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY) + return cast("dict[str, Any]", annotation) if isinstance(annotation, dict) else None + + +def _build_history() -> list[Message]: + """Build a multi-turn conversation long enough to trigger summarization.""" + return [ + Message(role="system", contents=["You are a project planning assistant."]), + Message(role="user", contents=["We are migrating a monolith to microservices. Where do we start?"]), + Message( + role="assistant", + contents=["Start by mapping bounded contexts and identifying the highest-churn modules to extract first."], + ), + Message(role="user", contents=["The billing module changes most often. What are the risks of extracting it?"]), + Message( + role="assistant", + contents=["Main risks: distributed transactions, invoices-table ownership, and latency on hot paths."], + ), + Message(role="user", contents=["How should we handle the shared invoices table?"]), + Message( + role="assistant", + contents=["Use the strangler-fig pattern: dual-write during transition, then make billing the owner."], + ), + Message(role="user", contents=["What is the most recent decision we made?"]), + Message(role="assistant", contents=["We decided to extract billing first using the strangler-fig pattern."]), + ] + + +def _print_messages(label: str, messages: list[Message]) -> None: + print(f"\n--- {label} ---") + print(f"Message count: {len(messages)}") + for index, message in enumerate(messages, start=1): + text = message.text or ", ".join(content.type for content in message.contents) + print(f"{index:02d}. [{message.role}] {text[:90]}") + + +async def main() -> None: + # 1. Create a real summarizing client. SummarizationStrategy only requires a + # SupportsChatGetResponse-compatible client, so any chat client works. + summarizer = OpenAIChatClient(model="gpt-4o-mini") + + # 2. Build a conversation and show it before compaction. + messages = _build_history() + _print_messages("Before compaction", messages) + + # 3. Configure the strategy. It triggers once the included non-system message + # count exceeds ``target_count + threshold`` (here 4 + 2 = 6), summarizing + # the oldest groups down toward ``target_count`` while keeping recent turns. + strategy = SummarizationStrategy( + client=summarizer, + target_count=4, + threshold=2, + ) + + # 4. Apply the strategy. The oldest groups are summarized into a single + # assistant message; the projected list is what the model would receive. + projected = await apply_compaction(messages, strategy=strategy) + _print_messages("After compaction (SummarizationStrategy)", projected) + + # 5. Inspect the generated summary and its bidirectional trace metadata. + print("\n--- Summary trace ---") + for message in messages: + annotation = _annotation(message) + if annotation is None: + continue + summarizes = annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY) + if summarizes: + print(f"Generated summary ({message.message_id}):") + print(f" {message.text}") + print(f" summarizes original ids: {summarizes}") + summarized_by: dict[str | None, Any] = {} + for message in messages: + annotation = _annotation(message) + if annotation is None: + continue + summary_id = annotation.get(SUMMARIZED_BY_SUMMARY_ID_KEY) + if summary_id: + summarized_by[message.message_id] = summary_id + if summarized_by: + print("Originals replaced by the summary:") + for original_id, summary_id in summarized_by.items(): + print(f" {original_id} -> {summary_id}") + + +if __name__ == "__main__": + asyncio.run(main()) + +""" +Sample output (summary text varies because it is generated by the model): + +--- Before compaction --- +Message count: 9 +01. [system] You are a project planning assistant. +02. [user] We are migrating a monolith to microservices. Where do we start? +03. [assistant] Start by mapping bounded contexts and identifying the highest-churn modules to ex +04. [user] The billing module changes most often. What are the risks of extracting it? +05. [assistant] Main risks: distributed transactions, data ownership of the invoices table, and lat +06. [user] How should we handle the shared invoices table? +07. [assistant] Use the strangler-fig pattern: dual-write during transition, then make billing the +08. [user] What is the most recent decision we made? +09. [assistant] We decided to extract billing first using the strangler-fig pattern. + +--- After compaction (SummarizationStrategy) --- +Message count: 6 +01. [system] You are a project planning assistant. +02. [assistant] The user is migrating a monolith to microservices and decided to extract the billin +03. [user] How should we handle the shared invoices table? +04. [assistant] Use the strangler-fig pattern: dual-write during transition, then make billing the +05. [user] What is the most recent decision we made? +06. [assistant] We decided to extract billing first using the strangler-fig pattern. + +--- Summary trace --- +Generated summary (summary_9): + The user is migrating a monolith to microservices and decided to extract the billing module first... + summarizes original ids: ['msg_1', 'msg_2', 'msg_3', 'msg_4', 'msg_5'] +Originals replaced by the summary: + msg_1 -> summary_9 + msg_2 -> summary_9 + msg_3 -> summary_9 + msg_4 -> summary_9 + msg_5 -> summary_9 +""" From fe08574a7cc068a562f320b9064e8905e7b5954b Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:42:35 -0700 Subject: [PATCH 52/61] Python: [BREAKING] Upgrade github-copilot-sdk to v1.0.0 (stable) (#6292) * Python: Upgrade github-copilot-sdk to v1.0.0 (stable) Upgrade agent-framework-github-copilot from github-copilot-sdk 1.0.0b2 to the stable 1.0.0 release, adapting to all breaking API changes. Source changes (_agent.py): - SubprocessConfig removed: use RuntimeConnection.for_stdio(path=...) + CopilotClient kwargs (connection, log_level, base_directory) - Import paths: copilot.generated.session_events -> copilot.session_events - Settings: copilot_home -> base_directory (env GITHUB_COPILOT_BASE_DIRECTORY) - Default deny handler: PermissionDecisionUserNotAvailable() (from copilot.generated.rpc) Test changes: - Updated imports and client-construction assertions (kwargs-based) - Permission handler tests use concrete decision types (PermissionDecisionApproveOnce, PermissionDecisionDeniedInteractivelyByUser) Sample changes: - Permission handlers use PermissionHandler.approve_all or sync approve_and_log pattern (v1.0.0 protocol v3 dispatch is incompatible with blocking input() in permission handlers) - Function approval sample uses asyncio.to_thread for interactive prompts - Simplified imports across all samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: scope permission handlers, widen type, add test - Shell sample: only approve kind='shell', deny others - URL sample: only approve kind='url', deny others - Use getattr() for kind-specific attributes to satisfy pyright - Widen PermissionHandlerType to accept async handlers (matches SDK) - Add test for _deny_all_permissions return value Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix validation script and strengthen test assertion - Update scripts/sample_validation/create_dynamic_workflow_executor.py to use copilot.session_events imports and PermissionHandler.approve_all - Assert isinstance(result, PermissionDecisionUserNotAvailable) instead of stringly-typed kind check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add integration tests for GitHubCopilotAgent Add 6 integration tests mirroring .NET coverage: - Basic non-streaming response - Streaming response - Function tool invocation - Session context (multi-turn) - Session resume by ID - Shell command execution Tests require COPILOT_GITHUB_TOKEN env var (skipped otherwise). Each test cleans up its Copilot session via delete_session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_github_copilot/_agent.py | 37 +-- python/packages/github_copilot/pyproject.toml | 2 +- .../tests/test_github_copilot_agent.py | 232 +++++++++++++++--- .../providers/github_copilot/README.md | 2 +- .../github_copilot/github_copilot_basic.py | 22 +- .../github_copilot_with_file_operations.py | 20 +- .../github_copilot_with_function_approval.py | 51 ++-- ...ub_copilot_with_instruction_directories.py | 17 +- .../github_copilot/github_copilot_with_mcp.py | 15 +- ...ithub_copilot_with_multiple_permissions.py | 32 +-- .../github_copilot_with_session.py | 24 +- .../github_copilot_with_shell.py | 29 ++- .../github_copilot/github_copilot_with_url.py | 29 ++- .../create_dynamic_workflow_executor.py | 6 +- python/uv.lock | 22 +- 15 files changed, 335 insertions(+), 205 deletions(-) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 0fc9c9dcf6..51b5791372 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -37,9 +37,10 @@ from agent_framework.exceptions import AgentException from agent_framework.observability import AgentTelemetryLayer try: - from copilot import CopilotClient, CopilotSession, SubprocessConfig - from copilot.generated.session_events import PermissionRequest, SessionEvent, SessionEventType + from copilot import CopilotClient, CopilotSession, RuntimeConnection + from copilot.generated.rpc import PermissionDecisionUserNotAvailable from copilot.session import MCPServerConfig, PermissionRequestResult, ProviderConfig, SystemMessageConfig + from copilot.session_events import PermissionRequest, SessionEvent, SessionEventType from copilot.tools import Tool as CopilotTool from copilot.tools import ToolInvocation, ToolResult except ImportError as _copilot_import_error: @@ -57,8 +58,10 @@ else: DEFAULT_TIMEOUT_SECONDS: float = 60.0 """Default timeout in seconds for Copilot requests.""" -PermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], PermissionRequestResult] -"""Type for permission request handlers.""" +PermissionHandlerType = Callable[ + [PermissionRequest, dict[str, str]], "PermissionRequestResult | Awaitable[PermissionRequestResult]" +] +"""Type for permission request handlers. Supports both sync and async callbacks.""" FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"] @@ -121,7 +124,7 @@ def _deny_all_permissions( _invocation: dict[str, str], ) -> PermissionRequestResult: """Default permission handler that denies all requests.""" - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() class GitHubCopilotSettings(TypedDict, total=False): @@ -140,9 +143,9 @@ class GitHubCopilotSettings(TypedDict, total=False): Can be set via environment variable GITHUB_COPILOT_TIMEOUT. log_level: CLI log level. Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL. - copilot_home: Directory where the CLI stores session state, configuration, + base_directory: Directory where the CLI stores session state, configuration, and other persistent data. Can be set via environment variable - GITHUB_COPILOT_COPILOT_HOME. Defaults to ~/.copilot when not set. + GITHUB_COPILOT_BASE_DIRECTORY. Defaults to ~/.copilot when not set. Only applicable when the SDK spawns the CLI process (ignored when connecting to an external server via a pre-configured client). """ @@ -151,7 +154,7 @@ class GitHubCopilotSettings(TypedDict, total=False): model: str | None timeout: float | None log_level: str | None - copilot_home: str | None + base_directory: str | None class GitHubCopilotOptions(TypedDict, total=False): @@ -314,7 +317,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): provider: ProviderConfig | None = opts.pop("provider", None) instruction_directories: list[str] | None = opts.pop("instruction_directories", None) on_function_approval: FunctionApprovalCallback | None = opts.pop("on_function_approval", None) - copilot_home = opts.pop("copilot_home", None) + base_directory = opts.pop("base_directory", None) self._settings = load_settings( GitHubCopilotSettings, @@ -323,7 +326,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): model=model, timeout=timeout, log_level=log_level, - copilot_home=copilot_home, + base_directory=base_directory, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) @@ -362,14 +365,16 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): if self._client is None: cli_path = self._settings.get("cli_path") or None log_level = self._settings.get("log_level") or None - copilot_home = self._settings.get("copilot_home") or None + base_directory = self._settings.get("base_directory") or None - subprocess_kwargs: dict[str, Any] = {"cli_path": cli_path} + client_kwargs: dict[str, Any] = {} + if cli_path: + client_kwargs["connection"] = RuntimeConnection.for_stdio(path=cli_path) if log_level: - subprocess_kwargs["log_level"] = log_level - if copilot_home: - subprocess_kwargs["copilot_home"] = copilot_home - self._client = CopilotClient(SubprocessConfig(**subprocess_kwargs)) + client_kwargs["log_level"] = log_level + if base_directory: + client_kwargs["base_directory"] = base_directory + self._client = CopilotClient(**client_kwargs) try: await self._client.start() diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 875e999804..2256c4255f 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.6.0,<2", - "github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'", + "github-copilot-sdk>=1.0.0,<2; python_version >= '3.11'", ] [tool.uv] diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index a0f0caef72..518d575820 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -2,6 +2,7 @@ # ruff: noqa: E402 +import os import unittest.mock from datetime import datetime, timezone from typing import Any @@ -20,9 +21,11 @@ from agent_framework import ( ContextProvider, HistoryProvider, Message, + tool, ) from agent_framework.exceptions import AgentException -from copilot.generated.session_events import ( +from copilot.session import PermissionHandler +from copilot.session_events import ( Data, SessionEvent, SessionEventType, @@ -308,27 +311,27 @@ class TestGitHubCopilotAgentLifecycle: ) await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.cli_path == "/custom/path" - assert call_args.log_level == "debug" + kwargs = MockClient.call_args.kwargs + assert kwargs["connection"].path == "/custom/path" + assert kwargs["log_level"] == "debug" - async def test_start_passes_copilot_home_to_subprocess_config(self) -> None: - """Test that copilot_home is passed through to SubprocessConfig.""" + async def test_start_passes_base_directory_to_client(self) -> None: + """Test that base_directory is passed through to CopilotClient.""" with patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient: mock_client = MagicMock() mock_client.start = AsyncMock() MockClient.return_value = mock_client agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"copilot_home": "/custom/copilot/home"} + default_options={"base_directory": "/custom/copilot/home"} ) await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.copilot_home == "/custom/copilot/home" + kwargs = MockClient.call_args.kwargs + assert kwargs["base_directory"] == "/custom/copilot/home" - async def test_start_copilot_home_not_set_when_unspecified(self) -> None: - """Test that copilot_home is not included in SubprocessConfig when not specified.""" + async def test_start_base_directory_not_set_when_unspecified(self) -> None: + """Test that base_directory is not included in client kwargs when not specified.""" with patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient: mock_client = MagicMock() mock_client.start = AsyncMock() @@ -337,14 +340,14 @@ class TestGitHubCopilotAgentLifecycle: agent = GitHubCopilotAgent() await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.copilot_home is None + kwargs = MockClient.call_args.kwargs + assert "base_directory" not in kwargs - async def test_start_copilot_home_from_env_variable(self) -> None: - """Test that copilot_home can be set via GITHUB_COPILOT_COPILOT_HOME env variable.""" + async def test_start_base_directory_from_env_variable(self) -> None: + """Test that base_directory can be set via GITHUB_COPILOT_BASE_DIRECTORY env variable.""" with ( patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient, - patch.dict("os.environ", {"GITHUB_COPILOT_COPILOT_HOME": "/env/copilot/home"}), + patch.dict("os.environ", {"GITHUB_COPILOT_BASE_DIRECTORY": "/env/copilot/home"}), ): mock_client = MagicMock() mock_client.start = AsyncMock() @@ -353,8 +356,8 @@ class TestGitHubCopilotAgentLifecycle: agent = GitHubCopilotAgent() await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.copilot_home == "/env/copilot/home" + kwargs = MockClient.call_args.kwargs + assert kwargs["base_directory"] == "/env/copilot/home" class TestGitHubCopilotAgentRun: @@ -1053,11 +1056,11 @@ class TestGitHubCopilotAgentSessionManagement: mock_session: MagicMock, ) -> None: """Test that resumed session config includes tools and permission handler.""" - from copilot.generated.session_events import PermissionRequest - from copilot.session import PermissionRequestResult + from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult + from copilot.session_events import PermissionRequest def my_handler(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - return PermissionRequestResult(kind="approved") + return PermissionDecisionApproveOnce() def my_tool(arg: str) -> str: """A test tool.""" @@ -1869,6 +1872,15 @@ class TestGitHubCopilotAgentErrorHandling: class TestGitHubCopilotAgentPermissions: """Test cases for permission handling.""" + def test_deny_all_permissions_returns_user_not_available(self) -> None: + """Test that the default deny handler returns PermissionDecisionUserNotAvailable.""" + from copilot.generated.rpc import PermissionDecisionUserNotAvailable + + from agent_framework_github_copilot._agent import _deny_all_permissions + + result = _deny_all_permissions(MagicMock(), {}) + assert isinstance(result, PermissionDecisionUserNotAvailable) + def test_no_permission_handler_when_not_provided(self) -> None: """Test that no handler is set when on_permission_request is not provided.""" agent = GitHubCopilotAgent() @@ -1876,13 +1888,14 @@ class TestGitHubCopilotAgentPermissions: def test_permission_handler_set_when_provided(self) -> None: """Test that a handler is set when on_permission_request is provided.""" - from copilot.generated.session_events import PermissionRequest - from copilot.session import PermissionRequestResult + from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser + from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult + from copilot.session_events import PermissionRequest def approve_shell(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: if request.kind == "shell": - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionDecisionApproveOnce() + return PermissionDecisionDeniedInteractivelyByUser() agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( default_options={"on_permission_request": approve_shell} @@ -1895,13 +1908,14 @@ class TestGitHubCopilotAgentPermissions: mock_session: MagicMock, ) -> None: """Test that session config includes permission handler when provided.""" - from copilot.generated.session_events import PermissionRequest - from copilot.session import PermissionRequestResult + from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser + from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult + from copilot.session_events import PermissionRequest def approve_shell_read(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: if request.kind in ("shell", "read"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionDecisionApproveOnce() + return PermissionDecisionDeniedInteractivelyByUser() agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( client=mock_client, @@ -2705,3 +2719,163 @@ class TestGitHubCopilotAgentContextProviders: assert call_kwargs.get("tools") is not None tool_names = [t.name for t in call_kwargs["tools"]] assert "load_skill" in tool_names + + +# --------------------------------------------------------------------------- +# Integration tests — require COPILOT_GITHUB_TOKEN env var +# --------------------------------------------------------------------------- + +skip_if_copilot_integration_tests_disabled = pytest.mark.skipif( + os.getenv("COPILOT_GITHUB_TOKEN", "") == "", + reason="No COPILOT_GITHUB_TOKEN provided; skipping integration tests.", +) + + +@tool(approval_mode="never_require") +def get_weather(location: str) -> str: + """Get the weather for a given location.""" + return f"The weather in {location} is sunny with a high of 25C." + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_simple_prompt_returns_response() -> None: + """Integration test: basic non-streaming response.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + response = await agent.run("What is 2 + 2? Answer with just the number.", session=session) + + assert response is not None + assert len(response.messages) > 0 + assert "4" in response.text + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_streaming_returns_updates() -> None: + """Integration test: streaming response yields updates.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + updates = [] + async for chunk in agent.run("Count from 1 to 5.", stream=True, session=session): + updates.append(chunk) + + assert len(updates) > 0 + full_text = "".join(u.text for u in updates if u.text) + assert len(full_text) > 0 + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_function_tool_invokes_tool() -> None: + """Integration test: function tool is invoked by the agent.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent. Use the get_weather tool to answer weather questions.", + tools=[get_weather], + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + response = await agent.run("What's the weather like in Seattle?", session=session) + + assert response is not None + assert len(response.messages) > 0 + assert any(word in response.text.lower() for word in ["sunny", "25", "weather", "seattle"]) + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_session_maintains_context() -> None: + """Integration test: session maintains conversation context across turns.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + + response1 = await agent.run("My name is Alice.", session=session) + assert response1 is not None + + response2 = await agent.run("What is my name?", session=session) + + assert response2 is not None + assert "alice" in response2.text.lower() + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_session_resume_continues_conversation() -> None: + """Integration test: session can be resumed by ID.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session1 = agent.create_session() + await agent.run("Remember this number: 42.", session=session1) + + session_id = session1.service_session_id + assert session_id is not None + + session2 = AgentSession() + session2.service_session_id = session_id + + response = await agent.run("What number did I ask you to remember?", session=session2) + + assert response is not None + assert "42" in response.text + + if agent._client: + await agent._client.delete_session(session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_shell_permissions_executes_command() -> None: + """Integration test: shell commands can be executed with permission handler.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant that can execute shell commands.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + response = await agent.run("Run a shell command to print 'hello world'", session=session) + + assert response is not None + assert "hello" in response.text.lower() + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index c3132ed1e9..d58698e7da 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -23,7 +23,7 @@ The following environment variables can be configured: | `GITHUB_COPILOT_MODEL` | Model to use (e.g., "gpt-5", "claude-sonnet-4") | Server default | | `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` | | `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` | -| `GITHUB_COPILOT_COPILOT_HOME` | Directory for CLI session state and config | `~/.copilot` | +| `GITHUB_COPILOT_BASE_DIRECTORY` | Directory for CLI session state and config | `~/.copilot` | ## Observability diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py b/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py index 93e37d89bb..3cbfe01795 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py @@ -19,8 +19,7 @@ from typing import Annotated from agent_framework import tool from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler from dotenv import load_dotenv from pydantic import Field @@ -28,19 +27,6 @@ from pydantic import Field load_dotenv() -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; # see samples/02-agents/tools/function_tool_with_approval.py # and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. @@ -60,7 +46,7 @@ async def non_streaming_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -77,7 +63,7 @@ async def streaming_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -97,7 +83,7 @@ async def runtime_options_example() -> None: agent = GitHubCopilotAgent( instructions="Always respond in exactly 3 words.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py index 1a82d9867d..67336259d0 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py @@ -4,8 +4,7 @@ GitHub Copilot Agent with File Operation Permissions This sample demonstrates how to enable file read and write operations with GitHubCopilotAgent. -By providing a permission handler that approves "read" and/or "write" requests, the agent can -read from and write to files on the filesystem. +By providing a permission handler, the agent can read from and write to files on the filesystem. SECURITY NOTE: Only enable file permissions when you trust the agent's actions. - "read" allows the agent to read any accessible file @@ -15,21 +14,18 @@ SECURITY NOTE: Only enable file permissions when you trust the agent's actions. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: +async def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: """Permission handler that prompts the user for approval.""" print(f"\n[Permission Request: {request.kind}]") - - if request.path is not None: - print(f" Path: {request.path}") - - response = input("Approve? (y/n): ").strip().lower() + response = (await asyncio.to_thread(input, "Approve? (y/n): ")).strip().lower() if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionHandler.approve_all(request, context) + return PermissionDecisionDeniedInteractivelyByUser() async def main() -> None: diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py index 17520fdf91..3348dbf1a5 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py @@ -32,6 +32,7 @@ from typing import Annotated from agent_framework import Content, tool from agent_framework.github import GitHubCopilotAgent +from copilot.session import PermissionHandler from dotenv import load_dotenv load_dotenv() @@ -48,37 +49,42 @@ def get_weather_detail(location: Annotated[str, "The city and state, e.g. San Fr ) -def prompt_for_approval(call: Content) -> bool: - """Synchronous approval prompt. +async def prompt_for_approval(call: Content) -> bool: + """Async approval callback that prompts the user interactively. The callback receives a ``FunctionCallContent`` so the operator can review the tool name and arguments before deciding. Returning ``True`` allows the call; returning ``False`` denies it and a tool-error is returned to the model. + + Uses ``asyncio.to_thread`` so the event loop is not blocked by ``input()``. """ - print(f"\n[Function Approval Request]\n Tool: {call.name}\n Arguments: {call.arguments}") - response = input("Approve this tool call? (y/n): ").strip().lower() + print(f"\n [Function Approval Request]\n Tool: {call.name}\n Arguments: {call.arguments}") + response = (await asyncio.to_thread(input, " Approve this tool call? (y/n): ")).strip().lower() return response in ("y", "yes") -async def prompt_for_approval_async(call: Content) -> bool: - """Async approval prompt. +def auto_approve(call: Content) -> bool: + """Synchronous approval callback that always approves. - Use an async callback when approval requires I/O (e.g. an HTTP call to a - review service or queueing the request to a UI). ``input()`` is wrapped - with ``asyncio.to_thread`` so the event loop is not blocked. + Use a sync callback for simple, non-blocking decisions that don't require + I/O (e.g. checking an allow-list of tool names). """ - print(f"\n[Function Approval Request - async]\n Tool: {call.name}\n Arguments: {call.arguments}") - response = await asyncio.to_thread(input, "Approve this tool call? (y/n): ") - return response.strip().lower() in ("y", "yes") + print(f"\n [Function Approval Request]\n Tool: {call.name}\n Arguments: {call.arguments}") + print(" -> Auto-approved") + return True -async def run_with_sync_callback() -> None: - print("\n=== GitHub Copilot Agent: synchronous approval callback ===") +async def run_with_interactive_callback() -> None: + """Demonstrates an interactive approval prompt before tool execution.""" + print("\n=== GitHub Copilot Agent: interactive approval callback ===") agent = GitHubCopilotAgent( instructions="You are a helpful weather assistant.", tools=[get_weather_detail], - default_options={"on_function_approval": prompt_for_approval}, + default_options={ + "on_function_approval": prompt_for_approval, + "on_permission_request": PermissionHandler.approve_all, + }, ) async with agent: query = "Give me the detailed weather for Seattle." @@ -87,12 +93,16 @@ async def run_with_sync_callback() -> None: print(f"Agent: {result}") -async def run_with_async_callback() -> None: - print("\n=== GitHub Copilot Agent: asynchronous approval callback ===") +async def run_with_auto_approve_callback() -> None: + """Demonstrates a synchronous callback that always approves.""" + print("\n=== GitHub Copilot Agent: synchronous auto-approve callback ===") agent = GitHubCopilotAgent( instructions="You are a helpful weather assistant.", tools=[get_weather_detail], - default_options={"on_function_approval": prompt_for_approval_async}, + default_options={ + "on_function_approval": auto_approve, + "on_permission_request": PermissionHandler.approve_all, + }, ) async with agent: query = "Give me the detailed weather for Tokyo." @@ -112,6 +122,7 @@ async def run_without_callback() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather assistant.", tools=[get_weather_detail], + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: query = "Give me the detailed weather for Paris." @@ -122,8 +133,8 @@ async def run_without_callback() -> None: async def main() -> None: print("=== GitHub Copilot Agent: Function approval enforcement ===") - await run_with_sync_callback() - await run_with_async_callback() + await run_with_interactive_callback() + await run_with_auto_approve_callback() await run_without_callback() diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py index e30f114f7d..4c7ae2c1a4 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py @@ -22,24 +22,13 @@ import asyncio from pathlib import Path from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - async def default_instructions_example() -> None: """Example of pointing the agent at project-specific instruction directories.""" print("=== Instruction Directories (Default) ===\n") @@ -58,7 +47,7 @@ async def default_instructions_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful coding assistant.", default_options={ - "on_permission_request": prompt_permission, + "on_permission_request": PermissionHandler.approve_all, "instruction_directories": instruction_dirs, }, ) @@ -79,7 +68,7 @@ async def runtime_override_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant.", default_options={ - "on_permission_request": prompt_permission, + "on_permission_request": PermissionHandler.approve_all, "instruction_directories": ["/team/shared/instructions"], }, ) diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py index 71bd67efb4..60e4704c07 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py @@ -15,24 +15,13 @@ of MCP-related actions. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import MCPServerConfig, PermissionRequestResult +from copilot.session import MCPServerConfig, PermissionHandler from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - async def main() -> None: print("=== GitHub Copilot Agent with MCP Servers ===\n") @@ -56,7 +45,7 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant with access to the local filesystem and Microsoft Learn.", default_options={ - "on_permission_request": prompt_permission, + "on_permission_request": PermissionHandler.approve_all, "mcp_servers": mcp_servers, }, ) diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py index 916061f939..5da43b3274 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py @@ -3,9 +3,8 @@ """ GitHub Copilot Agent with Multiple Permissions -This sample demonstrates how to enable multiple permission types with GitHubCopilotAgent. -By combining different permission kinds in the handler, the agent can perform complex tasks -that require multiple capabilities. +This sample demonstrates how multiple permission types are requested when GitHubCopilotAgent +performs complex tasks that require different capabilities. Available permission kinds: - "shell": Execute shell commands @@ -21,23 +20,14 @@ More permissions mean more potential for unintended actions. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - if request.path is not None: - print(f" Path: {request.path}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") +def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that auto-approves and logs each permission kind.""" + print(f" [Permission: {request.kind}]", flush=True) + return PermissionHandler.approve_all(request, context) async def main() -> None: @@ -45,14 +35,14 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful development assistant that can read, write files and run commands.", - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": approve_and_log}, ) async with agent: query = "List the first 3 Python files, then read the first one and create a summary in summary.txt" - print(f"User: {query}") + print(f"User: {query}\n") result = await agent.run(query) - print(f"Agent: {result}\n") + print(f"\nAgent: {result}\n") if __name__ == "__main__": diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py index 801edf52f3..70a0f3c826 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py @@ -14,24 +14,10 @@ from typing import Annotated from agent_framework import tool from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler from pydantic import Field -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; # see samples/02-agents/tools/function_tool_with_approval.py # and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. @@ -51,7 +37,7 @@ async def example_with_automatic_session_creation() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -76,7 +62,7 @@ async def example_with_session_persistence() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -113,7 +99,7 @@ async def example_with_existing_session_id() -> None: agent1 = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent1: @@ -135,7 +121,7 @@ async def example_with_existing_session_id() -> None: agent2 = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent2: diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py index 729aad6863..66ead3bf99 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py @@ -14,21 +14,20 @@ Shell commands have full access to your system within the permissions of the run import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionUserNotAvailable +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") +def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that approves only shell commands and logs them.""" + if request.kind == "shell": + print(f"\n [Permission: {request.kind}]", flush=True) + command = getattr(request, "full_command_text", None) + if command is not None: + print(f" Command: {command}", flush=True) + return PermissionHandler.approve_all(request, context) + return PermissionDecisionUserNotAvailable() async def main() -> None: @@ -36,14 +35,14 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant that can execute shell commands.", - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": approve_and_log}, ) async with agent: query = "List the first 3 Python files in the current directory" print(f"User: {query}") result = await agent.run(query) - print(f"Agent: {result}\n") + print(f"\nAgent: {result}\n") if __name__ == "__main__": diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py index 2f14648bae..61fa90cd5d 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py @@ -14,21 +14,20 @@ URL fetching allows the agent to access any URL accessible from your network. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionUserNotAvailable +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.url is not None: - print(f" URL: {request.url}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") +def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that approves only URL requests and logs them.""" + if request.kind == "url": + print(f"\n [Permission: {request.kind}]", flush=True) + url = getattr(request, "url", None) + if url is not None: + print(f" URL: {url}", flush=True) + return PermissionHandler.approve_all(request, context) + return PermissionDecisionUserNotAvailable() async def main() -> None: @@ -36,14 +35,14 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant that can fetch and summarize web content.", - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": approve_and_log}, ) async with agent: query = "Fetch https://learn.microsoft.com/agent-framework/tutorials/quick-start and summarize its contents" print(f"User: {query}") result = await agent.run(query) - print(f"Agent: {result}\n") + print(f"\nAgent: {result}\n") if __name__ == "__main__": diff --git a/python/scripts/sample_validation/create_dynamic_workflow_executor.py b/python/scripts/sample_validation/create_dynamic_workflow_executor.py index 01af408097..6ebe25a8d4 100644 --- a/python/scripts/sample_validation/create_dynamic_workflow_executor.py +++ b/python/scripts/sample_validation/create_dynamic_workflow_executor.py @@ -14,8 +14,8 @@ from agent_framework import ( handler, ) from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest from pydantic import BaseModel from sample_validation.const import WORKER_COMPLETED from sample_validation.discovery import DiscoveryResult @@ -103,7 +103,7 @@ def prompt_permission( logger.debug( f"[Permission Request: {request.kind}] ({context})Automatically approved for sample validation." ) - return PermissionRequestResult(kind="approved") + return PermissionHandler.approve_all(request, context) class CustomAgentExecutor(Executor): diff --git a/python/uv.lock b/python/uv.lock index 676413c1cc..3203a0016c 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -609,7 +609,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0,<2" }, ] [[package]] @@ -2608,19 +2608,19 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "1.0.0b2" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", 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 = "(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')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fe/2cb98d4b9f57f8062ea72775bde72aed1958305016753f7296398e0ceb45/github_copilot_sdk-1.0.0b2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1b5941d8b6e3d94d42a5bec6607a26f562e6535d5c981089d23d3d224b94601c", size = 67061619, upload-time = "2026-05-06T20:02:08.636Z" }, - { url = "https://files.pythonhosted.org/packages/57/45/76567821b2d36f81e6bca78c98d265e2762733f765fa51d69602b7f81867/github_copilot_sdk-1.0.0b2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b8f6a087a0cf02bb0d33976e8f8c009578d84d701a0b28d52051304791ac70", size = 63790955, upload-time = "2026-05-06T20:02:12.354Z" }, - { url = "https://files.pythonhosted.org/packages/15/67/684b0da0b1207a2bdf025c22ee075d34a1736d61a4973651035d4fd4d8dc/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f403638c11b82bddb81c94675fc4e8014a1bb2e86a679a39fa167dcc3ad5416a", size = 69538664, upload-time = "2026-05-06T20:02:16.363Z" }, - { url = "https://files.pythonhosted.org/packages/57/1d/80d88ecf83683535d1a16d4817f1683db3b125f52a924ebdfe9764f5e4c3/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:433d16bb31171fee8d3a5b70259c527f63b297e83a8f8761ae1f16f14d641f32", size = 68163648, upload-time = "2026-05-06T20:02:21.139Z" }, - { url = "https://files.pythonhosted.org/packages/32/d3/b72aa2fbb3194b50b53e8cb1484f5606a1f8eedcdb0bfb5747da52079553/github_copilot_sdk-1.0.0b2-py3-none-win_amd64.whl", hash = "sha256:a6e9782dae4c3c2ab3527b45bb5de0f61998104c10e9ff64698280eaf37ab5dd", size = 62649144, upload-time = "2026-05-06T20:02:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e2/be95b8ea0ac11d1ca474e28a59284f4e395c2710734eadfb657f5de8ace2/github_copilot_sdk-1.0.0b2-py3-none-win_arm64.whl", hash = "sha256:2e97d0ce4bad67dc5929091cb429e7bbae7d4643e4908a6af256a41439000740", size = 60374365, upload-time = "2026-05-06T20:02:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d2/e74fdf476d0dde5c3802b3ba360f1b1e250e55d6d39c03f578c28ac9864e/github_copilot_sdk-1.0.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:3cae245fb825e26a74395b74f10d9fd90bc464aa77005848ae0809c9a46c96df", size = 94986104, upload-time = "2026-06-02T14:59:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/b6/81/e4d9dd01b0a563e488427aa879166287c88de3fccf7b8a95e22a6c652fc3/github_copilot_sdk-1.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b344a00a877c86ef717244e42bd01acb3694b7377644661c82fc278ccc990e37", size = 91435649, upload-time = "2026-06-02T15:00:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/e94b8f5a299850e600ffe1fe14bd21b48e01172b9e8b490a0ebd0d0c8d27/github_copilot_sdk-1.0.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:dd3a6b7637a3b12476854aeb599c6bed030f6a166fbd942d872c9a11a695517c", size = 97301959, upload-time = "2026-06-02T15:00:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/dfba743a11d9745b0664ec5e1ae6e05055a5cbef0ccc6d593222319184eb/github_copilot_sdk-1.0.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:bbd2c64fe37016c74620a02d778eaacbd526b4c3b668a3cdff019f831c752eee", size = 96071193, upload-time = "2026-06-02T15:00:22.634Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9b/d953dcbb898f4d44efc0cb592e9a703ad43a4b673aafb5bbd763962ab2fd/github_copilot_sdk-1.0.0-py3-none-win_amd64.whl", hash = "sha256:2d46fff634eece978532b1329c0d9e1d784b08ad521e71e6af06c5c28ae2e7c5", size = 90374124, upload-time = "2026-06-02T15:00:31.376Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f7/0f9943b1439e3dcc52854140676b65d8f63405c471a77c58291a8f4bfb52/github_copilot_sdk-1.0.0-py3-none-win_arm64.whl", hash = "sha256:ebfb80395caa834df8ab16ab4aab3e5d8db883ed3b024f723c394b1514e47221", size = 87874846, upload-time = "2026-06-02T15:00:38.737Z" }, ] [[package]] @@ -2708,6 +2708,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, @@ -2715,6 +2716,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, @@ -2723,6 +2725,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, @@ -2731,6 +2734,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -2739,6 +2743,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, @@ -2747,6 +2752,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, From 4268080c20f382655d9c25c8209839fa3f1d26c9 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:59:04 +0900 Subject: [PATCH 53/61] Python: Fix spurious Magentic custom manager warning (#6261) * Fix magentic manager warning * Use typing_extensions.Sentinel for _MISSING sentinel value Replace the bare object() sentinel with typing_extensions.Sentinel per PEP 661 (now final). Sentinel provides a proper name and repr ('<_MISSING>') and is the idiomatic approach going forward. Refs #4306 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct Sentinel type annotation for max_stall_count param (#6261) Use int | Sentinel for max_stall_count parameter type annotation instead of int with cast(Any, _MISSING) to properly express that the parameter can hold either an int or the _MISSING sentinel value. This fixes the pyright reportUnnecessaryComparison errors caused by the types int and Sentinel having no overlap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename _MISSING sentinel to UNSET in orchestrations The sentinel is user-visible as a default in public init signatures, so use UNSET (no leading underscore) instead of the private _MISSING name. Drop the now-unnecessary reportPrivateUsage ignores on the UNSET imports. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_bedrock/_chat_client.py | 13 ++---- .../tests/test_bedrock_structured_output.py | 1 + .../foundry_hosting/tests/test_responses.py | 8 +--- .../_concurrent.py | 4 +- .../_group_chat.py | 4 +- .../_handoff.py | 4 +- .../_magentic.py | 19 +++++---- .../_participant_output_config.py | 7 ++-- .../_sequential.py | 4 +- .../orchestrations/tests/test_magentic.py | 42 +++++++++++++++++++ 10 files changed, 70 insertions(+), 36 deletions(-) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index cb8545f9a3..2fd7887721 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -795,10 +795,7 @@ class BedrockChatClient( schema = copy.deepcopy(schema_src) else: if not isinstance(response_format, type) or not issubclass(response_format, BaseModel): - raise TypeError( - "response_format must be None, a dict JSON schema, " - "or a Pydantic BaseModel subclass." - ) + raise TypeError("response_format must be None, a dict JSON schema, or a Pydantic BaseModel subclass.") # response_format is a Pydantic model class schema = response_format.model_json_schema() name = response_format.__name__ @@ -817,9 +814,7 @@ class BedrockChatClient( return { "textFormat": { "type": "json_schema", - "structure": { - "jsonSchema": json_schema - }, + "structure": {"jsonSchema": json_schema}, } } @@ -840,9 +835,7 @@ class BedrockChatClient( if node_id in visited: return visited.add(node_id) - if node.get("type") == "object" or ( - "properties" in node and "type" not in node - ): + if node.get("type") == "object" or ("properties" in node and "type" not in node): existing = node.get("additionalProperties") if existing is None or existing is True: node["additionalProperties"] = False diff --git a/python/packages/bedrock/tests/test_bedrock_structured_output.py b/python/packages/bedrock/tests/test_bedrock_structured_output.py index 8df04b5e75..7b39f67d69 100644 --- a/python/packages/bedrock/tests/test_bedrock_structured_output.py +++ b/python/packages/bedrock/tests/test_bedrock_structured_output.py @@ -238,6 +238,7 @@ async def test_chat_response_value_populated_streaming() -> None: async def test_unsupported_model_validation_exception() -> None: """When a model doesn't support outputConfig, a clear error should be raised.""" + class _FailingStubBedrockRuntime: def converse(self, **kwargs: Any) -> dict[str, Any]: # Simulate botocore ClientError for ValidationException diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index 0bfff345a7..9c65a9ea42 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -2118,15 +2118,11 @@ class TestMultiTurnMixedContent: assert resp2.json()["status"] == "completed" second_call_messages = agent.run.call_args_list[1].kwargs["messages"] - mcp_call_contents = [ - c for m in second_call_messages for c in m.contents if c.type == "mcp_server_tool_call" - ] + mcp_call_contents = [c for m in second_call_messages for c in m.contents if c.type == "mcp_server_tool_call"] mcp_result_contents = [ c for m in second_call_messages for c in m.contents if c.type == "mcp_server_tool_result" ] - function_result_contents = [ - c for m in second_call_messages for c in m.contents if c.type == "function_result" - ] + function_result_contents = [c for m in second_call_messages for c in m.contents if c.type == "function_result"] assert len(mcp_call_contents) >= 1 assert len(mcp_result_contents) >= 1 diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py index 6fc29c79b3..9db8878ef4 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py @@ -19,7 +19,7 @@ from typing_extensions import Never from ._orchestration_request_info import AgentApprovalExecutor from ._participant_output_config import ( - _MISSING, # pyright: ignore[reportPrivateUsage] + UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] @@ -213,7 +213,7 @@ class ConcurrentBuilder: *, participants: Sequence[SupportsAgentRun | Executor], checkpoint_storage: CheckpointStorage | None = None, - output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, UNSET), intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the ConcurrentBuilder. diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py index 3778e5d110..728f3e388c 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py @@ -52,7 +52,7 @@ from ._base_group_chat_orchestrator import ( from ._orchestration_request_info import AgentApprovalExecutor from ._orchestrator_helpers import clean_conversation_for_handoff from ._participant_output_config import ( - _MISSING, # pyright: ignore[reportPrivateUsage] + UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] @@ -626,7 +626,7 @@ class GroupChatBuilder: termination_condition: TerminationCondition | None = None, max_rounds: int | None = None, checkpoint_storage: CheckpointStorage | None = None, - output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, UNSET), intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the GroupChatBuilder. diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index 70f28e7f04..65da3b8709 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -54,7 +54,7 @@ from agent_framework._workflows._workflow_context import WorkflowContext from ._base_group_chat_orchestrator import TerminationCondition from ._orchestrator_helpers import clean_conversation_for_handoff from ._participant_output_config import ( - _MISSING, # pyright: ignore[reportPrivateUsage] + UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] @@ -597,7 +597,7 @@ class HandoffBuilder: description: str | None = None, checkpoint_storage: CheckpointStorage | None = None, termination_condition: TerminationCondition | None = None, - output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, UNSET), intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: r"""Initialize a HandoffBuilder for creating conversational handoff workflows. diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py b/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py index 53ca4052ff..f8cbf88fd7 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py @@ -28,7 +28,7 @@ from agent_framework._workflows._request_info_mixin import response_handler from agent_framework._workflows._workflow import Workflow from agent_framework._workflows._workflow_builder import WorkflowBuilder from agent_framework._workflows._workflow_context import WorkflowContext -from typing_extensions import Never +from typing_extensions import Never, Sentinel from ._base_group_chat_orchestrator import ( BaseGroupChatOrchestrator, @@ -39,7 +39,7 @@ from ._base_group_chat_orchestrator import ( ParticipantRegistry, ) from ._participant_output_config import ( - _MISSING, # pyright: ignore[reportPrivateUsage] + UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] @@ -1411,13 +1411,13 @@ class MagenticBuilder: task_ledger_plan_update_prompt: str | None = None, progress_ledger_prompt: str | None = None, final_answer_prompt: str | None = None, - max_stall_count: int = 3, + max_stall_count: int | Sentinel = UNSET, max_reset_count: int | None = None, max_round_count: int | None = None, # Existing params enable_plan_review: bool = False, checkpoint_storage: CheckpointStorage | None = None, - output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, UNSET), intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the Magentic workflow builder. @@ -1621,7 +1621,7 @@ class MagenticBuilder: progress_ledger_prompt: str | None = None, final_answer_prompt: str | None = None, # Limits - max_stall_count: int = 3, + max_stall_count: int | Sentinel = UNSET, max_reset_count: int | None = None, max_round_count: int | None = None, ) -> None: @@ -1656,8 +1656,10 @@ class MagenticBuilder: "Exactly one of manager, manager_agent, manager_factory, or manager_agent_factory must be provided." ) + resolved_max_stall_count: int = 3 if max_stall_count is UNSET else cast(int, max_stall_count) + def _log_warning_if_constructor_args_provided() -> None: - if any( + if max_stall_count is not UNSET or any( arg is not None for arg in [ task_ledger, @@ -1668,7 +1670,6 @@ class MagenticBuilder: task_ledger_plan_update_prompt, progress_ledger_prompt, final_answer_prompt, - max_stall_count, max_reset_count, max_round_count, ] @@ -1689,7 +1690,7 @@ class MagenticBuilder: task_ledger_plan_update_prompt=task_ledger_plan_update_prompt, progress_ledger_prompt=progress_ledger_prompt, final_answer_prompt=final_answer_prompt, - max_stall_count=max_stall_count, + max_stall_count=resolved_max_stall_count, max_reset_count=max_reset_count, max_round_count=max_round_count, ) @@ -1707,7 +1708,7 @@ class MagenticBuilder: "task_ledger_plan_update_prompt": task_ledger_plan_update_prompt, "progress_ledger_prompt": progress_ledger_prompt, "final_answer_prompt": final_answer_prompt, - "max_stall_count": max_stall_count, + "max_stall_count": resolved_max_stall_count, "max_reset_count": max_reset_count, "max_round_count": max_round_count, } diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py b/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py index 49138b7d0d..dfb22fc85a 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py @@ -8,8 +8,9 @@ from typing import Any, Literal from agent_framework import SupportsAgentRun from agent_framework._workflows._agent_utils import resolve_agent_id from agent_framework._workflows._executor import Executor +from typing_extensions import Sentinel -_MISSING = object() +UNSET = Sentinel("UNSET") _ALL_OUTPUTS: Literal["all"] = "all" _ALL_OTHER_OUTPUTS: Literal["all_other"] = "all_other" _ParticipantOutputSpecifier = str | SupportsAgentRun | Executor @@ -20,10 +21,10 @@ _WorkflowExecutorSpecifier = Executor | SupportsAgentRun def _coalesce_output_from( # pyright: ignore[reportUnusedFunction] *, - output_from: Any = _MISSING, + output_from: Any = UNSET, ) -> _ParticipantOutputSelection: """Resolve orchestration output selection to ``output_from``.""" - if output_from is not _MISSING: + if output_from is not UNSET: return _coerce_output_from(output_from) return None diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py index 70796d5e26..4f8720b0bf 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py @@ -33,7 +33,7 @@ from agent_framework._workflows._workflow_context import WorkflowContext from ._orchestration_request_info import AgentApprovalExecutor from ._participant_output_config import ( - _MISSING, # pyright: ignore[reportPrivateUsage] + UNSET, _coalesce_output_from, # pyright: ignore[reportPrivateUsage] _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] @@ -99,7 +99,7 @@ class SequentialBuilder: participants: Sequence[SupportsAgentRun | Executor], checkpoint_storage: CheckpointStorage | None = None, chain_only_agent_responses: bool = False, - output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, UNSET), intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the SequentialBuilder. diff --git a/python/packages/orchestrations/tests/test_magentic.py b/python/packages/orchestrations/tests/test_magentic.py index 5c94d2fb14..615ba998bc 100644 --- a/python/packages/orchestrations/tests/test_magentic.py +++ b/python/packages/orchestrations/tests/test_magentic.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. +import logging import sys from collections.abc import AsyncIterable, Awaitable, Sequence from dataclasses import dataclass @@ -987,6 +988,33 @@ def test_magentic_builder_requires_exactly_one_manager_option(): MagenticBuilder(participants=[agent], manager=manager, manager_factory=manager_factory) +def test_magentic_with_custom_manager_does_not_warn_without_standard_manager_options(caplog: Any) -> None: + caplog.set_level(logging.WARNING, logger="agent_framework_orchestrations._magentic") + + MagenticBuilder(participants=[StubAgent("agentA", "reply")], manager=FakeManager()) + + assert "Custom manager provided; all other manager arguments will be ignored." not in caplog.text + + +def test_magentic_with_custom_manager_factory_does_not_warn_without_standard_manager_options(caplog: Any) -> None: + caplog.set_level(logging.WARNING, logger="agent_framework_orchestrations._magentic") + + def manager_factory() -> MagenticManagerBase: + return FakeManager() + + MagenticBuilder(participants=[StubAgent("agentA", "reply")], manager_factory=manager_factory) + + assert "Custom manager provided; all other manager arguments will be ignored." not in caplog.text + + +def test_magentic_with_custom_manager_warns_when_standard_manager_option_is_provided(caplog: Any) -> None: + caplog.set_level(logging.WARNING, logger="agent_framework_orchestrations._magentic") + + MagenticBuilder(participants=[StubAgent("agentA", "reply")], manager=FakeManager(), max_stall_count=3) + + assert "Custom manager provided; all other manager arguments will be ignored." in caplog.text + + async def test_magentic_with_manager_factory(): """Test workflow creation using manager_factory.""" factory_call_count = 0 @@ -1037,6 +1065,20 @@ async def test_magentic_with_agent_factory(): assert event_count > 0 +def test_magentic_agent_factory_uses_default_max_stall_count() -> None: + def agent_factory() -> SupportsAgentRun: + return cast(SupportsAgentRun, StubManagerAgent()) + + participant = StubAgent("agentA", "reply from agentA") + workflow = MagenticBuilder(participants=[participant], manager_agent_factory=agent_factory).build() + + orchestrator = next(e for e in workflow.executors.values() if isinstance(e, MagenticOrchestrator)) + manager = orchestrator._manager # type: ignore[reportPrivateUsage] + + assert isinstance(manager, StandardMagenticManager) + assert manager.max_stall_count == 3 + + async def test_magentic_manager_factory_reusable_builder(): """Test that the builder can be reused to build multiple workflows with manager factory.""" factory_call_count = 0 From bc0e65d7162e9195249b43d869c6361789d73202 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 5 Jun 2026 02:11:24 +0800 Subject: [PATCH 54/61] fix: drop hosted MCP calls when reasoning is stripped (#6210) --- .../agent_framework_openai/_chat_client.py | 22 +++++- .../tests/openai/test_openai_chat_client.py | 73 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 261554fba3..bf43b21e32 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -1454,10 +1454,21 @@ class RawOpenAIChatClient( # type: ignore[misc] Returns: The prepared chat messages for a request. """ + drops_reasoning_without_storage = not request_uses_service_side_storage and any( + content.type == "text_reasoning" for message in chat_messages for content in message.contents + ) + drop_mcp_call_ids: set[str] = set() + if drops_reasoning_without_storage: + for message in chat_messages: + for content in message.contents: + if content.type == "mcp_server_tool_call" and content.call_id: + drop_mcp_call_ids.add(content.call_id) + list_of_list = [ self._prepare_message_for_openai( message, request_uses_service_side_storage=request_uses_service_side_storage, + drop_mcp_call_ids=drop_mcp_call_ids, ) for message in chat_messages ] @@ -1472,6 +1483,7 @@ class RawOpenAIChatClient( # type: ignore[misc] message: Message, *, request_uses_service_side_storage: bool = True, + drop_mcp_call_ids: set[str] | None = None, ) -> list[dict[str, Any]]: """Prepare a chat message for the OpenAI Responses API format.""" all_messages: list[dict[str, Any]] = [] @@ -1491,7 +1503,10 @@ class RawOpenAIChatClient( # type: ignore[misc] # (replays_local_storage) still need stripping when the request also carries a continuation # marker, since the server-stored items would otherwise duplicate the inline ones. Without # storage, standalone reasoning items are invalid per the API ("reasoning was provided - # without its required following item"), so the reasoning branch always drops. + # without its required following item"), so the reasoning branch always drops. When that + # happens, `_prepare_messages_for_openai` also drops the paired hosted-MCP IDs across + # message boundaries rather than replaying bare MCP items. + drop_mcp_call_ids = drop_mcp_call_ids or set() for content in message.contents: match content.type: case "text_reasoning": @@ -1546,7 +1561,10 @@ class RawOpenAIChatClient( # type: ignore[misc] # server-side `id`, so under continuation it would duplicate # the prior response's items (#3295). Drop the call here; the # orphan result is dropped by the coalesce step that follows. - if request_uses_service_side_storage: + # + # Without storage, a reasoning + hosted-MCP pair cannot be replayed + # partially: reasoning is stripped above, and a bare mcp_call is rejected. + if request_uses_service_side_storage or content.call_id in drop_mcp_call_ids: continue prepared_mcp = self._prepare_content_for_openai( message.role, diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index e604742e7e..f02450a457 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -5648,6 +5648,79 @@ def test_prepare_messages_for_openai_coalesces_mcp_call_and_result_into_single_i assert fco_items == [], f"unexpected orphan function_call_output items: {fco_items}" +def test_prepare_messages_for_openai_drops_mcp_call_when_paired_reasoning_is_stripped() -> None: + client = OpenAIChatClient(model="test-model", api_key="test-key") + + messages = [ + Message( + role="assistant", + contents=[ + Content.from_text_reasoning(id="rs_abc123", text="Need the MCP server."), + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ), + ], + ), + Message( + role="tool", + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_abc123", + output=[Content.from_text(text="found 10 cats")], + ) + ], + ), + ] + + result = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=False) + + types = [item.get("type") for item in result if isinstance(item, dict)] + assert "reasoning" not in types + assert "mcp_call" not in types + assert "function_call_output" not in types + + +def test_prepare_messages_for_openai_drops_mcp_call_across_reasoning_messages() -> None: + client = OpenAIChatClient(model="test-model", api_key="test-key") + + messages = [ + Message( + role="assistant", + contents=[Content.from_text_reasoning(id="rs_abc123", text="Need a tool call.")], + ), + Message( + role="assistant", + contents=[ + Content.from_mcp_server_tool_call( + call_id="mcp_abc123", + tool_name="search", + server_name="api_specs", + arguments='{"q": "cats"}', + ) + ], + ), + Message( + role="tool", + contents=[ + Content.from_mcp_server_tool_result( + call_id="mcp_abc123", + output=[Content.from_text(text="found 10 cats")], + ) + ], + ), + ] + + result = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=False) + + types = [item.get("type") for item in result if isinstance(item, dict)] + assert "reasoning" not in types + assert "mcp_call" not in types + assert "function_call_output" not in types + + def test_prepare_messages_for_openai_drops_orphan_mcp_server_tool_result() -> None: """When an mcp_server_tool_result has no matching mcp_server_tool_call in the message list, it must be dropped, NOT serialized as a From 6b94315161497960711f236fa6ce7ead5e63bfff Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:25:18 +0900 Subject: [PATCH 55/61] Python: Add `timeout` parameter to `FoundryAgent` to fix `ConnectTimeout` on multi-turn conversations (#6263) * Python: fix ConnectTimeout on multi-turn FoundryAgent conversations (#6241) Expose a `timeout` parameter on `RawFoundryAgentChatClient`, `_FoundryAgentChatClient`, `RawFoundryAgent`, `FoundryAgent`, and `RawOpenAIChatClient` so callers can override the HTTP timeout used by the underlying AsyncOpenAI client. Root cause: `RawFoundryAgentChatClient.__init__` called `project_client.get_openai_client()` without configuring any timeout, inheriting the OpenAI SDK default of `httpx.Timeout(connect=5.0)`. When connections are recycled between turns under load, the 5 s connect timeout fires and surfaces as `openai.APITimeoutError`. Fix: - `load_openai_service_settings` (`_shared.py`): accept `timeout` and include it in `client_args` for all three `AsyncOpenAI`/ `AsyncAzureOpenAI` construction paths. - `RawOpenAIChatClient.__init__` (`_chat_client.py`): accept `timeout` and forward to `load_openai_service_settings`. - `RawFoundryAgentChatClient.__init__` (`_agent.py`): accept `timeout` and set `openai_client.timeout = timeout` on the client returned by `get_openai_client()` before passing it to the base class. - `_FoundryAgentChatClient`, `RawFoundryAgent`, `FoundryAgent`: accept and propagate `timeout` through the construction chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add timeout parameter to FoundryAgent and RawOpenAIChatClient Expose a timeout parameter on RawFoundryAgentChatClient, _FoundryAgentChatClient, RawFoundryAgent, FoundryAgent, and RawOpenAIChatClient. When provided, the value is applied to the underlying AsyncOpenAI client so that connect timeouts under load or after connection recycling can be tuned by callers. Previously, get_openai_client() was called without any timeout override, so the SDK default of httpx.Timeout(connect=5.0) was inherited and could fire on multi-turn conversations where the underlying connection is recycled between turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Add `timeout` parameter to `FoundryAgent` to fix `ConnectTimeout` on multi-turn conversations Fixes #6241 * fix(foundry): use with_options to avoid mutating shared OpenAI client timeout (#6241) Replace direct assignment with in RawFoundryAgentChatClient.__init__. The Azure AI Projects SDK caches and returns a shared AsyncOpenAI client per AIProjectClient. Mutating its .timeout attribute leaked the override to all other code paths sharing that client (other agents, user code). with_options() returns a new client instance with the override applied, leaving the original shared client untouched. Update tests to assert with_options is called with the correct timeout and that the original shared client's timeout attribute is not mutated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(foundry): assert with_options return value flows to instance.client (#6241) The four timeout propagation tests verified that with_options was called but did not confirm that the returned (timeout-configured) client was actually stored on the instance. A silent discard of the return value would have left the tests green while the timeout had no effect. Each test now captures the constructed instance and asserts: assert .client is openai_client_mock.with_options.return_value Affected tests: - test_raw_foundry_agent_chat_client_init_applies_timeout_to_openai_client - test_raw_foundry_agent_chat_client_init_applies_timeout_with_preview_enabled - test_foundry_agent_chat_client_init_propagates_timeout - test_foundry_agent_init_propagates_timeout_to_openai_client Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../foundry/agent_framework_foundry/_agent.py | 20 ++- .../tests/foundry/test_foundry_agent.py | 117 ++++++++++++++++++ .../agent_framework_openai/_chat_client.py | 8 ++ .../openai/agent_framework_openai/_shared.py | 7 ++ .../tests/openai/test_openai_chat_client.py | 24 +++- 5 files changed, 173 insertions(+), 3 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 433380580d..7001e0bf81 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -191,6 +191,7 @@ class RawFoundryAgentChatClient( # type: ignore[misc] compaction_strategy: CompactionStrategy | None = None, tokenizer: TokenizerProtocol | None = None, additional_properties: dict[str, Any] | None = None, + timeout: float | None = None, ) -> None: """Initialize a raw Foundry Agent client. @@ -211,6 +212,8 @@ class RawFoundryAgentChatClient( # type: ignore[misc] compaction_strategy: Optional per-client compaction override. tokenizer: Optional tokenizer for compaction strategies. additional_properties: Additional properties stored on the client instance. + timeout: HTTP timeout in seconds for requests. When not provided, the + OpenAI SDK default is used (connect: 5s, total: 600s). """ settings = load_settings( FoundryAgentSettings, @@ -260,8 +263,11 @@ class RawFoundryAgentChatClient( # type: ignore[misc] openai_client_kwargs["default_headers"] = dict(default_headers) if allow_preview: openai_client_kwargs["agent_name"] = self.agent_name + openai_client = self.project_client.get_openai_client(**openai_client_kwargs) + if timeout is not None: + openai_client = openai_client.with_options(timeout=timeout) super().__init__( - async_client=self.project_client.get_openai_client(**openai_client_kwargs), + async_client=openai_client, default_headers=default_headers, instruction_role=instruction_role, compaction_strategy=compaction_strategy, @@ -537,6 +543,7 @@ class _FoundryAgentChatClient( # type: ignore[misc] additional_properties: dict[str, Any] | None = None, middleware: (Sequence[ChatAndFunctionMiddlewareTypes] | None) = None, function_invocation_configuration: FunctionInvocationConfiguration | None = None, + timeout: float | None = None, ) -> None: """Initialize a Foundry Agent client with full middleware support. @@ -556,6 +563,8 @@ class _FoundryAgentChatClient( # type: ignore[misc] additional_properties: Additional properties stored on the client instance. middleware: Optional sequence of middleware. function_invocation_configuration: Optional function invocation configuration. + timeout: HTTP timeout in seconds for requests. When not provided, the + OpenAI SDK default is used (connect: 5s, total: 600s). """ super().__init__( project_endpoint=project_endpoint, @@ -573,6 +582,7 @@ class _FoundryAgentChatClient( # type: ignore[misc] additional_properties=additional_properties, middleware=middleware, function_invocation_configuration=function_invocation_configuration, + timeout=timeout, ) @@ -625,6 +635,7 @@ class RawFoundryAgent( # type: ignore[misc] compaction_strategy: CompactionStrategy | None = None, tokenizer: TokenizerProtocol | None = None, additional_properties: Mapping[str, Any] | None = None, + timeout: float | None = None, ) -> None: """Initialize a Foundry Agent. @@ -657,6 +668,8 @@ class RawFoundryAgent( # type: ignore[misc] compaction_strategy: Optional agent-level in-run compaction override. tokenizer: Optional agent-level tokenizer override. additional_properties: Additional properties stored on the local agent wrapper. + timeout: HTTP timeout in seconds for requests. When not provided, the + OpenAI SDK default is used (connect: 5s, total: 600s). """ # Create the client actual_client_type = client_type or _FoundryAgentChatClient @@ -675,6 +688,7 @@ class RawFoundryAgent( # type: ignore[misc] "default_headers": default_headers, "env_file_path": env_file_path, "env_file_encoding": env_file_encoding, + "timeout": timeout, } if function_invocation_configuration is not None: if not issubclass(actual_client_type, FunctionInvocationLayer): @@ -912,6 +926,7 @@ class FoundryAgent( # type: ignore[misc] compaction_strategy: CompactionStrategy | None = None, tokenizer: TokenizerProtocol | None = None, additional_properties: Mapping[str, Any] | None = None, + timeout: float | None = None, ) -> None: """Initialize a Foundry Agent with full middleware and telemetry. @@ -958,6 +973,8 @@ class FoundryAgent( # type: ignore[misc] compaction_strategy: Optional agent-level in-run compaction override. tokenizer: Optional agent-level tokenizer override. additional_properties: Additional properties stored on the local agent wrapper. + timeout: HTTP timeout in seconds for requests. When not provided, the + OpenAI SDK default is used (connect: 5s, total: 600s). """ super().__init__( project_endpoint=project_endpoint, @@ -983,4 +1000,5 @@ class FoundryAgent( # type: ignore[misc] compaction_strategy=compaction_strategy, tokenizer=tokenizer, additional_properties=additional_properties, + timeout=timeout, ) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 2cbf491c16..672a7aba69 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -109,9 +109,67 @@ def test_raw_foundry_agent_chat_client_init_uses_explicit_parameters() -> None: assert "compaction_strategy" in signature.parameters assert "tokenizer" in signature.parameters assert "additional_properties" in signature.parameters + assert "timeout" in signature.parameters assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) +def test_raw_foundry_agent_chat_client_init_applies_timeout_to_openai_client() -> None: + """Test that timeout is applied via with_options without mutating the shared OpenAI client.""" + + mock_project = MagicMock() + openai_client_mock = MagicMock() + openai_client_mock.timeout = 5.0 + mock_project.get_openai_client.return_value = openai_client_mock + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + timeout=60.0, + ) + + openai_client_mock.with_options.assert_called_once_with(timeout=60.0) + assert openai_client_mock.timeout == 5.0, "Original shared client must not be mutated" + assert client.client is openai_client_mock.with_options.return_value + + +def test_raw_foundry_agent_chat_client_init_timeout_none_leaves_client_unchanged() -> None: + """Test that timeout=None does not call with_options and leaves the shared client intact.""" + + mock_project = MagicMock() + openai_client_mock = MagicMock() + openai_client_mock.timeout = 5.0 + mock_project.get_openai_client.return_value = openai_client_mock + + RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + timeout=None, + ) + + openai_client_mock.with_options.assert_not_called() + assert openai_client_mock.timeout == 5.0 + + +def test_raw_foundry_agent_chat_client_init_applies_timeout_with_preview_enabled() -> None: + """Test that timeout uses with_options even when allow_preview=True (hosted agent path).""" + + mock_project = MagicMock() + openai_client_mock = MagicMock() + openai_client_mock.timeout = 5.0 + mock_project.get_openai_client.return_value = openai_client_mock + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="hosted-agent", + allow_preview=True, + timeout=120.0, + ) + + openai_client_mock.with_options.assert_called_once_with(timeout=120.0) + assert openai_client_mock.timeout == 5.0, "Original shared client must not be mutated" + assert client.client is openai_client_mock.with_options.return_value + + def test_raw_foundry_agent_chat_client_as_agent_preserves_client_type() -> None: """Test that as_agent() wraps the client in FoundryAgent using the same client class.""" @@ -552,9 +610,29 @@ def test_foundry_agent_chat_client_init_uses_explicit_parameters() -> None: assert "compaction_strategy" in signature.parameters assert "tokenizer" in signature.parameters assert "additional_properties" in signature.parameters + assert "timeout" in signature.parameters assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) +def test_foundry_agent_chat_client_init_propagates_timeout() -> None: + """Test that _FoundryAgentChatClient calls with_options instead of mutating the shared client.""" + + mock_project = MagicMock() + openai_client_mock = MagicMock() + openai_client_mock.timeout = 5.0 + mock_project.get_openai_client.return_value = openai_client_mock + + client = _FoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + timeout=45.0, + ) + + openai_client_mock.with_options.assert_called_once_with(timeout=45.0) + assert openai_client_mock.timeout == 5.0, "Original shared client must not be mutated" + assert client.client is openai_client_mock.with_options.return_value + + def test_raw_foundry_agent_init_creates_client() -> None: """Test that RawFoundryAgent creates a client internally.""" @@ -629,6 +707,7 @@ def test_raw_foundry_agent_init_uses_explicit_parameters() -> None: assert "compaction_strategy" in signature.parameters assert "tokenizer" in signature.parameters assert "additional_properties" in signature.parameters + assert "timeout" in signature.parameters assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) @@ -641,9 +720,47 @@ def test_foundry_agent_init_uses_explicit_parameters() -> None: assert "compaction_strategy" in signature.parameters assert "tokenizer" in signature.parameters assert "additional_properties" in signature.parameters + assert "timeout" in signature.parameters assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) +def test_foundry_agent_init_propagates_timeout_to_openai_client() -> None: + """Test that FoundryAgent uses with_options instead of mutating the shared OpenAI client.""" + + mock_project = MagicMock() + openai_client_mock = MagicMock() + openai_client_mock.timeout = 5.0 + mock_project.get_openai_client.return_value = openai_client_mock + + agent = FoundryAgent( + project_client=mock_project, + agent_name="test-agent", + timeout=90.0, + ) + + openai_client_mock.with_options.assert_called_once_with(timeout=90.0) + assert openai_client_mock.timeout == 5.0, "Original shared client must not be mutated" + assert agent.client.client is openai_client_mock.with_options.return_value + + +def test_foundry_agent_init_timeout_none_leaves_client_default() -> None: + """Test that FoundryAgent with timeout=None does not call with_options or mutate the client.""" + + mock_project = MagicMock() + openai_client_mock = MagicMock() + openai_client_mock.timeout = 5.0 + mock_project.get_openai_client.return_value = openai_client_mock + + FoundryAgent( + project_client=mock_project, + agent_name="test-agent", + timeout=None, + ) + + openai_client_mock.with_options.assert_not_called() + assert openai_client_mock.timeout == 5.0 + + def test_raw_foundry_agent_init_rejects_invalid_client_type() -> None: """Test that invalid client_type raises TypeError.""" diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index bf43b21e32..d998875d1c 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -385,6 +385,7 @@ class RawOpenAIChatClient( # type: ignore[misc] additional_properties: dict[str, Any] | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, + timeout: float | None = None, ) -> None: """Initialize a raw OpenAI Chat client. @@ -406,6 +407,7 @@ class RawOpenAIChatClient( # type: ignore[misc] env_file_path: Optional ``.env`` file that is checked before the process environment for ``OPENAI_*`` values. env_file_encoding: Encoding for the ``.env`` file. + timeout: Optional timeout in seconds for requests. """ ... @@ -427,6 +429,7 @@ class RawOpenAIChatClient( # type: ignore[misc] additional_properties: dict[str, Any] | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, + timeout: float | None = None, ) -> None: """Initialize a raw OpenAI Chat client. @@ -455,6 +458,7 @@ class RawOpenAIChatClient( # type: ignore[misc] env_file_path: Optional ``.env`` file that is checked before process environment variables for ``AZURE_OPENAI_*`` values. env_file_encoding: Encoding for the ``.env`` file. + timeout: Optional timeout in seconds for requests. """ ... @@ -476,6 +480,7 @@ class RawOpenAIChatClient( # type: ignore[misc] additional_properties: dict[str, Any] | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, + timeout: float | None = None, ) -> None: """Initialize a raw OpenAI Chat client. @@ -511,6 +516,8 @@ class RawOpenAIChatClient( # type: ignore[misc] variables. The same file is used for both ``OPENAI_*`` and ``AZURE_OPENAI_*`` lookups. env_file_encoding: Encoding for the ``.env`` file. + timeout: HTTP timeout in seconds for requests. When not provided, the + OpenAI SDK default is used (connect: 5s, total: 600s). Notes: Environment resolution and routing precedence are: @@ -541,6 +548,7 @@ class RawOpenAIChatClient( # type: ignore[misc] openai_model_fields=("chat_model", "model"), azure_model_fields=("chat_model", "model"), responses_mode=True, + timeout=timeout, ) self.client = client diff --git a/python/packages/openai/agent_framework_openai/_shared.py b/python/packages/openai/agent_framework_openai/_shared.py index 7fb12ad14e..894ee3b612 100644 --- a/python/packages/openai/agent_framework_openai/_shared.py +++ b/python/packages/openai/agent_framework_openai/_shared.py @@ -162,6 +162,7 @@ def load_openai_service_settings( openai_model_fields: Sequence[OpenAIModelSettingName] = ("model",), azure_model_fields: Sequence[OpenAIModelSettingName] = ("model",), responses_mode: bool = False, + timeout: float | None = None, ) -> tuple[dict[str, Any], AsyncOpenAI, bool]: """Load OpenAI settings, including Azure OpenAI model aliases. @@ -218,6 +219,8 @@ def load_openai_service_settings( } if base_url := openai_settings.get("base_url"): client_args["base_url"] = base_url + if timeout is not None: + client_args["timeout"] = timeout return openai_settings, AsyncOpenAI(**client_args), False # type: ignore[return-value] checked_openai = True azure_settings = load_settings( @@ -299,8 +302,12 @@ def load_openai_service_settings( openai_args["api_key"] = _ensure_async_token_provider(client_args["azure_ad_token_provider"]) elif "api_key" in client_args: openai_args["api_key"] = client_args["api_key"] + if timeout is not None: + openai_args["timeout"] = timeout return azure_settings, AsyncOpenAI(**openai_args), True # type: ignore[return-value] + if timeout is not None: + client_args["timeout"] = timeout return azure_settings, AsyncAzureOpenAI(**client_args), True # type: ignore[return-value] diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index f02450a457..9bc598d3cb 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -36,7 +36,7 @@ from agent_framework.exceptions import ( ChatClientInvalidRequestException, SettingNotFoundError, ) -from openai import BadRequestError +from openai import AsyncOpenAI, BadRequestError from openai.types.responses.response_reasoning_item import Summary from openai.types.responses.response_reasoning_summary_text_delta_event import ( ResponseReasoningSummaryTextDeltaEvent, @@ -55,7 +55,7 @@ from pydantic import BaseModel from pytest import param from agent_framework_openai import OpenAIChatClient -from agent_framework_openai._chat_client import OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY +from agent_framework_openai._chat_client import OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY, RawOpenAIChatClient from agent_framework_openai._exceptions import OpenAIContentFilterException skip_if_openai_integration_tests_disabled = pytest.mark.skipif( @@ -194,6 +194,26 @@ def test_init_uses_explicit_parameters() -> None: assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) +def test_raw_openai_chat_client_init_uses_explicit_parameters() -> None: + signature = inspect.signature(RawOpenAIChatClient.__init__) + + assert "additional_properties" in signature.parameters + assert "compaction_strategy" in signature.parameters + assert "tokenizer" in signature.parameters + assert "timeout" in signature.parameters + assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()) + + +def test_raw_openai_chat_client_accepts_preconfigured_client_with_timeout() -> None: + """Test that timeout is accepted without error when async_client is pre-provided.""" + + mock_client = MagicMock(spec=AsyncOpenAI) + mock_client.timeout = 5.0 + + client = RawOpenAIChatClient(async_client=mock_client, timeout=30.0) + assert client is not None + + def test_openai_chat_client_supports_all_tool_protocols() -> None: assert isinstance(OpenAIChatClient, SupportsCodeInterpreterTool) assert isinstance(OpenAIChatClient, SupportsWebSearchTool) From bb9ed63a347b3e437106b27ff7547bd388fd5bbe Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:15:29 +0100 Subject: [PATCH 56/61] .NET: Restructure skill script schemas XML and remove resources from body (#6343) * Restore UTF-8 BOMs and fix BuildScriptSchemasBlock doc comment - Restore UTF-8 BOM on all changed files to match repo convention - Fix XML doc: -> to match emitted output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments: fix doc remarks and rename tests - Update script doc remarks to clarify only parameter schemas are included - Fix grammar: 'arguments format' -> 'argument format' - Rename misleading test methods to match actual assertions - Clarify comment about removed wrapper element Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: SergeyMenshykh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Skills/File/AgentFileSkill.cs | 5 +- .../Skills/Programmatic/AgentClassSkill.cs | 25 ++++++++- .../Skills/Programmatic/AgentInlineSkill.cs | 14 ++++- .../AgentInlineSkillContentBuilder.cs | 54 +++++-------------- .../AgentSkills/AgentClassSkillTests.cs | 18 +++---- .../AgentSkills/AgentFileSkillScriptTests.cs | 9 ++-- .../AgentSkills/AgentInlineSkillTests.cs | 47 ++++++++-------- 7 files changed, 88 insertions(+), 84 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs index d1c792de21..8a74fa034e 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs @@ -49,14 +49,13 @@ public sealed class AgentFileSkill : AgentSkill /// /// /// Returns the raw SKILL.md content. When the skill has scripts, a - /// <scripts><script name="..."><parameters_schema>...</parameters_schema></script></scripts> - /// block is appended with a per-script entry describing the expected argument format. + /// <script_schemas> block is appended describing the argument format. /// The result is cached after the first access. /// public override ValueTask GetContentAsync(CancellationToken cancellationToken = default) { var content = this._content ??= this._scripts is { Count: > 0 } - ? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptsBlock(this._scripts) + ? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptSchemasBlock(this._scripts) : this._originalContent; return new(content); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs index 32f461e32a..84174154a2 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs @@ -114,7 +114,6 @@ public abstract class AgentClassSkill< this.Frontmatter.Name, this.Frontmatter.Description, this.Instructions, - this.Resources, this.Scripts)); } @@ -147,11 +146,17 @@ public abstract class AgentClassSkill< /// Gets the resources associated with this skill, or if none. /// /// + /// /// The default implementation returns resources discovered via reflection by scanning /// for members annotated with . /// This discovery is compatible with Native AOT because is annotated with /// . The result is cached after the first access. /// Override this property in derived classes to provide skill-specific resources. + /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference resources by name in the skill's instructions or in other resources. + /// /// public virtual IReadOnlyList? Resources => this._resources.Value; @@ -159,11 +164,17 @@ public abstract class AgentClassSkill< /// Gets the scripts associated with this skill, or if none. /// /// + /// /// The default implementation returns scripts discovered via reflection by scanning /// for methods annotated with . /// This discovery is compatible with Native AOT because is annotated with /// . The result is cached after the first access. /// Override this property in derived classes to provide skill-specific scripts. + /// + /// + /// Only script parameter schemas are included in the skill body (as a <script_schemas> block). + /// To enable discovery, reference scripts by name in the skill's instructions or in a resource. + /// /// public virtual IReadOnlyList? Scripts => this._scripts.Value; @@ -184,6 +195,10 @@ public abstract class AgentClassSkill< /// /// Creates a skill resource backed by a static value. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// The static resource value. /// An optional description of the resource. @@ -194,6 +209,10 @@ public abstract class AgentClassSkill< /// /// Creates a skill resource backed by a delegate that produces a dynamic value. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// A method that produces the resource value when requested. /// An optional description of the resource. @@ -208,6 +227,10 @@ public abstract class AgentClassSkill< /// /// Creates a skill script backed by a delegate. /// + /// + /// Only the script's parameter schema is included in the skill body (as a <script_schemas> block). + /// To enable discovery, reference the script by name in the skill's instructions or in a resource. + /// /// The script name. /// A method to execute when the script is invoked. /// An optional description of the script. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs index 4fb2b045cb..2465431622 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs @@ -95,7 +95,7 @@ public sealed class AgentInlineSkill : AgentSkill /// public override ValueTask GetContentAsync(CancellationToken cancellationToken = default) { - return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts)); + return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._scripts)); } /// @@ -115,6 +115,10 @@ public sealed class AgentInlineSkill : AgentSkill /// /// Registers a static resource with this skill. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// The static resource value. /// An optional description of the resource. @@ -129,6 +133,10 @@ public sealed class AgentInlineSkill : AgentSkill /// Registers a dynamic resource with this skill, backed by a C# delegate. /// The delegate's parameters and return type are automatically marshaled via AIFunctionFactory. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// A method that produces the resource value when requested. /// An optional description of the resource. @@ -147,6 +155,10 @@ public sealed class AgentInlineSkill : AgentSkill /// Registers a script with this skill, backed by a C# delegate. /// The delegate's parameters and return type are automatically marshaled via AIFunctionFactory. /// + /// + /// Only the script's parameter schema is included in the skill body (as a <script_schemas> block). + /// To enable discovery, reference the script by name in the skill's instructions or in a resource. + /// /// The script name. /// A method to execute when the script is invoked. /// An optional description of the script. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs index dabf75fa1a..d2f27edadc 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs @@ -12,19 +12,17 @@ namespace Microsoft.Agents.AI; internal static class AgentInlineSkillContentBuilder { /// - /// Builds the complete skill content containing name, description, instructions, resources, and scripts. + /// Builds the complete skill content containing name, description, instructions, and script parameter schemas. /// /// The skill name. /// The skill description. /// The raw instructions text. - /// Optional resources associated with the skill. /// Optional scripts associated with the skill. /// An XML-structured content string. public static string Build( string name, string description, string instructions, - IReadOnlyList? resources, IReadOnlyList? scripts) { _ = Throw.IfNullOrWhitespace(name); @@ -39,41 +37,24 @@ internal static class AgentInlineSkillContentBuilder .Append(EscapeXmlString(instructions)) .Append("\n"); - if (resources is { Count: > 0 }) - { - sb.Append("\n\n\n"); - foreach (var resource in resources) - { - if (resource.Description is not null) - { - sb.Append($" \n"); - } - else - { - sb.Append($" \n"); - } - } - - sb.Append(""); - } - if (scripts is { Count: > 0 }) { sb.Append('\n'); - sb.Append(BuildScriptsBlock(scripts)); + sb.Append(BuildScriptSchemasBlock(scripts)); } return sb.ToString(); } /// - /// Builds a <scripts>...</scripts> XML block for the given scripts. - /// Each script is emitted as a <script name="..."> element with optional - /// description attribute and <parameters_schema> child element. + /// Builds a <script_schemas>...</script_schemas> XML block for the given scripts. + /// Each script is emitted as a <schema script="..."> element containing only + /// the parameter schema. This block serves as a reference for the model to know how to + /// format arguments when calling scripts, not as a discovery mechanism. /// /// The scripts to include in the block. - /// An XML string starting with \n<scripts>, or an empty string if the list is empty. - public static string BuildScriptsBlock(IReadOnlyList scripts) + /// An XML string starting with \n<script_schemas>, or an empty string if the list is empty. + public static string BuildScriptSchemasBlock(IReadOnlyList scripts) { _ = Throw.IfNull(scripts); @@ -83,32 +64,23 @@ internal static class AgentInlineSkillContentBuilder } var sb = new StringBuilder(); - sb.Append("\n\n"); + sb.Append("\n\n"); foreach (var script in scripts) { var parametersSchema = script.ParametersSchema; - if (script.Description is null && parametersSchema is null) + if (parametersSchema is null) { - sb.Append($" \n"); + sb.Append($" {EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}\n"); } } - sb.Append(""); + sb.Append(""); return sb.ToString(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs index 1248866a52..17bf71a388 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs @@ -51,9 +51,8 @@ public sealed class AgentClassSkillTests // Act & Assert — Content is cached Assert.Same(await skill.GetContentAsync(), await skill.GetContentAsync()); - // Act & Assert — Content includes parameter schema from typed script - Assert.Contains("parameters_schema", await skill.GetContentAsync()); - Assert.Contains("value", await skill.GetContentAsync()); + // Act & Assert — Content includes parameter schema from typed script (with preserved quotes) + Assert.Contains("\"value\"", await skill.GetContentAsync()); } [Fact] @@ -383,10 +382,9 @@ public sealed class AgentClassSkillTests // Arrange var skill = new AttributedFullSkill(); - // Act & Assert — Content includes reflected resources and scripts - Assert.Contains("", await skill.GetContentAsync()); - Assert.Contains("conversion-table", await skill.GetContentAsync()); - Assert.Contains("", await skill.GetContentAsync()); + // Act & Assert — Content no longer includes resources in body; scripts are in script_schemas + Assert.DoesNotContain("", await skill.GetContentAsync()); + Assert.Contains("", await skill.GetContentAsync()); Assert.Contains("convert", await skill.GetContentAsync()); // Act & Assert — discovered members are cached @@ -504,7 +502,7 @@ public sealed class AgentClassSkillTests } [Fact] - public async Task Content_IncludesDescription_ForReflectedResourcesAsync() + public async Task Content_DoesNotRenderResources_InBodyAsync() { // Arrange var skill = new AttributedResourcePropertiesSkill(); @@ -512,8 +510,8 @@ public sealed class AgentClassSkillTests // Act var content = await skill.GetContentAsync(); - // Assert — descriptions from [Description] attribute appear in synthesized content - Assert.Contains("Some important data.", content); + // Assert — resources are no longer rendered in body content + Assert.DoesNotContain("", content); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs index 9a27528051..aa001fd2a0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs @@ -122,11 +122,10 @@ public sealed class AgentFileSkillScriptTests // Assert — content starts with original and appends per-script entries Assert.StartsWith("Original content", content); - Assert.Contains("", content); - Assert.Contains("