diff --git a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
index a121a5f446..1b28626ea8 100644
--- a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
+++ b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
@@ -1636,4 +1636,4 @@ The property mapping guide from a `AutoFunctionInvocationContext` to a `Function
| Result | Use `return` from the delegate |
| Terminate | Terminate |
| CancellationToken | provided via argument to middleware delegate |
-| Arguments | Arguments |
\ No newline at end of file
+| Arguments | Arguments |
diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
index 09a6caaf61..109f5c11aa 100644
--- a/.github/workflows/dotnet-build-and-test.yml
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -79,7 +79,7 @@ jobs:
workflow-samples
- name: Setup dotnet
- uses: actions/setup-dotnet@v5.0.0
+ uses: actions/setup-dotnet@v5.0.1
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build dotnet solutions
diff --git a/agent-samples/azure/AzureOpenAIAssistants.yaml b/agent-samples/azure/AzureOpenAIAssistants.yaml
index 8c0d889598..f973d05acc 100644
--- a/agent-samples/azure/AzureOpenAIAssistants.yaml
+++ b/agent-samples/azure/AzureOpenAIAssistants.yaml
@@ -1,9 +1,9 @@
kind: Prompt
name: Assistant
description: Helpful assistant
-instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
model:
- id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
+ id: gpt-4o-mini
provider: AzureOpenAI
apiType: Assistants
options:
@@ -12,14 +12,14 @@ model:
outputSchema:
properties:
language:
- kind: string
+ type: string
required: true
description: The language of the answer.
answer:
- kind: string
+ type: string
required: true
description: The answer text.
type:
- kind: string
+ type: string
required: true
description: The type of the response.
diff --git a/agent-samples/azure/AzureOpenAIChat.yaml b/agent-samples/azure/AzureOpenAIChat.yaml
new file mode 100644
index 0000000000..d02e0c6039
--- /dev/null
+++ b/agent-samples/azure/AzureOpenAIChat.yaml
@@ -0,0 +1,25 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
+model:
+ id: gpt-4o-mini
+ provider: AzureOpenAI
+ apiType: Chat
+ options:
+ temperature: 0.9
+ topP: 0.95
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/azure/AzureOpenAIResponses.yaml b/agent-samples/azure/AzureOpenAIResponses.yaml
index 5db218ade3..006c1476f4 100644
--- a/agent-samples/azure/AzureOpenAIResponses.yaml
+++ b/agent-samples/azure/AzureOpenAIResponses.yaml
@@ -1,28 +1,25 @@
kind: Prompt
name: Assistant
description: Helpful assistant
-instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
model:
- id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
+ id: gpt-4o-mini
provider: AzureOpenAI
apiType: Responses
options:
- text:
- verbosity: medium
- connection:
- kind: remote
- endpoint: =Env.AZURE_OPENAI_ENDPOINT
+ temperature: 0.9
+ topP: 0.95
outputSchema:
properties:
language:
- kind: string
+ type: string
required: true
description: The language of the answer.
answer:
- kind: string
+ type: string
required: true
description: The answer text.
type:
- kind: string
+ type: string
required: true
description: The type of the response.
diff --git a/agent-samples/chatclient/Assistant.yaml b/agent-samples/chatclient/Assistant.yaml
index b34add2d23..3332d54540 100644
--- a/agent-samples/chatclient/Assistant.yaml
+++ b/agent-samples/chatclient/Assistant.yaml
@@ -1,7 +1,7 @@
kind: Prompt
name: Assistant
description: Helpful assistant
-instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
model:
options:
temperature: 0.9
@@ -9,10 +9,10 @@ model:
outputSchema:
properties:
language:
- kind: string
+ type: string
required: true
description: The language of the answer.
answer:
- kind: string
+ type: string
required: true
description: The answer text.
diff --git a/agent-samples/chatclient/GetWeather.yaml b/agent-samples/chatclient/GetWeather.yaml
index 9ed637894d..f32411be98 100644
--- a/agent-samples/chatclient/GetWeather.yaml
+++ b/agent-samples/chatclient/GetWeather.yaml
@@ -4,6 +4,8 @@ description: Helpful assistant
instructions: You are a helpful assistant. You answer questions using the tools provided.
model:
options:
+ temperature: 0.9
+ topP: 0.95
allowMultipleToolCalls: true
chatToolMode: auto
tools:
diff --git a/agent-samples/foundry/FoundryAgent.yaml b/agent-samples/foundry/FoundryAgent.yaml
new file mode 100644
index 0000000000..2de2ea069e
--- /dev/null
+++ b/agent-samples/foundry/FoundryAgent.yaml
@@ -0,0 +1,22 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
+model:
+ id: gpt-4.1-mini
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: Remote
+ endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
diff --git a/agent-samples/openai/OpenAIAssistants.yaml b/agent-samples/openai/OpenAIAssistants.yaml
index 78bd48d701..c1f20beb38 100644
--- a/agent-samples/openai/OpenAIAssistants.yaml
+++ b/agent-samples/openai/OpenAIAssistants.yaml
@@ -1,30 +1,28 @@
kind: Prompt
name: Assistant
description: Helpful assistant
-instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
model:
- id: =Env.OPENAI_MODEL
+ id: gpt-4.1-mini
provider: OpenAI
apiType: Assistants
options:
temperature: 0.9
topP: 0.95
connection:
- kind: key
+ kind: ApiKey
key: =Env.OPENAI_APIKEY
outputSchema:
- name: AssistantResponse
- description: The response from the assistant.
properties:
language:
- kind: string
+ type: string
required: true
description: The language of the answer.
answer:
- kind: string
+ type: string
required: true
description: The answer text.
type:
- kind: string
+ type: string
required: true
description: The type of the response.
diff --git a/agent-samples/openai/OpenAIChat.yaml b/agent-samples/openai/OpenAIChat.yaml
new file mode 100644
index 0000000000..832ef4eb15
--- /dev/null
+++ b/agent-samples/openai/OpenAIChat.yaml
@@ -0,0 +1,28 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
+model:
+ id: gpt-4.1-mini
+ provider: OpenAI
+ apiType: Chat
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: ApiKey
+ key: =Env.OPENAI_APIKEY
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/openai/OpenAIResponses.yaml b/agent-samples/openai/OpenAIResponses.yaml
index 0fcda30c9c..efe822233e 100644
--- a/agent-samples/openai/OpenAIResponses.yaml
+++ b/agent-samples/openai/OpenAIResponses.yaml
@@ -1,28 +1,28 @@
kind: Prompt
name: Assistant
description: Helpful assistant
-instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
model:
- id: =Env.OPENAI_MODEL
+ id: gpt-4.1-mini
provider: OpenAI
apiType: Responses
options:
- text:
- verbosity: medium
+ temperature: 0.9
+ topP: 0.95
connection:
- kind: key
+ kind: ApiKey
key: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
- kind: string
+ type: string
required: true
description: The language of the answer.
answer:
- kind: string
+ type: string
required: true
description: The answer text.
type:
- kind: string
+ type: string
required: true
description: The type of the response.
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index b4a1ea3ff1..5d4a80026e 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -7,12 +7,12 @@
- 13.0.0
+ 13.0.1
-
-
+
+
@@ -25,6 +25,9 @@
+
+
+
@@ -97,7 +100,6 @@
-
@@ -160,17 +162,17 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 00763b09c8..816ffdb678 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -52,6 +52,7 @@
+
@@ -78,6 +79,10 @@
+
+
+
+
@@ -343,12 +348,13 @@
-
+
+
@@ -384,11 +390,12 @@
-
+
+
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj
index 002bd066fe..eb29d1d310 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj
@@ -11,6 +11,7 @@
+
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs
index 531b56e1a1..df070c335b 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs
@@ -2,10 +2,9 @@
// This sample shows how to create and use an AI agent with Anthropic as the backend.
-using System.ClientModel;
using System.Net.Http.Headers;
using Anthropic;
-using Anthropic.Core;
+using Anthropic.Foundry;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -15,8 +14,8 @@
// The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri
// ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions
-var resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
-var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
+string? resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
+string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
@@ -24,8 +23,8 @@
AnthropicClient? client = (resource is null)
? new AnthropicClient() { APIKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API
: (apiKey is not null)
- ? new AnthropicFoundryClient(resource, new ApiKeyCredential(apiKey)) // If an apiKey is provided, use Foundry with ApiKey authentication
- : new AnthropicFoundryClient(resource, new AzureCliCredential()); // Otherwise, use Foundry with Azure Client authentication
+ ? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication
+ : new AnthropicFoundryClient(new AnthropicAzureTokenCredential(new AzureCliCredential(), resource)); // Otherwise, use Foundry with Azure Client authentication
AIAgent agent = client.CreateAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName);
@@ -35,67 +34,41 @@
namespace Sample
{
///
- /// Provides methods for invoking the Azure hosted Anthropic api.
+ /// Provides methods for invoking the Azure hosted Anthropic models using types.
///
- public class AnthropicFoundryClient : AnthropicClient
+ public sealed class AnthropicAzureTokenCredential : IAnthropicFoundryCredentials
{
private readonly TokenCredential _tokenCredential;
- private readonly string _resourceName;
+ private readonly Lock _lock = new();
+ private AccessToken? _cachedAccessToken;
+
+ ///
+ public string ResourceName { get; }
///
- /// Creates a new instance of the .
+ /// Creates a new instance of the .
///
- /// The service resource subdomain name to use in the anthropic azure endpoint
/// The credential provider. Use any specialization of to get your access token in supported environments.
- /// Set of client option configurations
- /// Resource is null
- /// TokenCredential is null
- ///
- /// Any APIKey or Bearer token provided will be ignored in favor of the provided in the constructor
- ///
- public AnthropicFoundryClient(string resourceName, TokenCredential tokenCredential, Anthropic.Core.ClientOptions? options = null) : base(options ?? new())
+ /// The service resource subdomain name to use in the anthropic azure endpoint
+ internal AnthropicAzureTokenCredential(TokenCredential tokenCredential, string resourceName)
{
- this._resourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
+ this.ResourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
this._tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
- this.BaseUrl = new Uri($"https://{this._resourceName}.services.ai.azure.com/anthropic", UriKind.Absolute);
}
- ///
- /// Creates a new instance of the .
- ///
- /// The service resource subdomain name to use in the anthropic azure endpoint
- /// The api key.
- /// Set of client option configurations
- /// Resource is null
- /// Api key is null
- ///
- /// Any APIKey or Bearer token provided will be ignored in favor of the provided in the constructor
- ///
- public AnthropicFoundryClient(string resourceName, ApiKeyCredential apiKeyCredential, Anthropic.Core.ClientOptions? options = null) :
- this(resourceName, apiKeyCredential is null
- ? throw new ArgumentNullException(nameof(apiKeyCredential))
- : DelegatedTokenCredential.Create((_, _) =>
- {
- apiKeyCredential.Deconstruct(out string dangerousCredential);
- return new AccessToken(dangerousCredential, DateTimeOffset.MaxValue);
- }),
- options)
- { }
-
- public override IAnthropicClient WithOptions(Func modifier)
- => this;
-
- protected override ValueTask BeforeSend(
- HttpRequest request,
- HttpRequestMessage requestMessage,
- CancellationToken cancellationToken
- )
+ ///
+ public void Apply(HttpRequestMessage requestMessage)
{
- var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken);
-
- requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);
+ lock (this._lock)
+ {
+ // Add a 5-minute buffer to avoid using tokens that are about to expire
+ if (this._cachedAccessToken is null || this._cachedAccessToken.Value.ExpiresOn <= DateTimeOffset.Now.AddMinutes(5))
+ {
+ this._cachedAccessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), CancellationToken.None);
+ }
+ }
- return default;
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", this._cachedAccessToken.Value.Token);
}
}
}
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj
new file mode 100644
index 0000000000..cf6774e8d8
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+ $(NoWarn);IDE0059
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GeminiChatClient.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GeminiChatClient.cs
new file mode 100644
index 0000000000..2a1d47a456
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GeminiChatClient.cs
@@ -0,0 +1,558 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Runtime.CompilerServices;
+using Google.Apis.Util;
+using Google.GenAI;
+using Google.GenAI.Types;
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides an implementation based on .
+internal sealed class GoogleGenAIChatClient : IChatClient
+{
+ /// The wrapped instance (optional).
+ private readonly Client? _client;
+
+ /// The wrapped instance.
+ private readonly Models _models;
+
+ /// The default model that should be used when no override is specified.
+ private readonly string? _defaultModelId;
+
+ /// Lazily-initialized metadata describing the implementation.
+ private ChatClientMetadata? _metadata;
+
+ /// Initializes a new instance.
+ public GoogleGenAIChatClient(Client client, string? defaultModelId)
+ {
+ this._client = client;
+ this._models = client.Models;
+ this._defaultModelId = defaultModelId;
+ }
+
+ /// Initializes a new instance.
+ public GoogleGenAIChatClient(Models client, string? defaultModelId)
+ {
+ this._models = client;
+ this._defaultModelId = defaultModelId;
+ }
+
+ ///
+ public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Utilities.ThrowIfNull(messages, nameof(messages));
+
+ // Create the request.
+ (string? modelId, List contents, GenerateContentConfig config) = this.CreateRequest(messages, options);
+
+ // Send it.
+ GenerateContentResponse generateResult = await this._models.GenerateContentAsync(modelId!, contents, config).ConfigureAwait(false);
+
+ // Create the response.
+ ChatResponse chatResponse = new(new ChatMessage(ChatRole.Assistant, new List()))
+ {
+ CreatedAt = generateResult.CreateTime is { } dt ? new DateTimeOffset(dt) : null,
+ ModelId = !string.IsNullOrWhiteSpace(generateResult.ModelVersion) ? generateResult.ModelVersion : modelId,
+ RawRepresentation = generateResult,
+ ResponseId = generateResult.ResponseId,
+ };
+
+ // Populate the response messages.
+ chatResponse.FinishReason = PopulateResponseContents(generateResult, chatResponse.Messages[0].Contents);
+
+ // Populate usage information if there is any.
+ if (generateResult.UsageMetadata is { } usageMetadata)
+ {
+ chatResponse.Usage = ExtractUsageDetails(usageMetadata);
+ }
+
+ // Return the response.
+ return chatResponse;
+ }
+
+ ///
+ public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Utilities.ThrowIfNull(messages, nameof(messages));
+
+ // Create the request.
+ (string? modelId, List contents, GenerateContentConfig config) = this.CreateRequest(messages, options);
+
+ // Send it, and process the results.
+ await foreach (GenerateContentResponse generateResult in this._models.GenerateContentStreamAsync(modelId!, contents, config).WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ // Create a response update for each result in the stream.
+ ChatResponseUpdate responseUpdate = new(ChatRole.Assistant, new List())
+ {
+ CreatedAt = generateResult.CreateTime is { } dt ? new DateTimeOffset(dt) : null,
+ ModelId = !string.IsNullOrWhiteSpace(generateResult.ModelVersion) ? generateResult.ModelVersion : modelId,
+ RawRepresentation = generateResult,
+ ResponseId = generateResult.ResponseId,
+ };
+
+ // Populate the response update contents.
+ responseUpdate.FinishReason = PopulateResponseContents(generateResult, responseUpdate.Contents);
+
+ // Populate usage information if there is any.
+ if (generateResult.UsageMetadata is { } usageMetadata)
+ {
+ responseUpdate.Contents.Add(new UsageContent(ExtractUsageDetails(usageMetadata)));
+ }
+
+ // Yield the update.
+ yield return responseUpdate;
+ }
+ }
+
+ ///
+ public object? GetService(System.Type serviceType, object? serviceKey = null)
+ {
+ Utilities.ThrowIfNull(serviceType, nameof(serviceType));
+
+ if (serviceKey is null)
+ {
+ // If there's a request for metadata, lazily-initialize it and return it. We don't need to worry about race conditions,
+ // as there's no requirement that the same instance be returned each time, and creation is idempotent.
+ if (serviceType == typeof(ChatClientMetadata))
+ {
+ return this._metadata ??= new("gcp.gen_ai", new("https://generativelanguage.googleapis.com/"), defaultModelId: this._defaultModelId);
+ }
+
+ // Allow a consumer to "break glass" and access the underlying client if they need it.
+ if (serviceType.IsInstanceOfType(this._models))
+ {
+ return this._models;
+ }
+
+ if (this._client is not null && serviceType.IsInstanceOfType(this._client))
+ {
+ return this._client;
+ }
+
+ if (serviceType.IsInstanceOfType(this))
+ {
+ return this;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ void IDisposable.Dispose() { /* nop */ }
+
+ /// Creates the message parameters for from and .
+ private (string? ModelId, List Contents, GenerateContentConfig Config) CreateRequest(IEnumerable messages, ChatOptions? options)
+ {
+ // Create the GenerateContentConfig object. If the options contains a RawRepresentationFactory, try to use it to
+ // create the request instance, allowing the caller to populate it with GenAI-specific options. Otherwise, create
+ // a new instance directly.
+ string? model = this._defaultModelId;
+ List contents = new();
+ GenerateContentConfig config = options?.RawRepresentationFactory?.Invoke(this) as GenerateContentConfig ?? new();
+
+ if (options is not null)
+ {
+ if (options.FrequencyPenalty is { } frequencyPenalty)
+ {
+ config.FrequencyPenalty ??= frequencyPenalty;
+ }
+
+ if (options.Instructions is { } instructions)
+ {
+ ((config.SystemInstruction ??= new()).Parts ??= new()).Add(new() { Text = instructions });
+ }
+
+ if (options.MaxOutputTokens is { } maxOutputTokens)
+ {
+ config.MaxOutputTokens ??= maxOutputTokens;
+ }
+
+ if (!string.IsNullOrWhiteSpace(options.ModelId))
+ {
+ model = options.ModelId;
+ }
+
+ if (options.PresencePenalty is { } presencePenalty)
+ {
+ config.PresencePenalty ??= presencePenalty;
+ }
+
+ if (options.Seed is { } seed)
+ {
+ config.Seed ??= (int)seed;
+ }
+
+ if (options.StopSequences is { } stopSequences)
+ {
+ (config.StopSequences ??= new()).AddRange(stopSequences);
+ }
+
+ if (options.Temperature is { } temperature)
+ {
+ config.Temperature ??= temperature;
+ }
+
+ if (options.TopP is { } topP)
+ {
+ config.TopP ??= topP;
+ }
+
+ if (options.TopK is { } topK)
+ {
+ config.TopK ??= topK;
+ }
+
+ // Populate tools. Each kind of tool is added on its own, except for function declarations,
+ // which are grouped into a single FunctionDeclaration.
+ List? functionDeclarations = null;
+ if (options.Tools is { } tools)
+ {
+ foreach (var tool in tools)
+ {
+ switch (tool)
+ {
+ case AIFunctionDeclaration af:
+ functionDeclarations ??= new();
+ functionDeclarations.Add(new()
+ {
+ Name = af.Name,
+ Description = af.Description ?? "",
+ ParametersJsonSchema = af.JsonSchema,
+ });
+ break;
+
+ case HostedCodeInterpreterTool:
+ (config.Tools ??= new()).Add(new() { CodeExecution = new() });
+ break;
+
+ case HostedFileSearchTool:
+ (config.Tools ??= new()).Add(new() { Retrieval = new() });
+ break;
+
+ case HostedWebSearchTool:
+ (config.Tools ??= new()).Add(new() { GoogleSearch = new() });
+ break;
+ }
+ }
+ }
+
+ if (functionDeclarations is { Count: > 0 })
+ {
+ Tool functionTools = new();
+ (functionTools.FunctionDeclarations ??= new()).AddRange(functionDeclarations);
+ (config.Tools ??= new()).Add(functionTools);
+ }
+
+ // Transfer over the tool mode if there are any tools.
+ if (options.ToolMode is { } toolMode && config.Tools?.Count > 0)
+ {
+ switch (toolMode)
+ {
+ case NoneChatToolMode:
+ config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.NONE } };
+ break;
+
+ case AutoChatToolMode:
+ config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.AUTO } };
+ break;
+
+ case RequiredChatToolMode required:
+ config.ToolConfig = new() { FunctionCallingConfig = new() { Mode = FunctionCallingConfigMode.ANY } };
+ if (required.RequiredFunctionName is not null)
+ {
+ ((config.ToolConfig.FunctionCallingConfig ??= new()).AllowedFunctionNames ??= new()).Add(required.RequiredFunctionName);
+ }
+ break;
+ }
+ }
+
+ // Set the response format if specified.
+ if (options.ResponseFormat is ChatResponseFormatJson responseFormat)
+ {
+ config.ResponseMimeType = "application/json";
+ if (responseFormat.Schema is { } schema)
+ {
+ config.ResponseJsonSchema = schema;
+ }
+ }
+ }
+
+ // Transfer messages to request, handling system messages specially
+ Dictionary? callIdToFunctionNames = null;
+ foreach (var message in messages)
+ {
+ if (message.Role == ChatRole.System)
+ {
+ string instruction = message.Text;
+ if (!string.IsNullOrWhiteSpace(instruction))
+ {
+ ((config.SystemInstruction ??= new()).Parts ??= new()).Add(new() { Text = instruction });
+ }
+
+ continue;
+ }
+
+ Content content = new() { Role = message.Role == ChatRole.Assistant ? "model" : "user" };
+ content.Parts ??= new();
+ AddPartsForAIContents(ref callIdToFunctionNames, message.Contents, content.Parts);
+
+ contents.Add(content);
+ }
+
+ // Make sure the request contains at least one content part (the request would always fail if empty).
+ if (!contents.SelectMany(c => c.Parts ?? Enumerable.Empty()).Any())
+ {
+ contents.Add(new() { Role = "user", Parts = new() { { new() { Text = "" } } } });
+ }
+
+ return (model, contents, config);
+ }
+
+ /// Creates s for and adds them to .
+ private static void AddPartsForAIContents(ref Dictionary? callIdToFunctionNames, IList contents, List parts)
+ {
+ for (int i = 0; i < contents.Count; i++)
+ {
+ var content = contents[i];
+
+ byte[]? thoughtSignature = null;
+ if (content is not TextReasoningContent { ProtectedData: not null } &&
+ i + 1 < contents.Count &&
+ contents[i + 1] is TextReasoningContent nextReasoning &&
+ string.IsNullOrWhiteSpace(nextReasoning.Text) &&
+ nextReasoning.ProtectedData is { } protectedData)
+ {
+ i++;
+ thoughtSignature = Convert.FromBase64String(protectedData);
+ }
+
+ Part? part = null;
+ switch (content)
+ {
+ case TextContent textContent:
+ part = new() { Text = textContent.Text };
+ break;
+
+ case TextReasoningContent reasoningContent:
+ part = new()
+ {
+ Thought = true,
+ Text = !string.IsNullOrWhiteSpace(reasoningContent.Text) ? reasoningContent.Text : null,
+ ThoughtSignature = reasoningContent.ProtectedData is not null ? Convert.FromBase64String(reasoningContent.ProtectedData) : null,
+ };
+ break;
+
+ case DataContent dataContent:
+ part = new()
+ {
+ InlineData = new()
+ {
+ MimeType = dataContent.MediaType,
+ Data = dataContent.Data.ToArray(),
+ DisplayName = dataContent.Name,
+ }
+ };
+ break;
+
+ case UriContent uriContent:
+ part = new()
+ {
+ FileData = new()
+ {
+ FileUri = uriContent.Uri.AbsoluteUri,
+ MimeType = uriContent.MediaType,
+ }
+ };
+ break;
+
+ case FunctionCallContent functionCallContent:
+ (callIdToFunctionNames ??= new())[functionCallContent.CallId] = functionCallContent.Name;
+ callIdToFunctionNames[""] = functionCallContent.Name; // track last function name in case calls don't have IDs
+
+ part = new()
+ {
+ FunctionCall = new()
+ {
+ Id = functionCallContent.CallId,
+ Name = functionCallContent.Name,
+ Args = functionCallContent.Arguments is null ? null : functionCallContent.Arguments as Dictionary ?? new(functionCallContent.Arguments!),
+ }
+ };
+ break;
+
+ case FunctionResultContent functionResultContent:
+ part = new()
+ {
+ FunctionResponse = new()
+ {
+ Id = functionResultContent.CallId,
+ Name = callIdToFunctionNames?.TryGetValue(functionResultContent.CallId, out string? functionName) is true || callIdToFunctionNames?.TryGetValue("", out functionName) is true ?
+ functionName :
+ null,
+ Response = functionResultContent.Result is null ? null : new() { ["result"] = functionResultContent.Result },
+ }
+ };
+ break;
+ }
+
+ if (part is not null)
+ {
+ part.ThoughtSignature ??= thoughtSignature;
+ parts.Add(part);
+ }
+ }
+ }
+
+ /// Creates s for and adds them to .
+ private static void AddAIContentsForParts(List parts, IList contents)
+ {
+ foreach (var part in parts)
+ {
+ AIContent? content = null;
+
+ if (!string.IsNullOrEmpty(part.Text))
+ {
+ content = part.Thought is true ?
+ new TextReasoningContent(part.Text) :
+ new TextContent(part.Text);
+ }
+ else if (part.InlineData is { } inlineData)
+ {
+ content = new DataContent(inlineData.Data, inlineData.MimeType ?? "application/octet-stream")
+ {
+ Name = inlineData.DisplayName,
+ };
+ }
+ else if (part.FileData is { FileUri: not null } fileData)
+ {
+ content = new UriContent(new Uri(fileData.FileUri), fileData.MimeType ?? "application/octet-stream");
+ }
+ else if (part.FunctionCall is { Name: not null } functionCall)
+ {
+ content = new FunctionCallContent(functionCall.Id ?? "", functionCall.Name, functionCall.Args!);
+ }
+ else if (part.FunctionResponse is { } functionResponse)
+ {
+ content = new FunctionResultContent(
+ functionResponse.Id ?? "",
+ functionResponse.Response?.TryGetValue("output", out var output) is true ? output :
+ functionResponse.Response?.TryGetValue("error", out var error) is true ? error :
+ null);
+ }
+
+ if (content is not null)
+ {
+ content.RawRepresentation = part;
+ contents.Add(content);
+
+ if (part.ThoughtSignature is { } thoughtSignature)
+ {
+ contents.Add(new TextReasoningContent(null)
+ {
+ ProtectedData = Convert.ToBase64String(thoughtSignature),
+ });
+ }
+ }
+ }
+ }
+
+ private static ChatFinishReason? PopulateResponseContents(GenerateContentResponse generateResult, IList responseContents)
+ {
+ ChatFinishReason? finishReason = null;
+
+ // Populate the response messages. There should only be at most one candidate, but if there are more, ignore all but the first.
+ if (generateResult.Candidates is { Count: > 0 } &&
+ generateResult.Candidates[0] is { Content: { } candidateContent } candidate)
+ {
+ // Grab the finish reason if one exists.
+ finishReason = ConvertFinishReason(candidate.FinishReason);
+
+ // Add all of the response content parts as AIContents.
+ if (candidateContent.Parts is { } parts)
+ {
+ AddAIContentsForParts(parts, responseContents);
+ }
+
+ // Add any citation metadata.
+ if (candidate.CitationMetadata is { Citations: { Count: > 0 } citations } &&
+ responseContents.OfType().FirstOrDefault() is TextContent textContent)
+ {
+ foreach (var citation in citations)
+ {
+ textContent.Annotations = new List()
+ {
+ new CitationAnnotation()
+ {
+ Title = citation.Title,
+ Url = Uri.TryCreate(citation.Uri, UriKind.Absolute, out Uri? uri) ? uri : null,
+ AnnotatedRegions = new List()
+ {
+ new TextSpanAnnotatedRegion()
+ {
+ StartIndex = citation.StartIndex,
+ EndIndex = citation.EndIndex,
+ }
+ },
+ }
+ };
+ }
+ }
+ }
+
+ // Populate error information if there is any.
+ if (generateResult.PromptFeedback is { } promptFeedback)
+ {
+ responseContents.Add(new ErrorContent(promptFeedback.BlockReasonMessage));
+ }
+
+ return finishReason;
+ }
+
+ /// Creates an M.E.AI from a Google .
+ private static ChatFinishReason? ConvertFinishReason(FinishReason? finishReason)
+ {
+ return finishReason switch
+ {
+ null => null,
+
+ FinishReason.MAX_TOKENS =>
+ ChatFinishReason.Length,
+
+ FinishReason.MALFORMED_FUNCTION_CALL or
+ FinishReason.UNEXPECTED_TOOL_CALL =>
+ ChatFinishReason.ToolCalls,
+
+ FinishReason.FINISH_REASON_UNSPECIFIED or
+ FinishReason.STOP =>
+ ChatFinishReason.Stop,
+
+ _ => ChatFinishReason.ContentFilter,
+ };
+ }
+
+ /// Creates a populated from the supplied .
+ private static UsageDetails ExtractUsageDetails(GenerateContentResponseUsageMetadata usageMetadata)
+ {
+ UsageDetails details = new()
+ {
+ InputTokenCount = usageMetadata.PromptTokenCount,
+ OutputTokenCount = usageMetadata.CandidatesTokenCount,
+ TotalTokenCount = usageMetadata.TotalTokenCount,
+ };
+
+ AddIfPresent(nameof(usageMetadata.CachedContentTokenCount), usageMetadata.CachedContentTokenCount);
+ AddIfPresent(nameof(usageMetadata.ThoughtsTokenCount), usageMetadata.ThoughtsTokenCount);
+ AddIfPresent(nameof(usageMetadata.ToolUsePromptTokenCount), usageMetadata.ToolUsePromptTokenCount);
+
+ return details;
+
+ void AddIfPresent(string key, int? value)
+ {
+ if (value is int i)
+ {
+ (details.AdditionalCounts ??= new())[key] = i;
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GoogleGenAIExtensions.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GoogleGenAIExtensions.cs
new file mode 100644
index 0000000000..b1044fa373
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/GoogleGenAIExtensions.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Google.Apis.Util;
+using Google.GenAI;
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides implementations of Microsoft.Extensions.AI abstractions based on .
+public static class GoogleGenAIExtensions
+{
+ ///
+ /// Creates an wrapper around the specified .
+ ///
+ /// The to wrap.
+ /// The default model ID to use for chat requests if not specified in .
+ /// An that wraps the specified client.
+ /// is .
+ public static IChatClient AsIChatClient(this Client client, string? defaultModelId = null)
+ {
+ Utilities.ThrowIfNull(client, nameof(client));
+ return new GoogleGenAIChatClient(client, defaultModelId);
+ }
+
+ ///
+ /// Creates an wrapper around the specified .
+ ///
+ /// The client to wrap.
+ /// The default model ID to use for chat requests if not specified in .
+ /// An that wraps the specified client.
+ /// is .
+ public static IChatClient AsIChatClient(this Models models, string? defaultModelId = null)
+ {
+ Utilities.ThrowIfNull(models, nameof(models));
+ return new GoogleGenAIChatClient(models, defaultModelId);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Program.cs
new file mode 100644
index 0000000000..db633dc47d
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Program.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to create and use an AI agent with Google Gemini
+
+using Google.GenAI;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Mscc.GenerativeAI.Microsoft;
+
+const string JokerInstructions = "You are good at telling jokes.";
+const string JokerName = "JokerAgent";
+
+string apiKey = Environment.GetEnvironmentVariable("GOOGLE_GENAI_API_KEY") ?? throw new InvalidOperationException("Please set the GOOGLE_GENAI_API_KEY environment variable.");
+string model = Environment.GetEnvironmentVariable("GOOGLE_GENAI_MODEL") ?? "gemini-2.5-fast";
+
+// Using a Google GenAI IChatClient implementation
+// Until the PR https://github.com/googleapis/dotnet-genai/pull/81 is not merged this option
+// requires usage of also both GeminiChatClient.cs and GoogleGenAIExtensions.cs polyfills to work.
+
+ChatClientAgent agentGenAI = new(
+ new Client(vertexAI: false, apiKey: apiKey).AsIChatClient(model),
+ name: JokerName,
+ instructions: JokerInstructions);
+
+AgentRunResponse response = await agentGenAI.RunAsync("Tell me a joke about a pirate.");
+Console.WriteLine($"Google GenAI client based agent response:\n{response}");
+
+// Using a community driven Mscc.GenerativeAI.Microsoft package
+
+ChatClientAgent agentCommunity = new(
+ new GeminiChatClient(apiKey, model),
+ name: JokerName,
+ instructions: JokerInstructions);
+
+response = await agentCommunity.RunAsync("Tell me a joke about a pirate.");
+Console.WriteLine($"Community client based agent response:\n{response}");
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/README.md
new file mode 100644
index 0000000000..bc3a3592e6
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/README.md
@@ -0,0 +1,37 @@
+# Creating an AIAgent with Google Gemini
+
+This sample demonstrates how to create an AIAgent using Google Gemini models as the underlying inference service.
+
+The sample showcases two different `IChatClient` implementations:
+
+1. **Google GenAI** - Using the official [Google.GenAI](https://www.nuget.org/packages/Google.GenAI) package
+2. **Mscc.GenerativeAI.Microsoft** - Using the community-driven [Mscc.GenerativeAI.Microsoft](https://www.nuget.org/packages/Mscc.GenerativeAI.Microsoft) package
+
+## Prerequisites
+
+Before you begin, ensure you have the following prerequisites:
+
+- .NET 10.0 SDK or later
+- Google AI Studio API key (get one at [Google AI Studio](https://aistudio.google.com/apikey))
+
+Set the following environment variables:
+
+```powershell
+$env:GOOGLE_GENAI_API_KEY="your-google-api-key" # Replace with your Google AI Studio API key
+$env:GOOGLE_GENAI_MODEL="gemini-2.5-fast" # Optional, defaults to gemini-2.5-fast
+```
+
+## Package Options
+
+### Google GenAI (Official)
+
+The official Google GenAI package provides direct access to Google's Generative AI models. This sample uses an extension method to convert the Google client to an `IChatClient`.
+
+> [!NOTE]
+> Until PR [googleapis/dotnet-genai#81](https://github.com/googleapis/dotnet-genai/pull/81) is merged, this option requires the additional `GeminiChatClient.cs` and `GoogleGenAIExtensions.cs` files included in this sample.
+>
+> We appreciate any community push by liking and commenting in the above PR to get it merged and release as part of official Google GenAI package.
+
+### Mscc.GenerativeAI.Microsoft (Community)
+
+The community-driven Mscc.GenerativeAI.Microsoft package provides a ready-to-use `IChatClient` implementation for Google Gemini models through the `GeminiChatClient` class.
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs
index 9e4c27cebb..19e1fead1e 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/Program.cs
@@ -32,7 +32,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
- Instructions = "You are good at telling jokes.",
+ ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
AIContextProviderFactory = (ctx) => new ChatHistoryMemoryProvider(
vectorStore,
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs
index feacead4dd..87f5842e2c 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/Program.cs
@@ -30,7 +30,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions()
{
- Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details.",
+ ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
// If each thread should have its own Mem0 scope, you can create a new id per thread here:
// ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs
index ad59deb97f..4b9b1866a9 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step03_CustomMemory/Program.cs
@@ -33,7 +33,7 @@
// and its storage to that user id.
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
- Instructions = "You are a friendly assistant. Always address the user by their name.",
+ ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
AIContextProviderFactory = ctx => new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions)
});
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
index ec665325a7..a30e0371a0 100644
--- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
@@ -62,7 +62,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
- Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
+ ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs
index 89ced52b69..89312f8597 100644
--- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs
@@ -71,7 +71,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
- Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief.",
+ ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs
index e2caedbb1c..5e7b2c4132 100644
--- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/Program.cs
@@ -29,7 +29,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
- Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
+ ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Program.cs
index b18d8e2d84..ef1849fe02 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Program.cs
@@ -22,7 +22,7 @@
.GetChatClient(deploymentName);
// Create the ChatClientAgent with the specified name and instructions.
-ChatClientAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant."));
+ChatClientAgent agent = chatClient.CreateAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
// Set PersonInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke the agent with some unstructured input.
AgentRunResponse response = await agent.RunAsync("Please provide information about John Smith, who is a 35-year-old software engineer.");
@@ -34,12 +34,10 @@
Console.WriteLine($"Occupation: {response.Result.Occupation}");
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
-ChatClientAgent agentWithPersonInfo = chatClient.CreateAIAgent(new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant.")
+ChatClientAgent agentWithPersonInfo = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
- ChatOptions = new()
- {
- ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema()
- }
+ Name = "HelpfulAssistant",
+ ChatOptions = new() { Instructions = "You are a helpful assistant.", ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema() }
});
// Invoke the agent with some unstructured input while streaming, to extract the structured information from.
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
index 8986734972..d1316b6c80 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step07_3rdPartyThreadStorage/Program.cs
@@ -28,7 +28,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
- Instructions = "You are good at telling jokes.",
+ ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = ctx =>
{
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs
index 894c034eb0..d1b75d2fe5 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Program.cs
@@ -18,8 +18,7 @@
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Add agent options to the service collection.
-builder.Services.AddSingleton(
- new ChatClientAgentOptions(instructions: "You are good at telling jokes.", name: "Joker"));
+builder.Services.AddSingleton(new ChatClientAgentOptions() { Name = "Joker", ChatOptions = new() { Instructions = "You are good at telling jokes." } });
// Add a chat client to the service collection.
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs
index 590b5308d5..04704b5da0 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step16_ChatReduction/Program.cs
@@ -21,7 +21,7 @@
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
- Instructions = "You are good at telling jokes.",
+ ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
});
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj
new file mode 100644
index 0000000000..550e1f22cb
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Declarative/Agent_Step19_Declarative.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step19_Declarative/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Declarative/Program.cs
new file mode 100644
index 0000000000..1fc985b3bb
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Declarative/Program.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to create an agent from a YAML based declarative representation.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+// Create the chat client
+IChatClient chatClient = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .AsIChatClient();
+
+// Define the agent using a YAML definition.
+var text =
+ """
+ kind: Prompt
+ name: Assistant
+ description: Helpful assistant
+ instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
+ model:
+ options:
+ temperature: 0.9
+ topP: 0.95
+ outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ """;
+
+// Create the agent from the YAML definition.
+var agentFactory = new ChatClientPromptAgentFactory(chatClient);
+var agent = await agentFactory.CreateFromYamlAsync(text);
+
+// Invoke the agent and output the text result.
+Console.WriteLine(await agent!.RunAsync("Tell me a joke about a pirate in English."));
+
+// Invoke the agent with streaming support.
+await foreach (var update in agent!.RunStreamingAsync("Tell me a joke about a pirate in French."))
+{
+ Console.WriteLine(update);
+}
diff --git a/dotnet/samples/GettingStarted/Agents/README.md b/dotnet/samples/GettingStarted/Agents/README.md
index cbe4b65047..d023d6455c 100644
--- a/dotnet/samples/GettingStarted/Agents/README.md
+++ b/dotnet/samples/GettingStarted/Agents/README.md
@@ -45,6 +45,7 @@ Before you begin, ensure you have the following prerequisites:
|[Reducing chat history size](./Agent_Step16_ChatReduction/)|This sample demonstrates how to reduce the chat history to constrain its size, where chat history is maintained locally|
|[Background responses](./Agent_Step17_BackgroundResponses/)|This sample demonstrates how to use background responses for long-running operations with polling and resumption support|
|[Deep research with an agent](./Agent_Step18_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
+|[Declarative agent](./Agent_Step19_Declarative/)|This sample demonstrates how to declaratively define an agent.|
## Running the samples from the console
diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj
new file mode 100644
index 0000000000..0fc316acac
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Program.cs b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Program.cs
new file mode 100644
index 0000000000..bed16f496a
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Program.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to load an AI agent from a YAML file and process a prompt using Azure OpenAI as the backend.
+
+using System.ComponentModel;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+// Create the chat client
+IChatClient chatClient = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .AsIChatClient();
+
+// Read command-line arguments
+if (args.Length < 2)
+{
+ Console.WriteLine("Usage: DeclarativeAgents ");
+ Console.WriteLine(" : The path to the YAML file containing the agent definition");
+ Console.WriteLine(" : The prompt to send to the agent");
+ return;
+}
+
+var yamlFilePath = args[0];
+var prompt = args[1];
+
+// Verify the YAML file exists
+if (!File.Exists(yamlFilePath))
+{
+ Console.WriteLine($"Error: File not found: {yamlFilePath}");
+ return;
+}
+
+// Read the YAML content from the file
+var text = await File.ReadAllTextAsync(yamlFilePath);
+
+// Example function tool that can be used by the agent.
+[Description("Get the weather for a given location.")]
+static string GetWeather(
+ [Description("The city and state, e.g. San Francisco, CA")] string location,
+ [Description("The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.")] string unit)
+ => $"The weather in {location} is cloudy with a high of {(unit.Equals("celsius", StringComparison.Ordinal) ? "15°C" : "59°F")}.";
+
+// Create the agent from the YAML definition.
+var agentFactory = new ChatClientPromptAgentFactory(chatClient, [AIFunctionFactory.Create(GetWeather, "GetWeather")]);
+var agent = await agentFactory.CreateFromYamlAsync(text);
+
+// Invoke the agent and output the text result.
+Console.WriteLine(await agent!.RunAsync(prompt));
diff --git a/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json
new file mode 100644
index 0000000000..5ec486626c
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DeclarativeAgents/ChatClient/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "GetWeather": {
+ "commandName": "Project",
+ "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\chatclient\\GetWeather.yaml \"What is the weather in Cambridge, MA in °C?\""
+ },
+ "Assistant": {
+ "commandName": "Project",
+ "commandLineArgs": "..\\..\\..\\..\\..\\..\\..\\..\\agent-samples\\chatclient\\Assistant.yaml \"Tell me a joke about a pirate in Italian.\""
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/Program.cs
index 0edbed70e8..ac05565836 100644
--- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/Program.cs
+++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/Program.cs
@@ -24,10 +24,12 @@
// Create ChatClientAgent directly
ChatClientAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
- new ChatClientAgentOptions(name: AssistantName, instructions: AssistantInstructions)
+ new ChatClientAgentOptions()
{
+ Name = AssistantName,
ChatOptions = new()
{
+ Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema()
}
});
@@ -44,10 +46,12 @@
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
ChatClientAgent agentWithPersonInfo = aiProjectClient.CreateAIAgent(
model: deploymentName,
- new ChatClientAgentOptions(name: AssistantName, instructions: AssistantInstructions)
+ new ChatClientAgentOptions()
{
+ Name = AssistantName,
ChatOptions = new()
{
+ Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema()
}
});
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
index f824f09991..123d666f09 100644
--- a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
@@ -34,9 +34,9 @@
options: new()
{
Name = "MicrosoftLearnAgent",
- Instructions = "You answer questions by searching the Microsoft Learn content only.",
ChatOptions = new()
{
+ Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = [mcpTool]
},
});
@@ -67,9 +67,9 @@
options: new()
{
Name = "MicrosoftLearnAgentWithApproval",
- Instructions = "You answer questions by searching the Microsoft Learn content only.",
ChatOptions = new()
{
+ Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = [mcpToolWithApproval]
},
});
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs
index 5d5369883c..91f58f460e 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/Program.cs
@@ -118,10 +118,11 @@ internal sealed class SloganWriterExecutor : Executor
/// The chat client to use for the AI agent.
public SloganWriterExecutor(string id, IChatClient chatClient) : base(id)
{
- ChatClientAgentOptions agentOptions = new(instructions: "You are a professional slogan writer. You will be given a task to create a slogan.")
+ ChatClientAgentOptions agentOptions = new()
{
ChatOptions = new()
{
+ Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
};
@@ -193,10 +194,11 @@ internal sealed class FeedbackExecutor : Executor
/// The chat client to use for the AI agent.
public FeedbackExecutor(string id, IChatClient chatClient) : base(id)
{
- ChatClientAgentOptions agentOptions = new(instructions: "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.")
+ ChatClientAgentOptions agentOptions = new()
{
ChatOptions = new()
{
+ Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
};
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs
index b6e3d4d513..0f762ea40d 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/Program.cs
@@ -85,10 +85,11 @@ private static async Task Main()
///
/// A ChatClientAgent configured for spam detection
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are a spam detection assistant that identifies spam emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
@@ -98,10 +99,11 @@ private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
///
/// A ChatClientAgent configured for email assistance
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Program.cs
index 13f0a75bc2..ccda3fa19e 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/Program.cs
@@ -100,10 +100,11 @@ private static async Task Main()
///
/// A ChatClientAgent configured for spam detection
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
@@ -113,10 +114,11 @@ private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
///
/// A ChatClientAgent configured for email assistance
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs
index 9d340cbae3..49faff39da 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs
@@ -140,10 +140,11 @@ private static async Task Main()
///
/// A ChatClientAgent configured for email analysis
private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are a spam detection assistant that identifies spam emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
@@ -153,10 +154,11 @@ private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient) =>
///
/// A ChatClientAgent configured for email assistance
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
@@ -166,10 +168,11 @@ private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
///
/// A ChatClientAgent configured for email summarization
private static ChatClientAgent GetEmailSummaryAgent(IChatClient chatClient) =>
- new(chatClient, new ChatClientAgentOptions(instructions: "You are an assistant that helps users summarize emails.")
+ new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
+ Instructions = "You are an assistant that helps users summarize emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj
index bc113c9f26..65d85d21af 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs
index c90131a27c..8b4ac7645d 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/Program.cs
@@ -12,19 +12,16 @@
IChatClient aws = new AmazonBedrockRuntimeClient(
Environment.GetEnvironmentVariable("BEDROCK_ACCESSKEY"!),
Environment.GetEnvironmentVariable("BEDROCK_SECRETACCESSKEY")!,
- Amazon.RegionEndpoint.USEast1).AsIChatClient("amazon.nova-pro-v1:0");
+ Amazon.RegionEndpoint.USEast1)
+ .AsIChatClient("amazon.nova-pro-v1:0");
-IChatClient anthropic = new Anthropic.SDK.AnthropicClient(
- Environment.GetEnvironmentVariable("ANTHROPIC_APIKEY")!).Messages.AsBuilder()
- .ConfigureOptions(o =>
- {
- o.ModelId ??= "claude-sonnet-4-20250514";
- o.MaxOutputTokens ??= 10 * 1024;
- })
- .Build();
+IChatClient anthropic = new Anthropic.AnthropicClient(
+ new() { APIKey = Environment.GetEnvironmentVariable("ANTHROPIC_APIKEY") })
+ .AsIChatClient("claude-sonnet-4-20250514");
IChatClient openai = new OpenAI.OpenAIClient(
- Environment.GetEnvironmentVariable("OPENAI_APIKEY")!).GetChatClient("gpt-4o-mini").AsIChatClient();
+ Environment.GetEnvironmentVariable("OPENAI_APIKEY")!).GetChatClient("gpt-4o-mini")
+ .AsIChatClient();
// Define our agents.
AIAgent researcher = new ChatClientAgent(aws,
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs
index d9cc30f395..265a87b5f6 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs
@@ -285,19 +285,19 @@ public CriticExecutor(IChatClient chatClient) : base("Critic")
this._agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "Critic",
- Instructions = """
- You are a constructive critic. Review the content and provide specific feedback.
- Always try to provide actionable suggestions for improvement and strive to identify improvement points.
- Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
-
- Provide your decision as structured output with:
- - approved: true if content is good, false if revisions needed
- - feedback: specific improvements needed (empty if approved)
-
- Be concise but specific in your feedback.
- """,
ChatOptions = new()
{
+ Instructions = """
+ You are a constructive critic. Review the content and provide specific feedback.
+ Always try to provide actionable suggestions for improvement and strive to identify improvement points.
+ Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
+
+ Provide your decision as structured output with:
+ - approved: true if content is good, false if revisions needed
+ - feedback: specific improvements needed (empty if approved)
+
+ Be concise but specific in your feedback.
+ """,
ResponseFormat = ChatResponseFormat.ForJsonSchema()
}
});
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj b/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj
index 5a8ffecf8c..60808f4051 100644
--- a/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -37,15 +37,15 @@
-
+
-
-
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -53,15 +53,15 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile b/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile
index 776f81041e..a2590fc112 100644
--- a/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile
@@ -1,5 +1,5 @@
# Build the application
-FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
+FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
# Copy files from the current directory on the host to the working directory in the container
@@ -7,10 +7,10 @@ COPY . .
RUN dotnet restore
RUN dotnet build -c Release --no-restore
-RUN dotnet publish -c Release --no-build -o /app
+RUN dotnet publish -c Release --no-build -o /app -f net10.0
# Run the application
-FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
# Copy everything needed to run the app from the "build" stage.
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
index 4843ab3c3b..1cc019a196 100644
--- a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -36,15 +36,15 @@
-
+
-
-
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -52,15 +52,15 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile
index b494ad2254..3d944c9883 100644
--- a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile
@@ -1,5 +1,5 @@
# Build the application
-FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
+FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
# Copy files from the current directory on the host to the working directory in the container
@@ -7,10 +7,10 @@ COPY . .
RUN dotnet restore
RUN dotnet build -c Release --no-restore
-RUN dotnet publish -c Release --no-build -o /app
+RUN dotnet publish -c Release --no-build -o /app -f net10.0
# Run the application
-FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
# Copy everything needed to run the app from the "build" stage.
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
index e9dd730ecc..1891ebab9d 100644
--- a/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -36,15 +36,15 @@
-
+
-
-
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -52,15 +52,15 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile b/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile
index 0d3e5757cd..86b6c156f3 100644
--- a/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile
@@ -1,5 +1,5 @@
# Build the application
-FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
+FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
# Copy files from the current directory on the host to the working directory in the container
@@ -7,10 +7,10 @@ COPY . .
RUN dotnet restore
RUN dotnet build -c Release --no-restore
-RUN dotnet publish -c Release --no-build -o /app
+RUN dotnet publish -c Release --no-build -o /app -f net10.0
# Run the application
-FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
# Copy everything needed to run the app from the "build" stage.
diff --git a/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs b/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
index e133170b36..740b959a7a 100644
--- a/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
+++ b/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
@@ -33,9 +33,9 @@ public WeatherForecastAgent(IChatClient chatClient)
new ChatClientAgentOptions()
{
Name = AgentName,
- Instructions = AgentInstructions,
ChatOptions = new ChatOptions()
{
+ Instructions = AgentInstructions,
Tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))],
// We want the agent to return structured output in a known format
// so that we can easily create adaptive cards from the response.
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs
index a9396ff722..f64177146f 100644
--- a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs
@@ -32,7 +32,7 @@ internal sealed class RunAgentInput
[JsonPropertyName("context")]
public AGUIContextItem[] Context { get; set; } = [];
- [JsonPropertyName("forwardedProperties")]
+ [JsonPropertyName("forwardedProps")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonElement ForwardedProperties { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs
index 9bf698bded..6b4f872a63 100644
--- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs
@@ -45,14 +45,20 @@ public static ChatClientAgent CreateAIAgent(
{
var options = new ChatClientAgentOptions
{
- Instructions = instructions,
Name = name,
Description = description,
};
+ if (!string.IsNullOrWhiteSpace(instructions))
+ {
+ options.ChatOptions ??= new();
+ options.ChatOptions.Instructions = instructions;
+ }
+
if (tools is { Count: > 0 })
{
- options.ChatOptions = new ChatOptions { Tools = tools };
+ options.ChatOptions ??= new();
+ options.ChatOptions.Tools = tools;
}
var chatClient = betaService.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs
index f43f4bd0ce..b4b8e2bc1e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs
@@ -45,14 +45,20 @@ public static ChatClientAgent CreateAIAgent(
{
var options = new ChatClientAgentOptions
{
- Instructions = instructions,
Name = name,
Description = description,
};
+ if (!string.IsNullOrWhiteSpace(instructions))
+ {
+ options.ChatOptions ??= new();
+ options.ChatOptions.Instructions = instructions;
+ }
+
if (tools is { Count: > 0 })
{
- options.ChatOptions = new ChatOptions { Tools = tools };
+ options.ChatOptions ??= new();
+ options.ChatOptions.Tools = tools;
}
var chatClient = client.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs
index ddb1ee7840..5ca1436587 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs
@@ -17,15 +17,21 @@ public static class PersistentAgentsClientExtensions
/// The response containing the persistent agent to be converted. Cannot be .
/// The default to use when interacting with the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the persistent agent.
- public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, Response persistentAgentResponse, ChatOptions? chatOptions = null, Func? clientFactory = null)
+ public static ChatClientAgent GetAIAgent(
+ this PersistentAgentsClient persistentAgentsClient,
+ Response persistentAgentResponse,
+ ChatOptions? chatOptions = null,
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (persistentAgentResponse is null)
{
throw new ArgumentNullException(nameof(persistentAgentResponse));
}
- return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, chatOptions, clientFactory);
+ return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, chatOptions, clientFactory, services);
}
///
@@ -35,8 +41,14 @@ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentA
/// The persistent agent metadata to be converted. Cannot be .
/// The default to use when interacting with the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the persistent agent.
- public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatOptions? chatOptions = null, Func? clientFactory = null)
+ public static ChatClientAgent GetAIAgent(
+ this PersistentAgentsClient persistentAgentsClient,
+ PersistentAgent persistentAgentMetadata,
+ ChatOptions? chatOptions = null,
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (persistentAgentMetadata is null)
{
@@ -55,14 +67,19 @@ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentA
chatClient = clientFactory(chatClient);
}
+ if (!string.IsNullOrWhiteSpace(persistentAgentMetadata.Instructions) && chatOptions?.Instructions is null)
+ {
+ chatOptions ??= new ChatOptions();
+ chatOptions.Instructions = persistentAgentMetadata.Instructions;
+ }
+
return new ChatClientAgent(chatClient, options: new()
{
Id = persistentAgentMetadata.Id,
Name = persistentAgentMetadata.Name,
Description = persistentAgentMetadata.Description,
- Instructions = persistentAgentMetadata.Instructions,
ChatOptions = chatOptions
- });
+ }, services: services);
}
///
@@ -73,6 +90,7 @@ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentA
/// The ID of the server side agent to create a for.
/// Options that should apply to all runs of the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the persistent agent.
public static ChatClientAgent GetAIAgent(
@@ -80,6 +98,7 @@ public static ChatClientAgent GetAIAgent(
string agentId,
ChatOptions? chatOptions = null,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -93,7 +112,7 @@ public static ChatClientAgent GetAIAgent(
}
var persistentAgentResponse = persistentAgentsClient.Administration.GetAgent(agentId, cancellationToken);
- return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory);
+ return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory, services);
}
///
@@ -104,6 +123,7 @@ public static ChatClientAgent GetAIAgent(
/// The ID of the server side agent to create a for.
/// Options that should apply to all runs of the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the persistent agent.
public static async Task GetAIAgentAsync(
@@ -111,6 +131,7 @@ public static async Task GetAIAgentAsync(
string agentId,
ChatOptions? chatOptions = null,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -124,7 +145,7 @@ public static async Task GetAIAgentAsync(
}
var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false);
- return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory);
+ return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory, services);
}
///
@@ -134,16 +155,22 @@ public static async Task GetAIAgentAsync(
/// The response containing the persistent agent to be converted. Cannot be .
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the persistent agent.
/// Thrown when or is .
- public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, Response persistentAgentResponse, ChatClientAgentOptions options, Func? clientFactory = null)
+ public static ChatClientAgent GetAIAgent(
+ this PersistentAgentsClient persistentAgentsClient,
+ Response persistentAgentResponse,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (persistentAgentResponse is null)
{
throw new ArgumentNullException(nameof(persistentAgentResponse));
}
- return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, options, clientFactory);
+ return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, options, clientFactory, services);
}
///
@@ -153,9 +180,15 @@ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentA
/// The persistent agent metadata to be converted. Cannot be .
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the persistent agent.
/// Thrown when or is .
- public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatClientAgentOptions options, Func? clientFactory = null)
+ public static ChatClientAgent GetAIAgent(
+ this PersistentAgentsClient persistentAgentsClient,
+ PersistentAgent persistentAgentMetadata,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (persistentAgentMetadata is null)
{
@@ -179,19 +212,24 @@ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentA
chatClient = clientFactory(chatClient);
}
+ if (!string.IsNullOrWhiteSpace(persistentAgentMetadata.Instructions) && options.ChatOptions?.Instructions is null)
+ {
+ options.ChatOptions ??= new ChatOptions();
+ options.ChatOptions.Instructions = persistentAgentMetadata.Instructions;
+ }
+
var agentOptions = new ChatClientAgentOptions()
{
Id = persistentAgentMetadata.Id,
Name = options.Name ?? persistentAgentMetadata.Name,
Description = options.Description ?? persistentAgentMetadata.Description,
- Instructions = options.Instructions ?? persistentAgentMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
};
- return new ChatClientAgent(chatClient, agentOptions);
+ return new ChatClientAgent(chatClient, agentOptions, services: services);
}
///
@@ -201,6 +239,7 @@ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentA
/// The ID of the server side agent to create a for.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the persistent agent.
/// Thrown when or is .
@@ -210,6 +249,7 @@ public static ChatClientAgent GetAIAgent(
string agentId,
ChatClientAgentOptions options,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -228,7 +268,7 @@ public static ChatClientAgent GetAIAgent(
}
var persistentAgentResponse = persistentAgentsClient.Administration.GetAgent(agentId, cancellationToken);
- return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory);
+ return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory, services);
}
///
@@ -238,6 +278,7 @@ public static ChatClientAgent GetAIAgent(
/// The ID of the server side agent to create a for.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the persistent agent.
/// Thrown when or is .
@@ -247,6 +288,7 @@ public static async Task GetAIAgentAsync(
string agentId,
ChatClientAgentOptions options,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -265,7 +307,7 @@ public static async Task GetAIAgentAsync(
}
var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false);
- return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory);
+ return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory, services);
}
///
@@ -283,6 +325,7 @@ public static async Task GetAIAgentAsync(
/// The response format for the agent.
/// The metadata for the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the newly created agent.
public static async Task CreateAIAgentAsync(
@@ -298,6 +341,7 @@ public static async Task CreateAIAgentAsync(
BinaryData? responseFormat = null,
IReadOnlyDictionary? metadata = null,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -319,7 +363,7 @@ public static async Task CreateAIAgentAsync(
cancellationToken: cancellationToken).ConfigureAwait(false);
// Get a local proxy for the agent to work with.
- return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, clientFactory: clientFactory, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, clientFactory: clientFactory, services: services, cancellationToken: cancellationToken).ConfigureAwait(false);
}
///
@@ -337,6 +381,7 @@ public static async Task CreateAIAgentAsync(
/// The response format for the agent.
/// The metadata for the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the newly created agent.
public static ChatClientAgent CreateAIAgent(
@@ -352,6 +397,7 @@ public static ChatClientAgent CreateAIAgent(
BinaryData? responseFormat = null,
IReadOnlyDictionary? metadata = null,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -373,7 +419,7 @@ public static ChatClientAgent CreateAIAgent(
cancellationToken: cancellationToken);
// Get a local proxy for the agent to work with.
- return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, clientFactory: clientFactory, cancellationToken: cancellationToken);
+ return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, clientFactory: clientFactory, services: services, cancellationToken: cancellationToken);
}
///
@@ -383,6 +429,7 @@ public static ChatClientAgent CreateAIAgent(
/// The model to be used by the agent.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the newly created agent.
/// Thrown when or or is .
@@ -392,6 +439,7 @@ public static ChatClientAgent CreateAIAgent(
string model,
ChatClientAgentOptions options,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -415,7 +463,7 @@ public static ChatClientAgent CreateAIAgent(
model: model,
name: options.Name,
description: options.Description,
- instructions: options.Instructions,
+ instructions: options.ChatOptions?.Instructions,
tools: toolDefinitionsAndResources.ToolDefinitions,
toolResources: toolDefinitionsAndResources.ToolResources,
temperature: null,
@@ -431,7 +479,7 @@ public static ChatClientAgent CreateAIAgent(
}
// Get a local proxy for the agent to work with.
- return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, cancellationToken: cancellationToken);
+ return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, services: services, cancellationToken: cancellationToken);
}
///
@@ -441,6 +489,7 @@ public static ChatClientAgent CreateAIAgent(
/// The model to be used by the agent.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the newly created agent.
/// Thrown when or or is .
@@ -450,6 +499,7 @@ public static async Task CreateAIAgentAsync(
string model,
ChatClientAgentOptions options,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
@@ -473,7 +523,7 @@ public static async Task CreateAIAgentAsync(
model: model,
name: options.Name,
description: options.Description,
- instructions: options.Instructions,
+ instructions: options.ChatOptions?.Instructions,
tools: toolDefinitionsAndResources.ToolDefinitions,
toolResources: toolDefinitionsAndResources.ToolResources,
temperature: null,
@@ -489,7 +539,7 @@ public static async Task CreateAIAgentAsync(
}
// Get a local proxy for the agent to work with.
- return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, services: services, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static (List? ToolDefinitions, ToolResources? ToolResources, List? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList? tools)
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
index 0ec5f593fd..dfbdad8e98 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
@@ -393,7 +393,7 @@ public static ChatClientAgent CreateAIAgent(
PromptAgentDefinition agentDefinition = new(model)
{
- Instructions = options.Instructions,
+ Instructions = options.ChatOptions?.Instructions,
Temperature = options.ChatOptions?.Temperature,
TopP = options.ChatOptions?.TopP,
TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) }
@@ -459,7 +459,7 @@ public static async Task CreateAIAgentAsync(
PromptAgentDefinition agentDefinition = new(model)
{
- Instructions = options.Instructions,
+ Instructions = options.ChatOptions?.Instructions,
Temperature = options.ChatOptions?.Temperature,
TopP = options.ChatOptions?.TopP,
TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) }
@@ -822,10 +822,9 @@ private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion
if (agentDefinition is PromptAgentDefinition promptAgentDefinition)
{
agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new();
- agentOptions.Instructions = promptAgentDefinition.Instructions;
+ agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
agentOptions.ChatOptions.Temperature = promptAgentDefinition.Temperature;
agentOptions.ChatOptions.TopP = promptAgentDefinition.TopP;
- agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
}
if (agentTools is { Count: > 0 })
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs
new file mode 100644
index 0000000000..808bf76462
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AgentBotElementYaml.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Bot.ObjectModel.Abstractions;
+using Microsoft.Bot.ObjectModel.Yaml;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Helper methods for creating from YAML.
+///
+internal static class AgentBotElementYaml
+{
+ ///
+ /// Convert the given YAML text to a model.
+ ///
+ /// YAML representation of the to use to create the prompt function.
+ /// Optional instance which provides environment variables to the template.
+ [RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
+ public static GptComponentMetadata FromYaml(string text, IConfiguration? configuration = null)
+ {
+ Throw.IfNullOrEmpty(text);
+
+ using var yamlReader = new StringReader(text);
+ BotElement rootElement = YamlSerializer.Deserialize(yamlReader) ?? throw new InvalidDataException("Text does not contain a valid agent definition.");
+
+ if (rootElement is not GptComponentMetadata promptAgent)
+ {
+ throw new InvalidDataException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(GptComponentMetadata)}.");
+ }
+
+ var botDefinition = WrapPromptAgentWithBot(promptAgent, configuration);
+
+ return botDefinition.Descendants().OfType().First();
+ }
+
+ #region private
+ private sealed class AgentFeatureConfiguration : IFeatureConfiguration
+ {
+ public long GetInt64Value(string settingName, long defaultValue) => defaultValue;
+
+ public string GetStringValue(string settingName, string defaultValue) => defaultValue;
+
+ public bool IsEnvironmentFeatureEnabled(string featureName, bool defaultValue) => true;
+
+ public bool IsTenantFeatureEnabled(string featureName, bool defaultValue) => defaultValue;
+ }
+
+ public static BotDefinition WrapPromptAgentWithBot(this GptComponentMetadata element, IConfiguration? configuration = null)
+ {
+ var botBuilder =
+ new BotDefinition.Builder
+ {
+ Components =
+ {
+ new GptComponent.Builder
+ {
+ SchemaName = "default-schema",
+ Metadata = element.ToBuilder(),
+ }
+ }
+ };
+
+ if (configuration is not null)
+ {
+ foreach (var kvp in configuration.AsEnumerable().Where(kvp => kvp.Value is not null))
+ {
+ botBuilder.EnvironmentVariables.Add(new EnvironmentVariableDefinition.Builder()
+ {
+ SchemaName = kvp.Key,
+ Id = Guid.NewGuid(),
+ DisplayName = kvp.Key,
+ ValueComponent = new EnvironmentVariableValue.Builder()
+ {
+ Id = Guid.NewGuid(),
+ Value = kvp.Value!,
+ },
+ });
+ }
+ }
+
+ return botBuilder.Build();
+ }
+ #endregion
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/AggregatorPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/AggregatorPromptAgentFactory.cs
new file mode 100644
index 0000000000..49027367f1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/AggregatorPromptAgentFactory.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides a which aggregates multiple agent factories.
+///
+public sealed class AggregatorPromptAgentFactory : PromptAgentFactory
+{
+ private readonly PromptAgentFactory[] _agentFactories;
+
+ /// Initializes the instance.
+ /// Ordered instances to aggregate.
+ ///
+ /// Where multiple instances are provided, the first factory that supports the will be used.
+ ///
+ public AggregatorPromptAgentFactory(params PromptAgentFactory[] agentFactories)
+ {
+ Throw.IfNullOrEmpty(agentFactories);
+
+ foreach (PromptAgentFactory agentFactory in agentFactories)
+ {
+ Throw.IfNull(agentFactory, nameof(agentFactories));
+ }
+
+ this._agentFactories = agentFactories;
+ }
+
+ ///
+ public override async Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(promptAgent);
+
+ foreach (var agentFactory in this._agentFactories)
+ {
+ var agent = await agentFactory.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false);
+ if (agent is not null)
+ {
+ return agent;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
new file mode 100644
index 0000000000..a7918de051
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/ChatClient/ChatClientPromptAgentFactory.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerFx;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Provides an which creates instances of .
+///
+public sealed class ChatClientPromptAgentFactory : PromptAgentFactory
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ChatClientPromptAgentFactory(IChatClient chatClient, IList? functions = null, RecalcEngine? engine = null, IConfiguration? configuration = null, ILoggerFactory? loggerFactory = null) : base(engine, configuration)
+ {
+ Throw.IfNull(chatClient);
+
+ this._chatClient = chatClient;
+ this._functions = functions;
+ this._loggerFactory = loggerFactory;
+ }
+
+ ///
+ public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(promptAgent);
+
+ var options = new ChatClientAgentOptions()
+ {
+ Name = promptAgent.Name,
+ Description = promptAgent.Description,
+ ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions),
+ };
+
+ var agent = new ChatClientAgent(this._chatClient, options, this._loggerFactory);
+
+ return Task.FromResult(agent);
+ }
+
+ #region private
+ private readonly IChatClient _chatClient;
+ private readonly IList? _functions;
+ private readonly ILoggerFactory? _loggerFactory;
+ #endregion
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs
new file mode 100644
index 0000000000..9926e0e6be
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/BoolExpressionExtensions.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class BoolExpressionExtensions
+{
+ ///
+ /// Evaluates the given using the provided .
+ ///
+ /// Expression to evaluate.
+ /// Recalc engine to use for evaluation.
+ /// The evaluated boolean value, or null if the expression is null or cannot be evaluated.
+ internal static bool? Eval(this BoolExpression? expression, RecalcEngine? engine)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ if (expression.IsLiteral)
+ {
+ return expression.LiteralValue;
+ }
+
+ if (engine is null)
+ {
+ return null;
+ }
+
+ if (expression.IsExpression)
+ {
+ return engine.Eval(expression.ExpressionText!).AsBoolean();
+ }
+ else if (expression.IsVariableReference)
+ {
+ var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
+ if (formulaValue is BooleanValue booleanValue)
+ {
+ return booleanValue.Value;
+ }
+
+ if (formulaValue is StringValue stringValue && bool.TryParse(stringValue.Value, out bool result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/CodeInterpreterToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/CodeInterpreterToolExtensions.cs
new file mode 100644
index 0000000000..e6f13d5f54
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/CodeInterpreterToolExtensions.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class CodeInterpreterToolExtensions
+{
+ ///
+ /// Creates a from a .
+ ///
+ /// Instance of
+ internal static HostedCodeInterpreterTool AsCodeInterpreterTool(this CodeInterpreterTool tool)
+ {
+ Throw.IfNull(tool);
+
+ return new HostedCodeInterpreterTool();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FileSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FileSearchToolExtensions.cs
new file mode 100644
index 0000000000..5e1cb1bb5f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FileSearchToolExtensions.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Linq;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class FileSearchToolExtensions
+{
+ ///
+ /// Create a from a .
+ ///
+ /// Instance of
+ internal static HostedFileSearchTool CreateFileSearchTool(this FileSearchTool tool)
+ {
+ Throw.IfNull(tool);
+
+ return new HostedFileSearchTool()
+ {
+ MaximumResultCount = (int?)tool.MaximumResultCount?.LiteralValue,
+ Inputs = tool.VectorStoreIds?.LiteralValue.Select(id => (AIContent)new HostedVectorStoreContent(id)).ToList(),
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FunctionToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FunctionToolExtensions.cs
new file mode 100644
index 0000000000..2c54d7e749
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/FunctionToolExtensions.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class FunctionToolExtensions
+{
+ ///
+ /// Creates a from a .
+ ///
+ ///
+ /// If a matching function already exists in the provided list, it will be returned.
+ /// Otherwise, a new function declaration will be created.
+ ///
+ /// Instance of
+ /// Instance of
+ internal static AITool CreateOrGetAITool(this InvokeClientTaskAction tool, IList? functions)
+ {
+ Throw.IfNull(tool);
+ Throw.IfNull(tool.Name);
+
+ // use the tool from the provided list if it exists
+ if (functions is not null)
+ {
+ var function = functions.FirstOrDefault(f => tool.Matches(f));
+
+ if (function is not null)
+ {
+ return function;
+ }
+ }
+
+ return AIFunctionFactory.CreateDeclaration(
+ name: tool.Name,
+ description: tool.Description,
+ jsonSchema: tool.ClientActionInputSchema?.GetSchema() ?? s_defaultSchema);
+ }
+
+ ///
+ /// Checks if a matches an .
+ ///
+ /// Instance of
+ /// Instance of
+ internal static bool Matches(this InvokeClientTaskAction tool, AIFunction aiFunc)
+ {
+ Throw.IfNull(tool);
+ Throw.IfNull(aiFunc);
+
+ return tool.Name == aiFunc.Name;
+ }
+
+ private static readonly JsonElement s_defaultSchema = JsonDocument.Parse("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}").RootElement;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs
new file mode 100644
index 0000000000..479d6ccea3
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/IntExpressionExtensions.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Globalization;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class IntExpressionExtensions
+{
+ ///
+ /// Evaluates the given using the provided .
+ ///
+ /// Expression to evaluate.
+ /// Recalc engine to use for evaluation.
+ /// The evaluated integer value, or null if the expression is null or cannot be evaluated.
+ internal static long? Eval(this IntExpression? expression, RecalcEngine? engine)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ if (expression.IsLiteral)
+ {
+ return expression.LiteralValue;
+ }
+
+ if (engine is null)
+ {
+ return null;
+ }
+
+ if (expression.IsExpression)
+ {
+ return (long)engine.Eval(expression.ExpressionText!).AsDouble();
+ }
+ else if (expression.IsVariableReference)
+ {
+ var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
+ if (formulaValue is NumberValue numberValue)
+ {
+ return (long)numberValue.Value;
+ }
+
+ if (formulaValue is StringValue stringValue && int.TryParse(stringValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolApprovalModeExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolApprovalModeExtensions.cs
new file mode 100644
index 0000000000..ee5632368b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolApprovalModeExtensions.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class McpServerToolApprovalModeExtensions
+{
+ ///
+ /// Converts a to a .
+ ///
+ /// Instance of
+ internal static HostedMcpServerToolApprovalMode AsHostedMcpServerToolApprovalMode(this McpServerToolApprovalMode mode)
+ {
+ return mode switch
+ {
+ McpServerToolNeverRequireApprovalMode => HostedMcpServerToolApprovalMode.NeverRequire,
+ McpServerToolAlwaysRequireApprovalMode => HostedMcpServerToolApprovalMode.AlwaysRequire,
+ McpServerToolRequireSpecificApprovalMode specificMode =>
+ HostedMcpServerToolApprovalMode.RequireSpecific(
+ specificMode?.AlwaysRequireApprovalToolNames?.LiteralValue ?? [],
+ specificMode?.NeverRequireApprovalToolNames?.LiteralValue ?? []
+ ),
+ _ => HostedMcpServerToolApprovalMode.AlwaysRequire,
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolExtensions.cs
new file mode 100644
index 0000000000..763e402625
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/McpServerToolExtensions.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class McpServerToolExtensions
+{
+ ///
+ /// Creates a from a .
+ ///
+ /// Instance of
+ internal static HostedMcpServerTool CreateHostedMcpTool(this McpServerTool tool)
+ {
+ Throw.IfNull(tool);
+ Throw.IfNull(tool.ServerName?.LiteralValue);
+ Throw.IfNull(tool.Connection);
+
+ var connection = tool.Connection as AnonymousConnection ?? throw new ArgumentException("Only AnonymousConnection is supported for MCP Server Tool connections.", nameof(tool));
+ var serverUrl = connection.Endpoint?.LiteralValue;
+ Throw.IfNullOrEmpty(serverUrl, nameof(connection.Endpoint));
+
+ return new HostedMcpServerTool(tool.ServerName.LiteralValue, serverUrl)
+ {
+ ServerDescription = tool.ServerDescription?.LiteralValue,
+ AllowedTools = tool.AllowedTools?.LiteralValue,
+ ApprovalMode = tool.ApprovalMode?.AsHostedMcpServerToolApprovalMode(),
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/ModelOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/ModelOptionsExtensions.cs
new file mode 100644
index 0000000000..7ad4d26a6b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/ModelOptionsExtensions.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class ModelOptionsExtensions
+{
+ ///
+ /// Converts the 'chatToolMode' property from a to a .
+ ///
+ /// Instance of
+ internal static ChatToolMode? AsChatToolMode(this ModelOptions modelOptions)
+ {
+ Throw.IfNull(modelOptions);
+
+ var mode = modelOptions.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("chatToolMode"))?.Value;
+ if (mode is null)
+ {
+ return null;
+ }
+
+ return mode switch
+ {
+ "auto" => ChatToolMode.Auto,
+ "none" => ChatToolMode.None,
+ "require_any" => ChatToolMode.RequireAny,
+ _ => ChatToolMode.RequireSpecific(mode),
+ };
+ }
+
+ ///
+ /// Retrieves the 'additional_properties' property from a .
+ ///
+ /// Instance of
+ /// List of properties which should not be included in additional properties.
+ internal static AdditionalPropertiesDictionary? GetAdditionalProperties(this ModelOptions modelOptions, string[] excludedProperties)
+ {
+ Throw.IfNull(modelOptions);
+
+ var options = modelOptions.ExtensionData;
+ if (options is null || options.Properties.Count == 0)
+ {
+ return null;
+ }
+
+ var additionalProperties = options.Properties
+ .Where(kvp => !excludedProperties.Contains(kvp.Key))
+ .ToDictionary(
+ kvp => kvp.Key,
+ kvp => kvp.Value?.ToObject());
+
+ if (additionalProperties is null || additionalProperties.Count == 0)
+ {
+ return null;
+ }
+
+ return new AdditionalPropertiesDictionary(additionalProperties);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs
new file mode 100644
index 0000000000..cfa36185cc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/NumberExpressionExtensions.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Globalization;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class NumberExpressionExtensions
+{
+ ///
+ /// Evaluates the given using the provided .
+ ///
+ /// Expression to evaluate.
+ /// Recalc engine to use for evaluation.
+ /// The evaluated number value, or null if the expression is null or cannot be evaluated.
+ internal static double? Eval(this NumberExpression? expression, RecalcEngine? engine)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ if (expression.IsLiteral)
+ {
+ return expression.LiteralValue;
+ }
+
+ if (engine is null)
+ {
+ return null;
+ }
+
+ if (expression.IsExpression)
+ {
+ return engine.Eval(expression.ExpressionText!).AsDouble();
+ }
+ else if (expression.IsVariableReference)
+ {
+ var formulaValue = engine.Eval(expression.VariableReference!.VariableName);
+ if (formulaValue is NumberValue numberValue)
+ {
+ return numberValue.Value;
+ }
+
+ if (formulaValue is StringValue stringValue && double.TryParse(stringValue.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs
new file mode 100644
index 0000000000..1597c0c54b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PromptAgentExtensions.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.AI;
+using Microsoft.PowerFx;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+public static class PromptAgentExtensions
+{
+ ///
+ /// Retrieves the 'options' property from a as a instance.
+ ///
+ /// Instance of
+ /// Instance of
+ /// Instance of
+ public static ChatOptions? GetChatOptions(this GptComponentMetadata promptAgent, RecalcEngine? engine, IList? functions)
+ {
+ Throw.IfNull(promptAgent);
+
+ var outputSchema = promptAgent.OutputType;
+ var modelOptions = promptAgent.Model?.Options;
+
+ var tools = promptAgent.GetAITools(functions);
+
+ if (modelOptions is null && tools is null)
+ {
+ return null;
+ }
+
+ return new ChatOptions()
+ {
+ Instructions = promptAgent.Instructions?.ToTemplateString(),
+ Temperature = (float?)modelOptions?.Temperature?.Eval(engine),
+ MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.Eval(engine),
+ TopP = (float?)modelOptions?.TopP?.Eval(engine),
+ TopK = (int?)modelOptions?.TopK?.Eval(engine),
+ FrequencyPenalty = (float?)modelOptions?.FrequencyPenalty?.Eval(engine),
+ PresencePenalty = (float?)modelOptions?.PresencePenalty?.Eval(engine),
+ Seed = modelOptions?.Seed?.Eval(engine),
+ ResponseFormat = outputSchema?.AsChatResponseFormat(),
+ ModelId = promptAgent.Model?.ModelNameHint,
+ StopSequences = modelOptions?.StopSequences,
+ AllowMultipleToolCalls = modelOptions?.AllowMultipleToolCalls?.Eval(engine),
+ ToolMode = modelOptions?.AsChatToolMode(),
+ Tools = tools,
+ AdditionalProperties = modelOptions?.GetAdditionalProperties(s_chatOptionProperties),
+ };
+ }
+
+ ///
+ /// Retrieves the 'tools' property from a .
+ ///
+ /// Instance of
+ /// Instance of
+ internal static List? GetAITools(this GptComponentMetadata promptAgent, IList? functions)
+ {
+ return promptAgent.Tools.Select(tool =>
+ {
+ return tool switch
+ {
+ CodeInterpreterTool => ((CodeInterpreterTool)tool).AsCodeInterpreterTool(),
+ InvokeClientTaskAction => ((InvokeClientTaskAction)tool).CreateOrGetAITool(functions),
+ McpServerTool => ((McpServerTool)tool).CreateHostedMcpTool(),
+ FileSearchTool => ((FileSearchTool)tool).CreateFileSearchTool(),
+ WebSearchTool => ((WebSearchTool)tool).CreateWebSearchTool(),
+ _ => throw new NotSupportedException($"Unable to create tool definition because of unsupported tool type: {tool.Kind}, supported tool types are: {string.Join(",", s_validToolKinds)}"),
+ };
+ }).ToList() ?? [];
+ }
+
+ #region private
+ private const string CodeInterpreterKind = "codeInterpreter";
+ private const string FileSearchKind = "fileSearch";
+ private const string FunctionKind = "function";
+ private const string WebSearchKind = "webSearch";
+ private const string McpKind = "mcp";
+
+ private static readonly string[] s_validToolKinds =
+ [
+ CodeInterpreterKind,
+ FileSearchKind,
+ FunctionKind,
+ WebSearchKind,
+ McpKind
+ ];
+
+ private static readonly string[] s_chatOptionProperties =
+ [
+ "allowMultipleToolCalls",
+ "conversationId",
+ "chatToolMode",
+ "frequencyPenalty",
+ "additionalInstructions",
+ "maxOutputTokens",
+ "modelId",
+ "presencePenalty",
+ "responseFormat",
+ "seed",
+ "stopSequences",
+ "temperature",
+ "topK",
+ "topP",
+ "toolMode",
+ "tools",
+ ];
+
+ #endregion
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PropertyInfoExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PropertyInfoExtensions.cs
new file mode 100644
index 0000000000..a62fddec88
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/PropertyInfoExtensions.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+public static class PropertyInfoExtensions
+{
+ ///
+ /// Creates a of and
+ /// from an of and .
+ ///
+ /// A read-only dictionary of property names and their corresponding objects.
+ public static Dictionary AsObjectDictionary(this IReadOnlyDictionary properties)
+ {
+ var result = new Dictionary();
+
+ foreach (var property in properties)
+ {
+ result[property.Key] = BuildPropertySchema(property.Value);
+ }
+
+ return result;
+ }
+
+ #region private
+ private static Dictionary BuildPropertySchema(PropertyInfo propertyInfo)
+ {
+ var propertySchema = new Dictionary();
+
+ // Map the DataType to JSON schema type and add type-specific properties
+ switch (propertyInfo.Type)
+ {
+ case StringDataType:
+ propertySchema["type"] = "string";
+ break;
+ case NumberDataType:
+ propertySchema["type"] = "number";
+ break;
+ case BooleanDataType:
+ propertySchema["type"] = "boolean";
+ break;
+ case DateTimeDataType:
+ propertySchema["type"] = "string";
+ propertySchema["format"] = "date-time";
+ break;
+ case DateDataType:
+ propertySchema["type"] = "string";
+ propertySchema["format"] = "date";
+ break;
+ case TimeDataType:
+ propertySchema["type"] = "string";
+ propertySchema["format"] = "time";
+ break;
+ case RecordDataType nestedRecordType:
+#pragma warning disable IL2026, IL3050
+ // For nested records, recursively build the schema
+ var nestedSchema = nestedRecordType.GetSchema();
+ var nestedJson = JsonSerializer.Serialize(nestedSchema, ElementSerializer.CreateOptions());
+ var nestedDict = JsonSerializer.Deserialize>(nestedJson, ElementSerializer.CreateOptions());
+#pragma warning restore IL2026, IL3050
+ if (nestedDict != null)
+ {
+ return nestedDict;
+ }
+ propertySchema["type"] = "object";
+ break;
+ case TableDataType tableType:
+ propertySchema["type"] = "array";
+ // TableDataType has Properties like RecordDataType
+ propertySchema["items"] = new Dictionary
+ {
+ ["type"] = "object",
+ ["properties"] = AsObjectDictionary(tableType.Properties),
+ ["additionalProperties"] = false
+ };
+ break;
+ default:
+ propertySchema["type"] = "string";
+ break;
+ }
+
+ // Add description if available
+ if (!string.IsNullOrEmpty(propertyInfo.Description))
+ {
+ propertySchema["description"] = propertyInfo.Description;
+ }
+
+ return propertySchema;
+ }
+ #endregion
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataTypeExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataTypeExtensions.cs
new file mode 100644
index 0000000000..b5c5793cab
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataTypeExtensions.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+public static class RecordDataTypeExtensions
+{
+ ///
+ /// Creates a from a .
+ ///
+ /// Instance of
+ internal static ChatResponseFormat? AsChatResponseFormat(this RecordDataType recordDataType)
+ {
+ Throw.IfNull(recordDataType);
+
+ if (recordDataType.Properties.Count == 0)
+ {
+ return null;
+ }
+
+ // TODO: Consider adding schemaName and schemaDescription parameters to this method.
+ return ChatResponseFormat.ForJsonSchema(
+ schema: recordDataType.GetSchema(),
+ schemaName: recordDataType.GetSchemaName(),
+ schemaDescription: recordDataType.GetSchemaDescription());
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// Instance of
+#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
+#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
+ public static JsonElement GetSchema(this RecordDataType recordDataType)
+ {
+ Throw.IfNull(recordDataType);
+
+ var schemaObject = new Dictionary
+ {
+ ["type"] = "object",
+ ["properties"] = recordDataType.Properties.AsObjectDictionary(),
+ ["additionalProperties"] = false
+ };
+
+ var json = JsonSerializer.Serialize(schemaObject, ElementSerializer.CreateOptions());
+ return JsonSerializer.Deserialize(json);
+ }
+#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
+#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
+
+ ///
+ /// Retrieves the 'schemaName' property from a .
+ ///
+ private static string? GetSchemaName(this RecordDataType recordDataType)
+ {
+ Throw.IfNull(recordDataType);
+
+ return recordDataType.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("schemaName"))?.Value;
+ }
+
+ ///
+ /// Retrieves the 'schemaDescription' property from a .
+ ///
+ private static string? GetSchemaDescription(this RecordDataType recordDataType)
+ {
+ Throw.IfNull(recordDataType);
+
+ return recordDataType.ExtensionData?.GetPropertyOrNull(InitializablePropertyPath.Create("schemaDescription"))?.Value;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataValueExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataValueExtensions.cs
new file mode 100644
index 0000000000..6351b7badb
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/RecordDataValueExtensions.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+public static class RecordDataValueExtensions
+{
+ ///
+ /// Retrieves a 'number' property from a
+ ///
+ /// Instance of
+ /// Path of the property to retrieve
+ public static decimal? GetNumber(this RecordDataValue recordData, string propertyPath)
+ {
+ Throw.IfNull(recordData);
+
+ var numberValue = recordData.GetPropertyOrNull(InitializablePropertyPath.Create(propertyPath));
+ return numberValue?.Value;
+ }
+
+ ///
+ /// Retrieves a nullable boolean value from the specified property path within the given record data.
+ ///
+ /// Instance of
+ /// Path of the property to retrieve
+ public static bool? GetBoolean(this RecordDataValue recordData, string propertyPath)
+ {
+ Throw.IfNull(recordData);
+
+ var booleanValue = recordData.GetPropertyOrNull(InitializablePropertyPath.Create(propertyPath));
+ return booleanValue?.Value;
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// Instance of
+ public static IReadOnlyDictionary ToDictionary(this RecordDataValue recordData)
+ {
+ Throw.IfNull(recordData);
+
+ return recordData.Properties.ToDictionary(
+ kvp => kvp.Key,
+ kvp => kvp.Value?.ToString() ?? string.Empty
+ );
+ }
+
+ ///
+ /// Retrieves the 'schema' property from a .
+ ///
+ /// Instance of
+#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
+#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
+ public static JsonElement? GetSchema(this RecordDataValue recordData)
+ {
+ Throw.IfNull(recordData);
+
+ try
+ {
+ var schemaStr = recordData.GetPropertyOrNull(InitializablePropertyPath.Create("json_schema.schema"));
+ if (schemaStr?.Value is not null)
+ {
+ return JsonSerializer.Deserialize(schemaStr.Value);
+ }
+ }
+ catch (InvalidCastException)
+ {
+ // Ignore and try next
+ }
+
+ var responseFormRec = recordData.GetPropertyOrNull(InitializablePropertyPath.Create("json_schema.schema"));
+ if (responseFormRec is not null)
+ {
+ var json = JsonSerializer.Serialize(responseFormRec, ElementSerializer.CreateOptions());
+ return JsonSerializer.Deserialize(json);
+ }
+
+ return null;
+ }
+#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
+#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
+
+ internal static object? ToObject(this DataValue? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+ return value switch
+ {
+ StringDataValue s => s.Value,
+ NumberDataValue n => n.Value,
+ BooleanDataValue b => b.Value,
+ TableDataValue t => t.Values.Select(v => v.ToObject()).ToList(),
+ RecordDataValue r => r.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToObject()),
+ _ => throw new NotSupportedException($"Unsupported DataValue type: {value.GetType().FullName}"),
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs
new file mode 100644
index 0000000000..40c1b7c9c8
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/StringExpressionExtensions.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+public static class StringExpressionExtensions
+{
+ ///
+ /// Evaluates the given using the provided .
+ ///
+ /// Expression to evaluate.
+ /// Recalc engine to use for evaluation.
+ /// The evaluated string value, or null if the expression is null or cannot be evaluated.
+ public static string? Eval(this StringExpression? expression, RecalcEngine? engine)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ if (expression.IsLiteral)
+ {
+ return expression.LiteralValue?.ToString();
+ }
+
+ if (engine is null)
+ {
+ return null;
+ }
+
+ if (expression.IsExpression)
+ {
+ return engine.Eval(expression.ExpressionText!).ToString();
+ }
+ else if (expression.IsVariableReference)
+ {
+ var stringValue = engine.Eval(expression.VariableReference!.VariableName) as StringValue;
+ return stringValue?.Value;
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/WebSearchToolExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/WebSearchToolExtensions.cs
new file mode 100644
index 0000000000..e6ee360308
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/WebSearchToolExtensions.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Bot.ObjectModel;
+
+///
+/// Extension methods for .
+///
+internal static class WebSearchToolExtensions
+{
+ ///
+ /// Create a from a .
+ ///
+ /// Instance of
+ internal static HostedWebSearchTool CreateWebSearchTool(this WebSearchTool tool)
+ {
+ Throw.IfNull(tool);
+
+ return new HostedWebSearchTool();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs
new file mode 100644
index 0000000000..1cc24055d9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Extensions/YamlAgentFactoryExtensions.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Extension methods for to support YAML based agent definitions.
+///
+public static class YamlAgentFactoryExtensions
+{
+ ///
+ /// Create a from the given agent YAML.
+ ///
+ /// which will be used to create the agent.
+ /// Text string containing the YAML representation of an .
+ /// Optional cancellation token
+ [RequiresDynamicCode("Calls YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()")]
+ public static Task CreateFromYamlAsync(this PromptAgentFactory agentFactory, string agentYaml, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(agentFactory);
+ Throw.IfNullOrEmpty(agentYaml);
+
+ var agentDefinition = AgentBotElementYaml.FromYaml(agentYaml);
+
+ return agentFactory.CreateAsync(
+ agentDefinition,
+ cancellationToken);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj b/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj
new file mode 100644
index 0000000000..306ba27e97
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj
@@ -0,0 +1,45 @@
+
+
+
+ preview
+ $(NoWarn);MEAI001
+ false
+
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+ Microsoft Agent Framework Declarative Agents
+ Provides Microsoft Agent Framework support for declarative agents.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs
new file mode 100644
index 0000000000..cb277b06da
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Declarative/PromptAgentFactory.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.Configuration;
+using Microsoft.PowerFx;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Represents a factory for creating instances.
+///
+public abstract class PromptAgentFactory
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Optional , if none is provided a default instance will be created.
+ /// Optional configuration to be added as variables to the .
+ protected PromptAgentFactory(RecalcEngine? engine = null, IConfiguration? configuration = null)
+ {
+ this.Engine = engine ?? new RecalcEngine();
+
+ if (configuration is not null)
+ {
+ foreach (var kvp in configuration.AsEnumerable())
+ {
+ this.Engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
+ }
+ }
+ }
+
+ ///
+ /// Gets the Power Fx recalculation engine used to evaluate expressions in agent definitions.
+ /// This engine is configured with variables from the provided during construction.
+ ///
+ protected RecalcEngine Engine { get; }
+
+ ///
+ /// Create a from the specified .
+ ///
+ /// Definition of the agent to create.
+ /// Optional cancellation token.
+ /// The created , if null the agent type is not supported.
+ public async Task CreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(promptAgent);
+
+ var agent = await this.TryCreateAsync(promptAgent, cancellationToken).ConfigureAwait(false);
+ return agent ?? throw new NotSupportedException($"Agent type {promptAgent.Kind} is not supported.");
+ }
+
+ ///
+ /// Tries to create a from the specified .
+ ///
+ /// Definition of the agent to create.
+ /// Optional cancellation token.
+ /// The created , if null the agent type is not supported.
+ public abstract Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs
index 07cb47da81..fb464fdc39 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs
@@ -28,19 +28,21 @@ public static class OpenAIAssistantClientExtensions
/// The client result containing the assistant.
/// Optional chat options.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the assistant.
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
ClientResult assistantClientResult,
ChatOptions? chatOptions = null,
- Func? clientFactory = null)
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (assistantClientResult is null)
{
throw new ArgumentNullException(nameof(assistantClientResult));
}
- return assistantClient.GetAIAgent(assistantClientResult.Value, chatOptions, clientFactory);
+ return assistantClient.GetAIAgent(assistantClientResult.Value, chatOptions, clientFactory, services);
}
///
@@ -50,12 +52,14 @@ public static ChatClientAgent GetAIAgent(
/// The assistant metadata.
/// Optional chat options.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the assistant.
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
Assistant assistantMetadata,
ChatOptions? chatOptions = null,
- Func? clientFactory = null)
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (assistantMetadata is null)
{
@@ -73,14 +77,19 @@ public static ChatClientAgent GetAIAgent(
chatClient = clientFactory(chatClient);
}
+ if (!string.IsNullOrWhiteSpace(assistantMetadata.Instructions) && chatOptions?.Instructions is null)
+ {
+ chatOptions ??= new ChatOptions();
+ chatOptions.Instructions = assistantMetadata.Instructions;
+ }
+
return new ChatClientAgent(chatClient, options: new()
{
Id = assistantMetadata.Id,
Name = assistantMetadata.Name,
Description = assistantMetadata.Description,
- Instructions = assistantMetadata.Instructions,
ChatOptions = chatOptions
- });
+ }, services: services);
}
///
@@ -90,6 +99,7 @@ public static ChatClientAgent GetAIAgent(
/// The ID of the server side agent to create a for.
/// Options that should apply to all runs of the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the assistant agent.
public static ChatClientAgent GetAIAgent(
@@ -97,6 +107,7 @@ public static ChatClientAgent GetAIAgent(
string agentId,
ChatOptions? chatOptions = null,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
@@ -110,7 +121,7 @@ public static ChatClientAgent GetAIAgent(
}
var assistant = assistantClient.GetAssistant(agentId, cancellationToken);
- return assistantClient.GetAIAgent(assistant, chatOptions, clientFactory);
+ return assistantClient.GetAIAgent(assistant, chatOptions, clientFactory, services);
}
///
@@ -120,6 +131,7 @@ public static ChatClientAgent GetAIAgent(
/// The ID of the server side agent to create a for.
/// Options that should apply to all runs of the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the assistant agent.
public static async Task GetAIAgentAsync(
@@ -127,6 +139,7 @@ public static async Task GetAIAgentAsync(
string agentId,
ChatOptions? chatOptions = null,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
@@ -140,7 +153,7 @@ public static async Task GetAIAgentAsync(
}
var assistantResponse = await assistantClient.GetAssistantAsync(agentId, cancellationToken).ConfigureAwait(false);
- return assistantClient.GetAIAgent(assistantResponse, chatOptions, clientFactory);
+ return assistantClient.GetAIAgent(assistantResponse, chatOptions, clientFactory, services);
}
///
@@ -150,20 +163,22 @@ public static async Task GetAIAgentAsync(
/// The client result containing the assistant.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the assistant.
/// or is .
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
ClientResult assistantClientResult,
ChatClientAgentOptions options,
- Func? clientFactory = null)
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (assistantClientResult is null)
{
throw new ArgumentNullException(nameof(assistantClientResult));
}
- return assistantClient.GetAIAgent(assistantClientResult.Value, options, clientFactory);
+ return assistantClient.GetAIAgent(assistantClientResult.Value, options, clientFactory, services);
}
///
@@ -173,13 +188,15 @@ public static ChatClientAgent GetAIAgent(
/// The assistant metadata.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations on the assistant.
/// or is .
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
Assistant assistantMetadata,
ChatClientAgentOptions options,
- Func? clientFactory = null)
+ Func? clientFactory = null,
+ IServiceProvider? services = null)
{
if (assistantMetadata is null)
{
@@ -203,19 +220,24 @@ public static ChatClientAgent GetAIAgent(
chatClient = clientFactory(chatClient);
}
+ if (string.IsNullOrWhiteSpace(options.ChatOptions?.Instructions) && !string.IsNullOrWhiteSpace(assistantMetadata.Instructions))
+ {
+ options.ChatOptions ??= new ChatOptions();
+ options.ChatOptions.Instructions = assistantMetadata.Instructions;
+ }
+
var mergedOptions = new ChatClientAgentOptions()
{
Id = assistantMetadata.Id,
Name = options.Name ?? assistantMetadata.Name,
Description = options.Description ?? assistantMetadata.Description,
- Instructions = options.Instructions ?? assistantMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
};
- return new ChatClientAgent(chatClient, mergedOptions);
+ return new ChatClientAgent(chatClient, mergedOptions, services: services);
}
///
@@ -225,6 +247,7 @@ public static ChatClientAgent GetAIAgent(
/// The ID of the server side agent to create a for.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the assistant agent.
/// or is .
@@ -234,6 +257,7 @@ public static ChatClientAgent GetAIAgent(
string agentId,
ChatClientAgentOptions options,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
@@ -252,7 +276,7 @@ public static ChatClientAgent GetAIAgent(
}
var assistant = assistantClient.GetAssistant(agentId, cancellationToken);
- return assistantClient.GetAIAgent(assistant, options, clientFactory);
+ return assistantClient.GetAIAgent(assistant, options, clientFactory, services);
}
///
@@ -262,6 +286,7 @@ public static ChatClientAgent GetAIAgent(
/// The ID of the server side agent to create a for.
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations on the assistant agent.
/// or is .
@@ -271,6 +296,7 @@ public static async Task GetAIAgentAsync(
string agentId,
ChatClientAgentOptions options,
Func? clientFactory = null,
+ IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
@@ -289,7 +315,7 @@ public static async Task GetAIAgentAsync(
}
var assistantResponse = await assistantClient.GetAssistantAsync(agentId, cancellationToken).ConfigureAwait(false);
- return assistantClient.GetAIAgent(assistantResponse, options, clientFactory);
+ return assistantClient.GetAIAgent(assistantResponse, options, clientFactory, services);
}
///
@@ -303,6 +329,7 @@ public static async Task GetAIAgentAsync(
/// Optional collection of AI tools that the agent can use during conversations.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for enabling logging within the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// An instance backed by the OpenAI Assistant service.
/// Thrown when or is .
/// Thrown when is empty or whitespace.
@@ -314,21 +341,23 @@ public static ChatClientAgent CreateAIAgent(
string? description = null,
IList? tools = null,
Func? clientFactory = null,
- ILoggerFactory? loggerFactory = null) =>
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null) =>
client.CreateAIAgent(
model,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
- Instructions = instructions,
- ChatOptions = tools is null ? null : new ChatOptions()
+ ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Tools = tools,
+ Instructions = instructions
}
},
clientFactory,
- loggerFactory);
+ loggerFactory,
+ services);
///
/// Creates an AI agent from an using the OpenAI Assistant API.
@@ -338,6 +367,7 @@ public static ChatClientAgent CreateAIAgent(
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for enabling logging within the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// An instance backed by the OpenAI Assistant service.
/// Thrown when or or is .
/// Thrown when is empty or whitespace.
@@ -346,7 +376,8 @@ public static ChatClientAgent CreateAIAgent(
string model,
ChatClientAgentOptions options,
Func? clientFactory = null,
- ILoggerFactory? loggerFactory = null)
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null)
{
Throw.IfNull(client);
Throw.IfNullOrEmpty(model);
@@ -356,7 +387,7 @@ public static ChatClientAgent CreateAIAgent(
{
Name = options.Name,
Description = options.Description,
- Instructions = options.Instructions,
+ Instructions = options.ChatOptions?.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
@@ -387,7 +418,7 @@ public static ChatClientAgent CreateAIAgent(
options.ChatOptions ??= new ChatOptions();
options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
- return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
+ return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services);
}
///
@@ -401,6 +432,8 @@ public static ChatClientAgent CreateAIAgent(
/// Optional collection of AI tools that the agent can use during conversations.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for enabling logging within the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
+ /// The to monitor for cancellation requests. The default is .
/// An instance backed by the OpenAI Assistant service.
/// Thrown when or is .
/// Thrown when is empty or whitespace.
@@ -412,20 +445,24 @@ public static async Task CreateAIAgentAsync(
string? description = null,
IList? tools = null,
Func? clientFactory = null,
- ILoggerFactory? loggerFactory = null) =>
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null,
+ CancellationToken cancellationToken = default) =>
await client.CreateAIAgentAsync(model,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
- Instructions = instructions,
- ChatOptions = tools is null ? null : new ChatOptions()
+ ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Tools = tools,
+ Instructions = instructions,
}
},
clientFactory,
- loggerFactory).ConfigureAwait(false);
+ loggerFactory,
+ services,
+ cancellationToken).ConfigureAwait(false);
///
/// Creates an AI agent from an using the OpenAI Assistant API.
@@ -435,6 +472,8 @@ await client.CreateAIAgentAsync(model,
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for enabling logging within the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
+ /// The to monitor for cancellation requests. The default is .
/// An instance backed by the OpenAI Assistant service.
/// Thrown when or is .
/// Thrown when is empty or whitespace.
@@ -443,7 +482,9 @@ public static async Task CreateAIAgentAsync(
string model,
ChatClientAgentOptions options,
Func? clientFactory = null,
- ILoggerFactory? loggerFactory = null)
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null,
+ CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
Throw.IfNull(model);
@@ -453,7 +494,7 @@ public static async Task CreateAIAgentAsync(
{
Name = options.Name,
Description = options.Description,
- Instructions = options.Instructions,
+ Instructions = options.ChatOptions?.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
@@ -468,7 +509,7 @@ public static async Task CreateAIAgentAsync(
}
// Create the assistant in the assistant service.
- var assistantCreateResult = await client.CreateAssistantAsync(model, assistantOptions).ConfigureAwait(false);
+ var assistantCreateResult = await client.CreateAssistantAsync(model, assistantOptions, cancellationToken).ConfigureAwait(false);
var assistantId = assistantCreateResult.Value.Id;
// Build the local agent object.
@@ -483,7 +524,7 @@ public static async Task CreateAIAgentAsync(
options.ChatOptions ??= new ChatOptions();
options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
- return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
+ return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services);
}
private static (List? ToolDefinitions, ToolResources? ToolResources, List? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList? tools)
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIChatClientExtensions.cs
index 36114d009c..b51679e42e 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIChatClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIChatClientExtensions.cs
@@ -47,9 +47,9 @@ public static ChatClientAgent CreateAIAgent(
{
Name = name,
Description = description,
- Instructions = instructions,
- ChatOptions = tools is null ? null : new ChatOptions()
+ ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
+ Instructions = instructions,
Tools = tools,
}
},
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs
index c9f2743229..dd25d46047 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs
@@ -30,6 +30,7 @@ public static class OpenAIResponseClientExtensions
/// Optional collection of AI tools that the agent can use during conversations.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for enabling logging within the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// An instance backed by the OpenAI Response service.
/// Thrown when is .
public static ChatClientAgent CreateAIAgent(
@@ -39,7 +40,8 @@ public static ChatClientAgent CreateAIAgent(
string? description = null,
IList? tools = null,
Func? clientFactory = null,
- ILoggerFactory? loggerFactory = null)
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null)
{
Throw.IfNull(client);
@@ -48,14 +50,15 @@ public static ChatClientAgent CreateAIAgent(
{
Name = name,
Description = description,
- Instructions = instructions,
- ChatOptions = tools is null ? null : new ChatOptions()
+ ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
+ Instructions = instructions,
Tools = tools,
}
},
clientFactory,
- loggerFactory);
+ loggerFactory,
+ services);
}
///
@@ -65,13 +68,15 @@ public static ChatClientAgent CreateAIAgent(
/// Full set of options to configure the agent.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for enabling logging within the agent.
+ /// An optional to use for resolving services required by the instances being invoked.
/// An instance backed by the OpenAI Response service.
/// Thrown when or is .
public static ChatClientAgent CreateAIAgent(
this OpenAIResponseClient client,
ChatClientAgentOptions options,
Func? clientFactory = null,
- ILoggerFactory? loggerFactory = null)
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null)
{
Throw.IfNull(client);
Throw.IfNull(options);
@@ -83,6 +88,6 @@ public static ChatClientAgent CreateAIAgent(
chatClient = clientFactory(chatClient);
}
- return new ChatClientAgent(chatClient, options, loggerFactory);
+ return new ChatClientAgent(chatClient, options, loggerFactory, services);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs
index b529e1151b..5870e2fdcc 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIChatClientAgent.cs
@@ -32,7 +32,7 @@ public OpenAIChatClientAgent(
{
Name = name,
Description = description,
- Instructions = instructions,
+ ChatOptions = new ChatOptions() { Instructions = instructions },
}, loggerFactory)
{
}
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs
index 8c5603fb05..9d554e6a84 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/OpenAIResponseClientAgent.cs
@@ -32,7 +32,7 @@ public OpenAIResponseClientAgent(
{
Name = name,
Description = description,
- Instructions = instructions,
+ ChatOptions = new ChatOptions() { Instructions = instructions },
}, loggerFactory)
{
}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
index 685dac8488..df7477241c 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
@@ -59,13 +59,13 @@ public ChatClientAgent(IChatClient chatClient, string? instructions = null, stri
chatClient,
new ChatClientAgentOptions
{
- Name = name,
- Description = description,
- Instructions = instructions,
- ChatOptions = tools is null ? null : new ChatOptions
+ ChatOptions = (tools is null && string.IsNullOrWhiteSpace(instructions)) ? null : new ChatOptions
{
Tools = tools,
- }
+ Instructions = instructions
+ },
+ Name = name,
+ Description = description
},
loggerFactory,
services)
@@ -141,7 +141,7 @@ public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options,
/// These instructions are typically provided to the AI model as system messages to establish
/// the context and expected behavior for the agent's responses.
///
- public string? Instructions => this._agentOptions?.Instructions;
+ public string? Instructions => this._agentOptions?.ChatOptions?.Instructions;
///
/// Gets of the default used by the agent.
@@ -492,7 +492,6 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
requestChatOptions.AllowMultipleToolCalls ??= this._agentOptions.ChatOptions.AllowMultipleToolCalls;
requestChatOptions.ConversationId ??= this._agentOptions.ChatOptions.ConversationId;
requestChatOptions.FrequencyPenalty ??= this._agentOptions.ChatOptions.FrequencyPenalty;
- requestChatOptions.Instructions ??= this._agentOptions.ChatOptions.Instructions;
requestChatOptions.MaxOutputTokens ??= this._agentOptions.ChatOptions.MaxOutputTokens;
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
@@ -503,6 +502,13 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
requestChatOptions.TopK ??= this._agentOptions.ChatOptions.TopK;
requestChatOptions.ToolMode ??= this._agentOptions.ChatOptions.ToolMode;
+ // Merge instructions by concatenating them if both are present.
+ requestChatOptions.Instructions = !string.IsNullOrWhiteSpace(requestChatOptions.Instructions) && !string.IsNullOrWhiteSpace(this.Instructions)
+ ? $"{this.Instructions}\n{requestChatOptions.Instructions}"
+ : (!string.IsNullOrWhiteSpace(requestChatOptions.Instructions)
+ ? requestChatOptions.Instructions
+ : this.Instructions);
+
// Merge only the additional properties from the agent if they are not already set in the request options.
if (requestChatOptions.AdditionalProperties is not null && this._agentOptions.ChatOptions.AdditionalProperties is not null)
{
@@ -685,12 +691,6 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
""");
}
- if (!string.IsNullOrWhiteSpace(this.Instructions))
- {
- chatOptions ??= new();
- chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? this.Instructions : $"{this.Instructions}\n{chatOptions.Instructions}";
- }
-
// Only create or update ChatOptions if we have an id on the thread and we don't have the same one already in ChatOptions.
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && typedThread.ConversationId != chatOptions?.ConversationId)
{
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
index f83e6912d5..019f4f42e4 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.AI;
@@ -17,35 +16,6 @@ namespace Microsoft.Agents.AI;
///
public class ChatClientAgentOptions
{
- ///
- /// Initializes a new instance of the class.
- ///
- public ChatClientAgentOptions()
- {
- }
-
- ///
- /// Initializes a new instance of the class with the specified parameters.
- ///
- /// If is provided, a new instance is created
- /// with the specified instructions and tools.
- /// The instructions or guidelines for the chat client agent. Can be if not specified.
- /// The name of the chat client agent. Can be if not specified.
- /// The description of the chat client agent. Can be if not specified.
- /// A list of instances available to the chat client agent. Can be if no
- /// tools are specified.
- public ChatClientAgentOptions(string? instructions, string? name = null, string? description = null, IList? tools = null)
- {
- this.Name = name;
- this.Instructions = instructions;
- this.Description = description;
-
- if (tools is not null)
- {
- (this.ChatOptions ??= new()).Tools = tools;
- }
- }
-
///
/// Gets or sets the agent id.
///
@@ -56,11 +26,6 @@ public ChatClientAgentOptions(string? instructions, string? name = null, string?
///
public string? Name { get; set; }
- ///
- /// Gets or sets the agent instructions.
- ///
- public string? Instructions { get; set; }
-
///
/// Gets or sets the agent description.
///
@@ -106,7 +71,6 @@ public ChatClientAgentOptions Clone()
{
Id = this.Id,
Name = this.Name,
- Instructions = this.Instructions,
Description = this.Description,
ChatOptions = this.ChatOptions?.Clone(),
ChatMessageStoreFactory = this.ChatMessageStoreFactory,
diff --git a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
index 5a9cb416c8..ad5b2e0fdd 100644
--- a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
+++ b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
@@ -32,6 +32,7 @@
+
diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs
index 76ca18d3de..72c0b14ae2 100644
--- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs
+++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs
@@ -84,8 +84,7 @@ public Task CreateChatClientAgentAsync(
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
- Instructions = instructions,
- ChatOptions = new() { Tools = aiTools }
+ ChatOptions = new() { Instructions = instructions, Tools = aiTools }
}));
}
diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs
index 4bb1c9cbfe..f626736418 100644
--- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs
+++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs
@@ -37,16 +37,20 @@ public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync(string create
{
"CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync(
model: s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- name: AgentName,
- description: AgentDescription)),
+ options: new ChatClientAgentOptions()
+ {
+ Name = AgentName,
+ Description = AgentDescription,
+ ChatOptions = new() { Instructions = AgentInstructions }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._client.CreateAIAgent(
model: s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- name: AgentName,
- description: AgentDescription)),
+ options: new ChatClientAgentOptions()
+ {
+ Name = AgentName,
+ Description = AgentDescription,
+ ChatOptions = new() { Instructions = AgentInstructions }
+ }),
"CreateWithFoundryOptionsAsync" => await this._client.CreateAIAgentAsync(
name: AgentName,
creationOptions: new AgentVersionCreationOptions(new PromptAgentDefinition(s_config.DeploymentName) { Instructions = AgentInstructions }) { Description = AgentDescription }),
@@ -239,16 +243,18 @@ public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync(string create
{
"CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync(
model: s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- name: AgentName,
- instructions: AgentInstructions,
- tools: [weatherFunction])),
+ options: new ChatClientAgentOptions()
+ {
+ Name = AgentName,
+ ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._client.CreateAIAgent(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- name: AgentName,
- instructions: AgentInstructions,
- tools: [weatherFunction])),
+ options: new ChatClientAgentOptions()
+ {
+ Name = AgentName,
+ ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] }
+ }),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs
index e3e9969a43..17fad1581f 100644
--- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs
+++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs
@@ -34,16 +34,20 @@ public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync(string create
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- name: AgentName,
- description: AgentDescription)),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new() { Instructions = AgentInstructions },
+ Name = AgentName,
+ Description = AgentDescription
+ }),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- name: AgentName,
- description: AgentDescription)),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new() { Instructions = AgentInstructions },
+ Name = AgentName,
+ Description = AgentDescription
+ }),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -109,14 +113,24 @@ You are a helpful agent that can help fetch data from files you know about.
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]
+ }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]
+ }
+ }),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -179,15 +193,24 @@ and report the SECRET_NUMBER value it prints. Respond only with the number.
// Hosted tool path (tools supplied via ChatClientAgentOptions)
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]
+ }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
- // Foundry (definitions + resources provided directly)
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]
+ }
+ }),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -232,14 +255,24 @@ public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync(string create
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [weatherFunction])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [weatherFunction]
+ }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [weatherFunction])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [weatherFunction]
+ }
+ }),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs
index af778c03c9..400bcf5456 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs
@@ -91,7 +91,7 @@ public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
Name = "Test Agent",
Description = "Test description",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs
index 7a9c34a508..c8bf4d6a5e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs
@@ -158,7 +158,7 @@ public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
Name = "Test Agent",
Description = "Test description",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs
index 56b89d2df8..b661a392be 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Azure;
@@ -309,7 +310,7 @@ public void GetAIAgent_WithResponseAndOptions_WorksCorrectly()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -336,7 +337,7 @@ public void GetAIAgent_WithPersistentAgentAndOptions_WorksCorrectly()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -385,7 +386,7 @@ public void GetAIAgent_WithAgentIdAndOptions_WorksCorrectly()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -412,7 +413,7 @@ public async Task GetAIAgentAsync_WithAgentIdAndOptions_WorksCorrectlyAsync()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -556,7 +557,7 @@ public void CreateAIAgent_WithOptions_WorksCorrectly()
{
Name = "Test Agent",
Description = "Test description",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -583,7 +584,7 @@ public async Task CreateAIAgentAsync_WithOptions_WorksCorrectlyAsync()
{
Name = "Test Agent",
Description = "Test description",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -726,6 +727,159 @@ public async Task CreateAIAgentAsync_WithEmptyModel_ThrowsArgumentExceptionAsync
Assert.Equal("model", exception.ParamName);
}
+ ///
+ /// Verify that CreateAIAgent with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var serviceProvider = new TestServiceProvider();
+ const string Model = "test-model";
+
+ // Act
+ var agent = client.CreateAIAgent(
+ Model,
+ instructions: "Test instructions",
+ name: "Test Agent",
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that CreateAIAgentAsync with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public async Task CreateAIAgentAsync_WithServices_PassesServicesToAgentAsync()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var serviceProvider = new TestServiceProvider();
+ const string Model = "test-model";
+
+ // Act
+ var agent = await client.CreateAIAgentAsync(
+ Model,
+ instructions: "Test instructions",
+ name: "Test Agent",
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that GetAIAgent with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void GetAIAgent_WithServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var serviceProvider = new TestServiceProvider();
+
+ // Act
+ var agent = client.GetAIAgent("agent_abc123", services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithServices_PassesServicesToAgentAsync()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var serviceProvider = new TestServiceProvider();
+
+ // Act
+ var agent = await client.GetAIAgentAsync("agent_abc123", services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that CreateAIAgent with both clientFactory and services works correctly.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithClientFactoryAndServices_AppliesBothCorrectly()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var serviceProvider = new TestServiceProvider();
+ TestChatClient? testChatClient = null;
+ const string Model = "test-model";
+
+ // Act
+ var agent = client.CreateAIAgent(
+ Model,
+ instructions: "Test instructions",
+ name: "Test Agent",
+ clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient),
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the custom chat client was applied
+ var retrievedTestClient = agent.GetService();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+
+ // Verify the IServiceProvider was passed through
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Uses reflection to access the FunctionInvocationServices property which is not public.
+ ///
+ private static IServiceProvider? GetFunctionInvocationServices(FunctionInvokingChatClient client)
+ {
+ var property = typeof(FunctionInvokingChatClient).GetProperty(
+ "FunctionInvocationServices",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return property?.GetValue(client) as IServiceProvider;
+ }
+
///
/// Test custom chat client that can be used to verify clientFactory functionality.
///
@@ -736,6 +890,14 @@ public TestChatClient(IChatClient innerClient) : base(innerClient)
}
}
+ ///
+ /// A simple test IServiceProvider implementation for testing.
+ ///
+ private sealed class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
+
public sealed class FakePersistentAgentsAdministrationClient : PersistentAgentsAdministrationClient
{
public FakePersistentAgentsAdministrationClient()
@@ -761,7 +923,7 @@ private static PersistentAgentsClient CreateFakePersistentAgentsClient()
{
var client = new PersistentAgentsClient("https://any.com", DelegatedTokenCredential.Create((_, _) => new AccessToken()));
- ((System.Reflection.TypeInfo)typeof(PersistentAgentsClient)).DeclaredFields.First(f => f.Name == "_client")
+ ((TypeInfo)typeof(PersistentAgentsClient)).DeclaredFields.First(f => f.Name == "_client")
.SetValue(client, new FakePersistentAgentsAdministrationClient());
return client;
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
index 33656a8486..1136d2b1f4 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
@@ -752,7 +752,7 @@ public void CreateAIAgent_WithModelAndOptions_CreatesValidAgent()
var options = new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -775,7 +775,7 @@ public void CreateAIAgent_WithModelAndOptions_WithClientFactory_AppliesFactoryCo
var options = new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
TestChatClient? testChatClient = null;
@@ -803,7 +803,7 @@ public async Task CreateAIAgentAsync_WithModelAndOptions_CreatesValidAgentAsync(
var options = new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -826,7 +826,7 @@ public async Task CreateAIAgentAsync_WithModelAndOptions_WithClientFactory_Appli
var options = new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
TestChatClient? testChatClient = null;
@@ -1575,8 +1575,8 @@ public void GetAIAgent_WithOptions_PreservesCustomProperties()
var options = new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Custom instructions",
- Description = "Custom description"
+ Description = "Custom description",
+ ChatOptions = new ChatOptions { Instructions = "Custom instructions" }
};
// Act
@@ -1610,8 +1610,7 @@ public void CreateAIAgent_WithOptionsAndTools_GeneratesCorrectOptions()
var options = new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test",
- ChatOptions = new ChatOptions { Tools = tools }
+ ChatOptions = new ChatOptions { Instructions = "Test", Tools = tools }
};
// Act
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs
index 647beb4451..eee9f520b6 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientTests.cs
@@ -49,8 +49,7 @@ public async Task ChatClient_UsesDefaultConversationIdAsync()
new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions",
- ChatOptions = new() { ConversationId = "conv_12345" }
+ ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_12345" }
});
// Act
@@ -99,7 +98,7 @@ public async Task ChatClient_UsesPerRequestConversationId_WhenNoDefaultConversat
new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions",
+ ChatOptions = new() { Instructions = "Test instructions" },
});
// Act
@@ -148,8 +147,7 @@ public async Task ChatClient_UsesPerRequestConversationId_EvenWhenDefaultConvers
new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions",
- ChatOptions = new() { ConversationId = "conv_should_not_use_default" }
+ ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_should_not_use_default" }
});
// Act
@@ -198,7 +196,7 @@ public async Task ChatClient_UsesPreviousResponseId_WhenConversationIsNotPrefixe
new ChatClientAgentOptions
{
Name = "test-agent",
- Instructions = "Test instructions",
+ ChatOptions = new() { Instructions = "Test instructions" },
});
// Act
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs
new file mode 100644
index 0000000000..31cadfb0ce
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AgentBotElementYamlTests.cs
@@ -0,0 +1,310 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.PowerFx;
+
+namespace Microsoft.Agents.AI.Declarative.UnitTests;
+
+///
+/// Unit tests for
+///
+public sealed class AgentBotElementYamlTests
+{
+ [Theory]
+ [InlineData(PromptAgents.AgentWithEverything)]
+ [InlineData(PromptAgents.AgentWithApiKeyConnection)]
+ [InlineData(PromptAgents.AgentWithVariableReferences)]
+ [InlineData(PromptAgents.AgentWithOutputSchema)]
+ [InlineData(PromptAgents.OpenAIChatAgent)]
+ [InlineData(PromptAgents.AgentWithCurrentModels)]
+ [InlineData(PromptAgents.AgentWithRemoteConnection)]
+ public void FromYaml_DoesNotThrow(string text)
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(text);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ [Fact]
+ public void FromYaml_NotPromptAgent_Throws()
+ {
+ // Arrange & Act & Assert
+ Assert.Throws(() => AgentBotElementYaml.FromYaml(PromptAgents.Workflow));
+ }
+
+ [Fact]
+ public void FromYaml_Properties()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("AgentName", agent.Name);
+ Assert.Equal("Agent description", agent.Description);
+ Assert.Equal("You are a helpful assistant.", agent.Instructions?.ToTemplateString());
+ Assert.NotNull(agent.Model);
+ Assert.True(agent.Tools.Length > 0);
+ }
+
+ [Fact]
+ public void FromYaml_CurrentModels()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithCurrentModels);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.Model);
+ Assert.Equal("gpt-4o", agent.Model.ModelNameHint);
+ Assert.NotNull(agent.Model.Options);
+ Assert.Equal(0.7f, (float?)agent.Model.Options?.Temperature?.LiteralValue);
+ Assert.Equal(0.9f, (float?)agent.Model.Options?.TopP?.LiteralValue);
+
+ // Assert contents using extension methods
+ Assert.Equal(1024, agent.Model.Options?.MaxOutputTokens?.LiteralValue);
+ Assert.Equal(50, agent.Model.Options?.TopK?.LiteralValue);
+ Assert.Equal(0.7f, (float?)agent.Model.Options?.FrequencyPenalty?.LiteralValue);
+ Assert.Equal(0.7f, (float?)agent.Model.Options?.PresencePenalty?.LiteralValue);
+ Assert.Equal(42, agent.Model.Options?.Seed?.LiteralValue);
+ Assert.Equal(PromptAgents.s_stopSequences, agent.Model.Options?.StopSequences);
+ Assert.True(agent.Model.Options?.AllowMultipleToolCalls?.LiteralValue);
+ Assert.Equal(ChatToolMode.Auto, agent.Model.Options?.AsChatToolMode());
+ }
+
+ [Fact]
+ public void FromYaml_OutputSchema()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithOutputSchema);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.OutputType);
+ ChatResponseFormatJson responseFormat = (agent.OutputType.AsChatResponseFormat() as ChatResponseFormatJson)!;
+ Assert.NotNull(responseFormat);
+ Assert.NotNull(responseFormat.Schema);
+ }
+
+ [Fact]
+ public void FromYaml_CodeInterpreter()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
+
+ // Assert
+ Assert.NotNull(agent);
+ var tools = agent.Tools;
+ var codeInterpreterTools = tools.Where(t => t is CodeInterpreterTool).ToArray();
+ Assert.Single(codeInterpreterTools);
+ CodeInterpreterTool codeInterpreterTool = (codeInterpreterTools[0] as CodeInterpreterTool)!;
+ Assert.NotNull(codeInterpreterTool);
+ }
+
+ [Fact]
+ public void FromYaml_FunctionTool()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
+
+ // Assert
+ Assert.NotNull(agent);
+ var tools = agent.Tools;
+ var functionTools = tools.Where(t => t is InvokeClientTaskAction).ToArray();
+ Assert.Single(functionTools);
+ InvokeClientTaskAction functionTool = (functionTools[0] as InvokeClientTaskAction)!;
+ Assert.NotNull(functionTool);
+ Assert.Equal("GetWeather", functionTool.Name);
+ Assert.Equal("Get the weather for a given location.", functionTool.Description);
+ // TODO check schema
+ }
+
+ [Fact]
+ public void FromYaml_MCP()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
+
+ // Assert
+ Assert.NotNull(agent);
+ var tools = agent.Tools;
+ var mcpTools = tools.Where(t => t is McpServerTool).ToArray();
+ Assert.Single(mcpTools);
+ McpServerTool mcpTool = (mcpTools[0] as McpServerTool)!;
+ Assert.NotNull(mcpTool);
+ Assert.Equal("PersonInfoTool", mcpTool.ServerName?.LiteralValue);
+ AnonymousConnection connection = (mcpTool.Connection as AnonymousConnection)!;
+ Assert.NotNull(connection);
+ Assert.Equal("https://my-mcp-endpoint.com/api", connection.Endpoint?.LiteralValue);
+ }
+
+ [Fact]
+ public void FromYaml_WebSearchTool()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
+
+ // Assert
+ Assert.NotNull(agent);
+ var tools = agent.Tools;
+ var webSearchTools = tools.Where(t => t is WebSearchTool).ToArray();
+ Assert.Single(webSearchTools);
+ Assert.NotNull(webSearchTools[0] as WebSearchTool);
+ }
+
+ [Fact]
+ public void FromYaml_FileSearchTool()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithEverything);
+
+ // Assert
+ Assert.NotNull(agent);
+ var tools = agent.Tools;
+ var fileSearchTools = tools.Where(t => t is FileSearchTool).ToArray();
+ Assert.Single(fileSearchTools);
+ FileSearchTool fileSearchTool = (fileSearchTools[0] as FileSearchTool)!;
+ Assert.NotNull(fileSearchTool);
+
+ // Verify vector store content property exists and has correct values
+ Assert.NotNull(fileSearchTool.VectorStoreIds);
+ Assert.Equal(3, fileSearchTool.VectorStoreIds.LiteralValue.Length);
+ Assert.Equal("1", fileSearchTool.VectorStoreIds.LiteralValue[0]);
+ Assert.Equal("2", fileSearchTool.VectorStoreIds.LiteralValue[1]);
+ Assert.Equal("3", fileSearchTool.VectorStoreIds.LiteralValue[2]);
+ }
+
+ [Fact]
+ public void FromYaml_ApiKeyConnection()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithApiKeyConnection);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.Model);
+ CurrentModels model = (agent.Model as CurrentModels)!;
+ Assert.NotNull(model);
+ Assert.NotNull(model.Connection);
+ Assert.IsType(model.Connection);
+ ApiKeyConnection connection = (model.Connection as ApiKeyConnection)!;
+ Assert.NotNull(connection);
+ Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", connection.Endpoint?.LiteralValue);
+ Assert.Equal("my-api-key", connection.Key?.LiteralValue);
+ }
+
+ [Fact]
+ public void FromYaml_RemoteConnection()
+ {
+ // Arrange & Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithRemoteConnection);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.Model);
+ CurrentModels model = (agent.Model as CurrentModels)!;
+ Assert.NotNull(model);
+ Assert.NotNull(model.Connection);
+ Assert.IsType(model.Connection);
+ RemoteConnection connection = (model.Connection as RemoteConnection)!;
+ Assert.NotNull(connection);
+ Assert.Equal("https://my-azure-openai-endpoint.openai.azure.com/", connection.Endpoint?.LiteralValue);
+ }
+
+ [Fact]
+ public void FromYaml_WithVariableReferences()
+ {
+ // Arrange
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["OpenAIEndpoint"] = "endpoint",
+ ["OpenAIApiKey"] = "apiKey",
+ ["Temperature"] = "0.9",
+ ["TopP"] = "0.8"
+ })
+ .Build();
+
+ // Act
+ var agent = AgentBotElementYaml.FromYaml(PromptAgents.AgentWithVariableReferences, configuration);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.Model);
+ CurrentModels model = (agent.Model as CurrentModels)!;
+ Assert.NotNull(model);
+ Assert.NotNull(model.Options);
+ Assert.Equal(0.9, Eval(model.Options?.Temperature, configuration));
+ Assert.Equal(0.8, Eval(model.Options?.TopP, configuration));
+ Assert.NotNull(model.Connection);
+ Assert.IsType(model.Connection);
+ ApiKeyConnection connection = (model.Connection as ApiKeyConnection)!;
+ Assert.NotNull(connection);
+ Assert.NotNull(connection.Endpoint);
+ Assert.NotNull(connection.Key);
+ Assert.Equal("endpoint", Eval(connection.Endpoint, configuration));
+ Assert.Equal("apiKey", Eval(connection.Key, configuration));
+ }
+
+ ///
+ /// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent.
+ ///
+ [Description("Information about a person including their name, age, and occupation")]
+ public sealed class PersonInfo
+ {
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("age")]
+ public int? Age { get; set; }
+
+ [JsonPropertyName("occupation")]
+ public string? Occupation { get; set; }
+ }
+
+ private static string? Eval(StringExpression? expression, IConfiguration? configuration = null)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ RecalcEngine engine = new();
+ if (configuration is not null)
+ {
+ foreach (var kvp in configuration.AsEnumerable())
+ {
+ engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
+ }
+ }
+
+ return expression.Eval(engine);
+ }
+
+ private static double? Eval(NumberExpression? expression, IConfiguration? configuration = null)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ RecalcEngine engine = new();
+ if (configuration != null)
+ {
+ foreach (var kvp in configuration.AsEnumerable())
+ {
+ engine.UpdateVariable(kvp.Key, kvp.Value ?? string.Empty);
+ }
+ }
+
+ return expression.Eval(engine);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs
new file mode 100644
index 0000000000..d20bd9be00
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/AggregatorPromptAgentFactoryTests.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Declarative.UnitTests;
+
+///
+/// Unit tests for
+///
+public sealed class AggregatorPromptAgentFactoryTests
+{
+ [Fact]
+ public void AggregatorAgentFactory_ThrowsForEmptyArray()
+ {
+ // Arrange & Act & Assert
+ Assert.Throws(() => new AggregatorPromptAgentFactory([]));
+ }
+
+ [Fact]
+ public async Task AggregatorAgentFactory_ReturnsNull()
+ {
+ // Arrange
+ var factory = new AggregatorPromptAgentFactory([new TestAgentFactory(null)]);
+
+ // Act
+ var agent = await factory.TryCreateAsync(new GptComponentMetadata("test"));
+
+ // Assert
+ Assert.Null(agent);
+ }
+
+ [Fact]
+ public async Task AggregatorAgentFactory_ReturnsAgent()
+ {
+ // Arrange
+ var agentToReturn = new TestAgent();
+ var factory = new AggregatorPromptAgentFactory([new TestAgentFactory(null), new TestAgentFactory(agentToReturn)]);
+
+ // Act
+ var agent = await factory.TryCreateAsync(new GptComponentMetadata("test"));
+
+ // Assert
+ Assert.Equal(agentToReturn, agent);
+ }
+
+ private sealed class TestAgentFactory : PromptAgentFactory
+ {
+ private readonly AIAgent? _agentToReturn;
+
+ public TestAgentFactory(AIAgent? agentToReturn = null)
+ {
+ this._agentToReturn = agentToReturn;
+ }
+
+ public override Task TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(this._agentToReturn);
+ }
+ }
+
+ private sealed class TestAgent : AIAgent
+ {
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override AgentThread GetNewThread()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs
new file mode 100644
index 0000000000..8590662000
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/ChatClient/ChatClientAgentFactoryTests.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.Declarative.UnitTests.ChatClient;
+
+///
+/// Unit tests for .
+///
+public sealed class ChatClientAgentFactoryTests
+{
+ private readonly Mock _mockChatClient;
+
+ public ChatClientAgentFactoryTests()
+ {
+ this._mockChatClient = new();
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_WithChatClientInConstructor_CreatesAgentAsync()
+ {
+ // Arrange
+ var promptAgent = PromptAgents.CreateTestPromptAgent();
+ ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
+
+ // Act
+ AIAgent? agent = await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.IsType(agent);
+ Assert.Equal("Test Agent", agent.Name);
+ Assert.Equal("Test Description", agent.Description);
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_Creates_ChatClientAgentAsync()
+ {
+ // Arrange
+ var promptAgent = PromptAgents.CreateTestPromptAgent();
+ ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
+
+ // Act
+ AIAgent? agent = await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.IsType(agent);
+ var chatClientAgent = agent as ChatClientAgent;
+ Assert.NotNull(chatClientAgent);
+ Assert.Equal("You are a helpful assistant.", chatClientAgent.Instructions);
+ Assert.NotNull(chatClientAgent.ChatClient);
+ Assert.NotNull(chatClientAgent.ChatOptions);
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_Creates_ChatOptionsAsync()
+ {
+ // Arrange
+ var promptAgent = PromptAgents.CreateTestPromptAgent();
+ ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
+
+ // Act
+ AIAgent? agent = await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.IsType(agent);
+ var chatClientAgent = agent as ChatClientAgent;
+ Assert.NotNull(chatClientAgent?.ChatOptions);
+ Assert.Equal("You are a helpful assistant.", chatClientAgent?.ChatOptions?.Instructions);
+ Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.Temperature);
+ Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.FrequencyPenalty);
+ Assert.Equal(1024, chatClientAgent?.ChatOptions?.MaxOutputTokens);
+ Assert.Equal(0.9F, chatClientAgent?.ChatOptions?.TopP);
+ Assert.Equal(50, chatClientAgent?.ChatOptions?.TopK);
+ Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.PresencePenalty);
+ Assert.Equal(42L, chatClientAgent?.ChatOptions?.Seed);
+ Assert.NotNull(chatClientAgent?.ChatOptions?.ResponseFormat);
+ Assert.Equal("gpt-4o", chatClientAgent?.ChatOptions?.ModelId);
+ Assert.Equal(["###", "END", "STOP"], chatClientAgent?.ChatOptions?.StopSequences);
+ Assert.True(chatClientAgent?.ChatOptions?.AllowMultipleToolCalls);
+ Assert.Equal(ChatToolMode.Auto, chatClientAgent?.ChatOptions?.ToolMode);
+ Assert.Equal("customValue", chatClientAgent?.ChatOptions?.AdditionalProperties?["customProperty"]);
+ }
+
+ [Fact]
+ public async Task TryCreateAsync_Creates_ToolsAsync()
+ {
+ // Arrange
+ var promptAgent = PromptAgents.CreateTestPromptAgent();
+ ChatClientPromptAgentFactory factory = new(this._mockChatClient.Object);
+
+ // Act
+ AIAgent? agent = await factory.TryCreateAsync(promptAgent);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.IsType(agent);
+ var chatClientAgent = agent as ChatClientAgent;
+ Assert.NotNull(chatClientAgent?.ChatOptions?.Tools);
+ var tools = chatClientAgent?.ChatOptions?.Tools;
+ Assert.Equal(5, tools?.Count);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj
new file mode 100644
index 0000000000..d348a0b433
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(NoWarn);IDE1006;VSTHRD200
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs
new file mode 100644
index 0000000000..163e4ded18
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Declarative.UnitTests/PromptAgents.cs
@@ -0,0 +1,386 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.AI.Declarative.UnitTests;
+
+internal static class PromptAgents
+{
+ internal const string AgentWithEverything =
+ """
+ kind: Prompt
+ name: AgentName
+ description: Agent description
+ instructions: You are a helpful assistant.
+ model:
+ id: gpt-4o
+ options:
+ temperature: 0.7
+ maxOutputTokens: 1024
+ topP: 0.9
+ topK: 50
+ frequencyPenalty: 0.0
+ presencePenalty: 0.0
+ seed: 42
+ responseFormat: text
+ stopSequences:
+ - "###"
+ - "END"
+ - "STOP"
+ allowMultipleToolCalls: true
+ tools:
+ - kind: codeInterpreter
+ inputs:
+ - kind: HostedFileContent
+ FileId: fileId123
+ - kind: function
+ name: GetWeather
+ description: Get the weather for a given location.
+ parameters:
+ - name: location
+ type: string
+ description: The city and state, e.g. San Francisco, CA
+ required: true
+ - name: unit
+ type: string
+ description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
+ required: false
+ enum:
+ - celsius
+ - fahrenheit
+ - kind: mcp
+ serverName: PersonInfoTool
+ serverDescription: Get information about a person.
+ connection:
+ kind: AnonymousConnection
+ endpoint: https://my-mcp-endpoint.com/api
+ allowedTools:
+ - "GetPersonInfo"
+ - "UpdatePersonInfo"
+ - "DeletePersonInfo"
+ approvalMode:
+ kind: HostedMcpServerToolRequireSpecificApprovalMode
+ AlwaysRequireApprovalToolNames:
+ - "UpdatePersonInfo"
+ - "DeletePersonInfo"
+ NeverRequireApprovalToolNames:
+ - "GetPersonInfo"
+ - kind: webSearch
+ name: WebSearchTool
+ description: Search the web for information.
+ - kind: fileSearch
+ name: FileSearchTool
+ description: Search files for information.
+ ranker: default
+ scoreThreshold: 0.5
+ maxResults: 5
+ maxContentLength: 2000
+ vectorStoreIds:
+ - 1
+ - 2
+ - 3
+ """;
+
+ internal const string AgentWithOutputSchema =
+ """
+ kind: Prompt
+ name: Translation Assistant
+ description: A helpful assistant that translates text to a specified language.
+ model:
+ id: gpt-4o
+ options:
+ temperature: 0.9
+ topP: 0.95
+ instructions: You are a helpful assistant. You answer questions in {language}. You return your answers in a JSON format.
+ additionalInstructions: You must always respond in the specified language.
+ tools:
+ - kind: codeInterpreter
+ template:
+ format: PowerFx # Mustache is the other option
+ parser: None # Prompty and XML are the other options
+ inputSchema:
+ properties:
+ language: string
+ outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ """;
+
+ internal const string AgentWithApiKeyConnection =
+ """
+ kind: Prompt
+ name: AgentName
+ description: Agent description
+ instructions: You are a helpful assistant.
+ model:
+ id: gpt-4o
+ connection:
+ kind: ApiKey
+ endpoint: https://my-azure-openai-endpoint.openai.azure.com/
+ key: my-api-key
+ """;
+
+ internal const string AgentWithRemoteConnection =
+ """
+ kind: Prompt
+ name: AgentName
+ description: Agent description
+ instructions: You are a helpful assistant.
+ model:
+ id: gpt-4o
+ connection:
+ kind: Remote
+ endpoint: https://my-azure-openai-endpoint.openai.azure.com/
+ """;
+
+ internal const string AgentWithVariableReferences =
+ """
+ kind: Prompt
+ name: AgentName
+ description: Agent description
+ instructions: You are a helpful assistant.
+ model:
+ id: gpt-4o
+ options:
+ temperature: =Env.Temperature
+ topP: =Env.TopP
+ connection:
+ kind: apiKey
+ endpoint: =Env.OpenAIEndpoint
+ key: =Env.OpenAIApiKey
+ """;
+
+ internal const string OpenAIChatAgent =
+ """
+ kind: Prompt
+ name: Assistant
+ description: Helpful assistant
+ instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
+ model:
+ id: =Env.OPENAI_MODEL
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: apiKey
+ key: =Env.OPENAI_APIKEY
+ outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ """;
+
+ internal const string AgentWithCurrentModels =
+ """
+ kind: Prompt
+ name: AgentName
+ description: Agent description
+ instructions: You are a helpful assistant.
+ model:
+ id: gpt-4o
+ options:
+ temperature: 0.7
+ maxOutputTokens: 1024
+ topP: 0.9
+ topK: 50
+ frequencyPenalty: 0.7
+ presencePenalty: 0.7
+ seed: 42
+ responseFormat: text
+ stopSequences:
+ - "###"
+ - "END"
+ - "STOP"
+ allowMultipleToolCalls: true
+ chatToolMode: auto
+ """;
+
+ internal const string AgentWithCurrentModelsSnakeCase =
+ """
+ kind: Prompt
+ name: AgentName
+ description: Agent description
+ instructions: You are a helpful assistant.
+ model:
+ id: gpt-4o
+ options:
+ temperature: 0.7
+ max_output_tokens: 1024
+ top_p: 0.9
+ top_k: 50
+ frequency_penalty: 0.7
+ presence_penalty: 0.7
+ seed: 42
+ response_format: text
+ stop_sequences:
+ - "###"
+ - "END"
+ - "STOP"
+ allow_multiple_tool_calls: true
+ chat_tool_mode: auto
+ """;
+
+ internal const string Workflow =
+ """
+ kind: Workflow
+ trigger:
+
+ kind: OnConversationStart
+ id: workflow_demo
+ actions:
+
+ - kind: InvokeAzureAgent
+ id: question_student
+ conversationId: =System.ConversationId
+ agent:
+ name: StudentAgent
+
+ - kind: InvokeAzureAgent
+ id: question_teacher
+ conversationId: =System.ConversationId
+ agent:
+ name: TeacherAgent
+ output:
+ messages: Local.TeacherResponse
+
+ - kind: SetVariable
+ id: set_count_increment
+ variable: Local.TurnCount
+ value: =Local.TurnCount + 1
+
+ - kind: ConditionGroup
+ id: check_completion
+ conditions:
+
+ - condition: =!IsBlank(Find("CONGRATULATIONS", Upper(MessageText(Local.TeacherResponse))))
+ id: check_turn_done
+ actions:
+
+ - kind: SendActivity
+ id: sendActivity_done
+ activity: GOLD STAR!
+
+ - condition: =Local.TurnCount < 4
+ id: check_turn_count
+ actions:
+
+ - kind: GotoAction
+ id: goto_student_agent
+ actionId: question_student
+
+ elseActions:
+
+ - kind: SendActivity
+ id: sendActivity_tired
+ activity: Let's try again later...
+
+ """;
+
+ internal static readonly string[] s_stopSequences = ["###", "END", "STOP"];
+
+ internal static GptComponentMetadata CreateTestPromptAgent(string? publisher = "OpenAI", string? apiType = "Chat")
+ {
+ string agentYaml =
+ $"""
+ kind: Prompt
+ name: Test Agent
+ description: Test Description
+ instructions: You are a helpful assistant.
+ additionalInstructions: Provide detailed and accurate responses.
+ model:
+ id: gpt-4o
+ publisher: {publisher}
+ apiType: {apiType}
+ options:
+ modelId: gpt-4o
+ temperature: 0.7
+ maxOutputTokens: 1024
+ topP: 0.9
+ topK: 50
+ frequencyPenalty: 0.7
+ presencePenalty: 0.7
+ seed: 42
+ responseFormat: text
+ stopSequences:
+ - "###"
+ - "END"
+ - "STOP"
+ allowMultipleToolCalls: true
+ chatToolMode: auto
+ customProperty: customValue
+ connection:
+ kind: apiKey
+ endpoint: https://my-azure-openai-endpoint.openai.azure.com/
+ key: my-api-key
+ tools:
+ - kind: codeInterpreter
+ - kind: function
+ name: GetWeather
+ description: Get the weather for a given location.
+ parameters:
+ - name: location
+ type: string
+ description: The city and state, e.g. San Francisco, CA
+ required: true
+ - name: unit
+ type: string
+ description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
+ required: false
+ enum:
+ - celsius
+ - fahrenheit
+ - kind: mcp
+ serverName: PersonInfoTool
+ serverDescription: Get information about a person.
+ allowedTools:
+ - "GetPersonInfo"
+ - "UpdatePersonInfo"
+ - "DeletePersonInfo"
+ approvalMode:
+ kind: HostedMcpServerToolRequireSpecificApprovalMode
+ AlwaysRequireApprovalToolNames:
+ - "UpdatePersonInfo"
+ - "DeletePersonInfo"
+ NeverRequireApprovalToolNames:
+ - "GetPersonInfo"
+ connection:
+ kind: AnonymousConnection
+ endpoint: https://my-mcp-endpoint.com/api
+ - kind: webSearch
+ name: WebSearchTool
+ description: Search the web for information.
+ - kind: fileSearch
+ name: FileSearchTool
+ description: Search files for information.
+ vectorStoreIds:
+ - 1
+ - 2
+ - 3
+ outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ """;
+
+ return AgentBotElementYaml.FromYaml(agentYaml);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs
new file mode 100644
index 0000000000..df8caea214
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ForwardedPropertiesTests.cs
@@ -0,0 +1,358 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net.Http;
+using System.Net.ServerSentEvents;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests;
+
+public sealed class ForwardedPropertiesTests : IAsyncDisposable
+{
+ private WebApplication? _app;
+ private HttpClient? _client;
+
+ [Fact]
+ public async Task ForwardedProps_AreParsedAndPassedToAgent_WhenProvidedInRequestAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ // Create request JSON with forwardedProps (per AG-UI protocol spec)
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test forwarded props" }],
+ "forwardedProps": { "customProp": "customValue", "sessionId": "test-session-123" }
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue();
+ fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
+ fakeAgent.ReceivedForwardedProperties.GetProperty("customProp").GetString().Should().Be("customValue");
+ fakeAgent.ReceivedForwardedProperties.GetProperty("sessionId").GetString().Should().Be("test-session-123");
+ }
+
+ [Fact]
+ public async Task ForwardedProps_WithNestedObjects_AreCorrectlyParsedAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test nested props" }],
+ "forwardedProps": {
+ "user": { "id": "user-1", "name": "Test User" },
+ "metadata": { "version": "1.0", "feature": "test" }
+ }
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue();
+ fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
+
+ JsonElement user = fakeAgent.ReceivedForwardedProperties.GetProperty("user");
+ user.GetProperty("id").GetString().Should().Be("user-1");
+ user.GetProperty("name").GetString().Should().Be("Test User");
+
+ JsonElement metadata = fakeAgent.ReceivedForwardedProperties.GetProperty("metadata");
+ metadata.GetProperty("version").GetString().Should().Be("1.0");
+ metadata.GetProperty("feature").GetString().Should().Be("test");
+ }
+
+ [Fact]
+ public async Task ForwardedProps_WithArrays_AreCorrectlyParsedAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test array props" }],
+ "forwardedProps": {
+ "tags": ["tag1", "tag2", "tag3"],
+ "scores": [1, 2, 3, 4, 5]
+ }
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue();
+ fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
+
+ JsonElement tags = fakeAgent.ReceivedForwardedProperties.GetProperty("tags");
+ tags.GetArrayLength().Should().Be(3);
+ tags[0].GetString().Should().Be("tag1");
+
+ JsonElement scores = fakeAgent.ReceivedForwardedProperties.GetProperty("scores");
+ scores.GetArrayLength().Should().Be(5);
+ scores[2].GetInt32().Should().Be(3);
+ }
+
+ [Fact]
+ public async Task ForwardedProps_WhenEmpty_DoesNotCauseErrorsAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test empty props" }],
+ "forwardedProps": {}
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task ForwardedProps_WhenNotProvided_AgentStillWorksAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test no props" }]
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue();
+ fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Undefined);
+ }
+
+ [Fact]
+ public async Task ForwardedProps_ReturnsValidSSEResponse_WithTextDeltaEventsAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test response" }],
+ "forwardedProps": { "customProp": "value" }
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+ response.EnsureSuccessStatusCode();
+
+ Stream stream = await response.Content.ReadAsStreamAsync();
+ List> events = [];
+ await foreach (SseItem item in SseParser.Create(stream).EnumerateAsync())
+ {
+ events.Add(item);
+ }
+
+ // Assert
+ events.Should().NotBeEmpty();
+
+ // SSE events have EventType = "message" and the actual type is in the JSON data
+ // Should have run_started event
+ events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"RUN_STARTED\""));
+
+ // Should have text_message_start event
+ events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"TEXT_MESSAGE_START\""));
+
+ // Should have text_message_content event with the response text
+ events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"TEXT_MESSAGE_CONTENT\""));
+
+ // Should have run_finished event
+ events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"RUN_FINISHED\""));
+ }
+
+ [Fact]
+ public async Task ForwardedProps_WithMixedTypes_AreCorrectlyParsedAsync()
+ {
+ // Arrange
+ FakeForwardedPropsAgent fakeAgent = new();
+ await this.SetupTestServerAsync(fakeAgent);
+
+ const string RequestJson = """
+ {
+ "threadId": "thread-123",
+ "runId": "run-456",
+ "messages": [{ "id": "msg-1", "role": "user", "content": "test mixed types" }],
+ "forwardedProps": {
+ "stringProp": "text",
+ "numberProp": 42,
+ "boolProp": true,
+ "nullProp": null,
+ "arrayProp": [1, "two", false],
+ "objectProp": { "nested": "value" }
+ }
+ }
+ """;
+
+ using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
+
+ // Act
+ HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
+
+ // Assert
+ response.IsSuccessStatusCode.Should().BeTrue();
+ fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
+
+ fakeAgent.ReceivedForwardedProperties.GetProperty("stringProp").GetString().Should().Be("text");
+ fakeAgent.ReceivedForwardedProperties.GetProperty("numberProp").GetInt32().Should().Be(42);
+ fakeAgent.ReceivedForwardedProperties.GetProperty("boolProp").GetBoolean().Should().BeTrue();
+ fakeAgent.ReceivedForwardedProperties.GetProperty("nullProp").ValueKind.Should().Be(JsonValueKind.Null);
+ fakeAgent.ReceivedForwardedProperties.GetProperty("arrayProp").GetArrayLength().Should().Be(3);
+ fakeAgent.ReceivedForwardedProperties.GetProperty("objectProp").GetProperty("nested").GetString().Should().Be("value");
+ }
+
+ private async Task SetupTestServerAsync(FakeForwardedPropsAgent fakeAgent)
+ {
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ builder.Services.AddAGUI();
+ builder.WebHost.UseTestServer();
+
+ this._app = builder.Build();
+
+ this._app.MapAGUI("/agent", fakeAgent);
+
+ await this._app.StartAsync();
+
+ TestServer testServer = this._app.Services.GetRequiredService() as TestServer
+ ?? throw new InvalidOperationException("TestServer not found");
+
+ this._client = testServer.CreateClient();
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ this._client?.Dispose();
+ if (this._app != null)
+ {
+ await this._app.DisposeAsync();
+ }
+ }
+}
+
+[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated in tests")]
+internal sealed class FakeForwardedPropsAgent : AIAgent
+{
+ public FakeForwardedPropsAgent()
+ {
+ }
+
+ public override string? Description => "Agent for forwarded properties testing";
+
+ public JsonElement ReceivedForwardedProperties { get; private set; }
+
+ public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
+ }
+
+ public override async IAsyncEnumerable RunStreamingAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ // Extract forwarded properties from ChatOptions.AdditionalProperties (set by AG-UI hosting layer)
+ if (options is ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } &&
+ properties.TryGetValue("ag_ui_forwarded_properties", out object? propsObj) &&
+ propsObj is JsonElement forwardedProps)
+ {
+ this.ReceivedForwardedProperties = forwardedProps;
+ }
+
+ // Always return a text response
+ string messageId = Guid.NewGuid().ToString("N");
+ yield return new AgentRunResponseUpdate
+ {
+ MessageId = messageId,
+ Role = ChatRole.Assistant,
+ Contents = [new TextContent("Forwarded props processed")]
+ };
+
+ await Task.CompletedTask;
+ }
+
+ public override AgentThread GetNewThread() => new FakeInMemoryAgentThread();
+
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
+ }
+
+ private sealed class FakeInMemoryAgentThread : InMemoryAgentThread
+ {
+ public FakeInMemoryAgentThread()
+ : base()
+ {
+ }
+
+ public FakeInMemoryAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ : base(serializedThread, jsonSerializerOptions)
+ {
+ }
+ }
+
+ public override object? GetService(Type serviceType, object? serviceKey = null) => null;
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs
index 61e3f5ef57..26f855d59e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs
@@ -4,6 +4,7 @@
using System.ClientModel;
using System.ClientModel.Primitives;
using System.IO;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -91,7 +92,7 @@ public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
Name = "Test Agent",
Description = "Test description",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -222,7 +223,7 @@ public void GetAIAgent_WithClientResultAndOptions_WorksCorrectly()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -249,7 +250,7 @@ public void GetAIAgent_WithAssistantAndOptions_WorksCorrectly()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -298,7 +299,7 @@ public void GetAIAgent_WithAgentIdAndOptions_WorksCorrectly()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -325,7 +326,7 @@ public async Task GetAIAgentAsync_WithAgentIdAndOptions_WorksCorrectlyAsync()
{
Name = "Override Name",
Description = "Override Description",
- Instructions = "Override Instructions"
+ ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -455,6 +456,162 @@ public async Task GetAIAgentAsync_WithEmptyAgentId_ThrowsArgumentExceptionAsync(
Assert.Equal("agentId", exception.ParamName);
}
+ ///
+ /// Verify that CreateAIAgent with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var serviceProvider = new TestServiceProvider();
+ const string ModelId = "test-model";
+
+ // Act
+ var agent = assistantClient.CreateAIAgent(
+ ModelId,
+ instructions: "Test instructions",
+ name: "Test Agent",
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that CreateAIAgent with options and services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithOptionsAndServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var serviceProvider = new TestServiceProvider();
+ const string ModelId = "test-model";
+ var options = new ChatClientAgentOptions
+ {
+ Name = "Test Agent",
+ ChatOptions = new() { Instructions = "Test instructions" }
+ };
+
+ // Act
+ var agent = assistantClient.CreateAIAgent(ModelId, options, services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("Test Agent", agent.Name);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that GetAIAgent with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void GetAIAgent_WithServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var serviceProvider = new TestServiceProvider();
+ var assistant = ModelReaderWriter.Read(BinaryData.FromString("""{"id": "asst_abc123", "name": "Test Agent"}"""))!;
+
+ // Act
+ var agent = assistantClient.GetAIAgent(assistant, services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithServices_PassesServicesToAgentAsync()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var serviceProvider = new TestServiceProvider();
+
+ // Act
+ var agent = await assistantClient.GetAIAgentAsync("asst_abc123", services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that CreateAIAgent with both clientFactory and services works correctly.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithClientFactoryAndServices_AppliesBothCorrectly()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var serviceProvider = new TestServiceProvider();
+ var testChatClient = new TestChatClient(assistantClient.AsIChatClient("test-model"));
+ const string ModelId = "test-model";
+
+ // Act
+ var agent = assistantClient.CreateAIAgent(
+ ModelId,
+ instructions: "Test instructions",
+ name: "Test Agent",
+ clientFactory: (innerClient) => testChatClient,
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the custom chat client was applied
+ var retrievedTestClient = agent.GetService();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+
+ // Verify the IServiceProvider was passed through
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Uses reflection to access the FunctionInvocationServices property which is not public.
+ ///
+ private static IServiceProvider? GetFunctionInvocationServices(FunctionInvokingChatClient client)
+ {
+ var property = typeof(FunctionInvokingChatClient).GetProperty(
+ "FunctionInvocationServices",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return property?.GetValue(client) as IServiceProvider;
+ }
+
///
/// Creates a test AssistantClient implementation for testing.
///
@@ -488,6 +645,11 @@ public TestChatClient(IChatClient innerClient) : base(innerClient)
}
}
+ private sealed class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
+
private sealed class FakePipelineResponse : PipelineResponse
{
public override int Status => throw new NotImplementedException();
diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIChatClientExtensionsTests.cs
index 72ea0395e9..ef9f27b01a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIChatClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIChatClientExtensionsTests.cs
@@ -130,7 +130,7 @@ public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
Name = "Test Agent",
Description = "Test description",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIResponseClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIResponseClientExtensionsTests.cs
index 2612f4bfa9..f31d343157 100644
--- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIResponseClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIResponseClientExtensionsTests.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -167,4 +168,114 @@ public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
Assert.Equal("options", exception.ParamName);
}
+
+ ///
+ /// Verify that CreateAIAgent with services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var responseClient = new TestOpenAIResponseClient();
+ var serviceProvider = new TestServiceProvider();
+
+ // Act
+ var agent = responseClient.CreateAIAgent(
+ instructions: "Test instructions",
+ name: "Test Agent",
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that CreateAIAgent with options and services parameter correctly passes it through to the ChatClientAgent.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithOptionsAndServices_PassesServicesToAgent()
+ {
+ // Arrange
+ var responseClient = new TestOpenAIResponseClient();
+ var serviceProvider = new TestServiceProvider();
+ var options = new ChatClientAgentOptions
+ {
+ Name = "Test Agent",
+ ChatOptions = new() { Instructions = "Test instructions" }
+ };
+
+ // Act
+ var agent = responseClient.CreateAIAgent(options, services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("Test Agent", agent.Name);
+
+ // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// Verify that CreateAIAgent with both clientFactory and services works correctly.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithClientFactoryAndServices_AppliesBothCorrectly()
+ {
+ // Arrange
+ var responseClient = new TestOpenAIResponseClient();
+ var serviceProvider = new TestServiceProvider();
+ var testChatClient = new TestChatClient(responseClient.AsIChatClient());
+
+ // Act
+ var agent = responseClient.CreateAIAgent(
+ instructions: "Test instructions",
+ name: "Test Agent",
+ clientFactory: (innerClient) => testChatClient,
+ services: serviceProvider);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the custom chat client was applied
+ var retrievedTestClient = agent.GetService();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+
+ // Verify the IServiceProvider was passed through
+ var chatClient = agent.GetService();
+ Assert.NotNull(chatClient);
+ var functionInvokingClient = chatClient.GetService();
+ Assert.NotNull(functionInvokingClient);
+ Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient));
+ }
+
+ ///
+ /// A simple test IServiceProvider implementation for testing.
+ ///
+ private sealed class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
+
+ ///
+ /// Uses reflection to access the FunctionInvocationServices property which is not public.
+ ///
+ private static IServiceProvider? GetFunctionInvocationServices(FunctionInvokingChatClient client)
+ {
+ var property = typeof(FunctionInvokingChatClient).GetProperty(
+ "FunctionInvocationServices",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ return property?.GetValue(client) as IServiceProvider;
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs
index dc983ef202..58cf5f718f 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs
@@ -19,7 +19,6 @@ public void DefaultConstructor_InitializesWithNullValues()
// Assert
Assert.Null(options.Name);
- Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
Assert.Null(options.ChatMessageStoreFactory);
@@ -27,90 +26,44 @@ public void DefaultConstructor_InitializesWithNullValues()
}
[Fact]
- public void ParameterizedConstructor_WithNullValues_SetsPropertiesCorrectly()
+ public void Constructor_WithNullValues_SetsPropertiesCorrectly()
{
// Act
- var options = new ChatClientAgentOptions(
- instructions: null,
- name: null,
- description: null,
- tools: null);
+ var options = new ChatClientAgentOptions() { Name = null, Description = null, ChatOptions = new() { Tools = null, Instructions = null } };
// Assert
Assert.Null(options.Name);
- Assert.Null(options.Instructions);
Assert.Null(options.Description);
- Assert.Null(options.ChatOptions);
Assert.Null(options.AIContextProviderFactory);
- }
-
- [Fact]
- public void ParameterizedConstructor_WithInstructionsOnly_SetsChatOptionsWithInstructions()
- {
- // Arrange
- const string Instructions = "Test instructions";
-
- // Act
- var options = new ChatClientAgentOptions(
- instructions: Instructions,
- name: null,
- description: null,
- tools: null);
-
- // Assert
- Assert.Null(options.Name);
- Assert.Equal(Instructions, options.Instructions);
- Assert.Null(options.Description);
- Assert.Null(options.ChatOptions);
- }
-
- [Fact]
- public void ParameterizedConstructor_WithToolsOnly_SetsChatOptionsWithTools()
- {
- // Arrange
- var tools = new List { AIFunctionFactory.Create(() => "test") };
-
- // Act
- var options = new ChatClientAgentOptions(
- instructions: null,
- name: null,
- description: null,
- tools: tools);
-
- // Assert
- Assert.Null(options.Name);
- Assert.Null(options.Instructions);
- Assert.Null(options.Description);
+ Assert.Null(options.ChatMessageStoreFactory);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
- Assert.Same(tools, options.ChatOptions.Tools);
+ Assert.Null(options.ChatOptions.Tools);
}
[Fact]
- public void ParameterizedConstructor_WithInstructionsAndTools_SetsChatOptionsWithBoth()
+ public void Constructor_WithToolsOnly_SetsChatOptionsWithTools()
{
// Arrange
- const string Instructions = "Test instructions";
var tools = new List { AIFunctionFactory.Create(() => "test") };
// Act
- var options = new ChatClientAgentOptions(
- instructions: Instructions,
- name: null,
- description: null,
- tools: tools);
+ var options = new ChatClientAgentOptions()
+ {
+ Name = null,
+ Description = null,
+ ChatOptions = new() { Tools = tools }
+ };
// Assert
Assert.Null(options.Name);
- Assert.Equal(Instructions, options.Instructions);
Assert.Null(options.Description);
Assert.NotNull(options.ChatOptions);
- Assert.Null(options.ChatOptions.Instructions);
- Assert.Same(tools, options.ChatOptions.Tools);
+ AssertSameTools(tools, options.ChatOptions.Tools);
}
[Fact]
- public void ParameterizedConstructor_WithAllParameters_SetsAllPropertiesCorrectly()
+ public void Constructor_WithAllParameters_SetsAllPropertiesCorrectly()
{
// Arrange
const string Instructions = "Test instructions";
@@ -119,38 +72,37 @@ public void ParameterizedConstructor_WithAllParameters_SetsAllPropertiesCorrectl
var tools = new List { AIFunctionFactory.Create(() => "test") };
// Act
- var options = new ChatClientAgentOptions(
- instructions: Instructions,
- name: Name,
- description: Description,
- tools: tools);
+ var options = new ChatClientAgentOptions()
+ {
+ Name = Name,
+ Description = Description,
+ ChatOptions = new() { Tools = tools, Instructions = Instructions }
+ };
// Assert
Assert.Equal(Name, options.Name);
- Assert.Equal(Instructions, options.Instructions);
+ Assert.Equal(Instructions, options.ChatOptions.Instructions);
Assert.Equal(Description, options.Description);
Assert.NotNull(options.ChatOptions);
- Assert.Null(options.ChatOptions.Instructions);
- Assert.Same(tools, options.ChatOptions.Tools);
+ AssertSameTools(tools, options.ChatOptions.Tools);
}
[Fact]
- public void ParameterizedConstructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
+ public void Constructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
{
// Arrange
const string Name = "Test name";
const string Description = "Test description";
// Act
- var options = new ChatClientAgentOptions(
- instructions: null,
- name: Name,
- description: Description,
- tools: null);
+ var options = new ChatClientAgentOptions()
+ {
+ Name = Name,
+ Description = Description,
+ };
// Assert
Assert.Equal(Name, options.Name);
- Assert.Null(options.Instructions);
Assert.Equal(Description, options.Description);
Assert.Null(options.ChatOptions);
}
@@ -159,7 +111,6 @@ public void ParameterizedConstructor_WithNameAndDescriptionOnly_DoesNotCreateCha
public void Clone_CreatesDeepCopyWithSameValues()
{
// Arrange
- const string Instructions = "Test instructions";
const string Name = "Test name";
const string Description = "Test description";
var tools = new List { AIFunctionFactory.Create(() => "test") };
@@ -171,8 +122,11 @@ static AIContextProvider AIContextProviderFactory(
ChatClientAgentOptions.AIContextProviderFactoryContext ctx) =>
new Mock().Object;
- var original = new ChatClientAgentOptions(Instructions, Name, Description, tools)
+ var original = new ChatClientAgentOptions()
{
+ Name = Name,
+ Description = Description,
+ ChatOptions = new() { Tools = tools },
Id = "test-id",
ChatMessageStoreFactory = ChatMessageStoreFactory,
AIContextProviderFactory = AIContextProviderFactory
@@ -185,7 +139,6 @@ static AIContextProvider AIContextProviderFactory(
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
- Assert.Equal(original.Instructions, clone.Instructions);
Assert.Equal(original.Description, clone.Description);
Assert.Same(original.ChatMessageStoreFactory, clone.ChatMessageStoreFactory);
Assert.Same(original.AIContextProviderFactory, clone.AIContextProviderFactory);
@@ -197,14 +150,13 @@ static AIContextProvider AIContextProviderFactory(
}
[Fact]
- public void Clone_WithNullChatOptions_ClonesCorrectly()
+ public void Clone_WithoutProvidingChatOptions_ClonesCorrectly()
{
// Arrange
var original = new ChatClientAgentOptions
{
Id = "test-id",
Name = "Test name",
- Instructions = "Test instructions",
Description = "Test description"
};
@@ -215,10 +167,19 @@ public void Clone_WithNullChatOptions_ClonesCorrectly()
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
- Assert.Equal(original.Instructions, clone.Instructions);
Assert.Equal(original.Description, clone.Description);
- Assert.Null(clone.ChatOptions);
+ Assert.Null(original.ChatOptions);
Assert.Null(clone.ChatMessageStoreFactory);
Assert.Null(clone.AIContextProviderFactory);
}
+
+ private static void AssertSameTools(IList? expected, IList? actual)
+ {
+ var index = 0;
+ foreach (var tool in expected ?? [])
+ {
+ Assert.Same(tool, actual?[index]);
+ index++;
+ }
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
index 8b46d7b57c..6e9d952b57 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
@@ -31,7 +31,7 @@ public void VerifyChatClientAgentDefinition()
Id = "test-agent-id",
Name = "test name",
Description = "test description",
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
});
// Assert
@@ -65,7 +65,7 @@ public async Task VerifyChatClientAgentInvocationAsync()
ChatClientAgent agent =
new(mockService.Object, options: new()
{
- Instructions = "test instructions"
+ ChatOptions = new() { Instructions = "base instructions" },
});
// Act
@@ -99,7 +99,7 @@ public async Task RunAsyncThrowsArgumentNullExceptionWhenMessagesIsNullAsync()
{
// Arrange
var chatClient = new Mock().Object;
- ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(chatClient, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Act & Assert
await Assert.ThrowsAsync(() => agent.RunAsync((IReadOnlyCollection)null!));
@@ -120,7 +120,7 @@ public async Task RunAsyncPassesChatOptionsWhenUsingChatClientAgentRunOptionsAsy
It.Is(opts => opts.MaxOutputTokens == 100),
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Act
await agent.RunAsync([new(ChatRole.User, "test")], options: new ChatClientAgentRunOptions(chatOptions));
@@ -181,7 +181,7 @@ public async Task RunAsyncIncludesBaseInstructionsInOptionsAsync()
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions" } });
var runOptions = new AgentRunOptions();
// Act
@@ -212,7 +212,7 @@ public async Task RunAsyncSetsAuthorNameOnAllResponseMessagesAsync(string? autho
It.IsAny(),
It.IsAny())).ReturnsAsync(new ChatResponse(responseMessages));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions", Name = authorName });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, Name = authorName });
// Act
var result = await agent.RunAsync([new(ChatRole.User, "test")]);
@@ -239,7 +239,7 @@ public async Task RunAsyncRetrievesMessagesFromThreadWhenThreadStoresMessagesThr
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Create a thread using the agent's GetNewThread method
var thread = agent.GetNewThread();
@@ -270,7 +270,7 @@ public async Task RunAsyncWorksWithoutInstructionsWhenInstructionsAreNullOrEmpty
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = null });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = null } });
// Act
await agent.RunAsync([new(ChatRole.User, "test message")]);
@@ -300,7 +300,7 @@ public async Task RunAsyncWorksWithEmptyMessagesWhenNoMessagesProvidedAsync()
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Act
await agent.RunAsync([]);
@@ -326,7 +326,7 @@ public async Task RunAsyncDoesNotThrowWhenSpecifyingTwoSameThreadIdsAsync()
It.Is(opts => opts.ConversationId == "ConvId"),
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -346,7 +346,7 @@ public async Task RunAsyncThrowsWhenSpecifyingTwoDifferentThreadIdsAsync()
var chatOptions = new ChatOptions { ConversationId = "ConvId" };
Mock mockService = new();
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ThreadId" };
@@ -369,7 +369,7 @@ public async Task RunAsyncClonesChatOptionsToAddThreadIdAsync()
It.Is(opts => opts.MaxOutputTokens == 100 && opts.ConversationId == "ConvId"),
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -394,7 +394,7 @@ public async Task RunAsyncThrowsForMissingConversationIdWithConversationIdThread
It.IsAny(),
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -415,7 +415,7 @@ public async Task RunAsyncSetsConversationIdOnThreadWhenReturnedByChatClientAsyn
It.IsAny>(),
It.IsAny(),
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new();
// Act
@@ -442,7 +442,7 @@ public async Task RunAsyncUsesChatMessageStoreWhenNoConversationIdReturnedByChat
mockFactory.Setup(f => f(It.IsAny())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -473,7 +473,7 @@ public async Task RunAsyncUsesDefaultInMemoryChatMessageStoreWhenNoConversationI
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
});
// Act
@@ -508,7 +508,7 @@ public async Task RunAsyncUsesChatMessageStoreFactoryWhenProvidedAndNoConversati
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -539,7 +539,7 @@ public async Task RunAsyncThrowsWhenChatMessageStoreFactoryProvidedAndConversati
mockFactory.Setup(f => f(It.IsAny())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -592,7 +592,7 @@ public async Task RunAsyncInvokesAIContextProviderAndUsesResultAsync()
.Setup(p => p.InvokedAsync(It.IsAny(), It.IsAny()))
.Returns(new ValueTask());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
@@ -654,7 +654,7 @@ public async Task RunAsyncInvokesAIContextProviderWhenGetResponseFailsAsync()
.Setup(p => p.InvokedAsync(It.IsAny(), It.IsAny()))
.Returns(new ValueTask());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await Assert.ThrowsAsync(() => agent.RunAsync(requestMessages));
@@ -700,7 +700,7 @@ public async Task RunAsyncInvokesAIContextProviderAndSucceedsWithEmptyAIContextA
.Setup(p => p.InvokingAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new AIContext());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await agent.RunAsync([new(ChatRole.User, "user message")]);
@@ -907,7 +907,7 @@ public void InstructionsReturnsMetadataInstructionsWhenMetadataProvided()
{
// Arrange
var chatClient = new Mock().Object;
- var metadata = new ChatClientAgentOptions { Instructions = "You are a helpful assistant" };
+ var metadata = new ChatClientAgentOptions { ChatOptions = new() { Instructions = "You are a helpful assistant" } };
ChatClientAgent agent = new(chatClient, metadata);
// Act & Assert
@@ -936,7 +936,7 @@ public void InstructionsReturnsNullWhenMetadataInstructionsIsNull()
{
// Arrange
var chatClient = new Mock().Object;
- var metadata = new ChatClientAgentOptions { Instructions = null };
+ var metadata = new ChatClientAgentOptions { ChatOptions = new() { Instructions = null } };
ChatClientAgent agent = new(chatClient, metadata);
// Act & Assert
@@ -967,10 +967,10 @@ public void ConstructorUsesOptionalParams()
}
///
- /// Verify that ChatOptions property returns null when no params are provided that require a ChatOptions instance.
+ /// Verify that ChatOptions is created with instructions when instructions are provided and no tools are provided.
///
[Fact]
- public void ChatOptionsReturnsNullWhenConstructorToolsNotProvided()
+ public void ChatOptionsCreatedWithInstructionsEvenWhenConstructorToolsNotProvided()
{
// Arrange
var chatClient = new Mock().Object;
@@ -980,7 +980,8 @@ public void ChatOptionsReturnsNullWhenConstructorToolsNotProvided()
Assert.Equal("TestInstructions", agent.Instructions);
Assert.Equal("TestName", agent.Name);
Assert.Equal("TestDescription", agent.Description);
- Assert.Null(agent.ChatOptions);
+ Assert.NotNull(agent.ChatOptions);
+ Assert.Equal("TestInstructions", agent.ChatOptions.Instructions);
}
#endregion
@@ -1071,7 +1072,7 @@ public void ChatOptionsReturnsClonedCopyWhenAgentOptionsHaveChatOptions()
public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
{
// Arrange
- var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f };
+ var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, Instructions = "test instructions" };
Mock mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
@@ -1085,7 +1086,6 @@ public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List { new(ChatRole.User, "test") };
@@ -1114,7 +1114,7 @@ public async Task ChatOptionsMergingUsesAgentOptionsConstructorWhenRequestHasNon
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- ChatClientAgent agent = new(mockService.Object, options: new("test instructions"));
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
var messages = new List { new(ChatRole.User, "test") };
// Act
@@ -1167,6 +1167,7 @@ public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsy
// Arrange
var agentChatOptions = new ChatOptions
{
+ Instructions = "test instructions",
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
@@ -1204,7 +1205,6 @@ public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsy
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List { new(ChatRole.User, "test") };
@@ -1263,6 +1263,7 @@ public async Task ChatOptionsMergingConcatenatesToolsFromAgentAndRequestAsync()
var agentChatOptions = new ChatOptions
{
+ Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
@@ -1283,7 +1284,6 @@ public async Task ChatOptionsMergingConcatenatesToolsFromAgentAndRequestAsync()
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List { new(ChatRole.User, "test") };
@@ -1312,6 +1312,7 @@ public async Task ChatOptionsMergingUsesAgentToolsWhenRequestHasNoToolsAsync()
var agentChatOptions = new ChatOptions
{
+ Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
@@ -1333,7 +1334,6 @@ public async Task ChatOptionsMergingUsesAgentToolsWhenRequestHasNoToolsAsync()
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List { new(ChatRole.User, "test") };
@@ -1360,6 +1360,7 @@ public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsyn
// Arrange
var agentChatOptions = new ChatOptions
{
+ Instructions = "test instructions",
RawRepresentationFactory = _ => agentSetting
};
var requestChatOptions = new ChatOptions
@@ -1380,7 +1381,6 @@ public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsyn
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List { new(ChatRole.User, "test") };
@@ -1436,7 +1436,7 @@ public async Task ChatOptionsMergingHandlesAllScalarPropertiesCorrectlyAsync()
TopK = 50,
PresencePenalty = 0.1f,
FrequencyPenalty = 0.2f,
- Instructions = "test instructions\nrequest instructions",
+ Instructions = "agent instructions\nrequest instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
@@ -1459,7 +1459,6 @@ public async Task ChatOptionsMergingHandlesAllScalarPropertiesCorrectlyAsync()
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List { new(ChatRole.User, "test") };
@@ -1509,7 +1508,7 @@ public void GetService_RequestingAIAgentMetadata_ReturnsMetadata()
{
Id = "test-agent-id",
Name = "TestAgent",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1532,7 +1531,7 @@ public void GetService_RequestingIChatClient_ReturnsChatClient()
var mockChatClient = new Mock();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1556,7 +1555,7 @@ public void GetService_RequestingChatClientAgent_ReturnsChatClientAgent()
var mockChatClient = new Mock();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1582,7 +1581,7 @@ public void GetService_RequestingUnknownServiceType_DelegatesToChatClient()
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1606,7 +1605,7 @@ public void GetService_RequestingUnknownServiceTypeWithNullFromChatClient_Return
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1632,7 +1631,7 @@ public void GetService_WithServiceKey_DelegatesToChatClient()
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1661,7 +1660,7 @@ public void GetService_RequestingAIAgentMetadata_ReturnsMetadataWithCorrectProvi
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1694,7 +1693,7 @@ public void GetService_RequestingAIAgentMetadata_ReturnsCorrectAIAgentMetadataBa
{
Id = "test-agent-id",
Name = "TestAgent",
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1721,7 +1720,7 @@ public void GetService_RequestingAIAgentMetadata_ReturnsConsistentMetadata()
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1756,12 +1755,12 @@ public void GetService_RequestingAIAgentMetadata_StructureIsConsistentAcrossConf
var chatClientAgent1 = new ChatClientAgent(mockChatClient1.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions 1"
+ ChatOptions = new() { Instructions = "Test instructions 1" }
});
var chatClientAgent2 = new ChatClientAgent(mockChatClient2.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions 2"
+ ChatOptions = new() { Instructions = "Test instructions 2" }
});
// Act
@@ -1796,7 +1795,7 @@ public void GetService_RequestingChatClientAgentType_ReturnsBaseImplementation()
var mockChatClient = new Mock();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1820,7 +1819,7 @@ public void GetService_RequestingAIAgentType_ReturnsBaseImplementation()
var mockChatClient = new Mock();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1845,7 +1844,7 @@ public void GetService_RequestingIChatClientWithServiceKey_ReturnsOwnChatClient(
var mockChatClient = new Mock();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act - Request IChatClient with a service key (base.GetService will return null due to serviceKey)
@@ -1870,7 +1869,7 @@ public void GetService_RequestingUnknownServiceWithServiceKey_CallsUnderlyingCha
mockChatClient.Setup(c => c.GetService(typeof(string), "some-key")).Returns("test-result");
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions"
+ ChatOptions = new() { Instructions = "Test instructions" }
});
// Act - Request string with a service key (base.GetService will return null due to serviceKey)
@@ -1911,7 +1910,7 @@ public async Task VerifyChatClientAgentStreamingAsync()
ChatClientAgent agent =
new(mockService.Object, options: new()
{
- Instructions = "test instructions"
+ ChatOptions = new() { Instructions = "test instructions" }
});
// Act
@@ -1958,7 +1957,7 @@ public async Task RunStreamingAsyncUsesChatMessageStoreWhenNoConversationIdRetur
mockFactory.Setup(f => f(It.IsAny())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -1996,7 +1995,7 @@ public async Task RunStreamingAsyncThrowsWhenChatMessageStoreFactoryProvidedAndC
mockFactory.Setup(f => f(It.IsAny())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
- Instructions = "test instructions",
+ ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -2049,7 +2048,7 @@ public async Task RunStreamingAsyncInvokesAIContextProviderAndUsesResultAsync()
.Setup(p => p.InvokedAsync(It.IsAny(), It.IsAny()))
.Returns(new ValueTask());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
@@ -2112,7 +2111,7 @@ public async Task RunStreamingAsyncInvokesAIContextProviderWhenGetResponseFailsA
.Setup(p => p.InvokedAsync(It.IsAny(), It.IsAny()))
.Returns(new ValueTask());
- ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
// Act
await Assert.ThrowsAsync(async () =>
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs
index 1fd9a71b98..04eabf36af 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_DeserializeThreadTests.cs
@@ -20,7 +20,7 @@ public void DeserializeThread_UsesAIContextProviderFactory_IfProvided()
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions",
+ ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = _ =>
{
factoryCalled = true;
@@ -53,7 +53,7 @@ public void DeserializeThread_UsesChatMessageStoreFactory_IfProvided()
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions",
+ ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = _ =>
{
factoryCalled = true;
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs
index 43e0bef8bc..628d738e72 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_GetNewThreadTests.cs
@@ -19,7 +19,7 @@ public void GetNewThread_UsesAIContextProviderFactory_IfProvided()
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions",
+ ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = _ =>
{
factoryCalled = true;
@@ -46,7 +46,7 @@ public void GetNewThread_UsesChatMessageStoreFactory_IfProvided()
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
- Instructions = "Test instructions",
+ ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = _ =>
{
factoryCalled = true;
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs
index 3877358644..3407f172a2 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientBuilderExtensionsTests.cs
@@ -90,7 +90,7 @@ public void BuildAIAgent_WithOptions_CreatesAgentWithOptions()
{
Name = "AgentWithOptions",
Description = "Desc",
- Instructions = "Instr",
+ ChatOptions = new() { Instructions = "Instr" },
UseProvidedChatClientAsIs = true
};
@@ -115,7 +115,7 @@ public void BuildAIAgent_WithOptionsAndServices_CreatesAgentCorrectly()
var options = new ChatClientAgentOptions
{
Name = "ServiceAgent",
- Instructions = "Service instructions"
+ ChatOptions = new() { Instructions = "Service instructions" }
};
// Act
@@ -148,7 +148,7 @@ public void BuildAIAgent_WithNullBuilderAndOptions_Throws()
ChatClientBuilder builder = null!;
// Act & Assert
- Assert.Throws(() => builder.BuildAIAgent(options: new() { Instructions = "instructions" }));
+ Assert.Throws(() => builder.BuildAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
}
[Fact]
@@ -166,7 +166,7 @@ public void BuildAIAgent_WithMiddleware_BuildsCorrectPipeline()
var agent = builder.BuildAIAgent(
new ChatClientAgentOptions
{
- Instructions = "Middleware test",
+ ChatOptions = new() { Instructions = "Middleware test" },
UseProvidedChatClientAsIs = true
}
);
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs
index 182de0be5b..51beb6aa2e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs
@@ -57,7 +57,7 @@ public void CreateAIAgent_WithOptions_CreatesAgentWithOptions()
{
Name = "AgentWithOptions",
Description = "Desc",
- Instructions = "Instr",
+ ChatOptions = new() { Instructions = "Instr" },
UseProvidedChatClientAsIs = true
};
@@ -89,6 +89,6 @@ public void CreateAIAgent_WithNullClientAndOptions_Throws()
IChatClient chatClient = null!;
// Act & Assert
- Assert.Throws(() => chatClient.CreateAIAgent(options: new() { Instructions = "instructions" }));
+ Assert.Throws(() => chatClient.CreateAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
}
}
diff --git a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
index 0bd084951d..d187d85b78 100644
--- a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
+++ b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
@@ -37,14 +37,24 @@ public async Task CreateAIAgentAsync_WithAIFunctionTool_InvokesFunctionAsync(str
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [weatherFunction])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [weatherFunction]
+ }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
- options: new ChatClientAgentOptions(
- instructions: AgentInstructions,
- tools: [weatherFunction])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = AgentInstructions,
+ Tools = [weatherFunction]
+ }
+ }),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: AgentInstructions,
@@ -94,14 +104,24 @@ public async Task CreateAIAgentAsync_WithHostedCodeInterpreter_RunsCodeAsync(str
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
- options: new ChatClientAgentOptions(
- instructions: Instructions,
- tools: [codeInterpreterTool])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = Instructions,
+ Tools = [codeInterpreterTool]
+ }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
- options: new ChatClientAgentOptions(
- instructions: Instructions,
- tools: [codeInterpreterTool])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = Instructions,
+ Tools = [codeInterpreterTool]
+ }
+ }),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
@@ -159,14 +179,24 @@ You are a helpful agent that can help fetch data from files you know about.
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
- options: new ChatClientAgentOptions(
- instructions: Instructions,
- tools: [fileSearchTool])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = Instructions,
+ Tools = [fileSearchTool]
+ }
+ }),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
- options: new ChatClientAgentOptions(
- instructions: Instructions,
- tools: [fileSearchTool])),
+ options: new ChatClientAgentOptions()
+ {
+ ChatOptions = new()
+ {
+ Instructions = Instructions,
+ Tools = [fileSearchTool]
+ }
+ }),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs
index f98540d8cc..656d310ddf 100644
--- a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs
+++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs
@@ -47,8 +47,7 @@ public Task CreateChatClientAgentAsync(
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
- Instructions = instructions,
- ChatOptions = new() { Tools = aiTools }
+ ChatOptions = new() { Instructions = instructions, Tools = aiTools }
}));
}
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
index fbb087a153..a58583fbca 100644
--- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
@@ -73,9 +73,9 @@ public async Task CreateChatClientAgentAsync(
options: new()
{
Name = name,
- Instructions = instructions,
ChatOptions = new ChatOptions
{
+ Instructions = instructions,
Tools = aiTools,
RawRepresentationFactory = new Func(_ => new ResponseCreationOptions() { StoredOutputEnabled = store })
},
diff --git a/python/.vscode/launch.json b/python/.vscode/launch.json
deleted file mode 100644
index fac3004e95..0000000000
--- a/python/.vscode/launch.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- // Use IntelliSense to learn about possible attributes.
- // Hover to view descriptions of existing attributes.
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Python Debugger: Current File",
- "type": "debugpy",
- "request": "launch",
- "program": "${file}",
- "console": "integratedTerminal",
- "justMyCode": false
- },
- {
- "name": "AG-UI Examples Server",
- "type": "debugpy",
- "request": "launch",
- "module": "agent_framework_ag_ui_examples",
- "cwd": "${workspaceFolder}/packages/ag-ui",
- "console": "integratedTerminal",
- "justMyCode": false
- },
- {
- "name": "Python Attach",
- "type": "debugpy",
- "request": "attach",
- "connect": {
- "host": "localhost",
- "port": 5678
- }
- }
- ]
-}
diff --git a/python/.vscode/settings.json b/python/.vscode/settings.json
deleted file mode 100644
index 47da1de9e4..0000000000
--- a/python/.vscode/settings.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "cSpell.languageSettings": [
- {
- "languageId": "py",
- "allowCompoundWords": true,
- "locale": "en-US"
- }
- ],
- "[python]": {
- "editor.codeActionsOnSave": {
- "source.organizeImports.ruff": "always",
- "source.fixAll.ruff": "always"
- },
- "editor.formatOnSave": true,
- "editor.formatOnPaste": true,
- "editor.formatOnType": true,
- "editor.defaultFormatter": "charliermarsh.ruff"
- },
- "python.analysis.autoFormatStrings": true,
- "python.analysis.importFormat": "relative",
- "python.analysis.exclude": [
- "samples/semantic-kernel-migration"
- ],
- "python.analysis.packageIndexDepths": [
- {
- "name": "agent_framework",
- "depth": 2
- },
- {
- "name": "extensions",
- "depth": 2
- },
- {
- "name": "openai",
- "depth": 2
- },
- {
- "name": "azure",
- "depth": 2
- }
- ]
-}
diff --git a/python/.vscode/tasks.json b/python/.vscode/tasks.json
deleted file mode 100644
index 87e340f79d..0000000000
--- a/python/.vscode/tasks.json
+++ /dev/null
@@ -1,210 +0,0 @@
-{
- // See https://go.microsoft.com/fwlink/?LinkId=733558
- // for the documentation about the tasks.json format
- "version": "2.0.0",
- "tasks": [
- {
- "label": "Run Checks",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "pre-commit",
- "run",
- "-a"
- ],
- "problemMatcher": {
- "owner": "python",
- "fileLocation": [
- "relative",
- "${workspaceFolder}"
- ],
- "pattern": {
- "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
- "file": 1,
- "line": 2,
- "column": 3,
- "message": 4
- }
- },
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "label": "Format",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "poe",
- "fmt",
- ],
- "problemMatcher": {
- "owner": "python",
- "fileLocation": [
- "relative",
- "${workspaceFolder}"
- ],
- "pattern": {
- "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
- "file": 1,
- "line": 2,
- "column": 3,
- "message": 4
- }
- },
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "label": "Lint",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "poe",
- "lint",
- ],
- "problemMatcher": {
- "owner": "python",
- "fileLocation": [
- "relative",
- "${workspaceFolder}"
- ],
- "pattern": {
- "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
- "file": 1,
- "line": 2,
- "column": 3,
- "message": 4
- }
- },
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "label": "Mypy",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "poe",
- "mypy",
- ],
- "problemMatcher": {
- "owner": "python",
- "fileLocation": [
- "relative",
- "${workspaceFolder}"
- ],
- "pattern": {
- "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
- "file": 1,
- "line": 2,
- "column": 3,
- "message": 4
- }
- },
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "label": "Pyright",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "poe",
- "pyright",
- ],
- "problemMatcher": {
- "owner": "python",
- "fileLocation": [
- "relative",
- "${workspaceFolder}"
- ],
- "pattern": {
- "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
- "file": 1,
- "line": 2,
- "column": 3,
- "message": 4
- }
- },
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "label": "Test",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "poe",
- "test",
- ],
- "problemMatcher": {
- "owner": "python",
- "fileLocation": [
- "relative",
- "${workspaceFolder}"
- ],
- "pattern": {
- "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
- "file": 1,
- "line": 2,
- "column": 3,
- "message": 4
- }
- },
- "presentation": {
- "panel": "shared"
- }
- },
- {
- "label": "Create Venv",
- "type": "shell",
- "command": "uv venv PYTHON=${input:py_version}",
- "presentation": {
- "reveal": "always",
- "panel": "new"
- },
- "problemMatcher": []
- },
- {
- "label": "Install all dependencies",
- "type": "shell",
- "command": "uv",
- "args": [
- "run",
- "poe",
- "setup",
- "--python=${input:py_version}"
- ],
- "presentation": {
- "reveal": "always",
- "panel": "new"
- },
- "problemMatcher": []
- }
- ],
- "inputs": [
- {
- "type": "pickString",
- "options": [
- "3.10",
- "3.11",
- "3.12",
- "3.13"
- ],
- "id": "py_version",
- "description": "Python version",
- "default": "3.10"
- }
- ]
-}
\ No newline at end of file
diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py
index 7e6ad8e621..a63ad1deb2 100644
--- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py
+++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py
@@ -5,7 +5,7 @@
from collections.abc import Awaitable, Callable, MutableSequence
from typing import TYPE_CHECKING, Any, ClassVar, Literal
-from agent_framework import ChatMessage, Context, ContextProvider, Role
+from agent_framework import AGENT_FRAMEWORK_USER_AGENT, ChatMessage, Context, ContextProvider, Role
from agent_framework._logging import get_logger
from agent_framework._pydantic import AFBaseSettings
from agent_framework.exceptions import ServiceInitializationError
@@ -129,6 +129,8 @@ class AzureAISearchSettings(AFBaseSettings):
Can be set via environment variable AZURE_SEARCH_ENDPOINT.
index_name: Name of the search index.
Can be set via environment variable AZURE_SEARCH_INDEX_NAME.
+ knowledge_base_name: Name of an existing Knowledge Base (for agentic mode).
+ Can be set via environment variable AZURE_SEARCH_KNOWLEDGE_BASE_NAME.
api_key: API key for authentication (optional, use managed identity if not provided).
Can be set via environment variable AZURE_SEARCH_API_KEY.
env_file_path: If provided, the .env settings are read from this file path location.
@@ -158,6 +160,7 @@ class AzureAISearchSettings(AFBaseSettings):
endpoint: str | None = None
index_name: str | None = None
+ knowledge_base_name: str | None = None
api_key: SecretStr | None = None
@@ -239,7 +242,6 @@ def __init__(
embedding_function: Callable[[str], Awaitable[list[float]]] | None = None,
context_prompt: str | None = None,
# Agentic mode parameters (Knowledge Base)
- azure_ai_project_endpoint: str | None = None,
azure_openai_resource_url: str | None = None,
model_deployment_name: str | None = None,
model_name: str | None = None,
@@ -277,22 +279,18 @@ def __init__(
Required if vector_field_name is specified and no server-side vectorization.
context_prompt: Custom prompt to prepend to retrieved context.
Default: "Use the following context to answer the question:"
- azure_ai_project_endpoint: Azure AI Foundry project endpoint URL.
- This is NOT the same as azure_openai_resource_url - the project endpoint is used
- for Azure AI Foundry services, while the OpenAI endpoint is used by the Knowledge
- Base to call the model for query planning. Required for agentic mode.
- Example: "https://myproject.services.ai.azure.com/api/projects/myproject"
azure_openai_resource_url: Azure OpenAI resource URL for Knowledge Base model calls.
- This is the OpenAI endpoint used by the Knowledge Base to call the LLM for
- query planning and reasoning. This is separate from the project endpoint because
- the Knowledge Base directly calls Azure OpenAI for its internal operations.
- Required for agentic mode. Example: "https://myresource.openai.azure.com"
+ Required when using agentic mode with index_name (to auto-create Knowledge Base).
+ Not required when using an existing knowledge_base_name.
+ Example: "https://myresource.openai.azure.com"
model_deployment_name: Model deployment name in Azure OpenAI for Knowledge Base.
- This is the deployment name the Knowledge Base uses to call the LLM.
- Required for agentic mode.
+ Required when using agentic mode with index_name (to auto-create Knowledge Base).
+ Not required when using an existing knowledge_base_name.
model_name: The underlying model name (e.g., "gpt-4o", "gpt-4o-mini").
If not provided, defaults to model_deployment_name. Used for Knowledge Base configuration.
- knowledge_base_name: Name for the Knowledge Base. Required for agentic mode.
+ knowledge_base_name: Name of an existing Knowledge Base to use.
+ Required for agentic mode if not providing index_name.
+ Supports KBs with any source type (web, blob, index, etc.).
retrieval_instructions: Custom instructions for the Knowledge Base's
retrieval planning. Only used in agentic mode.
azure_openai_api_key: Azure OpenAI API key for Knowledge Base to call the model.
@@ -340,6 +338,7 @@ def __init__(
settings = AzureAISearchSettings(
endpoint=endpoint,
index_name=index_name,
+ knowledge_base_name=knowledge_base_name,
api_key=api_key if isinstance(api_key, str) else None,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
@@ -353,11 +352,36 @@ def __init__(
"Azure AI Search endpoint is required. Set via 'endpoint' parameter "
"or 'AZURE_SEARCH_ENDPOINT' environment variable."
)
- if not settings.index_name:
- raise ServiceInitializationError(
- "Azure AI Search index name is required. Set via 'index_name' parameter "
- "or 'AZURE_SEARCH_INDEX_NAME' environment variable."
- )
+
+ # Validate index_name and knowledge_base_name based on mode
+ # Note: settings.* contains the resolved value (explicit param OR env var)
+ if mode == "semantic":
+ # Semantic mode: always requires index_name
+ if not settings.index_name:
+ raise ServiceInitializationError(
+ "Azure AI Search index name is required for semantic mode. "
+ "Set via 'index_name' parameter or 'AZURE_SEARCH_INDEX_NAME' environment variable."
+ )
+ elif mode == "agentic":
+ # Agentic mode: requires exactly ONE of index_name or knowledge_base_name
+ if settings.index_name and settings.knowledge_base_name:
+ raise ServiceInitializationError(
+ "For agentic mode, provide either 'index_name' OR 'knowledge_base_name', not both. "
+ "Use 'index_name' to auto-create a Knowledge Base, or 'knowledge_base_name' to use an existing one."
+ )
+ if not settings.index_name and not settings.knowledge_base_name:
+ raise ServiceInitializationError(
+ "For agentic mode, provide either 'index_name' (to auto-create Knowledge Base) "
+ "or 'knowledge_base_name' (to use existing Knowledge Base). "
+ "Set via parameters or environment variables "
+ "AZURE_SEARCH_INDEX_NAME / AZURE_SEARCH_KNOWLEDGE_BASE_NAME."
+ )
+ # If using index_name to create KB, model config is required
+ if settings.index_name and not model_deployment_name:
+ raise ServiceInitializationError(
+ "model_deployment_name is required for agentic mode when creating Knowledge Base from index. "
+ "This is the Azure OpenAI deployment used by the Knowledge Base for query planning."
+ )
# Determine the credential to use
resolved_credential: AzureKeyCredential | AsyncTokenCredential
@@ -389,14 +413,27 @@ def __init__(
self.azure_openai_deployment_name = model_deployment_name
# If model_name not provided, default to deployment name
self.model_name = model_name or model_deployment_name
- self.knowledge_base_name = knowledge_base_name
+ # Use resolved KB name (from explicit param or env var)
+ self.knowledge_base_name = settings.knowledge_base_name
self.retrieval_instructions = retrieval_instructions
self.azure_openai_api_key = azure_openai_api_key
- self.azure_ai_project_endpoint = azure_ai_project_endpoint
self.knowledge_base_output_mode = knowledge_base_output_mode
self.retrieval_reasoning_effort = retrieval_reasoning_effort
self.agentic_message_history_count = agentic_message_history_count
+ # Determine if using existing Knowledge Base or auto-creating from index
+ # Since validation ensures exactly one of index_name/knowledge_base_name for agentic mode:
+ # - knowledge_base_name provided: use existing KB
+ # - index_name provided: auto-create KB from index
+ self._use_existing_knowledge_base = False
+ if mode == "agentic":
+ if settings.knowledge_base_name:
+ # Use existing KB directly (supports any source type: web, blob, index, etc.)
+ self._use_existing_knowledge_base = True
+ else:
+ # Auto-generate KB name from index name
+ self.knowledge_base_name = f"{settings.index_name}-kb"
+
# Auto-discover vector field if not specified
self._auto_discovered_vector_field = False
self._use_vectorizable_query = False # Will be set to True if server-side vectorization detected
@@ -415,22 +452,24 @@ def __init__(
"Agentic retrieval requires azure-search-documents >= 11.7.0b1 with Knowledge Base support. "
"Please upgrade: pip install azure-search-documents>=11.7.0b1"
)
- if not self.azure_openai_resource_url:
+ # Only require OpenAI resource URL if NOT using existing KB
+ # (existing KB already has its model configuration)
+ # Note: model_deployment_name is already validated at initialization
+ if not self._use_existing_knowledge_base and not self.azure_openai_resource_url:
raise ValueError(
- "azure_openai_resource_url is required for agentic mode. "
+ "azure_openai_resource_url is required for agentic mode when creating Knowledge Base from index. "
"This should be your Azure OpenAI endpoint (e.g., 'https://myresource.openai.azure.com')"
)
- if not self.azure_openai_deployment_name:
- raise ValueError("model_deployment_name is required for agentic mode")
- if not knowledge_base_name:
- raise ValueError("knowledge_base_name is required for agentic mode")
-
- # Create search client for semantic mode
- self._search_client = SearchClient(
- endpoint=self.endpoint,
- index_name=self.index_name,
- credential=self.credential,
- )
+
+ # Create search client for semantic mode (only if index_name is available)
+ self._search_client: SearchClient | None = None
+ if self.index_name:
+ self._search_client = SearchClient(
+ endpoint=self.endpoint,
+ index_name=self.index_name,
+ credential=self.credential,
+ user_agent=AGENT_FRAMEWORK_USER_AGENT,
+ )
# Create index client and retrieval client for agentic mode (Knowledge Base)
self._index_client: SearchIndexClient | None = None
@@ -439,6 +478,7 @@ def __init__(
self._index_client = SearchIndexClient(
endpoint=self.endpoint,
credential=self.credential,
+ user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
# Retrieval client will be created after Knowledge Base initialization
@@ -574,10 +614,19 @@ async def _auto_discover_vector_field(self) -> None:
try:
# Use existing index client or create temporary one
if not self._index_client:
- self._index_client = SearchIndexClient(endpoint=self.endpoint, credential=self.credential)
+ self._index_client = SearchIndexClient(
+ endpoint=self.endpoint,
+ credential=self.credential,
+ user_agent=AGENT_FRAMEWORK_USER_AGENT,
+ )
index_client = self._index_client
- # Get index schema
+ # Get index schema (index_name is guaranteed to be set for semantic mode)
+ if not self.index_name:
+ logger.warning("Cannot auto-discover vector field: index_name is not set.")
+ self._auto_discovered_vector_field = True
+ return
+
index = await index_client.get_index(self.index_name)
# Step 1: Find all vector fields
@@ -694,7 +743,10 @@ async def _semantic_search(self, query: str) -> list[str]:
search_params["semantic_configuration_name"] = self.semantic_configuration_name
search_params["query_caption"] = QueryCaptionType.EXTRACTIVE
- # Execute search
+ # Execute search (search client is guaranteed to exist for semantic mode)
+ if not self._search_client:
+ raise RuntimeError("Search client is not initialized. This should not happen in semantic mode.")
+
results = await self._search_client.search(**search_params) # type: ignore[reportUnknownVariableType]
# Format results with citations
@@ -711,27 +763,48 @@ async def _semantic_search(self, query: str) -> list[str]:
return formatted_results
async def _ensure_knowledge_base(self) -> None:
- """Ensure Knowledge Base and knowledge source are created.
+ """Ensure Knowledge Base and knowledge source are created or use existing KB.
This method is idempotent - it will only create resources if they don't exist.
Note: Azure SDK uses KnowledgeAgent classes internally, but the feature
is marketed as "Knowledge Bases" in Azure AI Search.
"""
- if self._knowledge_base_initialized or not self._index_client:
+ if self._knowledge_base_initialized:
return
- # Runtime validation for agentic mode parameters
+ # Runtime validation
if not self.knowledge_base_name:
raise ValueError("knowledge_base_name is required for agentic mode")
- if not self.azure_openai_resource_url:
- raise ValueError("azure_openai_resource_url is required for agentic mode")
- if not self.azure_openai_deployment_name:
- raise ValueError("model_deployment_name is required for agentic mode")
knowledge_base_name = self.knowledge_base_name
- # Step 1: Create or get knowledge source
+ # Path 1: Use existing Knowledge Base directly (no index needed)
+ # This supports KB with any source type (web, blob, index, etc.)
+ if self._use_existing_knowledge_base:
+ # Just create the retrieval client - KB already exists with its own sources
+ if _agentic_retrieval_available and self._retrieval_client is None:
+ self._retrieval_client = KnowledgeBaseRetrievalClient(
+ endpoint=self.endpoint,
+ knowledge_base_name=knowledge_base_name,
+ credential=self.credential,
+ user_agent=AGENT_FRAMEWORK_USER_AGENT,
+ )
+ self._knowledge_base_initialized = True
+ return
+
+ # Path 2: Auto-create Knowledge Base from search index
+ # Requires index_client and OpenAI configuration
+ if not self._index_client:
+ raise ValueError("Index client is required when creating Knowledge Base from index")
+ if not self.azure_openai_resource_url:
+ raise ValueError("azure_openai_resource_url is required when creating Knowledge Base from index")
+ if not self.azure_openai_deployment_name:
+ raise ValueError("model_deployment_name is required when creating Knowledge Base from index")
+ if not self.index_name:
+ raise ValueError("index_name is required when creating Knowledge Base from index")
+
+ # Step 1: Create or get knowledge source from index
knowledge_source_name = f"{self.index_name}-source"
try:
@@ -794,6 +867,7 @@ async def _ensure_knowledge_base(self) -> None:
endpoint=self.endpoint,
knowledge_base_name=knowledge_base_name,
credential=self.credential,
+ user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
async def _agentic_search(self, messages: list[ChatMessage]) -> list[str]:
diff --git a/python/packages/azure-ai-search/tests/test_search_provider.py b/python/packages/azure-ai-search/tests/test_search_provider.py
index 8d49c1532c..66ead79a6b 100644
--- a/python/packages/azure-ai-search/tests/test_search_provider.py
+++ b/python/packages/azure-ai-search/tests/test_search_provider.py
@@ -148,74 +148,105 @@ def test_init_semantic_mode_with_vector_field_requires_embedding_function(self)
vector_field_name="embedding",
)
- def test_init_agentic_mode_requires_azure_openai_resource_url(self) -> None:
- """Test that agentic mode requires azure_openai_resource_url."""
- with pytest.raises(ValueError, match="azure_openai_resource_url"):
+ def test_init_agentic_mode_with_kb_only(self) -> None:
+ """Test agentic mode with existing knowledge_base_name (simplest path)."""
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ env_file_path="", # Disable .env file loading
+ )
+ assert provider.mode == "agentic"
+ assert provider.knowledge_base_name == "test-kb"
+ assert provider._use_existing_knowledge_base is True
+
+ def test_init_agentic_mode_with_index_requires_model(self) -> None:
+ """Test that agentic mode with index_name requires model_deployment_name."""
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with (
+ patch.dict(os.environ, clean_env, clear=True),
+ pytest.raises(ServiceInitializationError, match="model_deployment_name"),
+ ):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
+ env_file_path="", # Disable .env file loading
)
- def test_init_agentic_mode_requires_model_deployment_name(self) -> None:
- """Test that agentic mode requires model_deployment_name."""
- with pytest.raises(ValueError, match="model_deployment_name"):
- AzureAISearchContextProvider(
+ def test_init_agentic_mode_with_index_and_model(self) -> None:
+ """Test agentic mode with index_name (auto-create KB path)."""
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ provider = AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
+ model_deployment_name="gpt-4o",
azure_openai_resource_url="https://test.openai.azure.com",
+ env_file_path="", # Disable .env file loading
)
-
- def test_init_agentic_mode_requires_knowledge_base_name(self) -> None:
- """Test that agentic mode requires knowledge_base_name."""
- with pytest.raises(ValueError, match="knowledge_base_name"):
+ assert provider.mode == "agentic"
+ assert provider.index_name == "test-index"
+ assert provider.knowledge_base_name == "test-index-kb" # Auto-generated
+ assert provider._use_existing_knowledge_base is False
+
+ def test_init_agentic_mode_rejects_both_index_and_kb(self) -> None:
+ """Test that agentic mode rejects both index_name AND knowledge_base_name."""
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with (
+ patch.dict(os.environ, clean_env, clear=True),
+ pytest.raises(ServiceInitializationError, match="either 'index_name' OR 'knowledge_base_name', not both"),
+ ):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="test-index",
api_key="test-key",
mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
+ knowledge_base_name="test-kb",
model_deployment_name="gpt-4o",
azure_openai_resource_url="https://test.openai.azure.com",
+ env_file_path="", # Disable .env file loading
)
- def test_init_agentic_mode_with_all_params(self) -> None:
- """Test initialization with all agentic mode parameters."""
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="my-gpt-4o-deployment",
- model_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- )
- assert provider.mode == "agentic"
- assert provider.azure_ai_project_endpoint == "https://test.services.ai.azure.com"
- assert provider.azure_openai_resource_url == "https://test.openai.azure.com"
- assert provider.azure_openai_deployment_name == "my-gpt-4o-deployment"
- assert provider.model_name == "gpt-4o"
- assert provider.knowledge_base_name == "test-kb"
+ def test_init_agentic_mode_requires_index_or_kb(self) -> None:
+ """Test that agentic mode requires either index_name or knowledge_base_name."""
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with (
+ patch.dict(os.environ, clean_env, clear=True),
+ pytest.raises(ServiceInitializationError, match="provide either 'index_name'.*or 'knowledge_base_name'"),
+ ):
+ AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ env_file_path="", # Disable .env file loading
+ )
def test_init_model_name_defaults_to_deployment_name(self) -> None:
"""Test that model_name defaults to deployment_name if not provided."""
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- )
- assert provider.model_name == "gpt-4o"
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ model_deployment_name="gpt-4o",
+ env_file_path="", # Disable .env file loading
+ )
+ assert provider.model_name == "gpt-4o"
def test_init_with_custom_context_prompt(self) -> None:
"""Test initialization with custom context prompt."""
@@ -335,7 +366,7 @@ class TestKnowledgeBaseSetup:
async def test_ensure_knowledge_base_creates_when_not_exists(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
- """Test that Knowledge Base is created when it doesn't exist."""
+ """Test that Knowledge Base is created when it doesn't exist (index_name path)."""
# Setup mocks
mock_index_client = AsyncMock()
mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
@@ -347,57 +378,58 @@ async def test_ensure_knowledge_base_creates_when_not_exists(
mock_search_client = AsyncMock()
mock_search_class.return_value = mock_search_client
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- model_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- )
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ # Use index_name path (auto-create KB)
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ index_name="test-index",
+ api_key="test-key",
+ mode="agentic",
+ model_deployment_name="gpt-4o",
+ azure_openai_resource_url="https://test.openai.azure.com",
+ env_file_path="", # Disable .env file loading
+ )
- await provider._ensure_knowledge_base()
+ await provider._ensure_knowledge_base()
- # Verify knowledge source was created
- mock_index_client.create_knowledge_source.assert_called_once()
- # Verify Knowledge Base was created
- mock_index_client.create_or_update_knowledge_base.assert_called_once()
+ # Verify knowledge source was created
+ mock_index_client.create_knowledge_source.assert_called_once()
+ # Verify Knowledge Base was created
+ mock_index_client.create_or_update_knowledge_base.assert_called_once()
@pytest.mark.asyncio
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
- async def test_ensure_knowledge_base_skips_when_exists(
+ async def test_ensure_knowledge_base_skips_when_using_existing_kb(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
- """Test that Knowledge Base setup is skipped when already exists."""
+ """Test that KB setup is skipped when using existing knowledge_base_name."""
# Setup mocks
mock_index_client = AsyncMock()
- mock_index_client.get_knowledge_source.return_value = MagicMock() # Exists
- mock_index_client.get_knowledge_base.return_value = MagicMock() # Exists
mock_index_class.return_value = mock_index_client
mock_search_client = AsyncMock()
mock_search_class.return_value = mock_search_client
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- )
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ # Use knowledge_base_name path (existing KB)
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ env_file_path="", # Disable .env file loading
+ )
- await provider._ensure_knowledge_base()
+ await provider._ensure_knowledge_base()
- # Verify nothing was created
- mock_index_client.create_knowledge_source.assert_not_called()
- mock_index_client.create_agent.assert_not_called()
+ # Verify nothing was created (using existing KB)
+ mock_index_client.create_knowledge_source.assert_not_called()
+ mock_index_client.create_or_update_knowledge_base.assert_not_called()
class TestContextProviderLifecycle:
@@ -437,21 +469,22 @@ async def test_context_manager_agentic_cleanup(
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
- async with AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- ) as provider:
- # Simulate retrieval client being created
- provider._retrieval_client = mock_retrieval_client
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ # Use knowledge_base_name path (existing KB)
+ async with AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ env_file_path="", # Disable .env file loading
+ ) as provider:
+ # Simulate retrieval client being created
+ provider._retrieval_client = mock_retrieval_client
- # Verify cleanup was called
- mock_retrieval_client.close.assert_called_once()
+ # Verify cleanup was called
+ mock_retrieval_client.close.assert_called_once()
def test_string_api_key_conversion(self) -> None:
"""Test that string api_key is converted to AzureKeyCredential."""
@@ -579,9 +612,6 @@ async def test_agentic_search_basic(
# Setup index client mock
mock_index_client = AsyncMock()
- mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
- mock_index_client.create_knowledge_source = AsyncMock()
- mock_index_client.create_or_update_knowledge_base = AsyncMock()
mock_index_class.return_value = mock_index_client
# Setup retrieval client mock with response
@@ -603,22 +633,23 @@ async def test_agentic_search_basic(
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- )
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ # Use knowledge_base_name path (existing KB)
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ env_file_path="", # Disable .env file loading
+ )
- context = await provider.invoking(sample_messages)
+ context = await provider.invoking(sample_messages)
- assert isinstance(context, Context)
- # Should have at least the prompt message
- assert len(context.messages) >= 1
+ assert isinstance(context, Context)
+ # Should have at least the prompt message
+ assert len(context.messages) >= 1
@pytest.mark.asyncio
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@@ -637,9 +668,6 @@ async def test_agentic_search_no_results(
mock_search_class.return_value = mock_search_client
mock_index_client = AsyncMock()
- mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
- mock_index_client.create_knowledge_source = AsyncMock()
- mock_index_client.create_or_update_knowledge_base = AsyncMock()
mock_index_class.return_value = mock_index_client
# Empty response
@@ -650,22 +678,23 @@ async def test_agentic_search_no_results(
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- )
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ # Use knowledge_base_name path (existing KB)
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ env_file_path="", # Disable .env file loading
+ )
- context = await provider.invoking(sample_messages)
+ context = await provider.invoking(sample_messages)
- assert isinstance(context, Context)
- # Should have fallback message
- assert len(context.messages) >= 1
+ assert isinstance(context, Context)
+ # Should have fallback message
+ assert len(context.messages) >= 1
@pytest.mark.asyncio
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@@ -684,9 +713,6 @@ async def test_agentic_search_with_medium_reasoning(
mock_search_class.return_value = mock_search_client
mock_index_client = AsyncMock()
- mock_index_client.get_knowledge_source.side_effect = ResourceNotFoundError("Not found")
- mock_index_client.create_knowledge_source = AsyncMock()
- mock_index_client.create_or_update_knowledge_base = AsyncMock()
mock_index_class.return_value = mock_index_client
mock_retrieval_client = AsyncMock()
@@ -706,22 +732,23 @@ async def test_agentic_search_with_medium_reasoning(
mock_retrieval_client.close = AsyncMock()
mock_retrieval_class.return_value = mock_retrieval_client
- provider = AzureAISearchContextProvider(
- endpoint="https://test.search.windows.net",
- index_name="test-index",
- api_key="test-key",
- mode="agentic",
- azure_ai_project_endpoint="https://test.services.ai.azure.com",
- model_deployment_name="gpt-4o",
- knowledge_base_name="test-kb",
- azure_openai_resource_url="https://test.openai.azure.com",
- retrieval_reasoning_effort="medium", # Test medium reasoning
- )
+ # Clear environment to ensure no env vars interfere
+ clean_env = {k: v for k, v in os.environ.items() if not k.startswith("AZURE_SEARCH_")}
+ with patch.dict(os.environ, clean_env, clear=True):
+ # Use knowledge_base_name path (existing KB)
+ provider = AzureAISearchContextProvider(
+ endpoint="https://test.search.windows.net",
+ api_key="test-key",
+ mode="agentic",
+ knowledge_base_name="test-kb",
+ retrieval_reasoning_effort="medium", # Test medium reasoning
+ env_file_path="", # Disable .env file loading
+ )
- context = await provider.invoking(sample_messages)
+ context = await provider.invoking(sample_messages)
- assert isinstance(context, Context)
- assert len(context.messages) >= 1
+ assert isinstance(context, Context)
+ assert len(context.messages) >= 1
class TestVectorFieldAutoDiscovery:
diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py
index e83a9244a7..7d8ebe0264 100644
--- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py
+++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py
@@ -562,6 +562,7 @@ async def mcp_tool_handler(context: str, client: df.DurableOrchestrationClient)
logger.debug("[MCP Tool Trigger] Received invocation for agent: %s", agent_name)
return await self._handle_mcp_tool_invocation(agent_name=agent_name, context=context, client=client)
+ _ = mcp_tool_handler
logger.debug("[AgentFunctionApp] Registered MCP tool trigger for agent: %s", agent_name)
async def _handle_mcp_tool_invocation(
@@ -587,15 +588,17 @@ async def _handle_mcp_tool_invocation(
# Parse JSON context string
try:
- parsed_context = json.loads(context)
+ parsed_context: Any = json.loads(context)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid MCP context format: {e}") from e
+ parsed_context = cast(Mapping[str, Any], parsed_context) if isinstance(parsed_context, dict) else {}
+
# Extract arguments from MCP context
- arguments = parsed_context.get("arguments", {}) if isinstance(parsed_context, dict) else {}
+ arguments: dict[str, Any] = parsed_context.get("arguments", {})
# Validate required 'query' argument
- query = arguments.get("query")
+ query: Any = arguments.get("query")
if not query or not isinstance(query, str):
raise ValueError("MCP Tool invocation is missing required 'query' argument of type string.")
@@ -951,10 +954,9 @@ def _extract_normalized_headers(self, req: func.HttpRequest) -> dict[str, str]:
"""Create a lowercase header mapping from the incoming request."""
headers: dict[str, str] = {}
raw_headers = req.headers
- if isinstance(raw_headers, Mapping):
- for key, value in raw_headers.items():
- if value is not None:
- headers[str(key).lower()] = str(value)
+ for key, value in cast(Mapping[str, str], raw_headers).items():
+ headers[key.lower()] = value
+
return headers
@staticmethod
diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_durable_agent_state.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_durable_agent_state.py
index 0d9166373f..ffb71d2367 100644
--- a/python/packages/azurefunctions/agent_framework_azurefunctions/_durable_agent_state.py
+++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_durable_agent_state.py
@@ -32,7 +32,7 @@
import json
from datetime import datetime, timezone
from enum import Enum
-from typing import Any
+from typing import Any, cast
from agent_framework import (
AgentRunResponse,
@@ -74,6 +74,130 @@ def _parse_created_at(value: Any) -> datetime:
return datetime.now(tz=timezone.utc)
+def _parse_messages(data: dict[str, Any]) -> list[DurableAgentStateMessage]:
+ """Parse messages from a dictionary, converting dicts to DurableAgentStateMessage objects.
+
+ Args:
+ data: Dictionary containing a 'messages' key with a list of message data
+
+ Returns:
+ List of DurableAgentStateMessage objects
+ """
+ messages: list[DurableAgentStateMessage] = []
+ raw_messages: list[Any] = data.get("messages", [])
+ for raw_msg in raw_messages:
+ if isinstance(raw_msg, dict):
+ messages.append(DurableAgentStateMessage.from_dict(cast(dict[str, Any], raw_msg)))
+ elif isinstance(raw_msg, DurableAgentStateMessage):
+ messages.append(raw_msg)
+ return messages
+
+
+def _parse_history_entries(data_dict: dict[str, Any]) -> list[DurableAgentStateEntry]:
+ """Parse conversation history entries from a dictionary.
+
+ Args:
+ data_dict: Dictionary containing a 'conversationHistory' key with a list of entry data
+
+ Returns:
+ List of DurableAgentStateEntry objects (requests and responses)
+ """
+ history_data: list[Any] = data_dict.get("conversationHistory", [])
+ deserialized_history: list[DurableAgentStateEntry] = []
+ for raw_entry in history_data:
+ if isinstance(raw_entry, dict):
+ entry_dict = cast(dict[str, Any], raw_entry)
+ entry_type = entry_dict.get("$type") or entry_dict.get("json_type")
+ if entry_type == DurableAgentStateEntryJsonType.RESPONSE:
+ deserialized_history.append(DurableAgentStateResponse.from_dict(entry_dict))
+ elif entry_type == DurableAgentStateEntryJsonType.REQUEST:
+ deserialized_history.append(DurableAgentStateRequest.from_dict(entry_dict))
+ else:
+ deserialized_history.append(DurableAgentStateEntry.from_dict(entry_dict))
+ elif isinstance(raw_entry, DurableAgentStateEntry):
+ deserialized_history.append(raw_entry)
+ return deserialized_history
+
+
+def _parse_contents(data: dict[str, Any]) -> list[DurableAgentStateContent]:
+ """Parse content items from a dictionary.
+
+ Args:
+ data: Dictionary containing a 'contents' key with a list of content data
+
+ Returns:
+ List of DurableAgentStateContent objects
+ """
+ contents: list[DurableAgentStateContent] = []
+ raw_contents: list[Any] = data.get("contents", [])
+ for raw_content in raw_contents:
+ if isinstance(raw_content, dict):
+ content_dict = cast(dict[str, Any], raw_content)
+ content_type: str | None = content_dict.get("$type")
+ if content_type == DurableAgentStateTextContent.type:
+ contents.append(DurableAgentStateTextContent(text=content_dict.get("text")))
+ elif content_type == DurableAgentStateDataContent.type:
+ contents.append(
+ DurableAgentStateDataContent(
+ uri=str(content_dict.get("uri", "")),
+ media_type=content_dict.get("mediaType"),
+ )
+ )
+ elif content_type == DurableAgentStateErrorContent.type:
+ contents.append(
+ DurableAgentStateErrorContent(
+ message=content_dict.get("message"),
+ error_code=content_dict.get("errorCode"),
+ details=content_dict.get("details"),
+ )
+ )
+ elif content_type == DurableAgentStateFunctionCallContent.type:
+ contents.append(
+ DurableAgentStateFunctionCallContent(
+ call_id=str(content_dict.get("callId", "")),
+ name=str(content_dict.get("name", "")),
+ arguments=content_dict.get("arguments", {}),
+ )
+ )
+ elif content_type == DurableAgentStateFunctionResultContent.type:
+ contents.append(
+ DurableAgentStateFunctionResultContent(
+ call_id=str(content_dict.get("callId", "")),
+ result=content_dict.get("result"),
+ )
+ )
+ elif content_type == DurableAgentStateHostedFileContent.type:
+ contents.append(DurableAgentStateHostedFileContent(file_id=str(content_dict.get("fileId", ""))))
+ elif content_type == DurableAgentStateHostedVectorStoreContent.type:
+ contents.append(
+ DurableAgentStateHostedVectorStoreContent(
+ vector_store_id=str(content_dict.get("vectorStoreId", ""))
+ )
+ )
+ elif content_type == DurableAgentStateTextReasoningContent.type:
+ contents.append(DurableAgentStateTextReasoningContent(text=content_dict.get("text")))
+ elif content_type == DurableAgentStateUriContent.type:
+ contents.append(
+ DurableAgentStateUriContent(
+ uri=str(content_dict.get("uri", "")),
+ media_type=str(content_dict.get("mediaType", "")),
+ )
+ )
+ elif content_type == DurableAgentStateUsageContent.type:
+ usage_data = content_dict.get("usage")
+ if usage_data and isinstance(usage_data, dict):
+ contents.append(
+ DurableAgentStateUsageContent(
+ usage=DurableAgentStateUsage.from_dict(cast(dict[str, Any], usage_data))
+ )
+ )
+ elif content_type == DurableAgentStateUnknownContent.type:
+ contents.append(DurableAgentStateUnknownContent(content=content_dict.get("content", {})))
+ elif isinstance(raw_content, DurableAgentStateContent):
+ contents.append(raw_content)
+ return contents
+
+
class DurableAgentStateContent:
"""Base class for all content types in durable agent state messages.
@@ -197,25 +321,8 @@ def to_dict(self) -> dict[str, Any]:
@classmethod
def from_dict(cls, data_dict: dict[str, Any]) -> DurableAgentStateData:
- # Restore the conversation history - deserialize entries from dicts to objects
- history_data = data_dict.get("conversationHistory", [])
- deserialized_history: list[DurableAgentStateEntry] = []
- for entry_dict in history_data:
- if isinstance(entry_dict, dict):
- # Deserialize based on $type discriminator
- entry_type = entry_dict.get("$type") or entry_dict.get("json_type")
- if entry_type == DurableAgentStateEntryJsonType.RESPONSE:
- deserialized_history.append(DurableAgentStateResponse.from_dict(entry_dict))
- elif entry_type == DurableAgentStateEntryJsonType.REQUEST:
- deserialized_history.append(DurableAgentStateRequest.from_dict(entry_dict))
- else:
- deserialized_history.append(DurableAgentStateEntry.from_dict(entry_dict))
- else:
- # Already an object
- deserialized_history.append(entry_dict)
-
return cls(
- conversation_history=deserialized_history,
+ conversation_history=_parse_history_entries(data_dict),
extension_data=data_dict.get("extensionData"),
)
@@ -227,7 +334,7 @@ class DurableAgentState:
in Azure Durable Entities. It maintains the conversation history as a sequence of request
and response entries, each with their messages, timestamps, and metadata.
- The state follows a versioned schema (currently 1.0.0) that defines the structure for:
+ The state follows a versioned schema (see SCHEMA_VERSION class constant) that defines the structure for:
- Request entries: User/system messages with optional response format specifications
- Response entries: Assistant messages with token usage information
- Messages: Individual chat messages with role, content items, and timestamps
@@ -235,7 +342,7 @@ class DurableAgentState:
State is serialized to JSON with this structure:
{
- "schemaVersion": "1.0.0",
+ "schemaVersion": "",
"data": {
"conversationHistory": [
{"$type": "request", "correlationId": "...", "createdAt": "...", "messages": [...]},
@@ -246,17 +353,20 @@ class DurableAgentState:
Attributes:
data: Container for conversation history and optional extension data
- schema_version: Schema version string (defaults to "1.0.0")
+ schema_version: Schema version string (defaults to SCHEMA_VERSION)
"""
+ # Durable Agent Schema version
+ SCHEMA_VERSION: str = "1.1.0"
+
data: DurableAgentStateData
- schema_version: str = "1.0.0"
+ schema_version: str = SCHEMA_VERSION
- def __init__(self, schema_version: str = "1.0.0"):
+ def __init__(self, schema_version: str = SCHEMA_VERSION):
"""Initialize a new durable agent state.
Args:
- schema_version: Schema version to use (defaults to "1.0.0")
+ schema_version: Schema version to use (defaults to SCHEMA_VERSION)
"""
self.data = DurableAgentStateData()
self.schema_version = schema_version
@@ -283,7 +393,7 @@ def from_dict(cls, state: dict[str, Any]) -> DurableAgentState:
logger.warning("Resetting state as it is incompatible with the current schema, all history will be lost")
return cls()
- instance = cls(schema_version=state.get("schemaVersion", "1.0.0"))
+ instance = cls(schema_version=state.get("schemaVersion", DurableAgentState.SCHEMA_VERSION))
instance.data = DurableAgentStateData.from_dict(state.get("data", {}))
return instance
@@ -325,7 +435,7 @@ def try_get_agent_response(self, correlation_id: str) -> dict[str, Any] | None:
if entry.correlation_id == correlation_id and isinstance(entry, DurableAgentStateResponse):
# Found the entry, extract response data
# Get the text content from assistant messages only
- content = "\n".join(message.text for message in entry.messages if message.text is not None)
+ content = "\n".join(message.text for message in entry.messages if message.text)
return {"content": content, "message_count": self.message_count, "correlationId": correlation_id}
return None
@@ -388,28 +498,17 @@ def __init__(
self.extension_data = extension_data
def to_dict(self) -> dict[str, Any]:
- # Ensure createdAt is never null
- created_at_value = self.created_at
- if created_at_value is None:
- created_at_value = datetime.now(tz=timezone.utc)
-
return {
"$type": self.json_type,
"correlationId": self.correlation_id,
- "createdAt": created_at_value.isoformat() if isinstance(created_at_value, datetime) else created_at_value,
+ "createdAt": self.created_at.isoformat(),
"messages": [m.to_dict() for m in self.messages],
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> DurableAgentStateEntry:
created_at = _parse_created_at(data.get("created_at"))
-
- messages = []
- for msg_dict in data.get("messages", []):
- if isinstance(msg_dict, dict):
- messages.append(DurableAgentStateMessage.from_dict(msg_dict))
- else:
- messages.append(msg_dict)
+ messages = _parse_messages(data)
return cls(
json_type=DurableAgentStateEntryJsonType(data.get("$type", "entry")),
@@ -430,6 +529,7 @@ class DurableAgentStateRequest(DurableAgentStateEntry):
Attributes:
response_type: Expected response type ("text" or "json")
response_schema: JSON schema for structured responses (when response_type is "json")
+ orchestration_id: ID of the orchestration that initiated this request (if any)
correlationId: Unique identifier linking this request to its response
created_at: Timestamp when the request was created
messages: List of messages included in this request
@@ -438,6 +538,7 @@ class DurableAgentStateRequest(DurableAgentStateEntry):
response_type: str | None = None
response_schema: dict[str, Any] | None = None
+ orchestration_id: str | None = None
def __init__(
self,
@@ -447,6 +548,7 @@ def __init__(
extension_data: dict[str, Any] | None = None,
response_type: str | None = None,
response_schema: dict[str, Any] | None = None,
+ orchestration_id: str | None = None,
) -> None:
super().__init__(
json_type=DurableAgentStateEntryJsonType.REQUEST,
@@ -457,9 +559,12 @@ def __init__(
)
self.response_type = response_type
self.response_schema = response_schema
+ self.orchestration_id = orchestration_id
def to_dict(self) -> dict[str, Any]:
data = super().to_dict()
+ if self.orchestration_id is not None:
+ data["orchestrationId"] = self.orchestration_id
if self.response_type is not None:
data["responseType"] = self.response_type
if self.response_schema is not None:
@@ -469,13 +574,7 @@ def to_dict(self) -> dict[str, Any]:
@classmethod
def from_dict(cls, data: dict[str, Any]) -> DurableAgentStateRequest:
created_at = _parse_created_at(data.get("created_at"))
-
- messages = []
- for msg_dict in data.get("messages", []):
- if isinstance(msg_dict, dict):
- messages.append(DurableAgentStateMessage.from_dict(msg_dict))
- else:
- messages.append(msg_dict)
+ messages = _parse_messages(data)
return cls(
correlation_id=data.get("correlationId", ""),
@@ -484,6 +583,7 @@ def from_dict(cls, data: dict[str, Any]) -> DurableAgentStateRequest:
extension_data=data.get("extensionData"),
response_type=data.get("responseType"),
response_schema=data.get("responseSchema"),
+ orchestration_id=data.get("orchestrationId"),
)
@staticmethod
@@ -495,6 +595,7 @@ def from_run_request(request: RunRequest) -> DurableAgentStateRequest:
created_at=datetime.now(tz=timezone.utc),
response_type=request.request_response_format,
response_schema=serialize_response_format(request.response_format),
+ orchestration_id=request.orchestration_id,
)
@@ -545,20 +646,12 @@ def to_dict(self) -> dict[str, Any]:
@classmethod
def from_dict(cls, data: dict[str, Any]) -> DurableAgentStateResponse:
created_at = _parse_created_at(data.get("created_at"))
-
- messages = []
- for msg_dict in data.get("messages", []):
- if isinstance(msg_dict, dict):
- messages.append(DurableAgentStateMessage.from_dict(msg_dict))
- else:
- messages.append(msg_dict)
+ messages = _parse_messages(data)
usage_dict = data.get("usage")
- usage = None
+ usage: DurableAgentStateUsage | None = None
if usage_dict and isinstance(usage_dict, dict):
- usage = DurableAgentStateUsage.from_dict(usage_dict)
- elif usage_dict:
- usage = usage_dict
+ usage = DurableAgentStateUsage.from_dict(cast(dict[str, Any], usage_dict))
return cls(
correlation_id=data.get("correlationId", ""),
@@ -639,68 +732,9 @@ def to_dict(self) -> dict[str, Any]:
@classmethod
def from_dict(cls, data: dict[str, Any]) -> DurableAgentStateMessage:
- contents: list[DurableAgentStateContent] = []
- for content_dict in data.get("contents", []):
- if isinstance(content_dict, dict):
- content_type = content_dict.get("$type")
- if content_type == DurableAgentStateTextContent.type:
- contents.append(DurableAgentStateTextContent(text=content_dict.get("text")))
- elif content_type == DurableAgentStateDataContent.type:
- contents.append(
- DurableAgentStateDataContent(
- uri=content_dict.get("uri", ""), media_type=content_dict.get("mediaType")
- )
- )
- elif content_type == DurableAgentStateErrorContent.type:
- contents.append(
- DurableAgentStateErrorContent(
- message=content_dict.get("message"),
- error_code=content_dict.get("errorCode"),
- details=content_dict.get("details"),
- )
- )
- elif content_type == DurableAgentStateFunctionCallContent.type:
- contents.append(
- DurableAgentStateFunctionCallContent(
- call_id=content_dict.get("callId", ""),
- name=content_dict.get("name", ""),
- arguments=content_dict.get("arguments", {}),
- )
- )
- elif content_type == DurableAgentStateFunctionResultContent.type:
- contents.append(
- DurableAgentStateFunctionResultContent(
- call_id=content_dict.get("callId", ""), result=content_dict.get("result")
- )
- )
- elif content_type == DurableAgentStateHostedFileContent.type:
- contents.append(DurableAgentStateHostedFileContent(file_id=content_dict.get("fileId", "")))
- elif content_type == DurableAgentStateHostedVectorStoreContent.type:
- contents.append(
- DurableAgentStateHostedVectorStoreContent(vector_store_id=content_dict.get("vectorStoreId", ""))
- )
- elif content_type == DurableAgentStateTextReasoningContent.type:
- contents.append(DurableAgentStateTextReasoningContent(text=content_dict.get("text")))
- elif content_type == DurableAgentStateUriContent.type:
- contents.append(
- DurableAgentStateUriContent(
- uri=content_dict.get("uri", ""), media_type=content_dict.get("mediaType", "")
- )
- )
- elif content_type == DurableAgentStateUsageContent.type:
- usage_data = content_dict.get("usage")
- if usage_data and isinstance(usage_data, dict):
- contents.append(
- DurableAgentStateUsageContent(usage=DurableAgentStateUsage.from_dict(usage_data))
- )
- elif content_type == DurableAgentStateUnknownContent.type:
- contents.append(DurableAgentStateUnknownContent(content=content_dict.get("content", {})))
- else:
- contents.append(content_dict) # type: ignore
-
return cls(
role=data.get("role", ""),
- contents=contents,
+ contents=_parse_contents(data),
author_name=data.get("authorName"),
created_at=_parse_created_at(data.get("createdAt")),
extension_data=data.get("extensionData"),
@@ -709,7 +743,7 @@ def from_dict(cls, data: dict[str, Any]) -> DurableAgentStateMessage:
@property
def text(self) -> str:
"""Extract text from the contents list."""
- text_parts = []
+ text_parts: list[str] = []
for content in self.contents:
if isinstance(content, DurableAgentStateTextContent):
text_parts.append(content.text or "")
diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_models.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_models.py
index e9ed6f7cad..2ab9667575 100644
--- a/python/packages/azurefunctions/agent_framework_azurefunctions/_models.py
+++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_models.py
@@ -287,6 +287,7 @@ class RunRequest:
thread_id: Optional thread ID for tracking
correlation_id: Optional correlation ID for tracking the response to this specific request
created_at: Optional timestamp when the request was created
+ orchestration_id: Optional ID of the orchestration that initiated this request
"""
message: str
@@ -297,6 +298,7 @@ class RunRequest:
thread_id: str | None = None
correlation_id: str | None = None
created_at: str | None = None
+ orchestration_id: str | None = None
def __init__(
self,
@@ -308,6 +310,7 @@ def __init__(
thread_id: str | None = None,
correlation_id: str | None = None,
created_at: str | None = None,
+ orchestration_id: str | None = None,
) -> None:
self.message = message
self.role = self.coerce_role(role)
@@ -317,6 +320,7 @@ def __init__(
self.thread_id = thread_id
self.correlation_id = correlation_id
self.created_at = created_at
+ self.orchestration_id = orchestration_id
@staticmethod
def coerce_role(value: Role | str | None) -> Role:
@@ -346,6 +350,8 @@ def to_dict(self) -> dict[str, Any]:
result["correlationId"] = self.correlation_id
if self.created_at:
result["created_at"] = self.created_at
+ if self.orchestration_id:
+ result["orchestrationId"] = self.orchestration_id
return result
@@ -361,4 +367,5 @@ def from_dict(cls, data: dict[str, Any]) -> RunRequest:
thread_id=data.get("thread_id"),
correlation_id=data.get("correlationId"),
created_at=data.get("created_at"),
+ orchestration_id=data.get("orchestrationId"),
)
diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py
index fb6613b85b..0f7e786778 100644
--- a/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py
+++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py
@@ -272,12 +272,14 @@ def my_orchestration(context):
)
# Prepare the request using RunRequest model
+ # Include the orchestration's instance_id so it can be stored in the agent's entity state
run_request = RunRequest(
message=message_str,
enable_tool_calls=enable_tool_calls,
correlation_id=correlation_id,
thread_id=session_id.key,
response_format=response_format,
+ orchestration_id=self.context.instance_id,
)
logger.debug("[DurableAIAgent] Calling entity %s with message: %s", entity_id, message_str[:100])
diff --git a/python/packages/azurefunctions/tests/test_entities.py b/python/packages/azurefunctions/tests/test_entities.py
index d47fae4a1f..1c3f5168a5 100644
--- a/python/packages/azurefunctions/tests/test_entities.py
+++ b/python/packages/azurefunctions/tests/test_entities.py
@@ -79,7 +79,7 @@ def test_init_creates_entity(self) -> None:
assert entity.agent == mock_agent
assert len(entity.state.data.conversation_history) == 0
assert entity.state.data.extension_data is None
- assert entity.state.schema_version == "1.0.0"
+ assert entity.state.schema_version == DurableAgentState.SCHEMA_VERSION
def test_init_stores_agent_reference(self) -> None:
"""Test that the agent reference is stored correctly."""
@@ -124,8 +124,7 @@ async def test_run_agent_executes_agent(self) -> None:
# Verify agent.run was called
mock_agent.run.assert_called_once()
_, kwargs = mock_agent.run.call_args
- sent_messages = kwargs.get("messages")
- assert isinstance(sent_messages, list)
+ sent_messages: list[Any] = kwargs.get("messages")
assert len(sent_messages) == 1
sent_message = sent_messages[0]
assert isinstance(sent_message, ChatMessage)
@@ -910,5 +909,98 @@ async def test_entity_function_with_run_request_dict(self) -> None:
assert text_found, f"Response text not found in message: {message}"
+class TestDurableAgentStateRequestOrchestrationId:
+ """Test suite for DurableAgentStateRequest orchestration_id field."""
+
+ def test_request_with_orchestration_id(self) -> None:
+ """Test creating a request with an orchestration_id."""
+ request = DurableAgentStateRequest(
+ correlation_id="corr-123",
+ created_at=datetime.now(),
+ messages=[
+ DurableAgentStateMessage(
+ role="user",
+ contents=[DurableAgentStateTextContent(text="test")],
+ )
+ ],
+ orchestration_id="orch-456",
+ )
+
+ assert request.orchestration_id == "orch-456"
+
+ def test_request_to_dict_includes_orchestration_id(self) -> None:
+ """Test that to_dict includes orchestrationId when set."""
+ request = DurableAgentStateRequest(
+ correlation_id="corr-123",
+ created_at=datetime.now(),
+ messages=[
+ DurableAgentStateMessage(
+ role="user",
+ contents=[DurableAgentStateTextContent(text="test")],
+ )
+ ],
+ orchestration_id="orch-789",
+ )
+
+ data = request.to_dict()
+
+ assert "orchestrationId" in data
+ assert data["orchestrationId"] == "orch-789"
+
+ def test_request_to_dict_excludes_orchestration_id_when_none(self) -> None:
+ """Test that to_dict excludes orchestrationId when not set."""
+ request = DurableAgentStateRequest(
+ correlation_id="corr-123",
+ created_at=datetime.now(),
+ messages=[
+ DurableAgentStateMessage(
+ role="user",
+ contents=[DurableAgentStateTextContent(text="test")],
+ )
+ ],
+ )
+
+ data = request.to_dict()
+
+ assert "orchestrationId" not in data
+
+ def test_request_from_dict_with_orchestration_id(self) -> None:
+ """Test from_dict correctly parses orchestrationId."""
+ data = {
+ "$type": "request",
+ "correlationId": "corr-123",
+ "createdAt": "2024-01-01T00:00:00Z",
+ "messages": [{"role": "user", "contents": [{"$type": "text", "text": "test"}]}],
+ "orchestrationId": "orch-from-dict",
+ }
+
+ request = DurableAgentStateRequest.from_dict(data)
+
+ assert request.orchestration_id == "orch-from-dict"
+
+ def test_request_from_run_request_with_orchestration_id(self) -> None:
+ """Test from_run_request correctly transfers orchestration_id."""
+ run_request = RunRequest(
+ message="test message",
+ correlation_id="corr-run",
+ orchestration_id="orch-from-run-request",
+ )
+
+ durable_request = DurableAgentStateRequest.from_run_request(run_request)
+
+ assert durable_request.orchestration_id == "orch-from-run-request"
+
+ def test_request_from_run_request_without_orchestration_id(self) -> None:
+ """Test from_run_request correctly handles missing orchestration_id."""
+ run_request = RunRequest(
+ message="test message",
+ correlation_id="corr-run",
+ )
+
+ durable_request = DurableAgentStateRequest.from_run_request(run_request)
+
+ assert durable_request.orchestration_id is None
+
+
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
diff --git a/python/packages/azurefunctions/tests/test_models.py b/python/packages/azurefunctions/tests/test_models.py
index b4341e8c45..74efa9c166 100644
--- a/python/packages/azurefunctions/tests/test_models.py
+++ b/python/packages/azurefunctions/tests/test_models.py
@@ -336,6 +336,71 @@ def test_round_trip_with_correlationId(self) -> None:
assert restored.correlation_id == original.correlation_id
assert restored.thread_id == original.thread_id
+ def test_init_with_orchestration_id(self) -> None:
+ """Test RunRequest initialization with orchestration_id."""
+ request = RunRequest(
+ message="Test message",
+ thread_id="thread-orch-init",
+ orchestration_id="orch-123",
+ )
+
+ assert request.message == "Test message"
+ assert request.orchestration_id == "orch-123"
+
+ def test_to_dict_with_orchestration_id(self) -> None:
+ """Test to_dict includes orchestrationId."""
+ request = RunRequest(
+ message="Test",
+ thread_id="thread-orch-to-dict",
+ orchestration_id="orch-456",
+ )
+ data = request.to_dict()
+
+ assert data["message"] == "Test"
+ assert data["orchestrationId"] == "orch-456"
+
+ def test_to_dict_excludes_orchestration_id_when_none(self) -> None:
+ """Test to_dict excludes orchestrationId when not set."""
+ request = RunRequest(
+ message="Test",
+ thread_id="thread-orch-none",
+ )
+ data = request.to_dict()
+
+ assert "orchestrationId" not in data
+
+ def test_from_dict_with_orchestration_id(self) -> None:
+ """Test from_dict with orchestrationId."""
+ data = {
+ "message": "Test",
+ "orchestrationId": "orch-789",
+ "thread_id": "thread-orch-from-dict",
+ }
+ request = RunRequest.from_dict(data)
+
+ assert request.message == "Test"
+ assert request.orchestration_id == "orch-789"
+ assert request.thread_id == "thread-orch-from-dict"
+
+ def test_round_trip_with_orchestration_id(self) -> None:
+ """Test round-trip to_dict and from_dict with orchestration_id."""
+ original = RunRequest(
+ message="Test message",
+ thread_id="thread-123",
+ role=Role.SYSTEM,
+ correlation_id="corr-123",
+ orchestration_id="orch-123",
+ )
+
+ data = original.to_dict()
+ restored = RunRequest.from_dict(data)
+
+ assert restored.message == original.message
+ assert restored.role == original.role
+ assert restored.correlation_id == original.correlation_id
+ assert restored.orchestration_id == original.orchestration_id
+ assert restored.thread_id == original.thread_id
+
class TestModelIntegration:
"""Test suite for integration between models."""
diff --git a/python/packages/azurefunctions/tests/test_orchestration.py b/python/packages/azurefunctions/tests/test_orchestration.py
index c65724c160..0f845d4105 100644
--- a/python/packages/azurefunctions/tests/test_orchestration.py
+++ b/python/packages/azurefunctions/tests/test_orchestration.py
@@ -302,6 +302,28 @@ def test_run_creates_entity_call(self) -> None:
assert request["correlationId"] == "correlation-guid"
assert "thread_id" in request
assert request["thread_id"] == "thread-guid"
+ # Verify orchestration ID is set from context.instance_id
+ assert "orchestrationId" in request
+ assert request["orchestrationId"] == "test-instance-001"
+
+ def test_run_sets_orchestration_id(self) -> None:
+ """Test that run() sets the orchestration_id from context.instance_id."""
+ mock_context = Mock()
+ mock_context.instance_id = "my-orchestration-123"
+ mock_context.new_uuid = Mock(side_effect=["thread-guid", "correlation-guid"])
+
+ entity_task = _create_entity_task()
+ mock_context.call_entity = Mock(return_value=entity_task)
+
+ agent = DurableAIAgent(mock_context, "TestAgent")
+ thread = agent.get_new_thread()
+
+ agent.run(messages="Test", thread=thread)
+
+ call_args = mock_context.call_entity.call_args
+ request = call_args[0][2]
+
+ assert request["orchestrationId"] == "my-orchestration-123"
def test_run_without_thread(self) -> None:
"""Test that run() works without explicit thread (creates unique session key)."""
diff --git a/python/packages/bedrock/LICENSE b/python/packages/bedrock/LICENSE
new file mode 100644
index 0000000000..79656060de
--- /dev/null
+++ b/python/packages/bedrock/LICENSE
@@ -0,0 +1,21 @@
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
\ No newline at end of file
diff --git a/python/packages/bedrock/README.md b/python/packages/bedrock/README.md
new file mode 100644
index 0000000000..a13b2c42fd
--- /dev/null
+++ b/python/packages/bedrock/README.md
@@ -0,0 +1,19 @@
+# Get Started with Microsoft Agent Framework Bedrock
+
+Install the provider package:
+
+```bash
+pip install agent-framework-bedrock --pre
+```
+
+## Bedrock Integration
+
+The Bedrock integration enables Microsoft Agent Framework applications to call Amazon Bedrock models with familiar chat abstractions, including tool/function calling when you attach tools through `ChatOptions`.
+
+### Basic Usage Example
+
+See the [Bedrock chat client sample](../../samples/bedrock_chatclient/README.md) for a runnable end-to-end script that:
+
+- Loads credentials from the `BEDROCK_*` environment variables
+- Instantiates `BedrockChatClient`
+- Sends a simple conversation turn and prints the response
\ No newline at end of file
diff --git a/python/packages/bedrock/agent_framework_bedrock/__init__.py b/python/packages/bedrock/agent_framework_bedrock/__init__.py
new file mode 100644
index 0000000000..84f3e5946c
--- /dev/null
+++ b/python/packages/bedrock/agent_framework_bedrock/__init__.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import importlib.metadata
+
+from ._chat_client import BedrockChatClient
+
+try:
+ __version__ = importlib.metadata.version(__name__)
+except importlib.metadata.PackageNotFoundError:
+ __version__ = "0.0.0"
+
+__all__ = [
+ "BedrockChatClient",
+ "__version__",
+]
diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py
new file mode 100644
index 0000000000..1f44e9bcb8
--- /dev/null
+++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py
@@ -0,0 +1,509 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+import json
+from collections import deque
+from collections.abc import AsyncIterable, MutableMapping, MutableSequence, Sequence
+from typing import Any, ClassVar
+from uuid import uuid4
+
+from agent_framework import (
+ AGENT_FRAMEWORK_USER_AGENT,
+ AIFunction,
+ BaseChatClient,
+ ChatMessage,
+ ChatOptions,
+ ChatResponse,
+ ChatResponseUpdate,
+ FinishReason,
+ FunctionCallContent,
+ FunctionResultContent,
+ Role,
+ TextContent,
+ ToolProtocol,
+ UsageContent,
+ UsageDetails,
+ get_logger,
+ use_chat_middleware,
+ use_function_invocation,
+)
+from agent_framework._pydantic import AFBaseSettings
+from agent_framework.exceptions import ServiceInitializationError
+from agent_framework.observability import use_observability
+from boto3.session import Session as Boto3Session
+from botocore.client import BaseClient
+from botocore.config import Config as BotoConfig
+from pydantic import SecretStr, ValidationError
+
+logger = get_logger("agent_framework.bedrock")
+
+DEFAULT_REGION = "us-east-1"
+DEFAULT_MAX_TOKENS = 1024
+
+ROLE_MAP: dict[Role, str] = {
+ Role.USER: "user",
+ Role.ASSISTANT: "assistant",
+ Role.SYSTEM: "user",
+ Role.TOOL: "user",
+}
+
+FINISH_REASON_MAP: dict[str, FinishReason] = {
+ "end_turn": FinishReason.STOP,
+ "stop_sequence": FinishReason.STOP,
+ "max_tokens": FinishReason.LENGTH,
+ "length": FinishReason.LENGTH,
+ "content_filtered": FinishReason.CONTENT_FILTER,
+ "tool_use": FinishReason.TOOL_CALLS,
+}
+
+
+class BedrockSettings(AFBaseSettings):
+ """Bedrock configuration settings pulled from environment variables or .env files."""
+
+ env_prefix: ClassVar[str] = "BEDROCK_"
+
+ region: str = DEFAULT_REGION
+ chat_model_id: str | None = None
+ access_key: SecretStr | None = None
+ secret_key: SecretStr | None = None
+ session_token: SecretStr | None = None
+
+
+@use_function_invocation
+@use_observability
+@use_chat_middleware
+class BedrockChatClient(BaseChatClient):
+ """Async chat client for Amazon Bedrock's Converse API."""
+
+ OTEL_PROVIDER_NAME: ClassVar[str] = "bedrock" # type: ignore[reportIncompatibleVariableOverride, misc]
+
+ def __init__(
+ self,
+ *,
+ region: str | None = None,
+ model_id: str | None = None,
+ access_key: str | None = None,
+ secret_key: str | None = None,
+ session_token: str | None = None,
+ client: BaseClient | None = None,
+ boto3_session: Boto3Session | None = None,
+ env_file_path: str | None = None,
+ env_file_encoding: str | None = None,
+ **kwargs: Any,
+ ) -> None:
+ try:
+ settings = BedrockSettings(
+ region=region,
+ chat_model_id=model_id,
+ access_key=access_key, # type: ignore[arg-type]
+ secret_key=secret_key, # type: ignore[arg-type]
+ session_token=session_token, # type: ignore[arg-type]
+ env_file_path=env_file_path,
+ env_file_encoding=env_file_encoding,
+ )
+ except ValidationError as ex:
+ raise ServiceInitializationError("Failed to initialize Bedrock settings.", ex) from ex
+
+ if client is None:
+ session = boto3_session or self._create_session(settings)
+ client = session.client(
+ "bedrock-runtime",
+ region_name=settings.region,
+ config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT),
+ )
+
+ super().__init__(**kwargs)
+ self._bedrock_client = client
+ self.model_id = settings.chat_model_id
+ self.region = settings.region
+
+ @staticmethod
+ def _create_session(settings: BedrockSettings) -> Boto3Session:
+ session_kwargs: dict[str, Any] = {"region_name": settings.region or DEFAULT_REGION}
+ if settings.access_key and settings.secret_key:
+ session_kwargs["aws_access_key_id"] = settings.access_key.get_secret_value()
+ session_kwargs["aws_secret_access_key"] = settings.secret_key.get_secret_value()
+ if settings.session_token:
+ session_kwargs["aws_session_token"] = settings.session_token.get_secret_value()
+ return Boto3Session(**session_kwargs)
+
+ async def _inner_get_response(
+ self,
+ *,
+ messages: MutableSequence[ChatMessage],
+ chat_options: ChatOptions,
+ **kwargs: Any,
+ ) -> ChatResponse:
+ request = self._build_converse_request(messages, chat_options, **kwargs)
+ raw_response = await asyncio.to_thread(self._bedrock_client.converse, **request)
+ return self._process_converse_response(raw_response)
+
+ async def _inner_get_streaming_response(
+ self,
+ *,
+ messages: MutableSequence[ChatMessage],
+ chat_options: ChatOptions,
+ **kwargs: Any,
+ ) -> AsyncIterable[ChatResponseUpdate]:
+ response = await self._inner_get_response(messages=messages, chat_options=chat_options, **kwargs)
+ contents = list(response.messages[0].contents if response.messages else [])
+ if response.usage_details:
+ contents.append(UsageContent(details=response.usage_details))
+ yield ChatResponseUpdate(
+ response_id=response.response_id,
+ contents=contents,
+ model_id=response.model_id,
+ finish_reason=response.finish_reason,
+ raw_representation=response.raw_representation,
+ )
+
+ def _build_converse_request(
+ self,
+ messages: MutableSequence[ChatMessage],
+ chat_options: ChatOptions,
+ **kwargs: Any,
+ ) -> dict[str, Any]:
+ model_id = chat_options.model_id or self.model_id
+ if not model_id:
+ raise ServiceInitializationError(
+ "Bedrock model_id is required. Set via chat options or BEDROCK_CHAT_MODEL_ID environment variable."
+ )
+
+ system_prompts, conversation = self._prepare_bedrock_messages(messages)
+ if not conversation:
+ raise ServiceInitializationError("At least one non-system message is required for Bedrock requests.")
+
+ payload: dict[str, Any] = {
+ "modelId": model_id,
+ "messages": conversation,
+ }
+ if system_prompts:
+ payload["system"] = system_prompts
+
+ inference_config: dict[str, Any] = {}
+ inference_config["maxTokens"] = (
+ chat_options.max_tokens if chat_options.max_tokens is not None else DEFAULT_MAX_TOKENS
+ )
+ if chat_options.temperature is not None:
+ inference_config["temperature"] = chat_options.temperature
+ if chat_options.top_p is not None:
+ inference_config["topP"] = chat_options.top_p
+ if chat_options.stop is not None:
+ inference_config["stopSequences"] = chat_options.stop
+ if inference_config:
+ payload["inferenceConfig"] = inference_config
+
+ tool_config = self._convert_tools_to_bedrock_config(chat_options.tools)
+ if tool_choice := self._convert_tool_choice(chat_options.tool_choice):
+ if tool_config is None:
+ tool_config = {}
+ tool_config["toolChoice"] = tool_choice
+ if tool_config:
+ payload["toolConfig"] = tool_config
+
+ if chat_options.additional_properties:
+ payload.update(chat_options.additional_properties)
+ if kwargs:
+ payload.update(kwargs)
+ return payload
+
+ def _prepare_bedrock_messages(
+ self, messages: Sequence[ChatMessage]
+ ) -> tuple[list[dict[str, str]], list[dict[str, Any]]]:
+ prompts: list[dict[str, str]] = []
+ conversation: list[dict[str, Any]] = []
+ pending_tool_use_ids: deque[str] = deque()
+ for message in messages:
+ if message.role == Role.SYSTEM:
+ text_value = self._gather_text_from_message(message)
+ if text_value:
+ prompts.append({"text": text_value})
+ continue
+
+ content_blocks = self._convert_message_to_content_blocks(message)
+ if not content_blocks:
+ continue
+
+ role = ROLE_MAP.get(message.role, "user")
+ if role == "assistant":
+ pending_tool_use_ids = deque(
+ block["toolUse"]["toolUseId"]
+ for block in content_blocks
+ if isinstance(block, MutableMapping) and "toolUse" in block
+ )
+ elif message.role == Role.TOOL:
+ content_blocks = self._align_tool_results_with_pending(content_blocks, pending_tool_use_ids)
+ pending_tool_use_ids.clear()
+ if not content_blocks:
+ continue
+ else:
+ pending_tool_use_ids.clear()
+
+ conversation.append({"role": role, "content": content_blocks})
+
+ return prompts, conversation
+
+ def _align_tool_results_with_pending(
+ self, content_blocks: list[dict[str, Any]], pending_tool_use_ids: deque[str]
+ ) -> list[dict[str, Any]]:
+ if not content_blocks:
+ return content_blocks
+ if not pending_tool_use_ids:
+ # No pending tool calls; drop toolResult blocks to avoid Bedrock validation errors
+ return [
+ block for block in content_blocks if not (isinstance(block, MutableMapping) and "toolResult" in block)
+ ]
+
+ aligned_blocks: list[dict[str, Any]] = []
+ pending = deque(pending_tool_use_ids)
+ for block in content_blocks:
+ if not isinstance(block, MutableMapping):
+ aligned_blocks.append(block)
+ continue
+ tool_result = block.get("toolResult")
+ if not tool_result:
+ aligned_blocks.append(block)
+ continue
+ if not pending:
+ logger.debug("Dropping extra tool result block due to missing pending tool uses: %s", block)
+ continue
+ tool_use_id = tool_result.get("toolUseId")
+ if tool_use_id:
+ try:
+ pending.remove(tool_use_id)
+ except ValueError:
+ logger.debug("Tool result references unknown toolUseId '%s'. Dropping block.", tool_use_id)
+ continue
+ else:
+ tool_result["toolUseId"] = pending.popleft()
+ aligned_blocks.append(block)
+
+ return aligned_blocks
+
+ def _convert_message_to_content_blocks(self, message: ChatMessage) -> list[dict[str, Any]]:
+ blocks: list[dict[str, Any]] = []
+ for content in message.contents:
+ block = self._convert_content_to_bedrock_block(content)
+ if block:
+ blocks.append(block)
+ if not blocks:
+ text_value = self._gather_text_from_message(message)
+ if text_value:
+ blocks.append({"text": text_value})
+ return blocks
+
+ def _convert_content_to_bedrock_block(self, content: Any) -> dict[str, Any] | None:
+ if isinstance(content, TextContent) and getattr(content, "text", None):
+ return {"text": content.text}
+ if isinstance(content, FunctionCallContent):
+ arguments = content.parse_arguments() or {}
+ return {
+ "toolUse": {
+ "toolUseId": content.call_id or self._generate_tool_call_id(),
+ "name": content.name,
+ "input": arguments,
+ }
+ }
+ if isinstance(content, FunctionResultContent):
+ tool_result_block = {
+ "toolResult": {
+ "toolUseId": content.call_id,
+ "content": self._convert_tool_result_to_blocks(content.result),
+ "status": "error" if content.exception else "success",
+ }
+ }
+ if content.exception:
+ tool_result = tool_result_block["toolResult"]
+ existing_content = tool_result.get("content")
+ content_list: list[dict[str, Any]]
+ if isinstance(existing_content, list):
+ content_list = existing_content
+ else:
+ content_list = []
+ tool_result["content"] = content_list
+ content_list.append({"text": str(content.exception)})
+ return tool_result_block
+ return None
+
+ def _convert_tool_result_to_blocks(self, result: Any) -> list[dict[str, Any]]:
+ if isinstance(result, list):
+ blocks: list[dict[str, Any]] = []
+ for item in result:
+ if isinstance(item, list):
+ blocks.extend(self._convert_tool_result_to_blocks(item))
+ else:
+ blocks.append(self._normalize_tool_result_value(item))
+ return blocks or [{"text": ""}]
+ return [self._normalize_tool_result_value(result)]
+
+ def _normalize_tool_result_value(self, value: Any) -> dict[str, Any]:
+ if isinstance(value, dict):
+ return {"json": value}
+ if isinstance(value, (list, tuple)):
+ return {"json": list(value)}
+ if isinstance(value, str):
+ return {"text": value}
+ if isinstance(value, (int, float, bool)) or value is None:
+ return {"json": value}
+ if isinstance(value, TextContent) and getattr(value, "text", None):
+ return {"text": value.text}
+ if hasattr(value, "to_dict"):
+ try:
+ return {"json": value.to_dict()} # type: ignore[call-arg]
+ except Exception: # pragma: no cover - defensive
+ return {"text": str(value)}
+ return {"text": str(value)}
+
+ def _convert_tools_to_bedrock_config(
+ self, tools: list[ToolProtocol | MutableMapping[str, Any]] | None
+ ) -> dict[str, Any] | None:
+ if not tools:
+ return None
+ converted: list[dict[str, Any]] = []
+ for tool in tools:
+ if isinstance(tool, MutableMapping):
+ converted.append(dict(tool))
+ continue
+ if isinstance(tool, AIFunction):
+ converted.append({
+ "toolSpec": {
+ "name": tool.name,
+ "description": tool.description or "",
+ "inputSchema": {"json": tool.parameters()},
+ }
+ })
+ continue
+ logger.debug("Ignoring unsupported tool type for Bedrock: %s", type(tool))
+ return {"tools": converted} if converted else None
+
+ def _convert_tool_choice(self, tool_choice: Any) -> dict[str, Any] | None:
+ if not tool_choice:
+ return None
+ mode = tool_choice.mode if hasattr(tool_choice, "mode") else str(tool_choice)
+ required_name = getattr(tool_choice, "required_function_name", None)
+ match mode:
+ case "auto":
+ return {"auto": {}}
+ case "none":
+ return {"none": {}}
+ case "required":
+ if required_name:
+ return {"tool": {"name": required_name}}
+ return {"any": {}}
+ case _:
+ logger.debug("Unsupported tool choice mode for Bedrock: %s", mode)
+ return None
+
+ @staticmethod
+ def _gather_text_from_message(message: ChatMessage) -> str:
+ text_parts: list[str] = []
+ for content in message.contents:
+ if isinstance(content, TextContent) and getattr(content, "text", None):
+ text_parts.append(content.text)
+ if not text_parts and getattr(message, "text", None):
+ text_parts.append(message.text)
+ return "\n\n".join(part for part in text_parts if part)
+
+ @staticmethod
+ def _generate_tool_call_id() -> str:
+ return f"tool-call-{uuid4().hex}"
+
+ def _process_converse_response(self, response: dict[str, Any]) -> ChatResponse:
+ output = response.get("output", {})
+ message = output.get("message", {})
+ content_blocks = message.get("content", []) or []
+ contents = self._parse_message_contents(content_blocks)
+ chat_message = ChatMessage(role=Role.ASSISTANT, contents=contents, raw_representation=message)
+ usage_details = self._parse_usage(response.get("usage") or output.get("usage"))
+ finish_reason = self._map_finish_reason(output.get("completionReason") or response.get("stopReason"))
+ response_id = response.get("responseId") or message.get("id")
+ model_id = response.get("modelId") or output.get("modelId") or self.model_id
+ return ChatResponse(
+ response_id=response_id,
+ messages=[chat_message],
+ usage_details=usage_details,
+ model_id=model_id,
+ finish_reason=finish_reason,
+ raw_representation=response,
+ )
+
+ def _parse_usage(self, usage: dict[str, Any] | None) -> UsageDetails | None:
+ if not usage:
+ return None
+ details = UsageDetails()
+ if (input_tokens := usage.get("inputTokens")) is not None:
+ details.input_token_count = input_tokens
+ if (output_tokens := usage.get("outputTokens")) is not None:
+ details.output_token_count = output_tokens
+ if (total_tokens := usage.get("totalTokens")) is not None:
+ details.additional_counts["bedrock.total_tokens"] = total_tokens
+ return details
+
+ def _parse_message_contents(self, content_blocks: Sequence[MutableMapping[str, Any]]) -> list[Any]:
+ contents: list[Any] = []
+ for block in content_blocks:
+ if text_value := block.get("text"):
+ contents.append(TextContent(text=text_value, raw_representation=block))
+ continue
+ if (json_value := block.get("json")) is not None:
+ contents.append(TextContent(text=json.dumps(json_value), raw_representation=block))
+ continue
+ tool_use = block.get("toolUse")
+ if isinstance(tool_use, MutableMapping):
+ contents.append(
+ FunctionCallContent(
+ call_id=tool_use.get("toolUseId") or self._generate_tool_call_id(),
+ name=tool_use.get("name", "tool"),
+ arguments=tool_use.get("input"),
+ raw_representation=block,
+ )
+ )
+ continue
+ tool_result = block.get("toolResult")
+ if isinstance(tool_result, MutableMapping):
+ status = (tool_result.get("status") or "success").lower()
+ exception = None
+ if status not in {"success", "ok"}:
+ exception = RuntimeError(f"Bedrock tool result status: {status}")
+ result_value = self._convert_bedrock_tool_result_to_value(tool_result.get("content"))
+ contents.append(
+ FunctionResultContent(
+ call_id=tool_result.get("toolUseId") or self._generate_tool_call_id(),
+ result=result_value,
+ exception=exception,
+ raw_representation=block,
+ )
+ )
+ continue
+ logger.debug("Ignoring unsupported Bedrock content block: %s", block)
+ return contents
+
+ def _map_finish_reason(self, reason: str | None) -> FinishReason | None:
+ if not reason:
+ return None
+ return FINISH_REASON_MAP.get(reason.lower())
+
+ def service_url(self) -> str:
+ return f"https://bedrock-runtime.{self.region}.amazonaws.com"
+
+ def _convert_bedrock_tool_result_to_value(self, content: Any) -> Any:
+ if not content:
+ return None
+ if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)):
+ values: list[Any] = []
+ for item in content:
+ if isinstance(item, MutableMapping):
+ if (text_value := item.get("text")) is not None:
+ values.append(text_value)
+ continue
+ if "json" in item:
+ values.append(item["json"])
+ continue
+ values.append(item)
+ return values[0] if len(values) == 1 else values
+ if isinstance(content, MutableMapping):
+ if (text_value := content.get("text")) is not None:
+ return text_value
+ if "json" in content:
+ return content["json"]
+ return content
diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml
new file mode 100644
index 0000000000..ea6cffda42
--- /dev/null
+++ b/python/packages/bedrock/pyproject.toml
@@ -0,0 +1,90 @@
+[project]
+name = "agent-framework-bedrock"
+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.0b251120"
+license-files = ["LICENSE"]
+urls.homepage = "https://aka.ms/agent-framework"
+urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
+urls.issues = "https://github.com/microsoft/agent-framework/issues"
+classifiers = [
+ "License :: OSI Approved :: MIT License",
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Typing :: Typed",
+]
+dependencies = [
+ "agent-framework-core",
+ "boto3>=1.35.0,<2.0.0",
+ "botocore>=1.35.0,<2.0.0",
+]
+
+
+[tool.uv]
+prerelease = "if-necessary-or-explicit"
+environments = [
+ "sys_platform == 'darwin'",
+ "sys_platform == 'linux'",
+ "sys_platform == 'win32'"
+]
+
+[tool.uv-dynamic-versioning]
+fallback-version = "0.0.0"
+
+[tool.pytest.ini_options]
+testpaths = 'tests'
+addopts = "-ra -q -r fEX"
+asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "function"
+filterwarnings = []
+timeout = 120
+
+[tool.ruff]
+extend = "../../pyproject.toml"
+
+[tool.coverage.run]
+omit = [
+ "**/__init__.py"
+]
+
+[tool.pyright]
+extends = "../../pyproject.toml"
+
+[tool.mypy]
+plugins = ['pydantic.mypy']
+strict = true
+python_version = "3.10"
+ignore_missing_imports = true
+disallow_untyped_defs = true
+no_implicit_optional = true
+check_untyped_defs = true
+warn_return_any = true
+show_error_codes = true
+warn_unused_ignores = false
+disallow_incomplete_defs = true
+disallow_untyped_decorators = true
+
+[tool.bandit]
+targets = ["agent_framework_bedrock"]
+exclude_dirs = ["tests"]
+
+[tool.poe]
+executor.type = "uv"
+include = "../../shared_tasks.toml"
+
+[tool.poe.tasks]
+mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock"
+test = "pytest --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
\ No newline at end of file
diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py
new file mode 100644
index 0000000000..4086dfa429
--- /dev/null
+++ b/python/packages/bedrock/tests/test_bedrock_client.py
@@ -0,0 +1,69 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+
+import pytest
+from agent_framework import ChatMessage, ChatOptions, Role, TextContent
+from agent_framework.exceptions import ServiceInitializationError
+
+from agent_framework_bedrock import BedrockChatClient
+
+
+class _StubBedrockRuntime:
+ def __init__(self) -> None:
+ self.calls: list[dict[str, Any]] = []
+
+ def converse(self, **kwargs: Any) -> dict[str, Any]:
+ self.calls.append(kwargs)
+ return {
+ "modelId": kwargs["modelId"],
+ "responseId": "resp-123",
+ "usage": {"inputTokens": 10, "outputTokens": 5, "totalTokens": 15},
+ "output": {
+ "completionReason": "end_turn",
+ "message": {
+ "id": "msg-1",
+ "role": "assistant",
+ "content": [{"text": "Bedrock says hi"}],
+ },
+ },
+ }
+
+
+def test_get_response_invokes_bedrock_runtime() -> None:
+ stub = _StubBedrockRuntime()
+ client = BedrockChatClient(
+ model_id="amazon.titan-text",
+ region="us-west-2",
+ client=stub,
+ )
+
+ messages = [
+ ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are concise.")]),
+ ChatMessage(role=Role.USER, contents=[TextContent(text="hello")]),
+ ]
+
+ response = asyncio.run(client.get_response(messages=messages, chat_options=ChatOptions(max_tokens=32)))
+
+ assert stub.calls, "Expected the runtime client to be called"
+ payload = stub.calls[0]
+ assert payload["modelId"] == "amazon.titan-text"
+ assert payload["messages"][0]["content"][0]["text"] == "hello"
+ assert response.messages[0].contents[0].text == "Bedrock says hi"
+ assert response.usage_details and response.usage_details.input_token_count == 10
+
+
+def test_build_request_requires_non_system_messages() -> None:
+ client = BedrockChatClient(
+ model_id="amazon.titan-text",
+ region="us-west-2",
+ client=_StubBedrockRuntime(),
+ )
+
+ messages = [ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="Only system text")])]
+
+ with pytest.raises(ServiceInitializationError):
+ client._build_converse_request(messages, ChatOptions())
diff --git a/python/packages/bedrock/tests/test_bedrock_settings.py b/python/packages/bedrock/tests/test_bedrock_settings.py
new file mode 100644
index 0000000000..a3b0894d28
--- /dev/null
+++ b/python/packages/bedrock/tests/test_bedrock_settings.py
@@ -0,0 +1,133 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from __future__ import annotations
+
+from unittest.mock import MagicMock
+
+import pytest
+from agent_framework import (
+ AIFunction,
+ ChatMessage,
+ ChatOptions,
+ FunctionCallContent,
+ FunctionResultContent,
+ Role,
+ TextContent,
+ ToolMode,
+)
+from pydantic import BaseModel
+
+from agent_framework_bedrock._chat_client import BedrockChatClient, BedrockSettings
+
+
+class _WeatherArgs(BaseModel):
+ location: str
+
+
+def _build_client() -> BedrockChatClient:
+ fake_runtime = MagicMock()
+ fake_runtime.converse.return_value = {}
+ return BedrockChatClient(model_id="test-model", client=fake_runtime)
+
+
+def _dummy_weather(location: str) -> str: # pragma: no cover - helper
+ return f"Weather in {location}"
+
+
+def test_settings_load_from_environment(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("BEDROCK_REGION", "us-west-2")
+ monkeypatch.setenv("BEDROCK_CHAT_MODEL_ID", "anthropic.claude-v2")
+ settings = BedrockSettings()
+ assert settings.region == "us-west-2"
+ assert settings.chat_model_id == "anthropic.claude-v2"
+
+
+def test_build_request_includes_tool_config() -> None:
+ client = _build_client()
+
+ tool = AIFunction(name="get_weather", description="desc", func=_dummy_weather, input_model=_WeatherArgs)
+ options = ChatOptions(tools=[tool], tool_choice=ToolMode.REQUIRED("get_weather"))
+ messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="hi")])]
+
+ request = client._build_converse_request(messages, options)
+
+ assert request["toolConfig"]["tools"][0]["toolSpec"]["name"] == "get_weather"
+ assert request["toolConfig"]["toolChoice"] == {"tool": {"name": "get_weather"}}
+
+
+def test_build_request_serializes_tool_history() -> None:
+ client = _build_client()
+ options = ChatOptions()
+ messages = [
+ ChatMessage(role=Role.USER, contents=[TextContent(text="how's weather?")]),
+ ChatMessage(
+ role=Role.ASSISTANT,
+ contents=[FunctionCallContent(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}')],
+ ),
+ ChatMessage(
+ role=Role.TOOL,
+ contents=[FunctionResultContent(call_id="call-1", result={"answer": "72F"})],
+ ),
+ ]
+
+ request = client._build_converse_request(messages, options)
+ assistant_block = request["messages"][1]["content"][0]["toolUse"]
+ result_block = request["messages"][2]["content"][0]["toolResult"]
+
+ assert assistant_block["name"] == "get_weather"
+ assert assistant_block["input"] == {"location": "SEA"}
+ assert result_block["toolUseId"] == "call-1"
+ assert result_block["content"][0]["json"] == {"answer": "72F"}
+
+
+def test_process_response_parses_tool_use_and_result() -> None:
+ client = _build_client()
+ response = {
+ "modelId": "model",
+ "output": {
+ "message": {
+ "id": "msg-1",
+ "content": [
+ {"toolUse": {"toolUseId": "call-1", "name": "get_weather", "input": {"location": "NYC"}}},
+ {"text": "Calling tool"},
+ ],
+ },
+ "completionReason": "tool_use",
+ },
+ }
+
+ chat_response = client._process_converse_response(response)
+ contents = chat_response.messages[0].contents
+
+ assert isinstance(contents[0], FunctionCallContent)
+ assert contents[0].name == "get_weather"
+ assert isinstance(contents[1], TextContent)
+ assert chat_response.finish_reason == client._map_finish_reason("tool_use")
+
+
+def test_process_response_parses_tool_result() -> None:
+ client = _build_client()
+ response = {
+ "modelId": "model",
+ "output": {
+ "message": {
+ "id": "msg-2",
+ "content": [
+ {
+ "toolResult": {
+ "toolUseId": "call-1",
+ "status": "success",
+ "content": [{"json": {"answer": 42}}],
+ }
+ }
+ ],
+ },
+ "completionReason": "end_turn",
+ },
+ }
+
+ chat_response = client._process_converse_response(response)
+ contents = chat_response.messages[0].contents
+
+ assert isinstance(contents[0], FunctionResultContent)
+ assert contents[0].result == {"answer": 42}
diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py
index 7ded0af8d5..b000d4d41d 100644
--- a/python/packages/core/agent_framework/_agents.py
+++ b/python/packages/core/agent_framework/_agents.py
@@ -337,6 +337,7 @@ async def _notify_thread_of_new_messages(
thread: AgentThread,
input_messages: ChatMessage | Sequence[ChatMessage],
response_messages: ChatMessage | Sequence[ChatMessage],
+ **kwargs: Any,
) -> None:
"""Notify the thread of new messages.
@@ -346,13 +347,14 @@ async def _notify_thread_of_new_messages(
thread: The thread to notify of new messages.
input_messages: The input messages to notify about.
response_messages: The response messages to notify about.
+ **kwargs: Any extra arguments to pass from the agent run.
"""
if isinstance(input_messages, ChatMessage) or len(input_messages) > 0:
await thread.on_new_messages(input_messages)
if isinstance(response_messages, ChatMessage) or len(response_messages) > 0:
await thread.on_new_messages(response_messages)
if thread.context_provider:
- await thread.context_provider.invoked(input_messages, response_messages)
+ await thread.context_provider.invoked(input_messages, response_messages, **kwargs)
@property
def display_name(self) -> str:
@@ -969,7 +971,7 @@ async def run_stream(
"""
input_messages = self._normalize_messages(messages)
thread, run_chat_options, thread_messages = await self._prepare_thread_and_messages(
- thread=thread, input_messages=input_messages
+ thread=thread, input_messages=input_messages, **kwargs
)
agent_name = self._get_agent_name()
# Resolve final tool list (runtime provided tools + local MCP server tools)
@@ -1039,7 +1041,7 @@ async def run_stream(
response = ChatResponse.from_chat_response_updates(response_updates, output_format_type=co.response_format)
await self._update_thread_with_type_and_conversation_id(thread, response.conversation_id)
- await self._notify_thread_of_new_messages(thread, input_messages, response.messages)
+ await self._notify_thread_of_new_messages(thread, input_messages, response.messages, **kwargs)
@override
def get_new_thread(
@@ -1234,6 +1236,7 @@ async def _prepare_thread_and_messages(
*,
thread: AgentThread | None,
input_messages: list[ChatMessage] | None = None,
+ **kwargs: Any,
) -> tuple[AgentThread, ChatOptions, list[ChatMessage]]:
"""Prepare the thread and messages for agent execution.
@@ -1243,6 +1246,7 @@ async def _prepare_thread_and_messages(
Keyword Args:
thread: The conversation thread.
input_messages: Messages to process.
+ **kwargs: Any extra arguments to pass from the agent run.
Returns:
A tuple containing:
@@ -1263,7 +1267,7 @@ async def _prepare_thread_and_messages(
context: Context | None = None
if self.context_provider:
async with self.context_provider:
- context = await self.context_provider.invoking(input_messages or [])
+ context = await self.context_provider.invoking(input_messages or [], **kwargs)
if context:
if context.messages:
thread_messages.extend(context.messages)
diff --git a/python/packages/core/agent_framework/_workflows/_magentic.py b/python/packages/core/agent_framework/_workflows/_magentic.py
index 1a6aaf2999..14fc98e990 100644
--- a/python/packages/core/agent_framework/_workflows/_magentic.py
+++ b/python/packages/core/agent_framework/_workflows/_magentic.py
@@ -1727,7 +1727,7 @@ async def _invoke_agent(
last: ChatMessage = messages[-1]
author = last.author_name or self._agent_id
role: Role = last.role if last.role else Role.ASSISTANT
- text = last.text or str(last)
+ text = last.text or ""
msg = ChatMessage(role=role, text=text, author_name=author)
await self._emit_agent_message_event(ctx, msg)
return msg
diff --git a/python/packages/core/agent_framework/_workflows/_validation.py b/python/packages/core/agent_framework/_workflows/_validation.py
index d6a246a3eb..88e37a121a 100644
--- a/python/packages/core/agent_framework/_workflows/_validation.py
+++ b/python/packages/core/agent_framework/_workflows/_validation.py
@@ -149,9 +149,9 @@ def validate_workflow(
# check only when there is at least one edge group defined.
if self._edges: # Only evaluate when the workflow defines edges
edge_executor_ids: set[str] = set()
- for _e in self._edges:
- edge_executor_ids.add(_e.source_id)
- edge_executor_ids.add(_e.target_id)
+ for e in self._edges:
+ edge_executor_ids.add(e.source_id)
+ edge_executor_ids.add(e.target_id)
if start_executor_id not in edge_executor_ids:
raise GraphConnectivityError(
f"Start executor '{start_executor_id}' is not present in the workflow graph"
diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py
index a14542b2a6..eb22d7c330 100644
--- a/python/packages/core/agent_framework/_workflows/_workflow.py
+++ b/python/packages/core/agent_framework/_workflows/_workflow.py
@@ -370,6 +370,10 @@ async def _run_workflow_with_tracing(
span.add_event(OtelAttr.WORKFLOW_COMPLETED)
except Exception as exc:
+ # Drain any pending events (for example, ExecutorFailedEvent) before yielding WorkflowFailedEvent
+ for event in await self._runner.context.drain_events():
+ yield event
+
# Surface structured failure details before propagating exception
details = WorkflowErrorDetails.from_exception(exc)
with _framework_event_origin():
diff --git a/python/packages/core/agent_framework/amazon/__init__.py b/python/packages/core/agent_framework/amazon/__init__.py
new file mode 100644
index 0000000000..fbe0cc274a
--- /dev/null
+++ b/python/packages/core/agent_framework/amazon/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import importlib
+from typing import Any
+
+IMPORT_PATH = "agent_framework_bedrock"
+PACKAGE_NAME = "agent-framework-bedrock"
+_IMPORTS = ["__version__", "BedrockChatClient"]
+
+
+def __getattr__(name: str) -> Any:
+ if name in _IMPORTS:
+ try:
+ return getattr(importlib.import_module(IMPORT_PATH), name)
+ except ModuleNotFoundError as exc:
+ raise ModuleNotFoundError(
+ f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
+ ) from exc
+ raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
+
+
+def __dir__() -> list[str]:
+ return _IMPORTS
diff --git a/python/packages/core/agent_framework/amazon/__init__.pyi b/python/packages/core/agent_framework/amazon/__init__.pyi
new file mode 100644
index 0000000000..d3089e0af1
--- /dev/null
+++ b/python/packages/core/agent_framework/amazon/__init__.pyi
@@ -0,0 +1,11 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from agent_framework_bedrock import (
+ BedrockChatClient,
+ __version__,
+)
+
+__all__ = [
+ "BedrockChatClient",
+ "__version__",
+]
diff --git a/python/packages/core/tests/workflow/test_workflow_states.py b/python/packages/core/tests/workflow/test_workflow_states.py
index 4e88ed26cb..53baf86383 100644
--- a/python/packages/core/tests/workflow/test_workflow_states.py
+++ b/python/packages/core/tests/workflow/test_workflow_states.py
@@ -39,6 +39,12 @@ async def test_executor_failed_and_workflow_failed_events_streaming():
async for ev in wf.run_stream(0):
events.append(ev)
+ # ExecutorFailedEvent should be emitted before WorkflowFailedEvent
+ executor_failed_events = [e for e in events if isinstance(e, ExecutorFailedEvent)]
+ assert executor_failed_events, "ExecutorFailedEvent should be emitted when start executor fails"
+ assert executor_failed_events[0].executor_id == "f"
+ assert executor_failed_events[0].origin is WorkflowEventSource.FRAMEWORK
+
# Workflow-level failure and FAILED status should be surfaced
failed_events = [e for e in events if isinstance(e, WorkflowFailedEvent)]
assert failed_events
@@ -47,6 +53,11 @@ async def test_executor_failed_and_workflow_failed_events_streaming():
assert status and status[-1].state == WorkflowRunState.FAILED
assert all(e.origin is WorkflowEventSource.FRAMEWORK for e in status)
+ # Verify ExecutorFailedEvent comes before WorkflowFailedEvent
+ executor_failed_idx = events.index(executor_failed_events[0])
+ workflow_failed_idx = events.index(failed_events[0])
+ assert executor_failed_idx < workflow_failed_idx, "ExecutorFailedEvent should be emitted before WorkflowFailedEvent"
+
async def test_executor_failed_event_emitted_on_direct_execute():
failing = FailingExecutor(id="f")
@@ -65,6 +76,42 @@ async def test_executor_failed_event_emitted_on_direct_execute():
assert all(e.origin is WorkflowEventSource.FRAMEWORK for e in failed)
+class PassthroughExecutor(Executor):
+ """Executor that passes message to the next executor."""
+
+ @handler
+ async def passthrough(self, msg: int, ctx: WorkflowContext[int]) -> None:
+ await ctx.send_message(msg)
+
+
+async def test_executor_failed_event_from_second_executor_in_chain():
+ """Test that ExecutorFailedEvent is emitted when a non-start executor fails."""
+ passthrough = PassthroughExecutor(id="passthrough")
+ failing = FailingExecutor(id="failing")
+ wf: Workflow = WorkflowBuilder().set_start_executor(passthrough).add_edge(passthrough, failing).build()
+
+ events: list[object] = []
+ with pytest.raises(RuntimeError, match="boom"):
+ async for ev in wf.run_stream(0):
+ events.append(ev)
+
+ # ExecutorFailedEvent should be emitted for the failing executor
+ executor_failed_events = [e for e in events if isinstance(e, ExecutorFailedEvent)]
+ assert executor_failed_events, "ExecutorFailedEvent should be emitted when second executor fails"
+ assert executor_failed_events[0].executor_id == "failing"
+ assert executor_failed_events[0].origin is WorkflowEventSource.FRAMEWORK
+
+ # Workflow-level failure should also be surfaced
+ failed_events = [e for e in events if isinstance(e, WorkflowFailedEvent)]
+ assert failed_events
+ assert all(e.origin is WorkflowEventSource.FRAMEWORK for e in failed_events)
+
+ # Verify ExecutorFailedEvent comes before WorkflowFailedEvent
+ executor_failed_idx = events.index(executor_failed_events[0])
+ workflow_failed_idx = events.index(failed_events[0])
+ assert executor_failed_idx < workflow_failed_idx, "ExecutorFailedEvent should be emitted before WorkflowFailedEvent"
+
+
class SimpleExecutor(Executor):
"""Executor that does nothing, for testing."""
diff --git a/python/packages/declarative/agent_framework_declarative/_models.py b/python/packages/declarative/agent_framework_declarative/_models.py
index 9ddab17d87..aaba468bdf 100644
--- a/python/packages/declarative/agent_framework_declarative/_models.py
+++ b/python/packages/declarative/agent_framework_declarative/_models.py
@@ -253,7 +253,7 @@ def from_dict(
# We're being called on a subclass, use the normal from_dict
return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[misc]
- kind = value.get("kind", "")
+ kind = value.get("kind", "").lower()
if kind == "reference":
return SerializationMixin.from_dict.__func__( # type: ignore[misc]
ReferenceConnection, value, dependencies=dependencies
@@ -262,7 +262,7 @@ def from_dict(
return SerializationMixin.from_dict.__func__( # type: ignore[misc]
RemoteConnection, value, dependencies=dependencies
)
- if kind == "key":
+ if kind in ("key", "apikey"):
return SerializationMixin.from_dict.__func__( # type: ignore[misc]
ApiKeyConnection, value, dependencies=dependencies
)
diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py
index 647b773905..fd32e3f623 100644
--- a/python/packages/devui/agent_framework_devui/_mapper.py
+++ b/python/packages/devui/agent_framework_devui/_mapper.py
@@ -1274,7 +1274,7 @@ async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) ->
"trace_type": "magentic_orchestrator",
"orchestrator_id": orchestrator_id,
"kind": kind,
- "text": text or str(message),
+ "text": text or "",
"timestamp": datetime.now().isoformat(),
},
span_id=f"magentic_orch_{uuid4().hex[:8]}",
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 58b0fc19ef..3547801c71 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -71,6 +71,11 @@ override-dependencies = [
"uvicorn==0.38.0",
# Similar problem with websockets, which is a dependency conflict between litellm[proxy] and mcp
"websockets==15.0.1",
+ # grpcio 1.67.x has no Python 3.14 wheels; grpcio 1.76.0+ supports Python 3.14
+ # litellm constrains grpcio<1.68.0 due to resource exhaustion bug (https://github.com/grpc/grpc/issues/38290)
+ # Use version-specific overrides to satisfy both constraints
+ "grpcio>=1.76.0; python_version >= '3.14'",
+ "grpcio>=1.62.3,<1.68.0; python_version < '3.14'",
]
[tool.uv.workspace]
diff --git a/python/samples/amazon/bedrock_sample.py b/python/samples/amazon/bedrock_sample.py
new file mode 100644
index 0000000000..2c2d8c972a
--- /dev/null
+++ b/python/samples/amazon/bedrock_sample.py
@@ -0,0 +1,62 @@
+import asyncio
+
+from collections.abc import Sequence
+
+from agent_framework import (
+ AgentRunResponse,
+ ChatAgent,
+ FunctionCallContent,
+ FunctionResultContent,
+ Role,
+ TextContent,
+ ToolMode,
+ ai_function,
+)
+
+from agent_framework.amazon import BedrockChatClient
+
+
+@ai_function
+def get_weather(city: str) -> dict[str, str]:
+ """Return a mock forecast for the requested city."""
+
+ normalized = city.strip() or "New york"
+ return {"city": normalized, "forecast": "72F and sunny"}
+
+
+async def main() -> None:
+ agent = ChatAgent(
+ chat_client=BedrockChatClient(),
+ instructions="You are a concise travel assistant.",
+ name="BedrockWeatherAgent",
+ tool_choice=ToolMode.AUTO,
+ tools=[get_weather],
+ )
+
+ response = await agent.run("Use the weather tool to check the forecast for new york.")
+ print("\nAssistant reply:", response.text or "")
+ _log_response(response)
+
+
+def _log_response(response: AgentRunResponse) -> None:
+ print("\nConversation transcript:")
+ for idx, message in enumerate(response.messages, start=1):
+ tag = f"{idx}. {message.role.value if isinstance(message.role, Role) else message.role}"
+ _log_contents(tag, message.contents)
+
+
+def _log_contents(tag: str, contents: Sequence[object]) -> None:
+ print(f"[{tag}] {len(contents)} content blocks")
+ for idx, content in enumerate(contents, start=1):
+ if isinstance(content, TextContent):
+ print(f" {idx}. text -> {content.text}")
+ elif isinstance(content, FunctionCallContent):
+ print(f" {idx}. tool_call ({content.name}) -> {content.arguments}")
+ elif isinstance(content, FunctionResultContent):
+ print(f" {idx}. tool_result ({content.call_id}) -> {content.result}")
+ else: # pragma: no cover - defensive
+ print(f" {idx}. {content.type}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py b/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py
index 7531f9977b..f8e1123b8f 100644
--- a/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py
+++ b/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py
@@ -25,18 +25,22 @@
For simple queries where speed is critical, use semantic mode instead (see azure_ai_with_search_context_semantic.py).
Prerequisites:
-1. An Azure AI Search service with a search index
+1. An Azure AI Search service
2. An Azure AI Foundry project with a model deployment
-3. An Azure OpenAI resource (for Knowledge Base model calls)
-4. Set the following environment variables:
+3. Either an existing Knowledge Base OR a search index (to auto-create a KB)
+
+Environment variables:
- AZURE_SEARCH_ENDPOINT: Your Azure AI Search endpoint
- - AZURE_SEARCH_API_KEY: (Optional) Your search API key - if not provided, uses DefaultAzureCredential for Entra ID
- - AZURE_SEARCH_INDEX_NAME: Your search index name
+ - AZURE_SEARCH_API_KEY: (Optional) API key - if not provided, uses DefaultAzureCredential
- AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name (e.g., "gpt-4o")
+
+For using an existing Knowledge Base (recommended):
- AZURE_SEARCH_KNOWLEDGE_BASE_NAME: Your Knowledge Base name
- - AZURE_OPENAI_RESOURCE_URL: Your Azure OpenAI resource URL (e.g., "https://myresource.openai.azure.com")
- Note: This is different from AZURE_AI_PROJECT_ENDPOINT - Knowledge Base needs the OpenAI endpoint for model calls
+
+For auto-creating a Knowledge Base from an index:
+ - AZURE_SEARCH_INDEX_NAME: Your search index name
+ - AZURE_OPENAI_RESOURCE_URL: Azure OpenAI resource URL (e.g., "https://myresource.openai.azure.com")
"""
# Sample queries to demonstrate agentic RAG
@@ -53,31 +57,52 @@ async def main() -> None:
# Get configuration from environment
search_endpoint = os.environ["AZURE_SEARCH_ENDPOINT"]
search_key = os.environ.get("AZURE_SEARCH_API_KEY")
- index_name = os.environ["AZURE_SEARCH_INDEX_NAME"]
project_endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
model_deployment = os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o")
- knowledge_base_name = os.environ["AZURE_SEARCH_KNOWLEDGE_BASE_NAME"]
- azure_openai_resource_url = os.environ["AZURE_OPENAI_RESOURCE_URL"]
+
+ # Agentic mode requires exactly ONE of: knowledge_base_name OR index_name
+ # Option 1: Use existing Knowledge Base (recommended)
+ knowledge_base_name = os.environ.get("AZURE_SEARCH_KNOWLEDGE_BASE_NAME")
+ # Option 2: Auto-create KB from index (requires azure_openai_resource_url)
+ index_name = os.environ.get("AZURE_SEARCH_INDEX_NAME")
+ azure_openai_resource_url = os.environ.get("AZURE_OPENAI_RESOURCE_URL")
# Create Azure AI Search context provider with agentic mode (recommended for accuracy)
print("Using AGENTIC mode (Knowledge Bases with query planning, recommended)\n")
- print("ℹ️ This mode is slightly slower but provides more accurate results.\n")
- search_provider = AzureAISearchContextProvider(
- endpoint=search_endpoint,
- index_name=index_name,
- api_key=search_key, # Use api_key for API key auth, or credential for managed identity
- credential=AzureCliCredential() if not search_key else None,
- mode="agentic", # Advanced mode for multi-hop reasoning
- # Agentic mode configuration
- azure_ai_project_endpoint=project_endpoint,
- azure_openai_resource_url=azure_openai_resource_url,
- model_deployment_name=model_deployment,
- knowledge_base_name=knowledge_base_name,
- # Optional: Configure retrieval behavior
- knowledge_base_output_mode="extractive_data", # or "answer_synthesis"
- retrieval_reasoning_effort="minimal", # or "medium", "low"
- top_k=3, # Note: In agentic mode, the server-side Knowledge Base determines final retrieval
- )
+ print("This mode is slightly slower but provides more accurate results.\n")
+
+ # Configure based on whether using existing KB or auto-creating from index
+ if knowledge_base_name:
+ # Use existing Knowledge Base - simplest approach
+ search_provider = AzureAISearchContextProvider(
+ endpoint=search_endpoint,
+ api_key=search_key,
+ credential=AzureCliCredential() if not search_key else None,
+ mode="agentic",
+ knowledge_base_name=knowledge_base_name,
+ # Optional: Configure retrieval behavior
+ knowledge_base_output_mode="extractive_data", # or "answer_synthesis"
+ retrieval_reasoning_effort="minimal", # or "medium", "low"
+ )
+ else:
+ # Auto-create Knowledge Base from index
+ if not index_name:
+ raise ValueError("Set AZURE_SEARCH_KNOWLEDGE_BASE_NAME or AZURE_SEARCH_INDEX_NAME")
+ if not azure_openai_resource_url:
+ raise ValueError("AZURE_OPENAI_RESOURCE_URL required when using index_name")
+ search_provider = AzureAISearchContextProvider(
+ endpoint=search_endpoint,
+ index_name=index_name,
+ api_key=search_key,
+ credential=AzureCliCredential() if not search_key else None,
+ mode="agentic",
+ azure_openai_resource_url=azure_openai_resource_url,
+ model_deployment_name=model_deployment,
+ # Optional: Configure retrieval behavior
+ knowledge_base_output_mode="extractive_data", # or "answer_synthesis"
+ retrieval_reasoning_effort="minimal", # or "medium", "low"
+ top_k=3,
+ )
# Create agent with search context provider
async with (
diff --git a/python/samples/getting_started/evaluation/self_reflection/.env.example b/python/samples/getting_started/evaluation/self_reflection/.env.example
index 9f6dc82564..413a62c0ff 100644
--- a/python/samples/getting_started/evaluation/self_reflection/.env.example
+++ b/python/samples/getting_started/evaluation/self_reflection/.env.example
@@ -1,2 +1,3 @@
AZURE_OPENAI_ENDPOINT="..."
AZURE_OPENAI_API_KEY="..."
+AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects//"
diff --git a/python/samples/getting_started/evaluation/self_reflection/README.md b/python/samples/getting_started/evaluation/self_reflection/README.md
index a6ab419b0d..c75aa62ce8 100644
--- a/python/samples/getting_started/evaluation/self_reflection/README.md
+++ b/python/samples/getting_started/evaluation/self_reflection/README.md
@@ -6,7 +6,7 @@ This sample demonstrates the self-reflection pattern using Agent Framework and A
**What it demonstrates:**
- Iterative self-reflection loop that automatically improves responses based on groundedness evaluation
-- Batch processing of prompts from Parquet files with progress tracking
+- Batch processing of prompts from JSONL files with progress tracking
- Using `AzureOpenAIChatClient` with Azure CLI authentication
- Comprehensive summary statistics and detailed result tracking
@@ -18,14 +18,13 @@ This sample demonstrates the self-reflection pattern using Agent Framework and A
### Python Environment
```bash
-pip install agent-framework-core azure-ai-evaluation pandas --pre
+pip install agent-framework-core azure-ai-projects pandas --pre
```
### Environment Variables
```bash
# .env file
-AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
-AZURE_OPENAI_API_KEY=your-api-key # Optional with Azure CLI
+AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects//
```
## Running the Sample
@@ -35,15 +34,15 @@ AZURE_OPENAI_API_KEY=your-api-key # Optional with Azure CLI
python self_reflection.py
# With options
-python self_reflection.py --input my_prompts.parquet \
- --output results.parquet \
+python self_reflection.py --input my_prompts.jsonl \
+ --output results.jsonl \
--max-reflections 5 \
-n 10
```
**CLI Options:**
-- `--input`, `-i`: Input parquet file
-- `--output`, `-o`: Output parquet file
+- `--input`, `-i`: Input JSONL file
+- `--output`, `-o`: Output JSONL file
- `--agent-model`, `-m`: Agent model name (default: gpt-4.1)
- `--judge-model`, `-e`: Evaluator model name (default: gpt-4.1)
- `--max-reflections`: Max iterations (default: 3)
diff --git a/python/samples/getting_started/evaluation/self_reflection/self_reflection.py b/python/samples/getting_started/evaluation/self_reflection/self_reflection.py
index 76ed8d6e65..b5b1d1131a 100644
--- a/python/samples/getting_started/evaluation/self_reflection/self_reflection.py
+++ b/python/samples/getting_started/evaluation/self_reflection/self_reflection.py
@@ -5,13 +5,20 @@
import time
import argparse
import pandas as pd
+import openai
from typing import Any
from dotenv import load_dotenv
+from openai.types.eval_create_params import DataSourceConfigCustom
+from openai.types.evals.create_eval_jsonl_run_data_source_param import (
+ CreateEvalJSONLRunDataSourceParam,
+ SourceFileContent,
+ SourceFileContentContent,
+)
from agent_framework import ChatAgent, ChatMessage
from agent_framework.azure import AzureOpenAIChatClient
+from azure.ai.projects import AIProjectClient
from azure.identity import AzureCliCredential
-from azure.ai.evaluation import GroundednessEvaluator, AzureOpenAIModelConfiguration
"""
Self-Reflection LLM Runner
@@ -41,30 +48,96 @@
DEFAULT_JUDGE_MODEL = "gpt-4.1"
-def create_groundedness_evaluator(judge_model: str) -> GroundednessEvaluator:
- """
- Create a groundedness evaluator.
+def create_openai_client():
+ endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
+ credential = AzureCliCredential()
+ project_client = AIProjectClient(endpoint=endpoint, credential=credential)
+ return project_client.get_openai_client()
+
+
+def create_eval(client: openai.OpenAI, judge_model: str) -> openai.types.EvalCreateResponse:
+ print("Creating Eval")
+ data_source_config = DataSourceConfigCustom({
+ "type": "custom",
+ "item_schema": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string"},
+ "response": {"type": "string"},
+ "context": {"type": "string"},
+ },
+ "required": [],
+ },
+ "include_sample_schema": True,
+ })
+
+ testing_criteria = [{
+ "type": "azure_ai_evaluator",
+ "name": "groundedness",
+ "evaluator_name": "builtin.groundedness",
+ "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}", "context": "{{item.context}}"},
+ "initialization_parameters": {"deployment_name": f"{judge_model}"},
+ }]
+
+ return client.evals.create(
+ name="Eval",
+ data_source_config=data_source_config,
+ testing_criteria=testing_criteria, # type: ignore
+ )
- Args:
- judge_model: Model deployment name for evaluation
- Returns:
- Configured GroundednessEvaluator
- """
- judge_model_config = AzureOpenAIModelConfiguration(
- azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
- api_key=os.environ.get("AZURE_OPENAI_API_KEY"),
- api_version="2024-12-01-preview",
- azure_deployment=judge_model,
+
+def run_eval(
+ client: openai.OpenAI,
+ eval_object: openai.types.EvalCreateResponse,
+ query: str,
+ response: str,
+ context: str,
+):
+ eval_run_object = client.evals.runs.create(
+ eval_id=eval_object.id,
+ name="inline_data_run",
+ metadata={"team": "eval-exp", "scenario": "inline-data-v1"},
+ data_source=CreateEvalJSONLRunDataSourceParam(
+ type="jsonl",
+ source=SourceFileContent(
+ type="file_content",
+ content=[
+ SourceFileContentContent(
+ item={
+ "query": query,
+ "context": context,
+ "response": response,
+ }
+ ),
+ ],
+ ),
+ ),
)
- return GroundednessEvaluator(model_config=judge_model_config)
+
+ eval_run_response = client.evals.runs.retrieve(run_id=eval_run_object.id, eval_id=eval_object.id)
+
+ MAX_RETRY = 10
+ for _ in range(0, MAX_RETRY):
+ run = client.evals.runs.retrieve(run_id=eval_run_response.id, eval_id=eval_object.id)
+ if run.status == "failed":
+ print(f"Eval run failed. Run ID: {run.id}, Status: {run.status}, Error: {getattr(run, 'error', 'Unknown error')}")
+ continue
+ elif run.status == "completed":
+ output_items = list(client.evals.runs.output_items.list(run_id=run.id, eval_id=eval_object.id))
+ return output_items
+ time.sleep(5)
+
+ print("Eval result retrieval timeout.")
+ return None
async def execute_query_with_self_reflection(
*,
+ client: openai.OpenAI,
agent: ChatAgent,
+ eval_object: openai.types.EvalCreateResponse,
full_user_query: str,
context: str,
- evaluator: GroundednessEvaluator,
max_self_reflections: int = 3,
) -> dict[str, Any]:
"""
@@ -108,17 +181,20 @@ async def execute_query_with_self_reflection(
# Evaluate groundedness
start_time_eval = time.time()
- groundedness_res = evaluator(
+ eval_run_output_items = run_eval(
+ client=client,
+ eval_object=eval_object,
query=full_user_query,
response=agent_response,
- context=context
+ context=context,
)
+ if eval_run_output_items is None:
+ print(f" ⚠️ Groundedness evaluation failed (timeout or error) for iteration {i+1}.")
+ continue
+ score = eval_run_output_items[0].results[0].score
end_time_eval = time.time()
total_groundedness_eval_time += (end_time_eval - start_time_eval)
- feedback = groundedness_res['groundedness_reason']
- score = int(groundedness_res['groundedness'])
-
# Store score in structured format
iteration_scores.append(score)
@@ -144,11 +220,7 @@ async def execute_query_with_self_reflection(
# Request improvement
reflection_prompt = (
f"The groundedness score of your response is {score}/{max_score}. "
- f"Explanation for score: [{feedback}]. "
f"Reflect on your answer and improve it to get the maximum score of {max_score} "
- f"considering the explanation. Now please provide an updated response, taking into "
- f"account the feedback, but make your answer sound as if it was your first response. "
- f"Don't refer to the feedback in your answer."
)
messages.append(ChatMessage(role="user", text=reflection_prompt))
@@ -226,10 +298,11 @@ async def run_self_reflection_batch(
# Configure clients
print(f"Configuring Azure OpenAI client...")
-
- print(f"Creating groundedness evaluator with model: {judge_model}")
- evaluator = create_groundedness_evaluator(judge_model)
-
+ client = create_openai_client()
+
+ # Create Eval
+ eval_object = create_eval(client=client, judge_model=judge_model)
+
# Process each prompt
print(f"Max self-reflections: {max_self_reflections}\n")
@@ -239,10 +312,11 @@ async def run_self_reflection_batch(
try:
result = await execute_query_with_self_reflection(
+ client=client,
agent=agent,
+ eval_object=eval_object,
full_user_query=row['full_prompt'],
context=row['context_document'],
- evaluator=evaluator,
max_self_reflections=max_self_reflections,
)
diff --git a/python/samples/getting_started/workflows/README.md b/python/samples/getting_started/workflows/README.md
index 4dbeeb6071..3146f3f38b 100644
--- a/python/samples/getting_started/workflows/README.md
+++ b/python/samples/getting_started/workflows/README.md
@@ -44,6 +44,7 @@ Once comfortable with these, explore the rest of the samples below.
| Magentic Workflow as Agent | [agents/magentic_workflow_as_agent.py](./agents/magentic_workflow_as_agent.py) | Configure Magentic orchestration with callbacks, then expose the workflow as an agent |
| Workflow as Agent (Reflection Pattern) | [agents/workflow_as_agent_reflection_pattern.py](./agents/workflow_as_agent_reflection_pattern.py) | Wrap a workflow so it can behave like an agent (reflection pattern) |
| Workflow as Agent + HITL | [agents/workflow_as_agent_human_in_the_loop.py](./agents/workflow_as_agent_human_in_the_loop.py) | Extend workflow-as-agent with human-in-the-loop capability |
+| Handoff Workflow as Agent | [agents/handoff_workflow_as_agent.py](./agents/handoff_workflow_as_agent.py) | Use a HandoffBuilder workflow as an agent with HITL via FunctionCallContent/FunctionResultContent |
### checkpoint
diff --git a/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py
new file mode 100644
index 0000000000..0dd1d9e644
--- /dev/null
+++ b/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py
@@ -0,0 +1,230 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from collections.abc import Mapping
+from typing import Any
+
+from agent_framework import (
+ ChatAgent,
+ ChatMessage,
+ FunctionCallContent,
+ FunctionResultContent,
+ HandoffBuilder,
+ HandoffUserInputRequest,
+ Role,
+ WorkflowAgent,
+)
+from agent_framework.azure import AzureOpenAIChatClient
+from azure.identity import AzureCliCredential
+
+"""
+Sample: Handoff Workflow as Agent with Human-in-the-Loop
+
+Purpose:
+This sample demonstrates how to use a HandoffBuilder workflow as an agent via
+`.as_agent()`, enabling human-in-the-loop interactions through the standard
+agent interface. The handoff pattern routes user requests through a triage agent
+to specialist agents, with the workflow requesting user input as needed.
+
+When using a handoff workflow as an agent:
+1. The workflow emits `HandoffUserInputRequest` when it needs user input
+2. `WorkflowAgent` converts this to a `FunctionCallContent` named "request_info"
+3. The caller extracts `HandoffUserInputRequest` from the function call arguments
+4. The caller provides a response via `FunctionResultContent`
+
+This differs from running the workflow directly:
+- Direct workflow: Use `workflow.run_stream()` and `workflow.send_responses_streaming()`
+- As agent: Use `agent.run()` with `FunctionCallContent`/`FunctionResultContent` messages
+
+Key Concepts:
+- HandoffBuilder: Creates triage-to-specialist routing workflows
+- WorkflowAgent: Wraps workflows to expose them as standard agents
+- HandoffUserInputRequest: Contains conversation context and the awaiting agent
+- FunctionCallContent/FunctionResultContent: Standard agent interface for HITL
+
+Prerequisites:
+- `az login` (Azure CLI authentication)
+- Environment variables configured for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
+"""
+
+
+def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent, ChatAgent]:
+ """Create and configure the triage and specialist agents.
+
+ The triage agent dispatches requests to the appropriate specialist.
+ Specialists handle their domain-specific queries.
+
+ Returns:
+ Tuple of (triage_agent, refund_agent, order_agent, support_agent)
+ """
+ triage = chat_client.create_agent(
+ instructions=(
+ "You are frontline support triage. Read the latest user message and decide whether "
+ "to hand off to refund_agent, order_agent, or support_agent. Provide a brief natural-language "
+ "response for the user. When delegation is required, call the matching handoff tool "
+ "(`handoff_to_refund_agent`, `handoff_to_order_agent`, or `handoff_to_support_agent`)."
+ ),
+ name="triage_agent",
+ )
+
+ refund = chat_client.create_agent(
+ instructions=(
+ "You handle refund workflows. Ask for any order identifiers you require and outline the refund steps."
+ ),
+ name="refund_agent",
+ )
+
+ order = chat_client.create_agent(
+ instructions=(
+ "You resolve shipping and fulfillment issues. Clarify the delivery problem and describe the actions "
+ "you will take to remedy it."
+ ),
+ name="order_agent",
+ )
+
+ support = chat_client.create_agent(
+ instructions=(
+ "You are a general support agent. Offer empathetic troubleshooting and gather missing details if the "
+ "issue does not match other specialists."
+ ),
+ name="support_agent",
+ )
+
+ return triage, refund, order, support
+
+
+def extract_handoff_request(
+ response_messages: list[ChatMessage],
+) -> tuple[FunctionCallContent, HandoffUserInputRequest]:
+ """Extract the HandoffUserInputRequest from agent response messages.
+
+ When a handoff workflow running as an agent needs user input, it emits a
+ FunctionCallContent with name="request_info" containing the HandoffUserInputRequest.
+
+ Args:
+ response_messages: Messages from the agent response
+
+ Returns:
+ Tuple of (function_call, handoff_request)
+
+ Raises:
+ ValueError: If no request_info function call is found or payload is invalid
+ """
+ for message in response_messages:
+ for content in message.contents:
+ if isinstance(content, FunctionCallContent) and content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
+ # Parse the function arguments to extract the HandoffUserInputRequest
+ args = content.arguments
+ if isinstance(args, str):
+ request_args = WorkflowAgent.RequestInfoFunctionArgs.from_json(args)
+ elif isinstance(args, Mapping):
+ request_args = WorkflowAgent.RequestInfoFunctionArgs.from_dict(dict(args))
+ else:
+ raise ValueError("Unexpected argument type for request_info function call.")
+
+ payload: Any = request_args.data
+ if not isinstance(payload, HandoffUserInputRequest):
+ raise ValueError(
+ f"Expected HandoffUserInputRequest in request_info payload, got {type(payload).__name__}"
+ )
+
+ return content, payload
+
+ raise ValueError("No request_info function call found in response messages.")
+
+
+def print_conversation(request: HandoffUserInputRequest) -> None:
+ """Display the conversation history from a HandoffUserInputRequest."""
+ print("\n=== Conversation History ===")
+ for message in request.conversation:
+ speaker = message.author_name or message.role.value
+ print(f" [{speaker}]: {message.text}")
+ print(f" [Awaiting]: {request.awaiting_agent_id}")
+ print("============================")
+
+
+async def main() -> None:
+ """Main entry point demonstrating handoff workflow as agent.
+
+ This demo:
+ 1. Builds a handoff workflow with triage and specialist agents
+ 2. Converts it to an agent using .as_agent()
+ 3. Runs a multi-turn conversation with scripted user responses
+ 4. Demonstrates the FunctionCallContent/FunctionResultContent pattern for HITL
+ """
+ print("Starting Handoff Workflow as Agent Demo")
+ print("=" * 55)
+
+ # Initialize the Azure OpenAI chat client
+ chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
+
+ # Create agents
+ triage, refund, order, support = create_agents(chat_client)
+
+ # Build the handoff workflow and convert to agent
+ # Termination condition: stop after 4 user messages
+ agent = (
+ HandoffBuilder(
+ name="customer_support_handoff",
+ participants=[triage, refund, order, support],
+ )
+ .set_coordinator("triage_agent")
+ .with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 4)
+ .build()
+ .as_agent() # Convert workflow to agent interface
+ )
+
+ # Scripted user responses for reproducible demo
+ scripted_responses = [
+ "My order 1234 arrived damaged and the packaging was destroyed.",
+ "Yes, I'd like a refund if that's possible.",
+ "Thanks for your help!",
+ ]
+
+ # Start the conversation
+ print("\n[User]: Hello, I need assistance with my recent purchase.")
+ response = await agent.run("Hello, I need assistance with my recent purchase.")
+
+ # Process conversation turns until workflow completes or responses exhausted
+ while True:
+ # Check if the agent is requesting user input
+ try:
+ function_call, handoff_request = extract_handoff_request(response.messages)
+ except ValueError:
+ # No request_info call found - workflow has completed
+ print("\n[Workflow completed - no pending requests]")
+ if response.messages:
+ final_text = response.messages[-1].text
+ if final_text:
+ print(f"[Final response]: {final_text}")
+ break
+
+ # Display the conversation context
+ print_conversation(handoff_request)
+
+ # Get the next scripted response
+ if not scripted_responses:
+ print("\n[No more scripted responses - ending conversation]")
+ break
+
+ user_input = scripted_responses.pop(0)
+
+ print(f"\n[User responding]: {user_input}")
+
+ # Create the function result to send back to the agent
+ # The result is the user's text response which gets converted to ChatMessage
+ function_result = FunctionResultContent(
+ call_id=function_call.call_id,
+ result=user_input,
+ )
+
+ # Send the response back to the agent
+ response = await agent.run(ChatMessage(role=Role.TOOL, contents=[function_result]))
+
+ print("\n" + "=" * 55)
+ print("Demo completed!")
+
+
+if __name__ == "__main__":
+ print("Initializing Handoff Workflow as Agent Sample...")
+ asyncio.run(main())
diff --git a/python/uv.lock b/python/uv.lock
index 87081e2bc5..fd24adc06d 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -2,15 +2,18 @@ version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
- "python_full_version >= '3.13' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version < '3.11' and sys_platform == 'darwin'",
- "python_full_version >= '3.13' and sys_platform == 'linux'",
+ "python_full_version >= '3.14' and sys_platform == 'linux'",
+ "python_full_version == '3.13.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
"python_full_version < '3.11' and sys_platform == 'linux'",
- "python_full_version >= '3.13' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version < '3.11' and sys_platform == 'win32'",
@@ -30,6 +33,7 @@ members = [
"agent-framework-azure-ai",
"agent-framework-azure-ai-search",
"agent-framework-azurefunctions",
+ "agent-framework-bedrock",
"agent-framework-chatkit",
"agent-framework-copilotstudio",
"agent-framework-core",
@@ -41,13 +45,15 @@ members = [
"agent-framework-redis",
]
overrides = [
+ { name = "grpcio", marker = "python_full_version < '3.14'", specifier = ">=1.62.3,<1.68.0" },
+ { name = "grpcio", marker = "python_full_version >= '3.14'", specifier = ">=1.76.0" },
{ name = "uvicorn", specifier = "==0.38.0" },
{ name = "websockets", specifier = "==15.0.1" },
]
[[package]]
name = "a2a-sdk"
-version = "0.3.17"
+version = "0.3.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -56,9 +62,9 @@ dependencies = [
{ name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/00/c1/695e724ca9fa88933625f694dd371257375d25b9567919d86eedf8530a05/a2a_sdk-0.3.17.tar.gz", hash = "sha256:c39bb5731d7386c323efe6b01d15fd82fb0e65d512d1b0caaa46ce180d4cd4df", size = 229014, upload-time = "2025-11-24T12:37:41.261Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/74/db61ee9d2663b291a7eec03bbc7685bec72b1ceb113001350766c03f20de/a2a_sdk-0.3.19.tar.gz", hash = "sha256:ecf526d1d7781228d8680292f913bad1099ba3335a7f0ea6811543c2bd3e601d", size = 229184, upload-time = "2025-11-25T13:48:05.185Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/04/75/d7254295a2073747d34aa4dba20fe6791f761781d718b88539bb3d4524c4/a2a_sdk-0.3.17-py3-none-any.whl", hash = "sha256:d3e04db524d6cfd087af0b1aede9cb790155ca8770b1d651a30df514dd2c056e", size = 141527, upload-time = "2025-11-24T12:37:39.704Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/cd/14c1242d171b9739770be35223f1cbc1fb0244ebea2c704f8ae0d9e6abf7/a2a_sdk-0.3.19-py3-none-any.whl", hash = "sha256:314123f84524259313ec0cd9826a34bae5de769dea44b8eb9a0eca79b8935772", size = 141519, upload-time = "2025-11-25T13:48:02.622Z" },
]
[[package]]
@@ -268,6 +274,23 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [{ name = "types-python-dateutil", specifier = ">=2.9.0" }]
+[[package]]
+name = "agent-framework-bedrock"
+version = "1.0.0b251120"
+source = { editable = "packages/bedrock" }
+dependencies = [
+ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "boto3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "agent-framework-core", editable = "packages/core" },
+ { name = "boto3", specifier = ">=1.35.0,<2.0.0" },
+ { name = "botocore", specifier = ">=1.35.0,<2.0.0" },
+]
+
[[package]]
name = "agent-framework-chatkit"
version = "1.0.0b251120"
@@ -817,17 +840,16 @@ wheels = [
[[package]]
name = "anyio"
-version = "4.11.0"
+version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", 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 = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
- { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
]
[[package]]
@@ -1323,7 +1345,7 @@ name = "clr-loader"
version = "0.2.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/88/9e0a80d59b28d394aad5d736bd47e5aa5883cf1d3674b313ba93e2a353e4/clr_loader-0.2.8.tar.gz", hash = "sha256:b4cd3a2ee5f0489885ef07ffd87eb38b2cee24ca65dcacea97b34e7b59913814", size = 61502, upload-time = "2025-10-20T21:03:16.548Z" }
wheels = [
@@ -1416,13 +1438,16 @@ name = "contourpy"
version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.13' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
- "python_full_version >= '3.13' and sys_platform == 'linux'",
+ "python_full_version >= '3.14' and sys_platform == 'linux'",
+ "python_full_version == '3.13.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
- "python_full_version >= '3.13' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
]
@@ -1817,7 +1842,7 @@ wheels = [
[[package]]
name = "fastapi"
-version = "0.122.0"
+version = "0.123.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -1825,9 +1850,9 @@ dependencies = [
{ name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b2/de/3ee97a4f6ffef1fb70bf20561e4f88531633bb5045dc6cebc0f8471f764d/fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b", size = 346436, upload-time = "2025-11-24T19:17:47.95Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/c7/d3956d7c2da2b66188eacc8db0919635b28313a30334dd78cba4c366caf0/fastapi-0.123.0.tar.gz", hash = "sha256:1410678b3c44418245eec85088b15140d894074b86e66061017e2b492c09b138", size = 347702, upload-time = "2025-11-30T14:49:17.848Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671, upload-time = "2025-11-24T19:17:45.96Z" },
+ { url = "https://files.pythonhosted.org/packages/17/17/62c82beab6536ea72576f90b84a3dbe6bcceb88d3d46afc4d05c376f0231/fastapi-0.123.0-py3-none-any.whl", hash = "sha256:cb56e69e874afa897bd3416c8a3dbfdae1730d0a308d4c63303f3f4b44136ae4", size = 110865, upload-time = "2025-11-30T14:49:16.164Z" },
]
[[package]]
@@ -1961,59 +1986,59 @@ wheels = [
[[package]]
name = "fonttools"
-version = "4.60.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/26/70/03e9d89a053caff6ae46053890eba8e4a5665a7c5638279ed4492e6d4b8b/fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", size = 2810747, upload-time = "2025-09-29T21:10:59.653Z" },
- { url = "https://files.pythonhosted.org/packages/6f/41/449ad5aff9670ab0df0f61ee593906b67a36d7e0b4d0cd7fa41ac0325bf5/fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", size = 2346909, upload-time = "2025-09-29T21:11:02.882Z" },
- { url = "https://files.pythonhosted.org/packages/9a/18/e5970aa96c8fad1cb19a9479cc3b7602c0c98d250fcdc06a5da994309c50/fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", size = 4864572, upload-time = "2025-09-29T21:11:05.096Z" },
- { url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" },
- { url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" },
- { url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" },
- { url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" },
- { url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" },
- { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
- { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
- { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
- { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
- { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
- { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
- { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
- { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
- { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
- { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
- { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
- { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
- { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
- { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
- { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
- { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
- { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
- { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
- { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
- { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
- { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
- { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
- { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
- { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
- { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
- { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
- { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
- { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
- { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
- { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
- { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
- { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
- { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
- { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
- { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
- { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
- { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
+version = "4.61.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/f3/91bba2721fb173fc68e09d15b6ccf3ad4f83d127fbff579be7e5984888a6/fonttools-4.61.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", size = 2850151, upload-time = "2025-11-28T17:04:14.214Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/8c/a1691dec01038ac7e7bb3ab83300dcc5087b11d8f48640928c02a873eb92/fonttools-4.61.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", size = 2389769, upload-time = "2025-11-28T17:04:16.443Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/dd/5bb369a44319d92ba25612511eb8ed2a6fa75239979e0388907525626902/fonttools-4.61.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", size = 4893189, upload-time = "2025-11-28T17:04:18.398Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/02/51373fa8846bd22bb54e5efb30a824b417b058083f775a194a432f21a45f/fonttools-4.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", size = 4854415, upload-time = "2025-11-28T17:04:20.421Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/64/9cdbbb804577a7e6191448851c57e6a36eb02aa4bf6a9668b528c968e44e/fonttools-4.61.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", size = 4870927, upload-time = "2025-11-28T17:04:22.625Z" },
+ { url = "https://files.pythonhosted.org/packages/92/68/e40b22919dc96dc30a70b58fec609ab85112de950bdecfadf8dd478c5a88/fonttools-4.61.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", size = 4988674, upload-time = "2025-11-28T17:04:24.675Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/5c/e857349ce8aedb2451b9448282e86544b2b7f1c8b10ea0fe49b7cb369b72/fonttools-4.61.0-cp310-cp310-win32.whl", hash = "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", size = 1497663, upload-time = "2025-11-28T17:04:26.598Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/0c/62961d5fe6f764d6cbc387ef2c001f5f610808c7aded837409836c0b3e7c/fonttools-4.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", size = 1546143, upload-time = "2025-11-28T17:04:28.432Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" },
+ { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" },
+ { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" },
+ { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" },
+ { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/17/45/334f0d7f181e5473cfb757e1b60f4e60e7fc64f28d406e5d364a952718c0/fonttools-4.61.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", size = 2841801, upload-time = "2025-11-28T17:05:01.621Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/63/97b9c78e1f79bc741d4efe6e51f13872d8edb2b36e1b9fb2bab0d4491bb7/fonttools-4.61.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", size = 2379024, upload-time = "2025-11-28T17:05:03.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", size = 4923706, upload-time = "2025-11-28T17:05:05.494Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/f6/a3b0374811a1de8c3f9207ec88f61ad1bb96f938ed89babae26c065c2e46/fonttools-4.61.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", size = 4979751, upload-time = "2025-11-28T17:05:07.665Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3b/30f63b4308b449091573285f9d27619563a84f399946bca3eadc9554afbe/fonttools-4.61.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", size = 4921113, upload-time = "2025-11-28T17:05:09.551Z" },
+ { url = "https://files.pythonhosted.org/packages/41/6c/58e6e9b7d9d8bf2d7010bd7bb493060b39b02a12d1cda64a8bfb116ce760/fonttools-4.61.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", size = 5063183, upload-time = "2025-11-28T17:05:11.677Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/e3/52c790ab2b07492df059947a1fd7778e105aac5848c0473029a4d20481a2/fonttools-4.61.0-cp313-cp313-win32.whl", hash = "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", size = 2263159, upload-time = "2025-11-28T17:05:13.292Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/1f/116013b200fbeba871046554d5d2a45fefa69a05c40e9cdfd0d4fff53edc/fonttools-4.61.0-cp313-cp313-win_amd64.whl", hash = "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", size = 2313530, upload-time = "2025-11-28T17:05:14.848Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/99/59b1e25987787cb714aa9457cee4c9301b7c2153f0b673e2b8679d37669d/fonttools-4.61.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", size = 2841429, upload-time = "2025-11-28T17:05:16.671Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/4c1911d4332c8a144bb3b44416e274ccca0e297157c971ea1b3fbb855590/fonttools-4.61.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", size = 2378987, upload-time = "2025-11-28T17:05:18.69Z" },
+ { url = "https://files.pythonhosted.org/packages/24/b0/f442e90fde5d2af2ae0cb54008ab6411edc557ee33b824e13e1d04925ac9/fonttools-4.61.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", size = 4873270, upload-time = "2025-11-28T17:05:20.625Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/04/f5d5990e33053c8a59b90b1d7e10ad9b97a73f42c745304da0e709635fab/fonttools-4.61.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", size = 4968270, upload-time = "2025-11-28T17:05:22.515Z" },
+ { url = "https://files.pythonhosted.org/packages/94/9f/2091402e0d27c9c8c4bab5de0e5cd146d9609a2d7d1c666bbb75c0011c1a/fonttools-4.61.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", size = 4919799, upload-time = "2025-11-28T17:05:24.437Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/72/86adab22fde710b829f8ffbc8f264df01928e5b7a8f6177fa29979ebf256/fonttools-4.61.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", size = 5030966, upload-time = "2025-11-28T17:05:26.115Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/a7/7c8e31b003349e845b853f5e0a67b95ff6b052fa4f5224f8b72624f5ac69/fonttools-4.61.0-cp314-cp314-win32.whl", hash = "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", size = 2267243, upload-time = "2025-11-28T17:05:27.807Z" },
+ { url = "https://files.pythonhosted.org/packages/20/ee/f434fe7749360497c52b7dcbcfdbccdaab0a71c59f19d572576066717122/fonttools-4.61.0-cp314-cp314-win_amd64.whl", hash = "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", size = 2318822, upload-time = "2025-11-28T17:05:29.882Z" },
+ { url = "https://files.pythonhosted.org/packages/33/b3/c16255320255e5c1863ca2b2599bb61a46e2f566db0bbb9948615a8fe692/fonttools-4.61.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", size = 2924917, upload-time = "2025-11-28T17:05:31.46Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/b8/08067ae21de705a817777c02ef36ab0b953cbe91d8adf134f9c2da75ed6d/fonttools-4.61.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", size = 2413576, upload-time = "2025-11-28T17:05:33.343Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f1/96ff43f92addce2356780fdc203f2966206f3d22ea20e242c27826fd7442/fonttools-4.61.0-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", size = 4877447, upload-time = "2025-11-28T17:05:35.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/1e/a3d8e51ed9ccfd7385e239ae374b78d258a0fb82d82cab99160a014a45d1/fonttools-4.61.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", size = 5095681, upload-time = "2025-11-28T17:05:37.142Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/f6/d256bd6c1065c146a0bdddf1c62f542e08ae5b3405dbf3fcc52be272f674/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", size = 4974140, upload-time = "2025-11-28T17:05:39.5Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/0c/96633eb4b26f138cc48561c6e0c44b4ea48acea56b20b507d6b14f8e80ce/fonttools-4.61.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", size = 5001741, upload-time = "2025-11-28T17:05:41.424Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/3b536bad3be4f26186f296e749ff17bad3e6d57232c104d752d24b2e265b/fonttools-4.61.0-cp314-cp314t-win32.whl", hash = "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", size = 2330707, upload-time = "2025-11-28T17:05:43.548Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ea/e6b9ac610451ee9f04477c311ad126de971f6112cb579fa391d2a8edb00b/fonttools-4.61.0-cp314-cp314t-win_amd64.whl", hash = "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", size = 2395950, upload-time = "2025-11-28T17:05:45.638Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" },
]
[[package]]
@@ -2297,12 +2322,75 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
]
+[[package]]
+name = "grpcio"
+version = "1.67.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
+ "python_full_version == '3.11.*' and sys_platform == 'darwin'",
+ "python_full_version < '3.11' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
+ "python_full_version == '3.11.*' and sys_platform == 'linux'",
+ "python_full_version < '3.11' and sys_platform == 'linux'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32'",
+ "python_full_version == '3.11.*' and sys_platform == 'win32'",
+ "python_full_version < '3.11' and sys_platform == 'win32'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/53/d9282a66a5db45981499190b77790570617a604a38f3d103d0400974aeb5/grpcio-1.67.1.tar.gz", hash = "sha256:3dc2ed4cabea4dc14d5e708c2b426205956077cc5de419b4d4079315017e9732", size = 12580022, upload-time = "2024-10-29T06:30:07.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/cd/f6ca5c49aa0ae7bc6d0757f7dae6f789569e9490a635eaabe02bc02de7dc/grpcio-1.67.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:8b0341d66a57f8a3119b77ab32207072be60c9bf79760fa609c5609f2deb1f3f", size = 5112450, upload-time = "2024-10-29T06:23:38.202Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f0/d9bbb4a83cbee22f738ee7a74aa41e09ccfb2dcea2cc30ebe8dab5b21771/grpcio-1.67.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:f5a27dddefe0e2357d3e617b9079b4bfdc91341a91565111a21ed6ebbc51b22d", size = 10937518, upload-time = "2024-10-29T06:23:43.535Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/17/0c5dbae3af548eb76669887642b5f24b232b021afe77eb42e22bc8951d9c/grpcio-1.67.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:43112046864317498a33bdc4797ae6a268c36345a910de9b9c17159d8346602f", size = 5633610, upload-time = "2024-10-29T06:23:47.168Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/e000614e00153d7b2760dcd9526b95d72f5cfe473b988e78f0ff3b472f6c/grpcio-1.67.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9b929f13677b10f63124c1a410994a401cdd85214ad83ab67cc077fc7e480f0", size = 6240678, upload-time = "2024-10-29T06:23:49.352Z" },
+ { url = "https://files.pythonhosted.org/packages/64/19/a16762a70eeb8ddfe43283ce434d1499c1c409ceec0c646f783883084478/grpcio-1.67.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7d1797a8a3845437d327145959a2c0c47c05947c9eef5ff1a4c80e499dcc6fa", size = 5884528, upload-time = "2024-10-29T06:23:52.345Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/dc/bd016aa3684914acd2c0c7fa4953b2a11583c2b844f3d7bae91fa9b98fbb/grpcio-1.67.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0489063974d1452436139501bf6b180f63d4977223ee87488fe36858c5725292", size = 6583680, upload-time = "2024-10-29T06:23:55.074Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/93/1441cb14c874f11aa798a816d582f9da82194b6677f0f134ea53d2d5dbeb/grpcio-1.67.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9fd042de4a82e3e7aca44008ee2fb5da01b3e5adb316348c21980f7f58adc311", size = 6162967, upload-time = "2024-10-29T06:23:57.286Z" },
+ { url = "https://files.pythonhosted.org/packages/29/e9/9295090380fb4339b7e935b9d005fa9936dd573a22d147c9e5bb2df1b8d4/grpcio-1.67.1-cp310-cp310-win32.whl", hash = "sha256:638354e698fd0c6c76b04540a850bf1db27b4d2515a19fcd5cf645c48d3eb1ed", size = 3616336, upload-time = "2024-10-29T06:23:59.69Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/de/7c783b8cb8f02c667ca075c49680c4aeb8b054bc69784bcb3e7c1bbf4985/grpcio-1.67.1-cp310-cp310-win_amd64.whl", hash = "sha256:608d87d1bdabf9e2868b12338cd38a79969eaf920c89d698ead08f48de9c0f9e", size = 4352071, upload-time = "2024-10-29T06:24:02.477Z" },
+ { url = "https://files.pythonhosted.org/packages/59/2c/b60d6ea1f63a20a8d09c6db95c4f9a16497913fb3048ce0990ed81aeeca0/grpcio-1.67.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:7818c0454027ae3384235a65210bbf5464bd715450e30a3d40385453a85a70cb", size = 5119075, upload-time = "2024-10-29T06:24:04.696Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9a/e1956f7ca582a22dd1f17b9e26fcb8229051b0ce6d33b47227824772feec/grpcio-1.67.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ea33986b70f83844cd00814cee4451055cd8cab36f00ac64a31f5bb09b31919e", size = 11009159, upload-time = "2024-10-29T06:24:07.781Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a8/35fbbba580c4adb1d40d12e244cf9f7c74a379073c0a0ca9d1b5338675a1/grpcio-1.67.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c7a01337407dd89005527623a4a72c5c8e2894d22bead0895306b23c6695698f", size = 5629476, upload-time = "2024-10-29T06:24:11.444Z" },
+ { url = "https://files.pythonhosted.org/packages/77/c9/864d336e167263d14dfccb4dbfa7fce634d45775609895287189a03f1fc3/grpcio-1.67.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b866f73224b0634f4312a4674c1be21b2b4afa73cb20953cbbb73a6b36c3cc", size = 6239901, upload-time = "2024-10-29T06:24:14.2Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/1e/0011408ebabf9bd69f4f87cc1515cbfe2094e5a32316f8714a75fd8ddfcb/grpcio-1.67.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fff78ba10d4250bfc07a01bd6254a6d87dc67f9627adece85c0b2ed754fa96", size = 5881010, upload-time = "2024-10-29T06:24:17.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/7d/fbca85ee9123fb296d4eff8df566f458d738186d0067dec6f0aa2fd79d71/grpcio-1.67.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8a23cbcc5bb11ea7dc6163078be36c065db68d915c24f5faa4f872c573bb400f", size = 6580706, upload-time = "2024-10-29T06:24:20.038Z" },
+ { url = "https://files.pythonhosted.org/packages/75/7a/766149dcfa2dfa81835bf7df623944c1f636a15fcb9b6138ebe29baf0bc6/grpcio-1.67.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1a65b503d008f066e994f34f456e0647e5ceb34cfcec5ad180b1b44020ad4970", size = 6161799, upload-time = "2024-10-29T06:24:22.604Z" },
+ { url = "https://files.pythonhosted.org/packages/09/13/5b75ae88810aaea19e846f5380611837de411181df51fd7a7d10cb178dcb/grpcio-1.67.1-cp311-cp311-win32.whl", hash = "sha256:e29ca27bec8e163dca0c98084040edec3bc49afd10f18b412f483cc68c712744", size = 3616330, upload-time = "2024-10-29T06:24:25.775Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/39/38117259613f68f072778c9638a61579c0cfa5678c2558706b10dd1d11d3/grpcio-1.67.1-cp311-cp311-win_amd64.whl", hash = "sha256:786a5b18544622bfb1e25cc08402bd44ea83edfb04b93798d85dca4d1a0b5be5", size = 4354535, upload-time = "2024-10-29T06:24:28.614Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/25/6f95bd18d5f506364379eabc0d5874873cc7dbdaf0757df8d1e82bc07a88/grpcio-1.67.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:267d1745894200e4c604958da5f856da6293f063327cb049a51fe67348e4f953", size = 5089809, upload-time = "2024-10-29T06:24:31.24Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3f/d79e32e5d0354be33a12db2267c66d3cfeff700dd5ccdd09fd44a3ff4fb6/grpcio-1.67.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:85f69fdc1d28ce7cff8de3f9c67db2b0ca9ba4449644488c1e0303c146135ddb", size = 10981985, upload-time = "2024-10-29T06:24:34.942Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/36fbc14b3542e3a1c20fb98bd60c4732c55a44e374a4eb68f91f28f14aab/grpcio-1.67.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f26b0b547eb8d00e195274cdfc63ce64c8fc2d3e2d00b12bf468ece41a0423a0", size = 5588770, upload-time = "2024-10-29T06:24:38.145Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/af/bbc1305df60c4e65de8c12820a942b5e37f9cf684ef5e49a63fbb1476a73/grpcio-1.67.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4422581cdc628f77302270ff839a44f4c24fdc57887dc2a45b7e53d8fc2376af", size = 6214476, upload-time = "2024-10-29T06:24:41.006Z" },
+ { url = "https://files.pythonhosted.org/packages/92/cf/1d4c3e93efa93223e06a5c83ac27e32935f998bc368e276ef858b8883154/grpcio-1.67.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d7616d2ded471231c701489190379e0c311ee0a6c756f3c03e6a62b95a7146e", size = 5850129, upload-time = "2024-10-29T06:24:43.553Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ca/26195b66cb253ac4d5ef59846e354d335c9581dba891624011da0e95d67b/grpcio-1.67.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8a00efecde9d6fcc3ab00c13f816313c040a28450e5e25739c24f432fc6d3c75", size = 6568489, upload-time = "2024-10-29T06:24:46.453Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/94/16550ad6b3f13b96f0856ee5dfc2554efac28539ee84a51d7b14526da985/grpcio-1.67.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:699e964923b70f3101393710793289e42845791ea07565654ada0969522d0a38", size = 6149369, upload-time = "2024-10-29T06:24:49.112Z" },
+ { url = "https://files.pythonhosted.org/packages/33/0d/4c3b2587e8ad7f121b597329e6c2620374fccbc2e4e1aa3c73ccc670fde4/grpcio-1.67.1-cp312-cp312-win32.whl", hash = "sha256:4e7b904484a634a0fff132958dabdb10d63e0927398273917da3ee103e8d1f78", size = 3599176, upload-time = "2024-10-29T06:24:51.443Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/36/0c03e2d80db69e2472cf81c6123aa7d14741de7cf790117291a703ae6ae1/grpcio-1.67.1-cp312-cp312-win_amd64.whl", hash = "sha256:5721e66a594a6c4204458004852719b38f3d5522082be9061d6510b455c90afc", size = 4346574, upload-time = "2024-10-29T06:24:54.587Z" },
+ { url = "https://files.pythonhosted.org/packages/12/d2/2f032b7a153c7723ea3dea08bffa4bcaca9e0e5bdf643ce565b76da87461/grpcio-1.67.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa0162e56fd10a5547fac8774c4899fc3e18c1aa4a4759d0ce2cd00d3696ea6b", size = 5091487, upload-time = "2024-10-29T06:24:57.416Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ae/ea2ff6bd2475a082eb97db1104a903cf5fc57c88c87c10b3c3f41a184fc0/grpcio-1.67.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:beee96c8c0b1a75d556fe57b92b58b4347c77a65781ee2ac749d550f2a365dc1", size = 10943530, upload-time = "2024-10-29T06:25:01.062Z" },
+ { url = "https://files.pythonhosted.org/packages/07/62/646be83d1a78edf8d69b56647327c9afc223e3140a744c59b25fbb279c3b/grpcio-1.67.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:a93deda571a1bf94ec1f6fcda2872dad3ae538700d94dc283c672a3b508ba3af", size = 5589079, upload-time = "2024-10-29T06:25:04.254Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/25/71513d0a1b2072ce80d7f5909a93596b7ed10348b2ea4fdcbad23f6017bf/grpcio-1.67.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e6f255980afef598a9e64a24efce87b625e3e3c80a45162d111a461a9f92955", size = 6213542, upload-time = "2024-10-29T06:25:06.824Z" },
+ { url = "https://files.pythonhosted.org/packages/76/9a/d21236297111052dcb5dc85cd77dc7bf25ba67a0f55ae028b2af19a704bc/grpcio-1.67.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e838cad2176ebd5d4a8bb03955138d6589ce9e2ce5d51c3ada34396dbd2dba8", size = 5850211, upload-time = "2024-10-29T06:25:10.149Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/fe/70b1da9037f5055be14f359026c238821b9bcf6ca38a8d760f59a589aacd/grpcio-1.67.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a6703916c43b1d468d0756c8077b12017a9fcb6a1ef13faf49e67d20d7ebda62", size = 6572129, upload-time = "2024-10-29T06:25:12.853Z" },
+ { url = "https://files.pythonhosted.org/packages/74/0d/7df509a2cd2a54814598caf2fb759f3e0b93764431ff410f2175a6efb9e4/grpcio-1.67.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:917e8d8994eed1d86b907ba2a61b9f0aef27a2155bca6cbb322430fc7135b7bb", size = 6149819, upload-time = "2024-10-29T06:25:15.803Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/08/bc3b0155600898fd10f16b79054e1cca6cb644fa3c250c0fe59385df5e6f/grpcio-1.67.1-cp313-cp313-win32.whl", hash = "sha256:e279330bef1744040db8fc432becc8a727b84f456ab62b744d3fdb83f327e121", size = 3596561, upload-time = "2024-10-29T06:25:19.348Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/96/44759eca966720d0f3e1b105c43f8ad4590c97bf8eb3cd489656e9590baa/grpcio-1.67.1-cp313-cp313-win_amd64.whl", hash = "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba", size = 4346042, upload-time = "2024-10-29T06:25:21.939Z" },
+]
+
[[package]]
name = "grpcio"
version = "1.76.0"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'linux'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+]
dependencies = [
- { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "typing-extensions", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
wheels = [
@@ -2483,7 +2571,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
-version = "1.1.5"
+version = "1.1.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2497,9 +2585,9 @@ dependencies = [
{ name = "typer-slim", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fb/02/c3d534d7498ba2792da1d2ce56b5d38bbcbcbbba62071c90ee289b408e8d/huggingface_hub-1.1.5.tar.gz", hash = "sha256:40ba5c9a08792d888fde6088920a0a71ab3cd9d5e6617c81a797c657f1fd9968", size = 607199, upload-time = "2025-11-20T15:49:32.809Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/fa/a1a94c55637f2b7cfeb05263ac3881aa87c82df92d8b4b31c909079f4419/huggingface_hub-1.1.7.tar.gz", hash = "sha256:3c84b6283caca928595f08fd42e9a572f17ec3501dec508c3f2939d94bfbd9d2", size = 607537, upload-time = "2025-12-01T11:05:28.137Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/35/f4/124858007ddf3c61e9b144107304c9152fa80b5b6c168da07d86fe583cc1/huggingface_hub-1.1.5-py3-none-any.whl", hash = "sha256:e88ecc129011f37b868586bbcfae6c56868cae80cd56a79d61575426a3aa0d7d", size = 516000, upload-time = "2025-11-20T15:49:30.926Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/4f/82e5ab009089a2c48472bf4248391fe4091cf0b9c3e951dbb8afe3b23d76/huggingface_hub-1.1.7-py3-none-any.whl", hash = "sha256:f3efa4779f4890e44c957bbbb0f197e6028887ad09f0cf95a21659fa7753605d", size = 516239, upload-time = "2025-12-01T11:05:25.981Z" },
]
[[package]]
@@ -2862,7 +2950,7 @@ wheels = [
[[package]]
name = "langfuse"
-version = "3.10.1"
+version = "3.10.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2876,19 +2964,94 @@ dependencies = [
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0f/82/030029b3490a90d25dff46e0e29230eb07c55a32ea6876a2397d7d0184b0/langfuse-3.10.1.tar.gz", hash = "sha256:11152b77f1869c55b42c3be8fba779cd9102bef171a597ea4baeca8a8000d11f", size = 222084, upload-time = "2025-11-19T17:50:00.755Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0d/83/254c8d38bd46e9c49cc2814da0ece5cb361315e6728578112ffae9bffbe8/langfuse-3.10.1-py3-none-any.whl", hash = "sha256:78582905874e17f923a3fa6eba9d1a15e1547139bbd5c11d498ce90670e1fdae", size = 391696, upload-time = "2025-11-19T17:49:59.069Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/7b/03/c4316cb0a91cff97118c21b973b3089c2fe1bdbcad02f3623d6ac572e954/langfuse-3.10.3.tar.gz", hash = "sha256:69d6eaf573212f8cdc1cebd2d6b47f271bfe76c7eb5a3c5d6766bb0d9bf0004c", size = 226617, upload-time = "2025-12-01T18:01:02.607Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/04/f07c2a23f2822f73f8576b1ba7348c014c4be65127384b4bee475f913f3b/langfuse-3.10.3-py3-none-any.whl", hash = "sha256:b9a2e6506f8f0923c2f4b8c9e3fa355231994197a17f75509a37f335660ce334", size = 399062, upload-time = "2025-12-01T18:01:00.688Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/c3/cdff3c10e2e608490dc0a310ccf11ba777b3943ad4fcead2a2ade98c21e1/librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae", size = 54209, upload-time = "2025-11-29T14:01:56.058Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/84/859df8db21dedab2538ddfbe1d486dda3eb66a98c6ad7ba754a99e25e45e/librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1", size = 27294, upload-time = "2025-11-29T14:00:35.053Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/01/ec3971cf9c4f827f17de6729bdfdbf01a67493147334f4ef8fac68936e3a/librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf", size = 27635, upload-time = "2025-11-29T14:00:36.496Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/f9/3efe201df84dd26388d2e0afa4c4dc668c8e406a3da7b7319152faf835a1/librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437", size = 81768, upload-time = "2025-11-29T14:00:37.451Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/13/f63e60bc219b17f3d8f3d13423cd4972e597b0321c51cac7bfbdd5e1f7b9/librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089", size = 85884, upload-time = "2025-11-29T14:00:38.433Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/42/0068f14f39a79d1ce8a19d4988dd07371df1d0a7d3395fbdc8a25b1c9437/librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513", size = 85830, upload-time = "2025-11-29T14:00:39.418Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1c/87f5af3a9e6564f09e50c72f82fc3057fd42d1facc8b510a707d0438c4ad/librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1", size = 88086, upload-time = "2025-11-29T14:00:40.555Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e5/22153b98b88a913b5b3f266f12e57df50a2a6960b3f8fcb825b1a0cfe40a/librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977", size = 86470, upload-time = "2025-11-29T14:00:41.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3c/ea1edb587799b1edcc22444e0630fa422e32d7aaa5bfb5115b948acc2d1c/librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d", size = 89079, upload-time = "2025-11-29T14:00:42.882Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ad/50bb4ae6b07c9f3ab19653e0830a210533b30eb9a18d515efb5a2b9d0c7c/librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46", size = 19820, upload-time = "2025-11-29T14:00:44.211Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/12/7426ee78f3b1dbe11a90619d54cb241ca924ca3c0ff9ade3992178e9b440/librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457", size = 21332, upload-time = "2025-11-29T14:00:45.427Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/80/bc60fd16fe24910bf5974fb914778a2e8540cef55385ab2cb04a0dfe42c4/librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08", size = 27285, upload-time = "2025-11-29T14:00:46.626Z" },
+ { url = "https://files.pythonhosted.org/packages/88/3c/26335536ed9ba097c79cffcee148393592e55758fe76d99015af3e47a6d0/librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa", size = 27629, upload-time = "2025-11-29T14:00:47.863Z" },
+ { url = "https://files.pythonhosted.org/packages/af/fd/2dcedeacfedee5d2eda23e7a49c1c12ce6221b5d58a13555f053203faafc/librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122", size = 82039, upload-time = "2025-11-29T14:00:49.131Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ff/6aa11914b83b0dc2d489f7636942a8e3322650d0dba840db9a1b455f3caa/librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f", size = 86560, upload-time = "2025-11-29T14:00:50.403Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a1/d25af61958c2c7eb978164aeba0350719f615179ba3f428b682b9a5fdace/librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040", size = 86494, upload-time = "2025-11-29T14:00:51.383Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/4b/40e75d3b258c801908e64b39788f9491635f9554f8717430a491385bd6f2/librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523", size = 88914, upload-time = "2025-11-29T14:00:52.688Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6d/0070c81aba8a169224301c75fb5fb6c3c25ca67e6ced086584fc130d5a67/librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f", size = 86944, upload-time = "2025-11-29T14:00:53.768Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/94/809f38887941b7726692e0b5a083dbdc87dbb8cf893e3b286550c5f0b129/librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47", size = 89852, upload-time = "2025-11-29T14:00:54.71Z" },
+ { url = "https://files.pythonhosted.org/packages/58/a3/b0e5b1cda675b91f1111d8ba941da455d8bfaa22f4d2d8963ba96ccb5b12/librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7", size = 19948, upload-time = "2025-11-29T14:00:55.989Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/73/70011c2b37e3be3ece3affd3abc8ebe5cda482b03fd6b3397906321a901e/librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8", size = 21406, upload-time = "2025-11-29T14:00:56.874Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ee/119aa759290af6ca0729edf513ca390c1afbeae60f3ecae9b9d56f25a8a9/librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45", size = 20875, upload-time = "2025-11-29T14:00:57.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/2c/b59249c566f98fe90e178baf59e83f628d6c38fb8bc78319301fccda0b5e/librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176", size = 27841, upload-time = "2025-11-29T14:00:58.925Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e8/9db01cafcd1a2872b76114c858f81cc29ce7ad606bc102020d6dabf470fb/librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057", size = 27844, upload-time = "2025-11-29T14:01:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4d/da449d3a7d83cc853af539dee42adc37b755d7eea4ad3880bacfd84b651d/librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610", size = 84091, upload-time = "2025-11-29T14:01:01.118Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6c/f90306906fb6cc6eaf4725870f0347115de05431e1f96d35114392d31fda/librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c", size = 88239, upload-time = "2025-11-29T14:01:02.11Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ae/473ce7b423cfac2cb503851a89d9d2195bf615f534d5912bf86feeebbee7/librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9", size = 88815, upload-time = "2025-11-29T14:01:03.114Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/6d/934df738c87fb9617cabefe4891eece585a06abe6def25b4bca3b174429d/librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c", size = 90598, upload-time = "2025-11-29T14:01:04.071Z" },
+ { url = "https://files.pythonhosted.org/packages/72/89/eeaa124f5e0f431c2b39119550378ae817a4b1a3c93fd7122f0639336fff/librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c", size = 88603, upload-time = "2025-11-29T14:01:05.02Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/ed/c60b3c1cfc27d709bc0288af428ce58543fcb5053cf3eadbc773c24257f5/librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf", size = 92112, upload-time = "2025-11-29T14:01:06.304Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/f56169be5f716ef4ab0277be70bcb1874b4effc262e655d85b505af4884d/librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d", size = 20127, upload-time = "2025-11-29T14:01:07.283Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/222750ce82bf95125529eaab585ac7e2829df252f3cfc05d68792fb1dd2c/librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01", size = 21545, upload-time = "2025-11-29T14:01:08.184Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c9/f731ddcfb72f446a92a8674c6b8e1e2242773cce43a04f41549bd8b958ff/librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55", size = 20946, upload-time = "2025-11-29T14:01:09.384Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/aa/3055dd440f8b8b3b7e8624539a0749dd8e1913e978993bcca9ce7e306231/librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3", size = 27874, upload-time = "2025-11-29T14:01:10.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/93/226d7dd455eaa4c26712b5ccb2dfcca12831baa7f898c8ffd3a831e29fda/librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5", size = 27852, upload-time = "2025-11-29T14:01:11.535Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/8b/db9d51191aef4e4cc06285250affe0bb0ad8b2ed815f7ca77951655e6f02/librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42", size = 84264, upload-time = "2025-11-29T14:01:12.461Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/53/297c96bda3b5a73bdaf748f1e3ae757edd29a0a41a956b9c10379f193417/librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70", size = 88432, upload-time = "2025-11-29T14:01:13.405Z" },
+ { url = "https://files.pythonhosted.org/packages/54/3a/c005516071123278e340f22de72fa53d51e259d49215295c212da16c4dc2/librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522", size = 89014, upload-time = "2025-11-29T14:01:14.373Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/9b/ea715f818d926d17b94c80a12d81a79e95c44f52848e61e8ca1ff29bb9a9/librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30", size = 90807, upload-time = "2025-11-29T14:01:15.377Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/fc/4e2e4c87e002fa60917a8e474fd13c4bac9a759df82be3778573bb1ab954/librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917", size = 88890, upload-time = "2025-11-29T14:01:16.633Z" },
+ { url = "https://files.pythonhosted.org/packages/70/7f/c7428734fbdfd4db3d5b9237fc3a857880b2ace66492836f6529fef25d92/librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530", size = 92300, upload-time = "2025-11-29T14:01:17.658Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/0c/738c4824fdfe74dc0f95d5e90ef9e759d4ecf7fd5ba964d54a7703322251/librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3", size = 20159, upload-time = "2025-11-29T14:01:18.61Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/95/93d0e61bc617306ecf4c54636b5cbde4947d872563565c4abdd9d07a39d3/librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88", size = 21484, upload-time = "2025-11-29T14:01:19.506Z" },
+ { url = "https://files.pythonhosted.org/packages/10/23/abd7ace79ab54d1dbee265f13529266f686a7ce2d21ab59a992f989009b6/librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9", size = 20935, upload-time = "2025-11-29T14:01:20.415Z" },
+ { url = "https://files.pythonhosted.org/packages/83/14/c06cb31152182798ed98be73f54932ab984894f5a8fccf9b73130897a938/librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a", size = 27566, upload-time = "2025-11-29T14:01:21.609Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/ce83ca7b057b06150519152f53a0b302d7c33c8692ce2f01f669b5a819d9/librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd", size = 27753, upload-time = "2025-11-29T14:01:22.558Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ec/739a885ef0a2839b6c25f1b01c99149d2cb6a34e933ffc8c051fcd22012e/librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b", size = 83178, upload-time = "2025-11-29T14:01:23.555Z" },
+ { url = "https://files.pythonhosted.org/packages/db/bd/dc18bb1489d48c0911b9f4d72eae2d304ea264e215ba80f1e6ba4a9fc41d/librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc", size = 87266, upload-time = "2025-11-29T14:01:24.532Z" },
+ { url = "https://files.pythonhosted.org/packages/94/f3/d0c5431b39eef15e48088b2d739ad84b17c2f1a22c0345c6d4c4a42b135e/librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3", size = 87623, upload-time = "2025-11-29T14:01:25.798Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/15/9a52e90834e4bd6ee16cdbaf551cb32227cbaad27398391a189c489318bc/librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751", size = 89436, upload-time = "2025-11-29T14:01:26.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/8a/a7e78e46e8486e023c50f21758930ef4793999115229afd65de69e94c9cc/librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8", size = 87540, upload-time = "2025-11-29T14:01:27.756Z" },
+ { url = "https://files.pythonhosted.org/packages/49/01/93799044a1cccac31f1074b07c583e181829d240539657e7f305ae63ae2a/librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e", size = 90597, upload-time = "2025-11-29T14:01:29.35Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/29/00c7f58b8f8eb1bad6529ffb6c9cdcc0890a27dac59ecda04f817ead5277/librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf", size = 18955, upload-time = "2025-11-29T14:01:30.325Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/13/2739e6e197a9f751375a37908a6a5b0bff637b81338497a1bcb5817394da/librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2", size = 20263, upload-time = "2025-11-29T14:01:31.287Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/73/393868fc2158705ea003114a24e73bb10b03bda31e9ad7b5c5ec6575338b/librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78", size = 19575, upload-time = "2025-11-29T14:01:32.229Z" },
+ { url = "https://files.pythonhosted.org/packages/48/6d/3c8ff3dec21bf804a205286dd63fd28dcdbe00b8dd7eb7ccf2e21a40a0b0/librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179", size = 28732, upload-time = "2025-11-29T14:01:33.165Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/90/e214b8b4aa34ed3d3f1040719c06c4d22472c40c5ef81a922d5af7876eb4/librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7", size = 29065, upload-time = "2025-11-29T14:01:34.088Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/90/ef61ed51f0a7770cc703422d907a757bbd8811ce820c333d3db2fd13542a/librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916", size = 93703, upload-time = "2025-11-29T14:01:35.057Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ae/c30bb119c35962cbe9a908a71da99c168056fc3f6e9bbcbc157d0b724d89/librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8", size = 98890, upload-time = "2025-11-29T14:01:36.031Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/96/47a4a78d252d36f072b79d592df10600d379a895c3880c8cbd2ac699f0ad/librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc", size = 98255, upload-time = "2025-11-29T14:01:37.058Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/28/779b5cc3cd9987683884eb5f5672e3251676bebaaae6b7da1cf366eb1da1/librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07", size = 100769, upload-time = "2025-11-29T14:01:38.413Z" },
+ { url = "https://files.pythonhosted.org/packages/28/d7/771755e57c375cb9d25a4e106f570607fd856e2cb91b02418db1db954796/librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f", size = 98580, upload-time = "2025-11-29T14:01:39.459Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ec/8b157eb8fbc066339a2f34b0aceb2028097d0ed6150a52e23284a311eafe/librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0", size = 101706, upload-time = "2025-11-29T14:01:40.474Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a8/4aaead9a06c795a318282aebf7d3e3e578fa889ff396e1b640c3be4c7806/librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a", size = 19465, upload-time = "2025-11-29T14:01:41.77Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/61/b7e6a02746c1731670c19ba07d86da90b1ae45d29e405c0b5615abf97cde/librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf", size = 21042, upload-time = "2025-11-29T14:01:42.865Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/3d/72cc9ec90bb80b5b1a65f0bb74a0f540195837baaf3b98c7fa4a7aa9718e/librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0", size = 20246, upload-time = "2025-11-29T14:01:44.13Z" },
]
[[package]]
name = "litellm"
-version = "1.80.5"
+version = "1.80.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "fastuuid", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "grpcio", version = "1.67.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
+ { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')" },
{ name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "importlib-metadata", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -2899,9 +3062,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/85/b8/357544534bef87dd2858432f3cbd3a0e5cc267caebca5ea86b03618786c5/litellm-1.80.5.tar.gz", hash = "sha256:922791c264845d9ed59e540c8fa74a74d237c1b209568a05ffeacd8b51770deb", size = 11885764, upload-time = "2025-11-22T23:41:42.25Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/3f/af532014449c3931ae6cad2d97d267dd43d0de006060a8cbf0962e004024/litellm-1.80.7.tar.gz", hash = "sha256:3977a8d195aef842d01c18bf9e22984829363c6a4b54daf9a43c9dd9f190b42c", size = 12023127, upload-time = "2025-11-27T23:03:52.474Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bd/af/1d4693746ff9fbbe27a6e7d6394b801acf234e00c83f45ad1cb5bf2eaa6c/litellm-1.80.5-py3-none-any.whl", hash = "sha256:2ac5f4e88cd57ae056e00da8f872e1c2956653750929fba2fd9b007b400fdb77", size = 10671970, upload-time = "2025-11-22T23:41:39.923Z" },
+ { url = "https://files.pythonhosted.org/packages/54/e0/2e60a0c09235fd7b55297390c557923f3c35a9cf001914222c26a7857d2b/litellm-1.80.7-py3-none-any.whl", hash = "sha256:f7d993f78c1e0e4e1202b2a925cc6540b55b6e5fb055dd342d88b145ab3102ed", size = 10848321, upload-time = "2025-11-27T23:03:50.002Z" },
]
[package.optional-dependencies]
@@ -2943,11 +3106,11 @@ wheels = [
[[package]]
name = "litellm-proxy-extras"
-version = "0.4.6"
+version = "0.4.9"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/38/1d/cb95aabbae94aeed1267b7f58a620a2add0387765e500573e9073a70d119/litellm_proxy_extras-0.4.6.tar.gz", hash = "sha256:3864bdae26a92906081e591dcf72e1864277e6d58582bee224befb7385fda508", size = 17434, upload-time = "2025-11-19T21:21:25.278Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/4c/2ada2de76fe5017fcd0bb9a63493b1d8e57fe0d3d41508f144c10ef3c7a4/litellm_proxy_extras-0.4.9.tar.gz", hash = "sha256:804b331c7691ddd040ea0dbe36465a1565bb40824db6345b2bc571ec076e3655", size = 18627, upload-time = "2025-11-26T22:14:29.428Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/18/78720f3743fa88d8f80dd8da8d21eec507073624288471bb52ff0dfcdc22/litellm_proxy_extras-0.4.6-py3-none-any.whl", hash = "sha256:665ad022fdfd06f3fe97071e49f3b240554a931e57d27850fb815a19d3ea901d", size = 37547, upload-time = "2025-11-19T21:21:23.455Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ad/4494b78f5af9257dd712d96b3846af08d9cc9508c2e6051c53a98cc29386/litellm_proxy_extras-0.4.9-py3-none-any.whl", hash = "sha256:7bea79fd015989ca8626fdb5199fca335cedf68e8469e10e8ad55528410a0229", size = 39607, upload-time = "2025-11-26T22:14:28.097Z" },
]
[[package]]
@@ -3194,31 +3357,31 @@ wheels = [
[[package]]
name = "microsoft-agents-activity"
-version = "0.6.0"
+version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/cb/c5/ec0b5786257b88a21245d9485e864088cbdc60442e21b637cc1433f6061b/microsoft_agents_activity-0.6.0.tar.gz", hash = "sha256:9b23a44def700d1d18670e28e2e7e66e8679dfb902dadcd94902d5591cda65a4", size = 57481, upload-time = "2025-11-18T18:12:33.351Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/73/8e/d19b8e9a595058b915f3bd65ba0d2d16b366ffe63bba57699e9cc15d722b/microsoft_agents_activity-0.6.1.tar.gz", hash = "sha256:744f16ae73ddc22315880b8ae94d8909da8aee25128cc85106591c593e6bdab9", size = 57786, upload-time = "2025-12-01T21:22:20.584Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/6b/e07cbd310881eda75def2d13220f27648f8c2632b16244536d33069e6bd5/microsoft_agents_activity-0.6.0-py3-none-any.whl", hash = "sha256:bfcd0f55ac332320ca927f829f6fb200acff1bd61fb6aba9945906130fec7d06", size = 131093, upload-time = "2025-11-18T18:12:41.796Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/37/327604224d546a7148a1b67ae7929f67aa3be0efad896b36c753b54a632f/microsoft_agents_activity-0.6.1-py3-none-any.whl", hash = "sha256:ca5b86bf6e1e9ebe31836031b0aad5726c8112734cfe20ce8144603fa6edfbfa", size = 131092, upload-time = "2025-12-01T21:22:30.041Z" },
]
[[package]]
name = "microsoft-agents-copilotstudio-client"
-version = "0.6.0"
+version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "microsoft-agents-hosting-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/88/23/67833eac41963a7a0110b8fe3b9329c65388ccf50f6b1f694e92e0cb3918/microsoft_agents_copilotstudio_client-0.6.0.tar.gz", hash = "sha256:2964e164cc0175614aa356aaab01fda48153961304f11cdaef2a45710d922805", size = 11756, upload-time = "2025-11-18T18:12:35.842Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/9b/819e8acf93fd34803aa6b2fba97c8435cae577ddeb2e2fe3cb6ca9665554/microsoft_agents_copilotstudio_client-0.6.1.tar.gz", hash = "sha256:eef8dc359b650358f012c6a2b0f498e565835c63d80881d5db073213179c8236", size = 11986, upload-time = "2025-12-01T21:22:22.644Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/56/1f/75e8618bed71da736104d5d4c41137fdb28672660e0257d59ea6d6fd4029/microsoft_agents_copilotstudio_client-0.6.0-py3-none-any.whl", hash = "sha256:733affebee8b4e7d204e66ff91e3162d4f498f2944b45a19eec0a8234eea67b5", size = 12340, upload-time = "2025-11-18T18:12:43.572Z" },
+ { url = "https://files.pythonhosted.org/packages/16/3e/6521af0ce8f36f1be909a400372f4162e4c0611071a403613406dd0fb98a/microsoft_agents_copilotstudio_client-0.6.1-py3-none-any.whl", hash = "sha256:194a62616c26d905d28cf1d18480e7b2d02b6f5ccaacb174bec81f0c8b0f38a3", size = 12339, upload-time = "2025-12-01T21:22:32.334Z" },
]
[[package]]
name = "microsoft-agents-hosting-core"
-version = "0.6.0"
+version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -3227,9 +3390,9 @@ dependencies = [
{ name = "pyjwt", 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'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ea/02/893e4a2f789b3c91550e6aefd53a9b88d3bea8f4f15ae1fee141691fb5fb/microsoft_agents_hosting_core-0.6.0.tar.gz", hash = "sha256:0331573dbc2ae8f4339658ed600676c3a4a76ade403b4adc49c4d38afd3fdc36", size = 82802, upload-time = "2025-11-18T18:12:37.518Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/dc/be911b94d4b85b4120b99c614748ba3915fcf3dc3867e2bb6434455d6c44/microsoft_agents_hosting_core-0.6.1.tar.gz", hash = "sha256:c8db2c974956ca1e15b4d5f83ee5873868b853c71c35165eb3e82bda5615749d", size = 83044, upload-time = "2025-12-01T21:22:24.603Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/46/01d12043fc6e31c3cd894ef6d17d4586ea2a0a918c83842e2db5b8728f1c/microsoft_agents_hosting_core-0.6.0-py3-none-any.whl", hash = "sha256:9f07df7835a4c0f527b66e64e57dbfcbd9ec7c6140cc829f6e99f061b4084783", size = 122494, upload-time = "2025-11-18T18:12:45.639Z" },
+ { url = "https://files.pythonhosted.org/packages/83/06/4a270fffc5d28da6f7ef3afa451c49dfa7cd323b873838d482ea3746151b/microsoft_agents_hosting_core-0.6.1-py3-none-any.whl", hash = "sha256:87bdcde62b2a19c9b245022abbdd5db7b82f0da9d3ecf27a3de49c688a8dc020", size = 122471, upload-time = "2025-12-01T21:22:34.141Z" },
]
[[package]]
@@ -3453,47 +3616,48 @@ wheels = [
[[package]]
name = "mypy"
-version = "1.18.2"
+version = "1.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "librt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "mypy-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pathspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" },
- { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" },
- { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" },
- { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" },
- { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" },
- { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" },
- { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" },
- { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" },
- { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" },
- { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" },
- { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" },
- { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" },
- { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" },
- { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" },
- { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" },
- { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" },
- { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" },
- { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" },
- { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" },
- { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" },
- { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" },
- { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" },
- { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" },
- { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" },
- { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" },
- { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" },
- { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" },
- { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" },
- { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" },
+ { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" },
+ { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" },
+ { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" },
+ { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" },
+ { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" },
+ { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" },
+ { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" },
+ { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" },
+ { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" },
+ { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" },
+ { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" },
]
[[package]]
@@ -3507,11 +3671,11 @@ wheels = [
[[package]]
name = "narwhals"
-version = "2.12.0"
+version = "2.13.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/93/f8/e1c28f24b641871c14ccae7ba6381f3c7827789a06e947ce975ae8a9075a/narwhals-2.12.0.tar.gz", hash = "sha256:075b6d56f3a222613793e025744b129439ecdff9292ea6615dd983af7ba6ea44", size = 590404, upload-time = "2025-11-17T10:53:28.381Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/ea/f82ef99ced4d03c33bb314c9b84a08a0a86c448aaa11ffd6256b99538aa5/narwhals-2.13.0.tar.gz", hash = "sha256:ee94c97f4cf7cfeebbeca8d274784df8b3d7fd3f955ce418af998d405576fdd9", size = 594555, upload-time = "2025-12-01T13:54:05.329Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/9a/c6f79de7ba3a0a8473129936b7b90aa461d3d46fec6f1627672b1dccf4e9/narwhals-2.12.0-py3-none-any.whl", hash = "sha256:baeba5d448a30b04c299a696bd9ee5ff73e4742143e06c49ca316b46539a7cbb", size = 425014, upload-time = "2025-11-17T10:53:26.65Z" },
+ { url = "https://files.pythonhosted.org/packages/87/0d/1861d1599571974b15b025e12b142d8e6b42ad66c8a07a89cb0fc21f1e03/narwhals-2.13.0-py3-none-any.whl", hash = "sha256:9b795523c179ca78204e3be53726da374168f906e38de2ff174c2363baaaf481", size = 426407, upload-time = "2025-12-01T13:54:03.861Z" },
]
[[package]]
@@ -3595,13 +3759,16 @@ name = "numpy"
version = "2.3.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.13' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
- "python_full_version >= '3.13' and sys_platform == 'linux'",
+ "python_full_version >= '3.14' and sys_platform == 'linux'",
+ "python_full_version == '3.13.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
- "python_full_version >= '3.13' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
]
@@ -3788,7 +3955,8 @@ version = "1.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
- { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "grpcio", version = "1.67.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
+ { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')" },
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -4302,8 +4470,8 @@ name = "powerfx"
version = "0.0.33"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
- { name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
+ { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/41/8f95f72f4f3b7ea54357c449bf5bd94813b6321dec31db9ffcbf578e2fa3/powerfx-0.0.33.tar.gz", hash = "sha256:85e8330bef8a7a207c3e010aa232df0ae38825e94d590c73daf3a3f44115cb09", size = 3236647, upload-time = "2025-11-20T19:31:09.414Z" }
wheels = [
@@ -4583,7 +4751,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.12.4"
+version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -4591,9 +4759,9 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[package.optional-dependencies]
@@ -4972,7 +5140,7 @@ name = "pythonnet"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
wheels = [
@@ -5079,7 +5247,8 @@ name = "qdrant-client"
version = "1.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "grpcio", version = "1.67.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
+ { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')" },
{ name = "httpx", extra = ["http2"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
{ name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
@@ -5107,7 +5276,7 @@ wheels = [
[[package]]
name = "redisvl"
-version = "0.12.0"
+version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpath-ng", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -5120,9 +5289,9 @@ dependencies = [
{ name = "redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ff/6b/10fe769e1102d99cd9e47633bacd01ab71fb416958e77469cc55f032f471/redisvl-0.12.0.tar.gz", hash = "sha256:205db9eb9639b78a9e479b012f6db64a12aa47129fdfaf3ad59623b5736e00d2", size = 683456, upload-time = "2025-11-21T23:20:57.218Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/ac/7c527765011d07652ff9d97fd16f563d625bd1887ad09bafe2626f77f225/redisvl-0.12.1.tar.gz", hash = "sha256:c4df3f7dd2d92c71a98e54ba32bcfb4f7bd526c749e4721de0fd1f08e0ecddec", size = 689730, upload-time = "2025-11-25T19:24:04.562Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/34/24/417f7c171caa460e45b688ee94e67788bd63544a90c3fdc411f248fce795/redisvl-0.12.0-py3-none-any.whl", hash = "sha256:406695793681c1f46f61b6a1141a6b6f86261bf690caf0de00595c511700012d", size = 175071, upload-time = "2025-11-21T23:20:55.605Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6a/f8c9f915a1d18fff2499684caff929d0c6e004ac5f6e5f9ecec88314cd2a/redisvl-0.12.1-py3-none-any.whl", hash = "sha256:c7aaea242508624b78a448362b7a33e3b411049271ce8bdc7ef95208b1095e6e", size = 176692, upload-time = "2025-11-25T19:24:03.013Z" },
]
[[package]]
@@ -5276,124 +5445,124 @@ wheels = [
[[package]]
name = "rpds-py"
-version = "0.29.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/7a/c5b2ff381b74bc742768e8d870f26babac4ef256ba160bdbf8d57af56461/rpds_py-0.29.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4ae4b88c6617e1b9e5038ab3fccd7bac0842fdda2b703117b2aa99bc85379113", size = 372385, upload-time = "2025-11-16T14:47:36.287Z" },
- { url = "https://files.pythonhosted.org/packages/28/36/531f1eb4d5bed4a9c150f363a7ec4a98d2dc746151bba5473bc38ee85dec/rpds_py-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d9128ec9d8cecda6f044001fde4fb71ea7c24325336612ef8179091eb9596b9", size = 362869, upload-time = "2025-11-16T14:47:38.196Z" },
- { url = "https://files.pythonhosted.org/packages/54/df/7e9c0493a2015d9c82807a2d5f023ea9774e27a4c15b33ef1cdb7456138d/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37812c3da8e06f2bb35b3cf10e4a7b68e776a706c13058997238762b4e07f4f", size = 391582, upload-time = "2025-11-16T14:47:39.746Z" },
- { url = "https://files.pythonhosted.org/packages/15/38/42a981c3592ef46fbd7e17adbf8730cc5ec87e6aa1770c658c44bbb52960/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66786c3fb1d8de416a7fa8e1cb1ec6ba0a745b2b0eee42f9b7daa26f1a495545", size = 405685, upload-time = "2025-11-16T14:47:41.472Z" },
- { url = "https://files.pythonhosted.org/packages/12/45/628b8c15856c3849c3f52ec6dac93c046ed5faeed4a435af03b70525fd29/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58f5c77f1af888b5fd1876c9a0d9858f6f88a39c9dd7c073a88e57e577da66d", size = 527067, upload-time = "2025-11-16T14:47:43.036Z" },
- { url = "https://files.pythonhosted.org/packages/dc/ba/6b56d09badeabd95098016d72a437d4a0fd82d4672ce92a7607df5d70a42/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:799156ef1f3529ed82c36eb012b5d7a4cf4b6ef556dd7cc192148991d07206ae", size = 412532, upload-time = "2025-11-16T14:47:44.484Z" },
- { url = "https://files.pythonhosted.org/packages/f1/39/2f1f3db92888314b50b8f9641f679188bd24b3665a8cb9923b7201ae8011/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453783477aa4f2d9104c4b59b08c871431647cb7af51b549bbf2d9eb9c827756", size = 392736, upload-time = "2025-11-16T14:47:46.053Z" },
- { url = "https://files.pythonhosted.org/packages/60/43/3c3b1dcd827e50f2ae28786d846b8a351080d8a69a3b49bc10ae44cc39b1/rpds_py-0.29.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:24a7231493e3c4a4b30138b50cca089a598e52c34cf60b2f35cebf62f274fdea", size = 406300, upload-time = "2025-11-16T14:47:47.268Z" },
- { url = "https://files.pythonhosted.org/packages/da/02/bc96021b67f8525e6bcdd68935c4543ada61e1f3dcb067ed037d68b8c6d2/rpds_py-0.29.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7033c1010b1f57bb44d8067e8c25aa6fa2e944dbf46ccc8c92b25043839c3fd2", size = 423641, upload-time = "2025-11-16T14:47:48.878Z" },
- { url = "https://files.pythonhosted.org/packages/38/e9/c435ddb602ced19a80b8277a41371734f33ad3f91cc4ceb4d82596800a3c/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0248b19405422573621172ab8e3a1f29141362d13d9f72bafa2e28ea0cdca5a2", size = 574153, upload-time = "2025-11-16T14:47:50.435Z" },
- { url = "https://files.pythonhosted.org/packages/84/82/dc3c32e1f89ecba8a59600d4cd65fe0ad81b6c636ccdbf6cd177fd6a7bac/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f9f436aee28d13b9ad2c764fc273e0457e37c2e61529a07b928346b219fcde3b", size = 600304, upload-time = "2025-11-16T14:47:51.599Z" },
- { url = "https://files.pythonhosted.org/packages/35/98/785290e0b7142470735dc1b1f68fb33aae29e5296f062c88396eedf796c8/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24a16cb7163933906c62c272de20ea3c228e4542c8c45c1d7dc2b9913e17369a", size = 562211, upload-time = "2025-11-16T14:47:53.094Z" },
- { url = "https://files.pythonhosted.org/packages/30/58/4eeddcb0737c6875f3e30c65dc9d7e7a10dfd5779646a990fa602c6d56c5/rpds_py-0.29.0-cp310-cp310-win32.whl", hash = "sha256:1a409b0310a566bfd1be82119891fefbdce615ccc8aa558aff7835c27988cbef", size = 221803, upload-time = "2025-11-16T14:47:54.404Z" },
- { url = "https://files.pythonhosted.org/packages/54/77/b35a8dbdcbeb32505500547cdafaa9f8863e85f8faac50ef34464ec5a256/rpds_py-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5523b0009e7c3c1263471b69d8da1c7d41b3ecb4cb62ef72be206b92040a950", size = 235530, upload-time = "2025-11-16T14:47:56.061Z" },
- { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" },
- { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" },
- { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" },
- { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" },
- { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" },
- { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" },
- { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" },
- { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" },
- { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" },
- { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" },
- { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" },
- { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" },
- { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" },
- { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" },
- { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" },
- { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" },
- { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" },
- { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" },
- { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" },
- { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" },
- { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" },
- { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" },
- { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" },
- { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" },
- { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" },
- { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" },
- { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" },
- { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" },
- { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" },
- { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" },
- { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" },
- { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" },
- { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" },
- { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" },
- { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" },
- { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" },
- { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" },
- { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" },
- { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" },
- { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" },
- { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" },
- { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" },
- { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" },
- { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" },
- { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" },
- { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" },
- { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" },
- { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" },
- { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" },
- { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" },
- { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" },
- { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" },
- { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" },
- { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" },
- { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" },
- { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" },
- { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" },
- { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" },
- { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" },
- { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" },
- { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" },
- { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" },
- { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" },
- { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" },
- { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" },
- { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" },
- { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" },
- { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" },
- { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" },
- { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" },
- { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" },
- { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" },
- { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" },
- { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" },
- { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" },
- { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" },
- { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" },
- { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" },
- { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" },
- { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" },
- { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" },
- { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" },
- { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" },
- { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" },
- { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" },
- { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" },
- { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" },
- { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" },
- { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" },
- { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" },
- { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" },
- { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" },
- { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" },
- { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" },
- { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" },
- { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" },
- { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" },
- { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" },
- { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" },
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
]
[[package]]
@@ -5424,28 +5593,28 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.14.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" },
- { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" },
- { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" },
- { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" },
- { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" },
- { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" },
- { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" },
- { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" },
- { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" },
- { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" },
- { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" },
- { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" },
- { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" },
- { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" },
- { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" },
- { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" },
- { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" },
- { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" },
+version = "0.14.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" },
+ { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" },
+ { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" },
+ { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" },
+ { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" },
+ { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" },
+ { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" },
+ { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" },
]
[[package]]
@@ -5572,13 +5741,16 @@ name = "scipy"
version = "1.16.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.13' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
- "python_full_version >= '3.13' and sys_platform == 'linux'",
+ "python_full_version >= '3.14' and sys_platform == 'linux'",
+ "python_full_version == '3.13.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
- "python_full_version >= '3.13' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
]
@@ -6303,28 +6475,28 @@ wheels = [
[[package]]
name = "uv"
-version = "0.9.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8a/08/3bf76403ea7c22feef634849137fab10b28ab5ba5bbf08a53390763d5448/uv-0.9.11.tar.gz", hash = "sha256:605a7a57f508aabd029fc0c5ef5c60a556f8c50d32e194f1a300a9f4e87f18d4", size = 3744387, upload-time = "2025-11-20T23:20:00.95Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/77/26/8f917e9faddd9cb49abcbc8c7dac5343b0f61d04c6ac36873d2a324fee1a/uv-0.9.11-py3-none-linux_armv6l.whl", hash = "sha256:803f85cf25ab7f1fca10fe2e40a1b9f5b1d48efc25efd6651ba3c9668db6a19e", size = 20787588, upload-time = "2025-11-20T23:18:53.738Z" },
- { url = "https://files.pythonhosted.org/packages/f5/1f/eafd39c719ddee19fc25884f68c1a7e736c0fca63c1cbef925caf8ebd739/uv-0.9.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6a31b0bd4eaec59bf97816aefbcd75cae4fcc8875c4b19ef1846b7bff3d67c70", size = 19922144, upload-time = "2025-11-20T23:18:57.569Z" },
- { url = "https://files.pythonhosted.org/packages/bf/f3/6b9fac39e5b65fa47dba872dcf171f1470490cd645343e8334f20f73885b/uv-0.9.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48548a23fb5a103b8955dfafff7d79d21112b8e25ce5ff25e3468dc541b20e83", size = 18380643, upload-time = "2025-11-20T23:19:01.02Z" },
- { url = "https://files.pythonhosted.org/packages/d6/9a/d4080e95950a4fc6fdf20d67b9a43ffb8e3d6d6b7c8dda460ae73ddbecd9/uv-0.9.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cb680948e678590b5960744af2ecea6f2c0307dbb74ac44daf5c00e84ad8c09f", size = 20310262, upload-time = "2025-11-20T23:19:04.914Z" },
- { url = "https://files.pythonhosted.org/packages/6d/b4/86d9c881bd6accf2b766f7193b50e9d5815f2b34806191d90ea24967965e/uv-0.9.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ef1982295e5aaf909a9668d6fb6abfc5089666c699f585a36f3a67f1a22916a", size = 20392988, upload-time = "2025-11-20T23:19:08.258Z" },
- { url = "https://files.pythonhosted.org/packages/a3/1d/6a227b7ca1829442c1419ba1db856d176b6e0861f9bf9355a8790a5d02b5/uv-0.9.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92ff773aa4193148019533c55382c2f9c661824bbf0c2e03f12aeefc800ede57", size = 21394892, upload-time = "2025-11-20T23:19:12.626Z" },
- { url = "https://files.pythonhosted.org/packages/5a/8f/df45b8409923121de8c4081c9d6d8ba3273eaa450645e1e542d83179c7b5/uv-0.9.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70137a46675bbecf3a8b43d292a61767f1b944156af3d0f8d5986292bd86f6cf", size = 22987735, upload-time = "2025-11-20T23:19:16.27Z" },
- { url = "https://files.pythonhosted.org/packages/89/51/bbf3248a619c9f502d310a11362da5ed72c312d354fb8f9667c5aa3be9dd/uv-0.9.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5af9117bab6c4b3a1cacb0cddfb3cd540d0adfb13c7b8a9a318873cf2d07e52", size = 22617321, upload-time = "2025-11-20T23:19:20.1Z" },
- { url = "https://files.pythonhosted.org/packages/3f/cd/a158ec989c5433dc86ebd9fea800f2aed24255b84ab65b6d7407251e5e31/uv-0.9.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cc86940d9b3a425575f25dc45247be2fb31f7fed7bf3394ae9daadd466e5b80", size = 21615712, upload-time = "2025-11-20T23:19:23.71Z" },
- { url = "https://files.pythonhosted.org/packages/73/da/2597becbc0fcbb59608d38fda5db79969e76dedf5b072f0e8564c8f0628b/uv-0.9.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97906ca1b90dac91c23af20e282e2e37c8eb80c3721898733928a295f2defda", size = 21661022, upload-time = "2025-11-20T23:19:27.385Z" },
- { url = "https://files.pythonhosted.org/packages/52/66/9b8f3b3529b23c2a6f5b9612da70ea53117935ec999757b4f1d640f63d63/uv-0.9.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d901269e1db72abc974ba61d37be6e56532e104922329e0b553d9df07ba224be", size = 20440548, upload-time = "2025-11-20T23:19:31.051Z" },
- { url = "https://files.pythonhosted.org/packages/72/b2/683afdb83e96dd966eb7cf3688af56a1b826c8bc1e8182fb10ec35b3e391/uv-0.9.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8abfb7d4b136de3e92dd239ea9a51d4b7bbb970dc1b33bec84d08facf82b9a6e", size = 21493758, upload-time = "2025-11-20T23:19:34.688Z" },
- { url = "https://files.pythonhosted.org/packages/f4/00/99848bc9834aab104fa74aa1a60b1ca478dee824d2e4aacb15af85673572/uv-0.9.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1f8afc13b3b94bce1e72514c598d41623387b2b61b68d7dbce9a01a0d8874860", size = 20332324, upload-time = "2025-11-20T23:19:38.376Z" },
- { url = "https://files.pythonhosted.org/packages/6c/94/8cfd1bb1cc5d768cb334f976ba2686c6327e4ac91c16b8469b284956d4d9/uv-0.9.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7d414cfa410f1850a244d87255f98d06ca61cc13d82f6413c4f03e9e0c9effc7", size = 20845062, upload-time = "2025-11-20T23:19:42.006Z" },
- { url = "https://files.pythonhosted.org/packages/a0/42/43f66bfc621464dabe9cfe3cbf69cddc36464da56ab786c94fc9ccf99cc7/uv-0.9.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:edc14143d0ba086a7da4b737a77746bb36bc00e3d26466f180ea99e3bf795171", size = 21857559, upload-time = "2025-11-20T23:19:46.026Z" },
- { url = "https://files.pythonhosted.org/packages/8f/4d/bfd41bf087522601c724d712c3727aeb62f51b1f67c4ab86a078c3947525/uv-0.9.11-py3-none-win32.whl", hash = "sha256:af5fd91eecaa04b4799f553c726307200f45da844d5c7c5880d64db4debdd5dc", size = 19639246, upload-time = "2025-11-20T23:19:50.254Z" },
- { url = "https://files.pythonhosted.org/packages/2c/2f/d51c02627de68a7ca5b82f0a5d61d753beee3fe696366d1a1c5d5e40cd58/uv-0.9.11-py3-none-win_amd64.whl", hash = "sha256:c65a024ad98547e32168f3a52360fe73ff39cd609a8fb9dd2509aac91483cfc8", size = 21626822, upload-time = "2025-11-20T23:19:54.424Z" },
- { url = "https://files.pythonhosted.org/packages/af/d8/e07e866ee328d3c9f27a6d57a018d8330f47be95ef4654a178779c968a66/uv-0.9.11-py3-none-win_arm64.whl", hash = "sha256:4907a696c745703542ed2559bdf5380b92c8b1d4bf290ebfed45bf9a2a2c6690", size = 20046856, upload-time = "2025-11-20T23:19:58.517Z" },
+version = "0.9.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/93/77/36977111f1a7a6625ba432959958d284124d5cda678b703b77f1e1c8e8b8/uv-0.9.14.tar.gz", hash = "sha256:e62ae030bb607abe9c2d6d2569c696804fa668a3b176d7cce20cfa1c66012855", size = 3766833, upload-time = "2025-12-01T17:22:51.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/f8/05876ea28ef1edd4a4dbcd5c9e44daa7d056bad25b0f114305a49457baa7/uv-0.9.14-py3-none-linux_armv6l.whl", hash = "sha256:876d0cf2a92113e1237ef71a7dc21e2cc82ab0664f98004d61abeb05c944ffd2", size = 20844416, upload-time = "2025-12-01T17:22:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c0/c88a40306b2d437704e25f19890da1f6f9b42cbe1695de0373e3ca1258d8/uv-0.9.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e14853fb7781251f75cbb200fa2a81f2ac087a7f0647ee8699689198d6496f05", size = 19981109, upload-time = "2025-12-01T17:22:07.422Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/65/6ba20daba11fc88d41cb03fe903d8440618f6033fba511f34c7bd9df02ad/uv-0.9.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd90bc5e364a2fdc89499de9c1cffe9036b0318e54644b5664a9c395bb21bb29", size = 18469837, upload-time = "2025-12-01T17:22:19.014Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a1/245dfacce0e2755b82b00dc5fbaea4a690e3fb7046a779c1fd719896f04b/uv-0.9.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c086218fe1f3f88281d2f881bbeb5ada062eb4ea5d28292f352e45de38aa125a", size = 20347846, upload-time = "2025-12-01T17:22:35.144Z" },
+ { url = "https://files.pythonhosted.org/packages/03/0d/9314fd85e8ab574c9433b014d49fe233cd8e0ae38274cc5716a9f8291f5e/uv-0.9.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6dc4d37a593e2843df96a32be4cdab682e7abb15552c967277ac29fe8e556cdb", size = 20441070, upload-time = "2025-12-01T17:22:46.793Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e9/1eab4b0e3b7eb6823a927a86bf34e3e0086c6321d794da4fafc1e168373c/uv-0.9.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7770890958273fe5f6222857be0981e06808f531a2d28cc8da5907b3036fa7dd", size = 21636744, upload-time = "2025-12-01T17:22:28.272Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/ca/3a25e8bce4402d410bdbe5dc327eb9cf1e441f29cde73a7838816b23a14b/uv-0.9.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2a1724160ab2429317ab7d340f95d34c93a4830fef7f2d952795754837fb2e2c", size = 23033527, upload-time = "2025-12-01T17:22:30.643Z" },
+ { url = "https://files.pythonhosted.org/packages/39/44/c3e3ac7e80de643aa34fc70661f668a121fb48cc515e0a263daaf24e92cb/uv-0.9.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:180496428c669244e6af4b4b05f3c450d7976367b4907312d609890a2ee03be5", size = 22666761, upload-time = "2025-12-01T17:22:13.771Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c7/14eddd397d6333673b1dc15f4f13548afae191b3dbf5a40d25bbf12c2789/uv-0.9.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:144bad91b4c4efd7104e219beab4a238ccf560a87323128f0d6471b85c08915e", size = 21653308, upload-time = "2025-12-01T17:22:21.358Z" },
+ { url = "https://files.pythonhosted.org/packages/38/9e/0ddb21e94fc7fd67547e74aa0cbb042d57f52fe283f3d517d1a8c9e5df66/uv-0.9.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b5a27f528af437d9cd7bd85905095f166d0c37bdf3404a8a900948068e03d6b", size = 21690920, upload-time = "2025-12-01T17:22:32.777Z" },
+ { url = "https://files.pythonhosted.org/packages/17/35/44a7aeafc1cc9b1ec55ab433bed0211c34ca77f230853735c6c8d8683783/uv-0.9.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cbf18113f0e07898af804f6f4a9ef521eb181865a94b7d162431dcae5b55f8fa", size = 20467749, upload-time = "2025-12-01T17:22:11.23Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/f8/6b087904c897f2e96c69c9386fdefbd6c5fdeecab6624c5e972a0e31dd91/uv-0.9.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:18231f386e3f153e9560f535bd224b618f4990c4f417504f915fe95fc5513448", size = 21513786, upload-time = "2025-12-01T17:22:25.953Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/4b/1959897d40affc078eca5812db6bdef0a331e594e8907d336db2e90d0252/uv-0.9.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e2cd3e885e4c30048f9c2c526bd340f6e082ca5fb6bf4516c90671a114746fc3", size = 20406081, upload-time = "2025-12-01T17:22:23.66Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ce/e7e27f7891e38c98f5c83b3c8068c6265f5dc96c12924f2a0fc31b4eb7ac/uv-0.9.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:da227183ab9860832533e7f152a83d0d749f8d0156348b68f48773d42f690ff1", size = 20965537, upload-time = "2025-12-01T17:22:16.582Z" },
+ { url = "https://files.pythonhosted.org/packages/71/44/b9cdb4137338b33a419ff4aff70ac00df4a5a68e1b9bd21a59f96caf6c6f/uv-0.9.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:70a55f189b2d9ec035194c927f2c0b4f746b251e329a5dc8391ab6a41fe14e1a", size = 21919764, upload-time = "2025-12-01T17:22:37.369Z" },
+ { url = "https://files.pythonhosted.org/packages/66/57/2e294c6d758b48883434ad979e089cfe5ec87584ec7ffee005be359f6035/uv-0.9.14-py3-none-win32.whl", hash = "sha256:06923d5ee88b50dabb364c4fcc2a0de84e079b6a2fb6cc6ca318e74e979affed", size = 19742562, upload-time = "2025-12-01T17:22:49.106Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2f/81d551db61228adb062ff29dec7634d82091e38f579d56ed27db40bd300e/uv-0.9.14-py3-none-win_amd64.whl", hash = "sha256:c0f18fd246726cdc194357aca50fd13153d719daecd765049f0ff4c2262143d3", size = 21655524, upload-time = "2025-12-01T17:22:39.643Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/91/deb722a8ddb076018aee02ab3bffcdda6f10b7ca96f72aeca06b5efaccec/uv-0.9.14-py3-none-win_arm64.whl", hash = "sha256:d974fcbec84aa7eb4ee1cc7e650a5b8973895a03f6d6f0c61b488e1d1b8179ea", size = 20121260, upload-time = "2025-12-01T17:22:44.502Z" },
]
[[package]]
@@ -6481,14 +6653,14 @@ wheels = [
[[package]]
name = "werkzeug"
-version = "3.1.3"
+version = "3.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" },
]
[[package]]