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]]