mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fc518b29 | ||
|
|
f3c3efed43 | ||
|
|
bbccb7c28c | ||
|
|
dbc312a78a | ||
|
|
bb9ed63a34 | ||
|
|
6b94315161 | ||
|
|
bc0e65d716 | ||
|
|
4268080c20 | ||
|
|
fe08574a7c | ||
|
|
f970a699d8 | ||
|
|
f29bae8fbc | ||
|
|
c3901a4ddd |
@@ -474,6 +474,45 @@ jobs:
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# GitHub Copilot integration tests
|
||||
python-tests-github-copilot:
|
||||
name: Python Integration Tests - GitHub Copilot
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
GITHUB_COPILOT_TIMEOUT: "120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.checkout-ref }}
|
||||
persist-credentials: false
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Test with pytest (GitHub Copilot integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/github_copilot/tests
|
||||
-m integration
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-github-copilot
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Integration test trend report (aggregates per-job JUnit XML results)
|
||||
python-integration-test-report:
|
||||
name: Integration Test Report
|
||||
@@ -490,6 +529,7 @@ jobs:
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
python-tests-github-copilot,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -553,7 +593,8 @@ jobs:
|
||||
python-tests-functions,
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos
|
||||
python-tests-cosmos,
|
||||
python-tests-github-copilot
|
||||
]
|
||||
steps:
|
||||
- name: Fail workflow if tests failed
|
||||
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
foundryChanged: ${{ steps.filter.outputs.foundry }}
|
||||
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
|
||||
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
|
||||
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
|
||||
@@ -85,6 +86,8 @@ jobs:
|
||||
- 'python/packages/foundry_hosting/**'
|
||||
cosmos:
|
||||
- 'python/packages/azure-cosmos/**'
|
||||
github_copilot:
|
||||
- 'python/packages/github_copilot/**'
|
||||
# run only if 'python' files were changed
|
||||
- name: python tests
|
||||
if: steps.filter.outputs.python == 'true'
|
||||
@@ -658,6 +661,58 @@ jobs:
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# GitHub Copilot integration tests
|
||||
python-tests-github-copilot:
|
||||
name: Python Tests - GitHub Copilot Integration
|
||||
needs: paths-filter
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
needs.paths-filter.outputs.pythonChanges == 'true' &&
|
||||
(github.event_name != 'merge_group' ||
|
||||
needs.paths-filter.outputs.githubCopilotChanged == 'true' ||
|
||||
needs.paths-filter.outputs.coreChanged == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
environment: integration
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
GITHUB_COPILOT_TIMEOUT: "120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ env.UV_PYTHON }}
|
||||
os: ${{ runner.os }}
|
||||
- name: Test with pytest (GitHub Copilot integration)
|
||||
run: >
|
||||
uv run pytest --import-mode=importlib
|
||||
packages/github_copilot/tests
|
||||
-m integration
|
||||
--timeout=120 --session-timeout=900 --timeout_method thread
|
||||
--retries 2 --retry-delay 5
|
||||
--junitxml=pytest.xml
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
|
||||
with:
|
||||
path: ./python/pytest.xml
|
||||
summary: true
|
||||
display-options: fEX
|
||||
fail-on-empty: false
|
||||
title: GitHub Copilot integration test results
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: test-results-github-copilot
|
||||
path: ./python/pytest.xml
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Integration test trend report (aggregates per-job JUnit XML results)
|
||||
python-integration-test-report:
|
||||
name: Integration Test Report
|
||||
@@ -674,6 +729,7 @@ jobs:
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
python-tests-github-copilot,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -735,6 +791,7 @@ jobs:
|
||||
python-tests-foundry,
|
||||
python-tests-foundry-hosting,
|
||||
python-tests-cosmos,
|
||||
python-tests-github-copilot,
|
||||
]
|
||||
steps:
|
||||
- name: Fail workflow if tests failed
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
|
||||
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
|
||||
<!-- MCP -->
|
||||
<PackageVersion Include="ModelContextProtocol" Version="1.1.0" />
|
||||
<PackageVersion Include="ModelContextProtocol" Version="1.2.0" />
|
||||
<!-- Hyperlight -->
|
||||
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
|
||||
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
|
||||
|
||||
@@ -50,12 +50,16 @@ internal static partial class WorkflowHelper
|
||||
/// <summary>
|
||||
/// Executor that starts the concurrent processing by sending messages to the agents.
|
||||
/// </summary>
|
||||
private sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
|
||||
[SendsMessage(typeof(List<ChatMessage>))]
|
||||
[SendsMessage(typeof(TurnToken))]
|
||||
private sealed partial class ConcurrentStartExecutor()
|
||||
: Executor("ConcurrentStartExecutor", declareCrossRunShareable: true), IResettableExecutor
|
||||
{
|
||||
[MessageHandler]
|
||||
internal ValueTask RouteMessages(List<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
internal ValueTask RouteMessages(IEnumerable<ChatMessage> messages, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
|
||||
List<ChatMessage> payload = messages as List<ChatMessage> ?? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executor that aggregates the results from the concurrent agents.
|
||||
/// </summary>
|
||||
[YieldsOutput(typeof(List<ChatMessage>))]
|
||||
private sealed partial class ConcurrentAggregationExecutor() : Executor<List<ChatMessage>>("ConcurrentAggregationExecutor")
|
||||
[YieldsOutput(typeof(string))]
|
||||
private sealed partial class ConcurrentAggregationExecutor() :
|
||||
Executor<List<ChatMessage>>("ConcurrentAggregationExecutor"), IResettableExecutor
|
||||
{
|
||||
private readonly List<ChatMessage> _messages = [];
|
||||
|
||||
@@ -90,5 +97,11 @@ internal static partial class WorkflowHelper
|
||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask ResetAsync()
|
||||
{
|
||||
this._messages.Clear();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="ModelContextProtocol" VersionOverride="1.2.0" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -49,14 +49,13 @@ public sealed class AgentFileSkill : AgentSkill
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Returns the raw SKILL.md content. When the skill has scripts, a
|
||||
/// <c><scripts><script name="..."><parameters_schema>...</parameters_schema></script></scripts></c>
|
||||
/// block is appended with a per-script entry describing the expected argument format.
|
||||
/// <c><script_schemas></c> block is appended describing the argument format.
|
||||
/// The result is cached after the first access.
|
||||
/// </remarks>
|
||||
public override ValueTask<string> 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);
|
||||
}
|
||||
|
||||
@@ -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 <see langword="null"/> if none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default implementation returns resources discovered via reflection by scanning
|
||||
/// <typeparamref name="TSelf"/> for members annotated with <see cref="AgentSkillResourceAttribute"/>.
|
||||
/// This discovery is compatible with Native AOT because <typeparamref name="TSelf"/> is annotated with
|
||||
/// <see cref="DynamicallyAccessedMembersAttribute"/>. The result is cached after the first access.
|
||||
/// Override this property in derived classes to provide skill-specific resources.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public virtual IReadOnlyList<AgentSkillResource>? Resources => this._resources.Value;
|
||||
|
||||
@@ -159,11 +164,17 @@ public abstract class AgentClassSkill<
|
||||
/// Gets the scripts associated with this skill, or <see langword="null"/> if none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default implementation returns scripts discovered via reflection by scanning
|
||||
/// <typeparamref name="TSelf"/> for methods annotated with <see cref="AgentSkillScriptAttribute"/>.
|
||||
/// This discovery is compatible with Native AOT because <typeparamref name="TSelf"/> is annotated with
|
||||
/// <see cref="DynamicallyAccessedMembersAttribute"/>. The result is cached after the first access.
|
||||
/// Override this property in derived classes to provide skill-specific scripts.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Only script parameter schemas are included in the skill body (as a <c><script_schemas></c> block).
|
||||
/// To enable discovery, reference scripts by name in the skill's instructions or in a resource.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public virtual IReadOnlyList<AgentSkillScript>? Scripts => this._scripts.Value;
|
||||
|
||||
@@ -184,6 +195,10 @@ public abstract class AgentClassSkill<
|
||||
/// <summary>
|
||||
/// Creates a skill resource backed by a static value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="value">The static resource value.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
@@ -194,6 +209,10 @@ public abstract class AgentClassSkill<
|
||||
/// <summary>
|
||||
/// Creates a skill resource backed by a delegate that produces a dynamic value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="method">A method that produces the resource value when requested.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
@@ -208,6 +227,10 @@ public abstract class AgentClassSkill<
|
||||
/// <summary>
|
||||
/// Creates a skill script backed by a delegate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the script's parameter schema is included in the skill body (as a <c><script_schemas></c> block).
|
||||
/// To enable discovery, reference the script by name in the skill's instructions or in a resource.
|
||||
/// </remarks>
|
||||
/// <param name="name">The script name.</param>
|
||||
/// <param name="method">A method to execute when the script is invoked.</param>
|
||||
/// <param name="description">An optional description of the script.</param>
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed class AgentInlineSkill : AgentSkill
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<string> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -115,6 +115,10 @@ public sealed class AgentInlineSkill : AgentSkill
|
||||
/// <summary>
|
||||
/// Registers a static resource with this skill.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="value">The static resource value.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
@@ -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 <c>AIFunctionFactory</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="name">The resource name.</param>
|
||||
/// <param name="method">A method that produces the resource value when requested.</param>
|
||||
/// <param name="description">An optional description of the resource.</param>
|
||||
@@ -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 <c>AIFunctionFactory</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the script's parameter schema is included in the skill body (as a <c><script_schemas></c> block).
|
||||
/// To enable discovery, reference the script by name in the skill's instructions or in a resource.
|
||||
/// </remarks>
|
||||
/// <param name="name">The script name.</param>
|
||||
/// <param name="method">A method to execute when the script is invoked.</param>
|
||||
/// <param name="description">An optional description of the script.</param>
|
||||
|
||||
+13
-41
@@ -12,19 +12,17 @@ namespace Microsoft.Agents.AI;
|
||||
internal static class AgentInlineSkillContentBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="name">The skill name.</param>
|
||||
/// <param name="description">The skill description.</param>
|
||||
/// <param name="instructions">The raw instructions text.</param>
|
||||
/// <param name="resources">Optional resources associated with the skill.</param>
|
||||
/// <param name="scripts">Optional scripts associated with the skill.</param>
|
||||
/// <returns>An XML-structured content string.</returns>
|
||||
public static string Build(
|
||||
string name,
|
||||
string description,
|
||||
string instructions,
|
||||
IReadOnlyList<AgentSkillResource>? resources,
|
||||
IReadOnlyList<AgentSkillScript>? scripts)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(name);
|
||||
@@ -39,41 +37,24 @@ internal static class AgentInlineSkillContentBuilder
|
||||
.Append(EscapeXmlString(instructions))
|
||||
.Append("\n</instructions>");
|
||||
|
||||
if (resources is { Count: > 0 })
|
||||
{
|
||||
sb.Append("\n\n<resources>\n");
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
if (resource.Description is not null)
|
||||
{
|
||||
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</resources>");
|
||||
}
|
||||
|
||||
if (scripts is { Count: > 0 })
|
||||
{
|
||||
sb.Append('\n');
|
||||
sb.Append(BuildScriptsBlock(scripts));
|
||||
sb.Append(BuildScriptSchemasBlock(scripts));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <c><scripts>...</scripts></c> XML block for the given scripts.
|
||||
/// Each script is emitted as a <c><script name="..."></c> element with optional
|
||||
/// <c>description</c> attribute and <c><parameters_schema></c> child element.
|
||||
/// Builds a <c><script_schemas>...</script_schemas></c> XML block for the given scripts.
|
||||
/// Each script is emitted as a <c><schema script="..."></c> 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.
|
||||
/// </summary>
|
||||
/// <param name="scripts">The scripts to include in the block.</param>
|
||||
/// <returns>An XML string starting with <c>\n<scripts></c>, or an empty string if the list is empty.</returns>
|
||||
public static string BuildScriptsBlock(IReadOnlyList<AgentSkillScript> scripts)
|
||||
/// <returns>An XML string starting with <c>\n<script_schemas></c>, or an empty string if the list is empty.</returns>
|
||||
public static string BuildScriptSchemasBlock(IReadOnlyList<AgentSkillScript> scripts)
|
||||
{
|
||||
_ = Throw.IfNull(scripts);
|
||||
|
||||
@@ -83,32 +64,23 @@ internal static class AgentInlineSkillContentBuilder
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("\n<scripts>\n");
|
||||
sb.Append("\n<script_schemas>\n");
|
||||
|
||||
foreach (var script in scripts)
|
||||
{
|
||||
var parametersSchema = script.ParametersSchema;
|
||||
|
||||
if (script.Description is null && parametersSchema is null)
|
||||
if (parametersSchema is null)
|
||||
{
|
||||
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
|
||||
sb.Append($" <schema script=\"{EscapeXmlString(script.Name)}\"/>\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(script.Description is not null
|
||||
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
|
||||
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
|
||||
|
||||
if (parametersSchema is not null)
|
||||
{
|
||||
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
|
||||
}
|
||||
|
||||
sb.Append(" </script>\n");
|
||||
sb.Append($" <schema script=\"{EscapeXmlString(script.Name)}\">{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</schema>\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("</scripts>");
|
||||
sb.Append("</script_schemas>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -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("<resources>", await skill.GetContentAsync());
|
||||
Assert.Contains("conversion-table", await skill.GetContentAsync());
|
||||
Assert.Contains("<scripts>", await skill.GetContentAsync());
|
||||
// Act & Assert — Content no longer includes resources in body; scripts are in script_schemas
|
||||
Assert.DoesNotContain("<resources>", await skill.GetContentAsync());
|
||||
Assert.Contains("<script_schemas>", 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("<resources>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -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("<scripts>", content);
|
||||
Assert.Contains("<script name=\"build\">", content);
|
||||
Assert.Contains("<script name=\"deploy\">", content);
|
||||
Assert.Contains("<parameters_schema>", content);
|
||||
Assert.Contains("</scripts>", content);
|
||||
Assert.Contains("<script_schemas>", content);
|
||||
Assert.Contains("<schema script=\"build\">", content);
|
||||
Assert.Contains("<schema script=\"deploy\">", content);
|
||||
Assert.Contains("</script_schemas>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -149,7 +149,7 @@ public sealed class AgentInlineSkillTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Content_IncludesResourcesAddedBeforeFirstAccessAsync()
|
||||
public async Task Content_DoesNotIncludeResourcesInBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
|
||||
@@ -158,13 +158,12 @@ public sealed class AgentInlineSkillTests
|
||||
// Act
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("<resources>", content);
|
||||
Assert.Contains("config", content);
|
||||
// Assert — resources are no longer rendered in the body; they're accessed via GetResourceAsync
|
||||
Assert.DoesNotContain("<resources>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Content_IncludesDelegateResourcesAddedBeforeFirstAccessAsync()
|
||||
public async Task Content_DoesNotIncludeDelegateResourcesInBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
|
||||
@@ -173,9 +172,8 @@ public sealed class AgentInlineSkillTests
|
||||
// Act
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("<resources>", content);
|
||||
Assert.Contains("dynamic", content);
|
||||
// Assert — resources are no longer rendered in the body
|
||||
Assert.DoesNotContain("<resources>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -189,7 +187,7 @@ public sealed class AgentInlineSkillTests
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("<scripts>", content);
|
||||
Assert.Contains("<script_schemas>", content);
|
||||
Assert.Contains("run", content);
|
||||
}
|
||||
|
||||
@@ -209,7 +207,7 @@ public sealed class AgentInlineSkillTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Content_IncludesResourcesAndScriptsAddedBeforeFirstAccessAsync()
|
||||
public async Task Content_IncludesScriptSchemasAddedBeforeFirstAccessAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
|
||||
@@ -220,9 +218,8 @@ public sealed class AgentInlineSkillTests
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("<resources>", content);
|
||||
Assert.Contains("r1", content);
|
||||
Assert.Contains("<scripts>", content);
|
||||
Assert.DoesNotContain("<resources>", content);
|
||||
Assert.Contains("<script_schemas>", content);
|
||||
Assert.Contains("s1", content);
|
||||
}
|
||||
|
||||
@@ -236,8 +233,9 @@ public sealed class AgentInlineSkillTests
|
||||
// Act
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert — JSON schema should be present and XML content chars escaped
|
||||
Assert.Contains("parameters_schema", content);
|
||||
// Assert — JSON schema should be present inside <schema> element (no extra wrapper) with preserved quotes
|
||||
Assert.Contains("<schema script=\"search\">", content);
|
||||
Assert.Contains("\"query\"", content);
|
||||
Assert.DoesNotContain("<![CDATA[", content);
|
||||
}
|
||||
|
||||
@@ -429,7 +427,7 @@ public sealed class AgentInlineSkillTests
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain("<resources>", content);
|
||||
Assert.DoesNotContain("<scripts>", content);
|
||||
Assert.DoesNotContain("<script_schemas>", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -463,7 +461,7 @@ public sealed class AgentInlineSkillTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Content_ScriptWithDescription_IncludesDescriptionAttributeAsync()
|
||||
public async Task Content_ScriptWithDescription_DoesNotEmitDescriptionAttributeAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
|
||||
@@ -472,8 +470,10 @@ public sealed class AgentInlineSkillTests
|
||||
// Act
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("description=\"Runs something.\"", content);
|
||||
// Assert — description is no longer emitted in the script_schemas block;
|
||||
// the block only contains parameter schemas for calling scripts.
|
||||
Assert.Contains("<schema script=\"my-script\"", content);
|
||||
Assert.DoesNotContain("description=\"Runs something.\"", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -492,7 +492,7 @@ public sealed class AgentInlineSkillTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Content_ResourceWithDescription_IncludesDescriptionAttributeAsync()
|
||||
public async Task Content_ResourceWithDescription_NotRenderedInBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
|
||||
@@ -502,9 +502,10 @@ public sealed class AgentInlineSkillTests
|
||||
// Act
|
||||
var content = await skill.GetContentAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("description=\"A described resource.\"", content);
|
||||
Assert.DoesNotContain("no-desc\" description", content);
|
||||
// Assert — resources are no longer rendered in the body
|
||||
Assert.DoesNotContain("<resources>", content);
|
||||
Assert.DoesNotContain("with-desc", content);
|
||||
Assert.DoesNotContain("no-desc", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+39
-1
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.8.0] - 2026-06-04
|
||||
|
||||
### Added
|
||||
- **agent-framework-core**: Add MCP-based skills discovery (`McpSkillsSource`) ([#6169](https://github.com/microsoft/agent-framework/pull/6169))
|
||||
- **agent-framework-core**: Progressive tool exposure via `FunctionInvocationContext` ([#6233](https://github.com/microsoft/agent-framework/pull/6233))
|
||||
- **agent-framework-core**: Add background agent support to harness agent ([#6155](https://github.com/microsoft/agent-framework/pull/6155))
|
||||
- **agent-framework-core**: Add `AgentFileStore` and `FileAccessProvider` for file access operations ([#6099](https://github.com/microsoft/agent-framework/pull/6099))
|
||||
- **agent-framework-core**: Coalesce code interpreter history chunks ([#5801](https://github.com/microsoft/agent-framework/pull/5801))
|
||||
- **agent-framework-core**: Run sync tools off the event loop ([#5773](https://github.com/microsoft/agent-framework/pull/5773))
|
||||
- **agent-framework-bedrock**: Implement native structured output support via Converse API ([#6052](https://github.com/microsoft/agent-framework/pull/6052))
|
||||
- **agent-framework-foundry**: Add Foundry Adaptive Evals integration for rubric-generation ([#6101](https://github.com/microsoft/agent-framework/pull/6101))
|
||||
- **agent-framework-foundry**: Add `timeout` parameter to `FoundryAgent` to fix `ConnectTimeout` on multi-turn conversations ([#6263](https://github.com/microsoft/agent-framework/pull/6263))
|
||||
- **agent-framework-mistral**: Add Mistral AI embedding client package ([#5480](https://github.com/microsoft/agent-framework/pull/5480))
|
||||
- **agent-framework-a2a**: Expose `supported_protocol_bindings` as configurable parameter ([#6098](https://github.com/microsoft/agent-framework/pull/6098))
|
||||
- **agent-framework-a2a**: Set `message_id` on `AgentResponseUpdate` for message-bearing paths ([#6163](https://github.com/microsoft/agent-framework/pull/6163))
|
||||
- **agent-framework-foundry-hosting**: Persist hosted MCP call/results as canonical `mcp_call` output ([#6070](https://github.com/microsoft/agent-framework/pull/6070))
|
||||
|
||||
### Changed
|
||||
- **agent-framework-github-copilot**: [BREAKING] Upgrade `github-copilot-sdk` to v1.0.0 (stable) ([#6292](https://github.com/microsoft/agent-framework/pull/6292))
|
||||
- **agent-framework-core**: [BREAKING — experimental] Refactor Skill API to async resource and script lookup ([#6135](https://github.com/microsoft/agent-framework/pull/6135))
|
||||
- **agent-framework-github-copilot**: Promote to release candidate (`1.0.0rc1`)
|
||||
- **agent-framework-declarative**: Promote to release candidate (`1.0.0rc1`) ([#6256](https://github.com/microsoft/agent-framework/pull/6256))
|
||||
|
||||
### Fixed
|
||||
- **agent-framework-core**: Fix compaction message-id collisions and tool-loop summary persistence ([#6299](https://github.com/microsoft/agent-framework/pull/6299))
|
||||
- **agent-framework-core**: Fix observability unsafe serialization of function-call arguments containing dataclass/framework objects ([#6026](https://github.com/microsoft/agent-framework/pull/6026))
|
||||
- **agent-framework-core**: Consolidate MCP reliability fixes ([#6145](https://github.com/microsoft/agent-framework/pull/6145))
|
||||
- **agent-framework-core**: Backfill chat span request model if unknown and response model is available ([#6160](https://github.com/microsoft/agent-framework/pull/6160))
|
||||
- **agent-framework-anthropic**: Skip orphan anthropic thinking signatures ([#5784](https://github.com/microsoft/agent-framework/pull/5784))
|
||||
- **agent-framework-foundry**: Fix `FoundryAgent` stripping model from `PromptAgent` requests ([#5526](https://github.com/microsoft/agent-framework/pull/5526))
|
||||
- **agent-framework-foundry-hosting**: Fix toolbox consent flow in hosted agent ([#6249](https://github.com/microsoft/agent-framework/pull/6249))
|
||||
- **agent-framework-foundry-hosting**: Drop hosted MCP calls when reasoning is stripped ([#6210](https://github.com/microsoft/agent-framework/pull/6210))
|
||||
- **agent-framework-openai**: Fix OTLP HTTP base-endpoint losing `/v1/{signal}` auto-append ([#5913](https://github.com/microsoft/agent-framework/pull/5913))
|
||||
- **agent-framework-openai**: Drop hosted MCP calls when reasoning is stripped ([#6210](https://github.com/microsoft/agent-framework/pull/6210))
|
||||
- **agent-framework-orchestrations**: Fix spurious Magentic custom manager warning ([#6261](https://github.com/microsoft/agent-framework/pull/6261))
|
||||
- **agent-framework-azurefunctions**: Fix integration test worker crashes on Py3.13 ([#4260](https://github.com/microsoft/agent-framework/pull/4260))
|
||||
|
||||
## [1.7.0] - 2026-05-28
|
||||
|
||||
### Added
|
||||
@@ -1132,7 +1169,8 @@ Release candidate for **agent-framework-core** and **agent-framework-azure-ai**
|
||||
|
||||
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
|
||||
|
||||
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.7.0...HEAD
|
||||
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.8.0...HEAD
|
||||
[1.8.0]: https://github.com/microsoft/agent-framework/compare/python-1.7.0...python-1.8.0
|
||||
[1.7.0]: https://github.com/microsoft/agent-framework/compare/python-1.6.0...python-1.7.0
|
||||
[1.6.0]: https://github.com/microsoft/agent-framework/compare/python-1.5.0...python-1.6.0
|
||||
[1.5.0]: https://github.com/microsoft/agent-framework/compare/python-1.4.0...python-1.5.0
|
||||
|
||||
@@ -33,7 +33,7 @@ Status is grouped into these buckets:
|
||||
| `agent-framework-foundry` | `python/packages/foundry` | `released` |
|
||||
| `agent-framework-foundry-local` | `python/packages/foundry_local` | `beta` |
|
||||
| `agent-framework-gemini` | `python/packages/gemini` | `alpha` |
|
||||
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `beta` |
|
||||
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `rc` |
|
||||
| `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` |
|
||||
| `agent-framework-lab` | `python/packages/lab` | `beta` |
|
||||
| `agent-framework-mem0` | `python/packages/mem0` | `beta` |
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260528"
|
||||
version = "1.0.0b260604"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.7.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"a2a-sdk>=1.0.0,<2",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0b260604"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.6.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"anthropic>=0.80.0,<0.80.1",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0b260604"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -22,8 +22,8 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.6.0,<2",
|
||||
"agent-framework-durabletask>=1.0.0b260521,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"agent-framework-durabletask>=1.0.0b260604,<2",
|
||||
"azure-functions>=1.24.0,<2",
|
||||
"azure-functions-durable>=1.3.1,<2",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0b260604"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.6.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"boto3>=1.35.0,<2.0.0",
|
||||
"botocore>=1.35.0,<2.0.0",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Foundry integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,8 +23,8 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.7.0,<2",
|
||||
"agent-framework-openai>=1.7.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"agent-framework-openai>=1.8.0,<2",
|
||||
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
|
||||
"azure-ai-projects>=2.1.0,<3.0",
|
||||
]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import tempfile
|
||||
import threading
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Generator, Mapping, Sequence
|
||||
from contextlib import AbstractAsyncContextManager, AsyncExitStack, suppress
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from dataclasses import asdict, dataclass, is_dataclass
|
||||
from pathlib import Path
|
||||
from typing import Protocol, cast
|
||||
|
||||
@@ -264,28 +264,73 @@ def _checkpoint_storage_for_context(root: str, context_id: str) -> FileCheckpoin
|
||||
|
||||
# Foundry Toolbox Auth integration
|
||||
# Consent-URL error code returned by the Foundry MCP gateway when calling `/list`
|
||||
CONSENT_ERROR_CODE = -32007
|
||||
CONSENT_ERROR_CODE = -32006
|
||||
|
||||
|
||||
def consent_url_from_error(exc: BaseException) -> str | None:
|
||||
"""Return the consent URL when ``exc`` wraps a Foundry MCP gateway consent error.
|
||||
@dataclass
|
||||
class ConsentError:
|
||||
name: str
|
||||
consent_url: str
|
||||
|
||||
The Agent Framework MCP layer surfaces gateway consent failures by wrapping the underlying
|
||||
``McpError`` inside an :class:`AgentFrameworkException` (typically a ``ToolExecutionException``
|
||||
raised from ``MCPStreamableHTTPTool.__aenter__``). This helper inspects ``exc.args`` for a
|
||||
wrapped ``McpError`` whose ``error.code`` is :data:`CONSENT_ERROR_CODE`; when found, the
|
||||
consent link the gateway returned in ``error.message`` is returned. Returns ``None`` for
|
||||
anything else, so callers can do ``if (url := consent_url_from_error(ex)) is None: raise``.
|
||||
|
||||
def consent_url_from_error(exc: BaseException) -> list[ConsentError] | None:
|
||||
"""Return the consent URLs when ``exc`` wraps Foundry MCP gateway consent errors.
|
||||
|
||||
Args:
|
||||
exc: The exception to inspect.
|
||||
|
||||
Returns:
|
||||
The consent URL if ``exc`` wraps a consent ``McpError``, otherwise ``None``.
|
||||
The consent URL(s) extracted from the error, or ``None`` if no consent error was found.
|
||||
"""
|
||||
inner_exception = next((arg for arg in exc.args if isinstance(arg, McpError)), None)
|
||||
if inner_exception is not None and inner_exception.error.code == CONSENT_ERROR_CODE:
|
||||
return inner_exception.error.message
|
||||
# Parse the error message
|
||||
# The error message is structured with the following format:
|
||||
# "tools/list failed for 1 tool source(s), succeeded for 0 tool source(s) {"errors":[{"name": ..."
|
||||
# where the second part is a JSON string that can be deserialized into an object with the following shape:
|
||||
# ruff: disable[ERA001]
|
||||
# {
|
||||
# "errors" : [
|
||||
# {
|
||||
# "name": "Name of the MCP tool that requires consent",
|
||||
# "type" : "mcp",
|
||||
# "error": {
|
||||
# "code": "CONSENT_REQUIRED",
|
||||
# "message": consent_url,
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ruff: enable[ERA001]
|
||||
try:
|
||||
consent_errors: list[ConsentError] = []
|
||||
error_message_start = inner_exception.error.message.find("{")
|
||||
if error_message_start == -1:
|
||||
logger.warning("Consent error message does not contain JSON: %s", inner_exception.error.message)
|
||||
return None
|
||||
consent_details_json = inner_exception.error.message[error_message_start:]
|
||||
consent_details = json.loads(consent_details_json)
|
||||
if "errors" not in consent_details or not isinstance(consent_details["errors"], list):
|
||||
logger.warning("Consent error message JSON does not contain 'errors' list: %s", consent_details_json)
|
||||
return None
|
||||
for error in consent_details["errors"]:
|
||||
if (
|
||||
isinstance(error, dict)
|
||||
and error.get("type") == "mcp" # type: ignore
|
||||
and "error" in error
|
||||
and isinstance(error["error"], dict)
|
||||
and error["error"].get("code") == "CONSENT_REQUIRED" # type: ignore
|
||||
and "message" in error["error"]
|
||||
):
|
||||
consent_url = error["error"]["message"] # type: ignore
|
||||
if isinstance(consent_url, str):
|
||||
consent_errors.append(ConsentError(name=error.get("name", "Unknown"), consent_url=consent_url)) # type: ignore
|
||||
else:
|
||||
logger.warning("Consent URL in error message is not a valid URL: %s", consent_url) # type: ignore
|
||||
if consent_errors:
|
||||
return consent_errors
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse consent details JSON: %s", inner_exception.error.message)
|
||||
return None
|
||||
|
||||
|
||||
@@ -448,18 +493,19 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
try:
|
||||
await self._ensure_agent_ready()
|
||||
except AgentFrameworkException as ex:
|
||||
consent_url = consent_url_from_error(ex)
|
||||
if consent_url is None:
|
||||
consent_errors = consent_url_from_error(ex)
|
||||
if consent_errors is None:
|
||||
raise
|
||||
logger.warning("OAuth consent required for Foundry MCP gateway.")
|
||||
oauth_item = OAuthConsentRequestOutputItem(
|
||||
id=IdGenerator.new_id("oacr"),
|
||||
consent_link=consent_url,
|
||||
server_label="Foundry Toolbox",
|
||||
)
|
||||
builder = response_event_stream.add_output_item(oauth_item.id)
|
||||
yield builder.emit_added(oauth_item)
|
||||
yield builder.emit_done(oauth_item)
|
||||
for consent_error in consent_errors:
|
||||
logger.warning("Consent URL for tool '%s': %s", consent_error.name, consent_error.consent_url)
|
||||
oauth_item = OAuthConsentRequestOutputItem(
|
||||
id=IdGenerator.new_id("oacr"),
|
||||
consent_link=consent_error.consent_url,
|
||||
server_label=consent_error.name,
|
||||
)
|
||||
builder = response_event_stream.add_output_item(oauth_item.id)
|
||||
yield builder.emit_added(oauth_item)
|
||||
yield builder.emit_done(oauth_item)
|
||||
yield response_event_stream.emit_completed()
|
||||
return
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Foundry Hosting integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0a260528"
|
||||
version = "1.0.0a260604"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.7.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"azure-ai-agentserver-core>=2.0.0b3,<3",
|
||||
"azure-ai-agentserver-responses>=1.0.0b7,<2",
|
||||
"azure-ai-agentserver-invocations>=1.0.0b3,<2",
|
||||
|
||||
@@ -39,6 +39,7 @@ from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from agent_framework_foundry_hosting._responses import (
|
||||
_AZURE_RESPONSES_MESSAGE_ROLE_TYPE, # pyright: ignore[reportPrivateUsage]
|
||||
CONSENT_ERROR_CODE,
|
||||
ConsentError,
|
||||
FileBasedFunctionApprovalStorage, # pyright: ignore[reportPrivateUsage]
|
||||
InMemoryFunctionApprovalStorage, # pyright: ignore[reportPrivateUsage]
|
||||
_item_to_message, # pyright: ignore[reportPrivateUsage]
|
||||
@@ -2118,15 +2119,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
|
||||
@@ -3264,7 +3261,10 @@ class TestCheckpointContextPathValidation:
|
||||
# region Agent lifecycle (lazy entry & OAuth consent surfacing)
|
||||
|
||||
|
||||
def _make_consent_error(url: str = "https://consent.example.com/auth") -> Exception:
|
||||
def _make_consent_error(
|
||||
url: str = "https://consent.example.com/auth",
|
||||
name: str = "Foundry Toolbox",
|
||||
) -> Exception:
|
||||
"""Build an exception wrapping a Foundry MCP gateway consent error.
|
||||
|
||||
Mirrors the real-world wrapping produced by ``MCPStreamableHTTPTool.__aenter__``,
|
||||
@@ -3272,17 +3272,34 @@ def _make_consent_error(url: str = "https://consent.example.com/auth") -> Except
|
||||
``ToolExecutionException`` (an ``AgentFrameworkException`` subclass) with the
|
||||
original error attached via ``inner_exception``. ``consent_url_from_error``
|
||||
then finds the wrapped ``McpError`` in ``exc.args``.
|
||||
|
||||
The McpError message uses the structured Foundry MCP gateway format:
|
||||
a human-readable prefix followed by a JSON document describing each
|
||||
failed tool source and its consent URL.
|
||||
"""
|
||||
from agent_framework.exceptions import ToolExecutionException
|
||||
|
||||
inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message=url))
|
||||
payload = json.dumps({
|
||||
"errors": [
|
||||
{
|
||||
"name": name,
|
||||
"type": "mcp",
|
||||
"error": {
|
||||
"code": "CONSENT_REQUIRED",
|
||||
"message": url,
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
message = f"tools/list failed for 1 tool source(s), succeeded for 0 tool source(s) {payload}"
|
||||
inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message=message))
|
||||
return ToolExecutionException("MCP consent required", inner_exception=inner)
|
||||
|
||||
|
||||
class TestConsentUrlFromError:
|
||||
def test_returns_consent_url_when_inner_arg_is_consent_mcp_error(self) -> None:
|
||||
exc = _make_consent_error("https://example.com/consent")
|
||||
assert consent_url_from_error(exc) == "https://example.com/consent"
|
||||
exc = _make_consent_error("https://example.com/consent", name="my-tool")
|
||||
assert consent_url_from_error(exc) == [ConsentError(name="my-tool", consent_url="https://example.com/consent")]
|
||||
|
||||
def test_returns_none_when_no_mcp_error_in_args(self) -> None:
|
||||
assert consent_url_from_error(Exception("boom")) is None
|
||||
@@ -3299,6 +3316,13 @@ class TestConsentUrlFromError:
|
||||
bare = McpError(ErrorData(code=CONSENT_ERROR_CODE, message="https://x"))
|
||||
assert consent_url_from_error(bare) is None
|
||||
|
||||
def test_returns_none_when_message_has_no_json(self) -> None:
|
||||
from agent_framework.exceptions import ToolExecutionException
|
||||
|
||||
inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message="no json here"))
|
||||
exc = ToolExecutionException("MCP consent required", inner_exception=inner)
|
||||
assert consent_url_from_error(exc) is None
|
||||
|
||||
|
||||
class TestAgentLifecycle:
|
||||
async def test_agent_entered_lazily_on_first_request(self) -> None:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260521"
|
||||
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"
|
||||
@@ -23,8 +23,8 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.6.0,<2",
|
||||
"github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"github-copilot-sdk>=1.0.0,<2; python_version >= '3.11'",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ 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"
|
||||
version = "1.0.0a260604"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.1.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"mistralai>=2.0.0,<3",
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1454,10 +1462,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 +1491,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 +1511,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 +1569,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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "OpenAI integrations for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.7.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
"openai>=1.99.0,<3",
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -5648,6 +5668,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0rc2"
|
||||
version = "1.0.0rc3"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.6.0,<2",
|
||||
"agent-framework-core>=1.8.0,<2",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core[all]==1.7.0",
|
||||
"agent-framework-core[all]==1.8.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "<non-text>"
|
||||
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 "<non-text>"
|
||||
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']
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+8
-12
@@ -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:
|
||||
|
||||
+31
-20
@@ -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()
|
||||
|
||||
|
||||
|
||||
+3
-14
@@ -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"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
+11
-21
@@ -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__":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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__":
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ You can connect to MCP servers in Foundry Toolbox that use different authenticat
|
||||
- **Agent identity authentication**: The tool requires an agent identity token to authenticate. Sample MCP server: `https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview` (Azure Language MCP server) with agent identity for authentication.
|
||||
- **Entra Pass-through authentication**: The tool requires an Entra pass-through token to authenticate. Sample MCP server: Microsoft Outlook MCP server with Entra pass-through for authentication.
|
||||
|
||||
> Definitions of these authentication methods can be found in the [agent.manifest.yaml](agent.manifest.yaml) file in this sample.
|
||||
> Definitions of these authentication methods can be found in the [agent.manifest.yaml](agent.manifest.yaml) file in this sample. The GitHub MCP connection defaults to using a PAT for authentication in this sample, but you can switch to OAuth2 by changing the `project_connection_id` field in the `agent.manifest.yaml` file and following the instructions in the comments.
|
||||
|
||||
There are also Non-MCP tools in the toolbox that support different authentication methods. Learn more at the [Foundry sample repository](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md).
|
||||
|
||||
|
||||
+79
-79
@@ -18,92 +18,92 @@ template:
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: TOOLBOX_NAME
|
||||
value: "agent-tools-2"
|
||||
# parameters:
|
||||
# properties:
|
||||
# - name: mcp_endpoint
|
||||
# # `azd ai agent init -m` will prompt for this value when initializing the agent manifest
|
||||
# secret: false
|
||||
# description: URL of the public MCP server (e.g. https://gitmcp.io/Azure/azure-rest-api-specs) that does not require authentication
|
||||
# - name: github_pat
|
||||
# # `azd ai agent init -m` will prompt for this value when initializing the agent manifest.
|
||||
# # Only needed when the GitHub MCP connection is configured to use the `github-mcp-pat-conn`
|
||||
# # PAT-based connection below; if you use the `github-mcp-oauth-conn` OAuth2 connection
|
||||
# # instead, you can leave this empty.
|
||||
# secret: true
|
||||
# description: GitHub Personal Access Token used to authenticate with the GitHub MCP server (only needed when using the PAT connection; press Enter if using OAuth2 instead)
|
||||
# - name: language_mcp_entra_audience
|
||||
# secret: false
|
||||
# description: Entra ID audience for the Azure Language MCP server (e.g. https://cognitiveservices.azure.com/)
|
||||
# - name: language_mcp_target_url
|
||||
# secret: false
|
||||
# description: URL of the Azure Language MCP server that accepts agent identity tokens (e.g. https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview)
|
||||
# - name: outlook_mail_entra_audience
|
||||
# secret: false
|
||||
# description: Entra ID audience for the Outlook Mail MCP server
|
||||
# - name: outlook_mail_entra_mcp_target
|
||||
# secret: false
|
||||
# description: URL of the Outlook Mail MCP server that accepts user Entra tokens
|
||||
value: "agent-tools"
|
||||
parameters:
|
||||
properties:
|
||||
- name: mcp_endpoint
|
||||
# `azd ai agent init -m` will prompt for this value when initializing the agent manifest
|
||||
secret: false
|
||||
description: URL of the public MCP server (e.g. https://gitmcp.io/Azure/azure-rest-api-specs) that does not require authentication
|
||||
- name: github_pat
|
||||
# `azd ai agent init -m` will prompt for this value when initializing the agent manifest.
|
||||
# Only needed when the GitHub MCP connection is configured to use the `github-mcp-pat-conn`
|
||||
# PAT-based connection below; if you use the `github-mcp-oauth-conn` OAuth2 connection
|
||||
# instead, you can leave this empty.
|
||||
secret: true
|
||||
description: GitHub Personal Access Token used to authenticate with the GitHub MCP server (only needed when using the PAT connection; press Enter if using OAuth2 instead)
|
||||
# - name: language_mcp_entra_audience
|
||||
# secret: false
|
||||
# description: Entra ID audience for the Azure Language MCP server (e.g. https://cognitiveservices.azure.com/)
|
||||
# - name: language_mcp_target_url
|
||||
# secret: false
|
||||
# description: URL of the Azure Language MCP server that accepts agent identity tokens (e.g. https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview)
|
||||
# - name: outlook_mail_entra_audience
|
||||
# secret: false
|
||||
# description: Entra ID audience for the Outlook Mail MCP server
|
||||
# - name: outlook_mail_entra_mcp_target
|
||||
# secret: false
|
||||
# description: URL of the Outlook Mail MCP server that accepts user Entra tokens
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
# - kind: connection
|
||||
# # A connection that uses a GitHub Personal Access Token (PAT) to authenticate with the GitHub MCP server
|
||||
# name: github-mcp-pat-conn
|
||||
# category: RemoteTool
|
||||
# authType: CustomKeys
|
||||
# target: https://api.githubcopilot.com/mcp
|
||||
# credentials:
|
||||
# type: CustomKeys
|
||||
# keys:
|
||||
# Authorization: "Bearer {{ github_pat }}"
|
||||
# - kind: connection
|
||||
# # A connection that uses OAuth2 to authenticate with the GitHub MCP server
|
||||
# name: github-mcp-oauth-conn
|
||||
# category: RemoteTool
|
||||
# authType: OAuth2
|
||||
# target: https://api.githubcopilot.com/mcp
|
||||
# connectorName: foundrygithubmcp
|
||||
# credentials:
|
||||
# type: OAuth2
|
||||
# clientId: managed
|
||||
# clientSecret: managed
|
||||
- kind: connection
|
||||
# A connection that uses a GitHub Personal Access Token (PAT) to authenticate with the GitHub MCP server
|
||||
name: github-mcp-pat-conn
|
||||
category: RemoteTool
|
||||
authType: CustomKeys
|
||||
target: https://api.githubcopilot.com/mcp
|
||||
credentials:
|
||||
type: CustomKeys
|
||||
keys:
|
||||
Authorization: "Bearer {{ github_pat }}"
|
||||
- kind: connection
|
||||
# A connection that uses OAuth2 to authenticate with the GitHub MCP server
|
||||
name: github-mcp-oauth-conn
|
||||
category: RemoteTool
|
||||
authType: OAuth2
|
||||
target: https://api.githubcopilot.com/mcp
|
||||
connectorName: foundrygithubmcp
|
||||
credentials:
|
||||
type: OAuth2
|
||||
clientId: managed
|
||||
clientSecret: managed
|
||||
# - kind: connection
|
||||
# name: language-mcp-conn
|
||||
# category: RemoteTool
|
||||
# authType: AgenticIdentity
|
||||
# audience: "{{ language_mcp_entra_audience }}"
|
||||
# target: "{{ language_mcp_target_url }}"
|
||||
# # - kind: connection
|
||||
# # name: outlook-mail-conn
|
||||
# # category: RemoteTool
|
||||
# # authType: UserEntraToken
|
||||
# # audience: "{{ outlook_mail_entra_audience }}"
|
||||
# # target: "{{ outlook_mail_entra_mcp_target }}"
|
||||
# - kind: toolbox
|
||||
# name: agent-tools
|
||||
# tools:
|
||||
# - type: web_search
|
||||
# name: web_search
|
||||
# - type: code_interpreter
|
||||
# name: code_interpreter
|
||||
# # - type: mcp
|
||||
# # # This MCP tool doesn't require authentication
|
||||
# # server_label: noauth_mcp
|
||||
# # server_url: "{{ mcp_endpoint }}"
|
||||
# # require_approval: "never"
|
||||
# - type: mcp
|
||||
# # This MCP tool uses the GitHub MCP server with a PAT for authentication or OAuth2
|
||||
# server_label: github
|
||||
# project_connection_id: github-mcp-pat-conn # use `github-mcp-oauth-conn` for OAuth2 authentication
|
||||
# require_approval: "never"
|
||||
# - type: mcp
|
||||
# # This MCP tool uses the Azure Language MCP server with agent identity for authentication
|
||||
# server_label: language-mcp
|
||||
# project_connection_id: language-mcp-conn
|
||||
# require_approval: "never"
|
||||
# # - type: mcp
|
||||
# # server_label: outlook-mail
|
||||
# # project_connection_id: outlook-mail-conn
|
||||
# # require_approval: "never"
|
||||
# - kind: connection
|
||||
# name: outlook-mail-conn
|
||||
# category: RemoteTool
|
||||
# authType: UserEntraToken
|
||||
# audience: "{{ outlook_mail_entra_audience }}"
|
||||
# target: "{{ outlook_mail_entra_mcp_target }}"
|
||||
- kind: toolbox
|
||||
name: agent-tools
|
||||
tools:
|
||||
- type: web_search
|
||||
name: web_search
|
||||
- type: code_interpreter
|
||||
name: code_interpreter
|
||||
- type: mcp
|
||||
# This MCP tool doesn't require authentication
|
||||
server_label: noauth_mcp
|
||||
server_url: "{{ mcp_endpoint }}"
|
||||
require_approval: "never"
|
||||
- type: mcp
|
||||
# This MCP tool uses the GitHub MCP server with a PAT for authentication or OAuth2
|
||||
server_label: github
|
||||
project_connection_id: github-mcp-pat-conn # use `github-mcp-oauth-conn` for OAuth2 authentication
|
||||
require_approval: "never"
|
||||
# - type: mcp
|
||||
# # This MCP tool uses the Azure Language MCP server with agent identity for authentication
|
||||
# server_label: language-mcp
|
||||
# project_connection_id: language-mcp-conn
|
||||
# require_approval: "never"
|
||||
# - type: mcp
|
||||
# server_label: outlook-mail
|
||||
# project_connection_id: outlook-mail-conn
|
||||
# require_approval: "never"
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
# agent-framework
|
||||
# agent-framework-foundry-hosting
|
||||
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
mcp>=1.24.0,<2
|
||||
|
||||
@@ -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):
|
||||
|
||||
Generated
+148
-256
@@ -115,7 +115,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -170,7 +170,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-a2a"
|
||||
version = "1.0.0b260528"
|
||||
version = "1.0.0b260604"
|
||||
source = { editable = "packages/a2a" }
|
||||
dependencies = [
|
||||
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -213,7 +213,7 @@ provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-anthropic"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0b260604"
|
||||
source = { editable = "packages/anthropic" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -279,7 +279,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-azurefunctions"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0b260604"
|
||||
source = { editable = "packages/azurefunctions" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -301,7 +301,7 @@ dev = []
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-bedrock"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0b260604"
|
||||
source = { editable = "packages/bedrock" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -363,7 +363,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-core"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = { editable = "packages/core" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -529,7 +529,7 @@ dev = [{ name = "types-python-dateutil", specifier = "==2.9.0.20260518" }]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = { editable = "packages/foundry" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -548,7 +548,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry-hosting"
|
||||
version = "1.0.0a260528"
|
||||
version = "1.0.0a260604"
|
||||
source = { editable = "packages/foundry_hosting" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -599,7 +599,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-github-copilot"
|
||||
version = "1.0.0b260521"
|
||||
version = "1.0.0rc1"
|
||||
source = { editable = "packages/github_copilot" }
|
||||
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.0,<2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -729,7 +729,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-mistral"
|
||||
version = "1.0.0a260505"
|
||||
version = "1.0.0a260604"
|
||||
source = { editable = "packages/mistral" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -774,7 +774,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-openai"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = { editable = "packages/openai" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -789,7 +789,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-orchestrations"
|
||||
version = "1.0.0rc2"
|
||||
version = "1.0.0rc3"
|
||||
source = { editable = "packages/orchestrations" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -913,7 +913,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.14.0"
|
||||
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'" },
|
||||
@@ -923,125 +923,112 @@ dependencies = [
|
||||
{ name = "frozenlist", 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'" },
|
||||
{ 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')" },
|
||||
{ name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" }
|
||||
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/ef/f0/f81190ba488cd106c2fc6d92680e56bb223bbbbf1e6908c2617011290112/aiohttp-3.14.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:692e409052e7436029bbb32977cd7c5bf806ac5fa4085b973996785ffadad33c", size = 760606, upload-time = "2026-06-01T19:36:39.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/54/444d37eebf0f15db661ca44ec7caf93962f3c5ca92eb4c9a5d888b70aaa2/aiohttp-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40af7ebe53c7990e110dc4ad03566b12c3ac996254298a3d39046dd69cfcb2c2", size = 514677, upload-time = "2026-06-01T19:36:42.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/d1/da280e23321c132c0a3fa7c8cc2830621d79174edc64c829443346489a36/aiohttp-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb2ffbb7da32f82e21ad9952669c45bd88a80e0878264c2f59fe1c6fb2badd", size = 510155, upload-time = "2026-06-01T19:36:44.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/b8/2e36d54d0991ec5bba451444004591ee0af58cb1662a3a81c562878b9c1f/aiohttp-3.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2514cb7195f6d7c219339635bea71ae47d1569b051300d32df9dcfabcdb869", size = 1699947, upload-time = "2026-06-01T19:36:45.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/95/a31d8ea1a0b9ecc084f5a7dd0b431ce64ef585918bb7bdc82afe11843877/aiohttp-3.14.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:30e8b7eeb42d02c120ca90d6c6e076a221a16b70a6dac9ae44c7ab5104cc7fe4", size = 1664364, upload-time = "2026-06-01T19:36:47.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f6/5de3ddffc87a9e8d09b3be38fbd6dd1a736b2ad477a7e787dcb85f57f338/aiohttp-3.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63e38be0d75a654deaa06be32fb4cab883a4222940be1d05861b6717679cbadb", size = 1761186, upload-time = "2026-06-01T19:36:49.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8c/03c5438ec35d7e3a4f33fe895d6c3ec7540a7cec46065f21851211e1ee4d/aiohttp-3.14.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1210d4c87cc00128160c7384ab41877a701295b97cffa6362f908a49b6e8a7ca", size = 1849727, upload-time = "2026-06-01T19:36:51.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/32/5a05303b0874458920b73f48b8779cc3a93d503f121b38dcc0456dbd698c/aiohttp-3.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a78a77366ed158a0a54b076990e575d7b7cdb728cbfd02711eadab150f2269f", size = 1708197, upload-time = "2026-06-01T19:36:53.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/478f169488d61414c0a05e7fe423b59ae3d9dcc933d1f0e4acc2c5d5bc3e/aiohttp-3.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f4d2038c64f36df96cfd3fa0937910e231eafbf897e70a06c155a817bb632fa6", size = 1578147, upload-time = "2026-06-01T19:36:55.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/af/b20af85765658972d3337834bd5eebba91b962794f2b4fc3e0ee8c85c0e1/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4714c70067a08b604d0bf3bc4dfdf82e52944afab41d0428d460862763d2f79b", size = 1665836, upload-time = "2026-06-01T19:36:56.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a3/771879cfd59948f4544b172189048905feff802f20f1c6c5411e998a3e06/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f79bfd2847513a7ac801bbafd1de02348a37926ac439eeb4bfe96fcff4eada15", size = 1680335, upload-time = "2026-06-01T19:36:58.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/16/582e36ad1d32133cd40659f3bc98e71c22179665a1cfbbb4713bce339c06/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:25e9f1d2465a210d60edb64d7b204a147e85d4c194eecef3d1604fb5ace678ce", size = 1731180, upload-time = "2026-06-01T19:37:00.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/bc/80708fe3f64a07a2c306a42fc7b009118a952709761d215f6d1b4c57195b/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b5314743ebe926c2fda35d0a298c565c885505f6635c2a30936363404cf274a7", size = 1565805, upload-time = "2026-06-01T19:37:02.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/8f/8d25897f8273a32fe4ad40a8885eec4f397377ed46e8e383078169f60316/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:28eee8de1d69711c53116df8202f1c2aa0e3f80ef912a88fc18d159d53e7110b", size = 1742496, upload-time = "2026-06-01T19:37:04.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7d/c341d32ab2dec56c8478740695743dc6c21b383cace9376a3eab16311a07/aiohttp-3.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89ed35666c95d3efe1955056afcde09e62a57a34e2a4398b17f9f6c1564f0b25", size = 1691240, upload-time = "2026-06-01T19:37:06.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/0f/a81207dd7a2d4a4f645b3a3f8b5a1da1159dc63117ffb137b698fd6df50f/aiohttp-3.14.0-cp310-cp310-win32.whl", hash = "sha256:5e4646e9a6af29af354204011bf5769cb0276ec5b64653e42f90b3e13845169f", size = 454686, upload-time = "2026-06-01T19:37:07.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/ae/842357f2afb9c915715c6f5775239d987f5d0f845abf7675fa794e0a9d40/aiohttp-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:22a8d06f204e0518a586d770032db3c7043c9ba3693081b3e3ad425e1458d594", size = 478677, upload-time = "2026-06-01T19:37:09.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/d1/330fb22c9535ec177b52396905131c6e39447244b6ca876262939af668ef/aiohttp-3.14.0-cp310-cp310-win_arm64.whl", hash = "sha256:4acfc34bd4d3c58754fc9f22ff1b5e92aabce68f3d4bf7b71a0b732d9bceb78a", size = 450364, upload-time = "2026-06-01T19:37:11.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" },
|
||||
{ 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]]
|
||||
@@ -1464,30 +1451,30 @@ 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.22"
|
||||
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/76/cf/840f1b8db16d45e3807c23d1ea779723eed1cd9cf3b6c49e16f372d2a777/botocore-1.43.22.tar.gz", hash = "sha256:b00de525e538289ed4a7a85263f1be4e47473c124cec87be6b23be49356bf745", size = 15458781, upload-time = "2026-06-03T19:33:02.882Z" }
|
||||
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/de/6b/576a1b0f915871e35f14a33104f2bcae635f19c6a72486ba639db0d1fc70/botocore-1.43.22-py3-none-any.whl", hash = "sha256:ceec9f81d0891abe7b28ca2b2ee47e32de7b3360ad11e80d351470f015217379", size = 15141375, upload-time = "2026-06-03T19:32:58.324Z" },
|
||||
{ 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]]
|
||||
@@ -2621,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]]
|
||||
@@ -2703,99 +2690,6 @@ wheels = [
|
||||
{ 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]]
|
||||
name = "granian"
|
||||
version = "2.5.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/b1/100c5add0409559ddbbecca5835c17217b7a2e026eff999bfa359a630686/granian-2.5.7.tar.gz", hash = "sha256:4702a7bcc736454803426bd2c4e7a374739ae1e4b11d27bcdc49b691d316fa0c", size = 112206, upload-time = "2025-11-05T12:18:29.258Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/6f/7719fc97aa081915024939f0d35fdae57dfd3d7214f7ef4a7fa664abbbc3/granian-2.5.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7d84a254e9c88da874ba349f7892278a871acc391ab6af21cc32f58d27cd50a9", size = 2854526, upload-time = "2025-11-05T12:15:29.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cd/af33b780602f962c282ba3341131f7ee3b224a6c856a9fb11a017750a48f/granian-2.5.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8857d5a6ed94ea64d6b92d1d5fa8f7c1676bbecd71e6ca3d71fcd7118448af1d", size = 2537151, upload-time = "2025-11-05T12:15:31.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/58/1a0d529d3d3ddc11b2b292b8f2a7566812d8691de7b1fc8ea5c8f36fd81a/granian-2.5.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9914dfc93f04a53a92d8cfdb059c11d620ff83e9326a99880491a9c5bc5940ef", size = 3017277, upload-time = "2025-11-05T12:15:33.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/78/2a3c198ee379392d9998e4ff0cfd9ffa95b2d2c683bd15a7266a09325d43/granian-2.5.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24c972fe009ca3a08fd7fb182e07fcb16bffe49c87b1c3489a6986c9e9248dc1", size = 2859098, upload-time = "2025-11-05T12:15:35.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/44/7b9fba226083170e9ba221b23ab29d7ffcb761b1ef2b6ed6dac2081bc7fe/granian-2.5.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034df207e62f104d39db479b693e03072c7eb8e202493cdf58948ff83e753cca", size = 3119567, upload-time = "2025-11-05T12:15:36.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/76/f1e348991c031a50d30d3ab0625fec3b7e811092cdb0d1e996885abf1605/granian-2.5.7-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0719052a27caca73bf4000ccdb0339a9d6705e7a4b6613b9fa88ba27c72ba659", size = 2901389, upload-time = "2025-11-05T12:15:39.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/69/71b3d7d90d56fda5617fd98838ac481756ad64f76c1fc1b5e21c43a51f15/granian-2.5.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:be5b9224ec2583ea3b6ca90788b7f59253b6e07fcf817d14c205e6611faaf2be", size = 2989856, upload-time = "2025-11-05T12:15:41.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/42/603db3d0ede778adc979c6acc1eaafa5c670c795f5e0e14feb07772ed197/granian-2.5.7-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:ff246af31840369a1d06030f4d291c6a93841f68ee1f836036bce6625ae73b30", size = 3147378, upload-time = "2025-11-05T12:15:42.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b5/cc557e30ba23c2934c33935768dd0233ef7a10b1e8c81dbbc63d5e2562b5/granian-2.5.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf79375e37a63217f9c1dc4ad15200bc5a89860b321ca30d8a5086a6ea1202e4", size = 3210930, upload-time = "2025-11-05T12:15:45.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/67/ba90520cafcd13b5c76d147d713556b9eef877ca001f9ccf44d5443738b6/granian-2.5.7-cp310-cp310-win_amd64.whl", hash = "sha256:b4269a390054c0f71d9ce9d7c75ce2da0c59e78cb522016eb2f5a506c3eb6573", size = 2176887, upload-time = "2025-11-05T12:15:46.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/21/da3ade91b49ae99146daac6426701cc25b2c5f1413b6c8cb1cc048877036/granian-2.5.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7aa90dcda1fbf03604e229465380138954d9c000eca2947a94dcfbd765414d32", size = 2854652, upload-time = "2025-11-05T12:15:48.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/67/a6fa402ca5ebddebec5d46dacf646ce073872e5251915a725f6abf2a23bb/granian-2.5.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da4f27323be1188f9e325711016ee108840e14a5971bb4b4d15b65b2d1b00a2d", size = 2537539, upload-time = "2025-11-05T12:15:50.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/70/accb5afd83ef785bd9e32067a13547c51cb0139076a8f2857d6d436773df/granian-2.5.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ca5b7028b6ebafce30419ddb6ee7fbfb236fdd0da89427811324ddd38c7d314", size = 3017554, upload-time = "2025-11-05T12:15:52.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/45/98356af5f36af2b6b47a91fef0d326c275e508bf4bcf0c08bd35ed314db8/granian-2.5.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b83e95b18be5dfa92296bc8acfeb353488123399c90cc5f0eccf451e88bc4caf", size = 2859127, upload-time = "2025-11-05T12:15:54.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/7a/04d3ec13b197509c40340ec80414fbbc2b0913f6e1a18c3987cc608c8571/granian-2.5.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aad9e920441232a7b8ad33bef7f04aae986e0e386ab7f13312477c3ea2c85df", size = 3119494, upload-time = "2025-11-05T12:15:56.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/5d/1a82a596725824f6e76b8f7b853ceb464cd0334b2b8143c278aa46f23b6d/granian-2.5.7-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:777d35961d5139d203cf54d872ad5979b171e6496a471a5bcb8032f4471bdec6", size = 2901511, upload-time = "2025-11-05T12:15:58.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/45/b53d6d7df5cd35c3b8bb329f5ee1c7b31ead7a61a6f2046f6562028d7e1b/granian-2.5.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae72c7ba1e8f35d3021dafb2ba6c4ef89f93f877218f8c6ed1cb672145cd81ad", size = 2989828, upload-time = "2025-11-05T12:16:00.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/80/bb57b0fa24fcd518cd64442249459bd214ab1ec5f32590fd30389944261c/granian-2.5.7-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3764d87edd3fddaf557dce32be396a2a56dfc5b9ad2989b1f98952983ae4a21c", size = 3147694, upload-time = "2025-11-05T12:16:01.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/00/f8747aaf8dcd488e4462db89f7273dd9ae702fd17a58d72193b48eff0470/granian-2.5.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5e21bbf1daebb0219253576cac4e5edc8fa8356ad85d66577c4f3ea2d5c6e3c", size = 3211169, upload-time = "2025-11-05T12:16:03.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/69/8593d539898a870692cad447d22c2c4cc34566ad9070040ca216db6ac184/granian-2.5.7-cp311-cp311-win_amd64.whl", hash = "sha256:d210dd98852825c8a49036a6ec23cdfaa7689d1cb12ddc651c6466b412047349", size = 2176921, upload-time = "2025-11-05T12:16:04.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/cf/f76d05e950f76924ffb6c5212561be4dd93fa569518869cc1233a0c77613/granian-2.5.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:41e3a293ac23c76d18628d1bd8376ce3230fb3afe3cf71126b8885e8da4e40c4", size = 2850787, upload-time = "2025-11-05T12:16:06.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/d7/6972aa8c38d26b4cf9f35bcc9b7d3a26a3aa930e612d5913d8f4181331a1/granian-2.5.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b345b539bcbe6dedf8a9323b0c960530cb1fb2cfb887139e6ae9513b6c04d8c", size = 2529552, upload-time = "2025-11-05T12:16:07.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/b4/cd5958b6af674a32296a0fef73fb499c2bf2874025062323f5dbc838f4fc/granian-2.5.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e4d7ba8e3223e2bf974860a59c29b06fa805a98ad4304be4e77180d3a28f55", size = 3009131, upload-time = "2025-11-05T12:16:08.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/69/f3828de736c2802fd7fcac0bb1a0387b3332d432f0eeacb8116094926f06/granian-2.5.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e727d3518f038b64cb0352b34f43b387aafe5eb12b6c4b57ef598b811e40d4ed", size = 2852544, upload-time = "2025-11-05T12:16:10.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c3/b8c65cf86d473b6e99e6d985c678cb192c9b9776a966a2f4b009696bb650/granian-2.5.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59fe2b352a828a2b04bcfd105e623d66786f217759d2d6245651a7b81e4ac294", size = 3131904, upload-time = "2025-11-05T12:16:13.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/7e/b60421bddf187ab2a46682423e4a94b2b22a6ddff6842bf9ca2194e62ac2/granian-2.5.7-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ec5fb593c2d436a323e711010e79718e6d5d1491d0d660fb7c9d97f7e5900830", size = 2908851, upload-time = "2025-11-05T12:16:15.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/cf/3f2426e19dc955a74dc94a5a47c4170e68acb060c541ac080f71a9d55d5d/granian-2.5.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:48fbc25f3717d01e11547afe0e9cdf9d7c41c9f316b9623a40c22ea6b2128d36", size = 2993270, upload-time = "2025-11-05T12:16:17.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/2e/67e1e05ee0d503cc6e9fe53b03f69eb2f267a589d7b40873d120c417385f/granian-2.5.7-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:770935fec3374b814d21c01508c0697842d7c3750731a8ea129738b537ac594c", size = 3134662, upload-time = "2025-11-05T12:16:18.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d5/9d3242bbd911434c4f3d4f14c48e73774a8ddb591e0f975eaeeaef1d5081/granian-2.5.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5db2600c92f74da74f624d2fdb01afe9e9365b50bd4e695a78e54961dc132f1b", size = 3220446, upload-time = "2025-11-05T12:16:20.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/27/b2baa0443a42d8eb59f3dfbe8186e8c80a090655584af4611f22f1592d7a/granian-2.5.7-cp312-cp312-win_amd64.whl", hash = "sha256:bc368bdeb21646a965adf9f43dd2f4a770647e50318ba1b7cf387d4916ed7e69", size = 2179465, upload-time = "2025-11-05T12:16:22.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ec/bf1b7eefe824630d1d3ae9a8af397d823f2339d3adec71e9ee49d667409c/granian-2.5.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:fafb9c17def635bb0a5e20e145601598a6767b879bc2501663dbb45a57d1bc2e", size = 2850581, upload-time = "2025-11-05T12:16:23.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f7/5172daf1968c3a2337c51c50f4a3013aaab564d012d3a79e8390cc66403b/granian-2.5.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9616a197eba637d59242661be8a46127c3f79f7c9bbfa44c0ea8c8c790a11d5e", size = 2529452, upload-time = "2025-11-05T12:16:25.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/10/4344ccacc3f8dea973d630306491de43fbd4a0248e3f7cc9ff09ed5cc524/granian-2.5.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cfd7a09d5eb00a271ec79e3e0bbf069aa62ce376b64825bdeacb668d2b2a4041", size = 3008798, upload-time = "2025-11-05T12:16:26.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/33/638cf8c7f23ab905d3f6a371b5f87d03fd611678424223a0f1d0f7766cc7/granian-2.5.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1438a82264690fce6e82de66a95c77f5b0a5c33b93269eb85fc69ce0112c12d5", size = 2852309, upload-time = "2025-11-05T12:16:28.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/42/6ec25d37ffc1f08679e6b325e9f9ac199ba5def948904c9205cd34fbfe6b/granian-2.5.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3573121da77aac1af64cf90a88f29b2daecbf92458beec187421a382039f366", size = 3131335, upload-time = "2025-11-05T12:16:29.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/db85dac58d84d3e50e427fe5b60b4f8e8a561d9784971fa3b2879198ad88/granian-2.5.7-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:34cdb82024efbcc9de01c7505213be17e4ba5e7a3acabe74ecd93ba31de7673e", size = 2908705, upload-time = "2025-11-05T12:16:31.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/25/a38fd12e1661bbd8535203a8b61240feac7b6b96726bff4de23b0078ab9f/granian-2.5.7-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:572451e94de69df228e4314cb91a50dee1565c4a53d33ffac5936c6ec9c5aba2", size = 2993118, upload-time = "2025-11-05T12:16:32.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/cd/852913a0fc30efc24495453c0f973dd74ef13aa0561afb352afa4b6ecbc2/granian-2.5.7-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6e1679a4b102511b483774397134d244108851ae7a1e8bef09a8ef927ab4d370", size = 3134260, upload-time = "2025-11-05T12:16:34.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/64/0dff100ce1e43c700918b39656cc000b1163c144eac3a12563a5f692dcd1/granian-2.5.7-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:285be70dcf3c70121afec03e691596db94bd786f9bebc229e9e0319686857d82", size = 3219987, upload-time = "2025-11-05T12:16:36.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ab/e66cf9bf57800dd7c2a2a4b8f23124603fce561a65a176f4cf3794a85b92/granian-2.5.7-cp313-cp313-win_amd64.whl", hash = "sha256:1273c9b1d38d19bcdd550a9a846d07112e541cfa1f99be04fbb926f2a003df3d", size = 2179201, upload-time = "2025-11-05T12:16:37.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0e/feca4a20e7b9e7de0e58103278c6581ebf3d5c1b972ed1c2dcfd25741f15/granian-2.5.7-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:75b9798bc13baa76e35165e5a778cd58a7258d5a2112ed6ef84ef84874244856", size = 2776744, upload-time = "2025-11-05T12:16:41.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/fe/65ca38ba9b9f4805495d96ed7b774dfd300f7c944f088db39c676c16501e/granian-2.5.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4cb8247728680ca308b7dc41a6d27582b78e15e902377e89000711f1126524dd", size = 2465942, upload-time = "2025-11-05T12:16:43.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/d1/b9dea32fbafabe5c7b049fb0209149a37c6b8468c698d066448cbe88dc85/granian-2.5.7-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64348b83f1ad2f7a29df7932dc518ad669cb61a08a9cde02ca8ede8e9b110506", size = 3015413, upload-time = "2025-11-05T12:16:45.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9e/d29485ab18896e4d911e33b006af7a9b7098316a78938d6b7455c523fea5/granian-2.5.7-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e2292d4a4661c79d471fa0ff6fe640018c923b6a6dd1bb5383b368b3d5ec2a0c", size = 2783371, upload-time = "2025-11-05T12:16:46.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/cd/58c67dc191caeecbbb15ee39d433136dd064c13778b4551661bd902b5a78/granian-2.5.7-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:45903d2f2f88a9cd4a7d0b8ec329db1fb2d9e15bf38153087a3b217b9cdb0046", size = 2979946, upload-time = "2025-11-05T12:16:48.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/0b/04e4977df3ef7607a8b6625caed7cac107a049120d2452c33392d4544875/granian-2.5.7-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:106e8988e42e527c18b763be5faae7e8f602caac6cb93657793638fc9ab41c98", size = 3123177, upload-time = "2025-11-05T12:16:49.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/89/4e10e18fc107e5929143a06d9257646963cf5621c928b3d2774e5a85652a/granian-2.5.7-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:711632e602c4ea08b827bf6095c2c6fbe6005c7a05f142ae2b4d9e1d45cefbd9", size = 3211773, upload-time = "2025-11-05T12:16:51.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/81/94e416056d8b4b1cd09cc8065a1e240b0af99f21301c209571530cd83dd0/granian-2.5.7-cp313-cp313t-win_amd64.whl", hash = "sha256:1c571733aa0fdb6755be9ffb3cd728ef965ae565ba896e407d6019bad929d7bb", size = 2174154, upload-time = "2025-11-05T12:16:53.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/89/207ebcbd084ed992ecb3739376fd292e6a5bf6ae80b35f06e4f382e1f193/granian-2.5.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:74ad35feeafc12efdc27d59a393f8b95235095c4e46c8b8dd6d50ee9e928118d", size = 2834664, upload-time = "2025-11-05T12:16:54.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/4b/f941c645d5e3ab495f0cb056abebdb16fb761f713c35a830521f4531674b/granian-2.5.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:875f5cc36b960039bfc99a37af32ad98b3abe753a6de92a9f91268c16bfeb192", size = 2510662, upload-time = "2025-11-05T12:16:56.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/14/af9bbf26389f6d0cbdd7445cc969da50965363b2c9635acdae08eb4f2d9b/granian-2.5.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:478123ee817f742a6f67050ae4de46bc807c874e397a379cf9fb9ed68b66d7ad", size = 3003249, upload-time = "2025-11-05T12:16:58.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/0e/4fa5d4317ff88eab5d061cb45339fdf09a044ae9c7b2496b81c2de5bc2c6/granian-2.5.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d0d0530960250ac9b78494999f2687c627ac5060013e4c63856afb493c2518", size = 2844121, upload-time = "2025-11-05T12:16:59.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/05/977fcfe66c9ecd72da47e5185bcd78150efcb5d3bca1ba77860fe8f7bad7/granian-2.5.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cf8cb02253bfc42ee1bb6c5912507f83bea0a39c3d8a09988939407e08787b", size = 3125524, upload-time = "2025-11-05T12:17:02.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c0/fd4d0b455d34c493cfbc6f450e0005206ab41a68f65f16f89e9ae84669ed/granian-2.5.7-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:78015fcb4d055e0eb2454d07f167ca2aa9f48609f90484750b99ca9b719701c4", size = 2902047, upload-time = "2025-11-05T12:17:04.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/55/13d53add16a349b5c9384afac14b519a54b7fa4bf73540338296f0963ee7/granian-2.5.7-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bd254e68cc8471b725aa6610b68a5e004aa92b8db53c0d01c408bef8bc9cdcb4", size = 2988366, upload-time = "2025-11-05T12:17:05.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/b3/addad51cef2472105b664b608a2b8eccc5691d08c532862cd21b52023661/granian-2.5.7-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:028480ddef683df00064664e7bf58358650722dfa40c2a6dcbf50b3d1996dbb0", size = 3128826, upload-time = "2025-11-05T12:17:07.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/2c/ceab57671c7ade9305ed9e86471507b7721e92435509bb3ecab7e1c28fa8/granian-2.5.7-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b42a254b2884b3060dcafc49dee477f3f6e8c63c567f179dbec7853d6739f124", size = 3212960, upload-time = "2025-11-05T12:17:09.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/5b/5458d995ed5a1fe4a7aa1e2587f550c00ec80d373531e270080e4d5e1ca5/granian-2.5.7-cp314-cp314-win_amd64.whl", hash = "sha256:8f6466077c76d92f8926885280166e6874640bbab11ce10c4a3b04c0ee182ac6", size = 2168248, upload-time = "2025-11-05T12:17:10.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/6d/3c6fdf84e9de25e0023302d5efd98d70fd6147cae98453591a317539bba6/granian-2.5.7-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:6fc06ac1c147e2f01639aa5c7c0f9553f8c6b283665d13d5527a051e917db150", size = 2763007, upload-time = "2025-11-05T12:17:12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/92/3fc35058908d1ecb3cb556de729e6f5853e888ac7022a141885f6a3079a5/granian-2.5.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dec92e09f512aaf532bb75de69b858958113efe52b16a9c5ef19d64063b4956c", size = 2448084, upload-time = "2025-11-05T12:17:13.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/82/3fc67aa247dcac09c948ae8a3dc02568d4eb8135f9938594ee5d2ba25a4f/granian-2.5.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01e18c9c63b89370e42d65bc4eccec349d0b676ee69ccbcbbf9bedf606ded129", size = 3008404, upload-time = "2025-11-05T12:17:15.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/4c/11f293a60892df7cfdcbb1648ddc31e9d4471b52843e4e838a2a58773fff/granian-2.5.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:035e3b145827a12fb25de5b5122a11d9dad93a943e2251d83ee593b28b0397dc", size = 2781744, upload-time = "2025-11-05T12:17:17.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/a0/d4f0063938431201fc7884c7e7bfc5488e3de09957cce37090af9131b7f4/granian-2.5.7-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:21278e2862d7e52996b03260a2a65288c731474c71a6d8311ef78025696b883d", size = 2977678, upload-time = "2025-11-05T12:17:19.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/85/327e15e9e96eb35fcca3fbd9848df6bc180f7fb04c9116e22d3c10ada98e/granian-2.5.7-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:fd6a7645117034753ec91e667316e93f3d0325f79462979af3e2e316278ae235", size = 3116889, upload-time = "2025-11-05T12:17:21.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/5c/67224ee8fa71ee3748d931c34cf6f85e30c77b2a3ac0b1ca70c640b37d10/granian-2.5.7-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:133d3453d29c5a22648c879d078d097a4ea74b8f84c530084c32debdfdd9d5fd", size = 3203908, upload-time = "2025-11-05T12:17:23.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e0/df08a75311c8d9505dc4f381a4a21bbfeed58b8c8f6d7c3a34b049ad9c34/granian-2.5.7-cp314-cp314t-win_amd64.whl", hash = "sha256:ab8f0f4f22d2efcce194f5b1d66beef2ba3d4bcd18f9afd6b749afa48fdb9a7d", size = 2161670, upload-time = "2025-11-05T12:17:25.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/25/2a4112983df5ce0ec8407121ad72c17d27ebfad57085749b8e4164d69e63/granian-2.5.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdae1c86357bfe895ffd0065c0403913bc008f752e2f77ab363d4e3b4276009b", size = 2838744, upload-time = "2025-11-05T12:17:45.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/0a/eb0c5b71355e8f99b89dc335f16cd5108763c554e96a2aae5e7162ef4997/granian-2.5.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:bc1d8aaf5bfc5fc9f8f590a42e9f88a43d19ad71f670c6969fa791b52ce1f5ec", size = 2538706, upload-time = "2025-11-05T12:17:47.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/9c/4c592c5a813a921033a37a0f003278b1f772a6c9abd16f821bcb119151f0/granian-2.5.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:288b62c19aea5b162d27e229469b6307a78cb272aa8fcc296dbfca9fbbda4d8f", size = 3117369, upload-time = "2025-11-05T12:17:49.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/35/96af9f0995a7c45f0cd31261ab6284e5d6028afa17c6fcfe757cccb0afb5/granian-2.5.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:66c3d2619dc5e845d658cf3ed4f7370f83d5323a85ff8338e7c7a27d9a333841", size = 2904972, upload-time = "2025-11-05T12:17:50.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/93/45c253983c2001f534ba2c7bc1e53718fc8cecf196b1e1a0469d5874ae54/granian-2.5.7-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:323e35d5d5054d2568fc824798471e7d33314f47aebd556c4fbf4894e539347d", size = 2991986, upload-time = "2025-11-05T12:17:52.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/77/c03e60c7bed386ab16cf15b317dea7f95dde5095af6e17cbd657cd82c21b/granian-2.5.7-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:026ef2588a2b991b250768bf47538fd5fd864549535f885239b6908b214299c4", size = 3163649, upload-time = "2025-11-05T12:17:54.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/c9/2bce3db4e3da8d3a697c363c8f699b71f05b7f7a0458e1ba345eaea53fcd/granian-2.5.7-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4717a62c0a1b79372c495b99ade18bfc3c4a365242bf75770c96a4767a9bcf66", size = 3201886, upload-time = "2025-11-05T12:17:56.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/66/997ebfd8cc4a0640befb970bc846a76437d1f0b55dff179e69f29fa4615b/granian-2.5.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4b57ae0a2e1dbc7a248e3c08440b490b3f247e7e4f997faa72e82f5a89d0ea4c", size = 2175219, upload-time = "2025-11-05T12:17:58.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/0f/da2588ac78254a4d0be90a6f733d0bb7dd1edb78a10d9e59fa9837687e94/granian-2.5.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bee545c9b9e38eabcdd675e3fec1a2112b8193dc864739952b9de8131433a31c", size = 2838886, upload-time = "2025-11-05T12:17:59.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/34/75def8343534e9d48362c43c3cbd06242a2d7804fbfbc824c8aa9fb75a30/granian-2.5.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:73c76c0f1ee46506224e92df193b4d271ea89f0d82cd69301784ca85bc1db515", size = 2538597, upload-time = "2025-11-05T12:18:01.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/5d/d828d97aad050cfc5b18a0163b532c289a35ad214e31f5a129695b2b4cae/granian-2.5.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68879c27aed972f647a8e8ef37f9046f71d7507dc9b3ceffa97d2fbffe6a16c8", size = 3117570, upload-time = "2025-11-05T12:18:03.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/57/b8380f3d6b6dcdcd454d720cf11dbecb0e2071a870f44eb834011f14b573/granian-2.5.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ea9cbdfbd750813866dcc9c020018e5f20a57a4e3a83bd049ccc1f6da0559b75", size = 2905089, upload-time = "2025-11-05T12:18:05.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/e9/04a7c3b83650afc4a4ad82b67e6306d99f80ac1a6aacb3a8ba182f7359d6/granian-2.5.7-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d142ff5ee6027515370e56f95d179ec3e81bd265d5b4958de2b19adcdf34887d", size = 2991867, upload-time = "2025-11-05T12:18:07.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/bf/a1cdbff73cbac4fddf817d06c13ce6cdc75c22d6da1b257e3563fea4c3c5/granian-2.5.7-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:222f0fb1688a62ca23cb3da974cefa69e7fdc40fd548d1ae87a953225e1d1cbb", size = 3164141, upload-time = "2025-11-05T12:18:09.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/cc/35c6a55ac2c211e86a9f0c728eb81b6ad19f05a3055d79c6f11a1b71f5d5/granian-2.5.7-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:40494c6cda1ad881ae07efbb2dc4a1ca8f12d5c6cf28d1ab8b0f2db13826617b", size = 3201599, upload-time = "2025-11-05T12:18:10.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/0a/5a95a3889532bc5a5f652cdc78dae8ffa16d4228b4d35256a98be89e33ef/granian-2.5.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c3942d08af2c8b67d0ef569b6c567284433ebf09b4af3ea68388abb7caccad2b", size = 2175240, upload-time = "2025-11-05T12:18:12.956Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphviz"
|
||||
version = "0.21"
|
||||
@@ -3641,7 +3535,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.87.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'" },
|
||||
@@ -3657,9 +3551,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/77/0d/ccdf682ccfd7f18bf0e179c39d85616b8f8ef05a798588285310412db13d/litellm-1.87.0.tar.gz", hash = "sha256:cafc1882cb0cbab8374c41180af86e4a067796e4524e15f59e99f6e689cd1bd8", size = 15453755, upload-time = "2026-06-02T03:53:29.076Z" }
|
||||
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/98/20/88a372fa7e50fc2c33458c6eef94a79afcf7bdfa43610079531b82b484a3/litellm-1.87.0-py3-none-any.whl", hash = "sha256:fbbba7e47ae29b55f878fe1acc80effb92761bc168f6236bd81a0cb6e147d855", size = 17103948, upload-time = "2026-06-02T03:53:25.677Z" },
|
||||
{ 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]
|
||||
@@ -3672,14 +3566,12 @@ proxy = [
|
||||
{ name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "fastapi-sso", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "granian", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "gunicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "litellm-enterprise", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "litellm-proxy-extras", 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 = "orjson", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "polars", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pydantic-settings", 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 = "pynacl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyroscope-io", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -3696,20 +3588,20 @@ proxy = [
|
||||
|
||||
[[package]]
|
||||
name = "litellm-enterprise"
|
||||
version = "0.1.41"
|
||||
version = "0.1.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/07/73412b99c6065ae49a5e87b5f5810b94c1743d7cd41d3a701ebf2c0a64d2/litellm_enterprise-0.1.41.tar.gz", hash = "sha256:3bbf37b6e997e28f9a39489ba532ac98f19b5176180ce091c04156ce3f048d54", size = 70437, upload-time = "2026-05-17T02:05:49.282Z" }
|
||||
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/65/16/284b7304dbf6eea7fe79352ca808f310b7e724648e5f3cf7c13a7a54d682/litellm_enterprise-0.1.41-py3-none-any.whl", hash = "sha256:7b31fd807dee8e1900fd15d8344e4509b6aaf05e10a211fc91c30a95e227685f", size = 137669, upload-time = "2026-05-17T02:05:48.24Z" },
|
||||
{ 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.73"
|
||||
version = "0.4.69"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/37/bed736f8a623b7891e9ff272fd60c2f08fc4a8fed372885f5df9ec09b769/litellm_proxy_extras-0.4.73.tar.gz", hash = "sha256:d4fb1238fb56cdaa21aef6b1d7683c2c0fe3a148ecd423f8bf4cef3c3d07bd36", size = 43599, upload-time = "2026-05-20T00:00:05.495Z" }
|
||||
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/1d/64/7e85f5f47495ebb0bb5f30a4f4b54b64277a40a14be79c34052df97ac7ab/litellm_proxy_extras-0.4.73-py3-none-any.whl", hash = "sha256:a4f460d15dd01a095dadb26f7660a259fa2a8757a9e27dee68c159a872b8db7e", size = 118593, upload-time = "2026-05-20T00:00:04.017Z" },
|
||||
{ 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]]
|
||||
@@ -5975,16 +5867,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]]
|
||||
@@ -6192,11 +6084,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]]
|
||||
@@ -6715,14 +6607,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.17.1"
|
||||
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/11/b3/bcdc2f58fa92592db511beda154c2c08d28f21f6c4637f06a42a24b10c21/s3transfer-0.17.1.tar.gz", hash = "sha256:042dd5e3b1b512355e35a23f0223e426b7042e80b97830ea2680ddce327fc45e", size = 159439, upload-time = "2026-05-26T19:45:01.714Z" }
|
||||
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/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl", hash = "sha256:5b9827d1044159bbb01b86ef8902760ea39281927f5de31de75e1d657177bf4c", size = 88264, upload-time = "2026-05-26T19:45:00.452Z" },
|
||||
{ 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]]
|
||||
|
||||
Reference in New Issue
Block a user