"
+export OPENAI_MODEL="gpt-4.1-mini"
+```
+
+## Running the Sample
+
+### Option 1: Docker Compose (Recommended)
+
+```bash
+cd dotnet/samples/05-end-to-end/AspNetAgentAuthorization
+docker compose up
+```
+
+This starts Keycloak, the AgentService, and the WebClient. Wait for Keycloak to finish importing the realm (you'll see `Running the server` in the logs).
+
+#### Running in GitHub Codespaces
+
+This sample has been built in such a way that it can be run from GitHub Codespaces.
+The Agent Framework repository has a C# specific dev container, named "C# (.NET)", that is configured for Codespaces.
+
+When running in Codespaces, the sample auto-detects the environment via
+`CODESPACE_NAME` and `GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN` and configures
+Keycloak and the web client accordingly. Just make the required ports public:
+
+```bash
+# Make Keycloak and WebClient ports publicly accessible
+gh codespace ports visibility 5002:public 8080:public -c $CODESPACE_NAME
+
+# Start the containers (Codespaces is auto-detected)
+docker compose up
+```
+
+Then open the Codespaces-forwarded URL for port 8080 (shown in the **Ports** tab) in your browser.
+
+### Option 2: Run Locally
+
+1. Start Keycloak:
+ ```bash
+ docker compose up keycloak
+ ```
+
+2. In a new terminal, start the AgentService:
+ ```bash
+ cd Service
+ dotnet run --urls "http://localhost:5001"
+ ```
+
+3. In another terminal, start the WebClient:
+ ```bash
+ cd RazorWebClient
+ dotnet run --urls "http://localhost:8080"
+ ```
+
+## Using the Sample
+
+1. Open `http://localhost:8080` in your browser
+2. Click **Login** — you'll be redirected to Keycloak
+3. Sign in with one of the pre-configured users:
+ - **`testuser` / `password`** — can chat, view expenses, and approve expenses (up to €1,000)
+ - **`viewer` / `password`** — can chat and view expenses, but **cannot approve** them
+4. Try asking the agent:
+ - _"Show me the pending expenses"_ — both users can do this
+ - _"Approve expense #1"_ — only `testuser` can do this; `viewer` will be denied
+ - _"Approve expense #3"_ — even `testuser` will be denied (€4,500 exceeds the €1,000 limit)
+
+## Pre-Configured Keycloak Realm
+
+The `keycloak/dev-realm.json` file auto-provisions:
+
+| Resource | Details |
+|----------|---------|
+| **Realm** | `dev` |
+| **Client: agent-service** | Confidential client (the API audience) |
+| **Client: web-client** | Public client for the Razor app's OIDC login |
+| **Scope: agent.chat** | Required to call the `/chat` endpoint |
+| **Scope: expenses.view** | Required to list pending expenses |
+| **Scope: expenses.approve** | Required to approve expenses |
+| **User: testuser** | Has `agent.chat`, `expenses.view`, and `expenses.approve` scopes |
+| **User: viewer** | Has `agent.chat` and `expenses.view` scopes (no approval) |
+
+### Pre-Seeded Expenses
+
+The service starts with five demo expenses:
+
+| # | Description | Amount | Status |
+|---|-------------|--------|--------|
+| 1 | Conference travel — Berlin | €850 | Pending |
+| 2 | Team dinner — Q4 celebration | €320 | Pending |
+| 3 | Cloud infrastructure — annual renewal | €4,500 | Pending (over limit) |
+| 4 | Office supplies — ergonomic keyboards | €675 | Pending |
+| 5 | Client gift baskets — holiday season | €980 | Pending |
+
+Keycloak admin console: `http://localhost:5002` (login: `admin` / `admin`).
+
+## API Endpoints
+
+### POST /chat (requires `agent.chat` scope)
+
+```bash
+# Get a token for testuser
+TOKEN=$(curl -s -X POST http://localhost:5002/realms/dev/protocol/openid-connect/token \
+ -d "grant_type=password&client_id=web-client&username=testuser&password=password&scope=openid agent.chat expenses.view expenses.approve" \
+ | jq -r '.access_token')
+
+# Chat with the agent
+curl -X POST http://localhost:5001/chat \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"message": "Show me the pending expenses"}'
+```
+
+## Key Concepts Demonstrated
+
+- **Endpoint-Level Authorization** — The `/chat` endpoint requires the `agent.chat` scope, gating access to the agent itself
+- **Tool-Level Authorization** — Each agent tool checks its own scope (`expenses.view`, `expenses.approve`) at runtime, so different users have different capabilities within the same chat session
+- **Scope-Based Role Mapping** — Keycloak realm roles map to OAuth scopes, allowing administrators to control which users can access which agent capabilities
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Dockerfile b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Dockerfile
new file mode 100644
index 0000000000..8e15ba2425
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Dockerfile
@@ -0,0 +1,29 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /repo
+
+# Copy solution-level files for restore
+COPY Directory.Build.props Directory.Build.targets Directory.Packages.props global.json nuget.config ./
+COPY eng/ eng/
+COPY src/Shared/ src/Shared/
+COPY samples/Directory.Build.props samples/
+
+# Create sentinel file so $(RepoRoot) resolves correctly inside the container.
+# RepoRoot is the parent of the dir containing CODE_OF_CONDUCT.md,
+# and src projects import $(RepoRoot)/dotnet/nuget/nuget-package.props.
+RUN touch /CODE_OF_CONDUCT.md
+
+# Copy project file for restore
+COPY samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/
+
+RUN dotnet restore samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj -p:TargetFramework=net10.0 -p:TreatWarningsAsErrors=false
+
+# Copy everything and build
+COPY samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/ samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/
+RUN dotnet publish samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj -c Release -f net10.0 -o /app -p:TreatWarningsAsErrors=false
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0
+WORKDIR /app
+COPY --from=build /app .
+ENV ASPNETCORE_URLS=http://+:8080
+EXPOSE 8080
+ENTRYPOINT ["dotnet", "RazorWebClient.dll"]
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Chat.cshtml b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Chat.cshtml
new file mode 100644
index 0000000000..edccf4c34e
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Chat.cshtml
@@ -0,0 +1,35 @@
+@page
+@using Microsoft.AspNetCore.Authorization
+@attribute [Authorize]
+@model AspNetAgentAuthorization.RazorWebClient.Pages.ChatModel
+@{
+ Layout = "_Layout";
+}
+
+Chat with the Agent
+
+
+
+@if (Model.Error is not null)
+{
+
+ Error: @Model.Error
+
+}
+
+@if (Model.Reply is not null)
+{
+
+
Agent (responding to @Model.ReplyUser):
+
@Model.Reply
+
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Chat.cshtml.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Chat.cshtml.cs
new file mode 100644
index 0000000000..5326e7ae9d
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Chat.cshtml.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace AspNetAgentAuthorization.RazorWebClient.Pages;
+
+public class ChatModel : PageModel
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ public ChatModel(IHttpClientFactory httpClientFactory)
+ {
+ this._httpClientFactory = httpClientFactory;
+ }
+
+ [BindProperty]
+ public string? Message { get; set; }
+
+ public string? Reply { get; set; }
+ public string? ReplyUser { get; set; }
+ public string? Error { get; set; }
+
+ public void OnGet()
+ {
+ }
+
+ public async Task OnPostAsync()
+ {
+ if (string.IsNullOrWhiteSpace(this.Message))
+ {
+ return;
+ }
+
+ try
+ {
+ // Get the access token stored during OIDC login
+ string? accessToken = await this.HttpContext.GetTokenAsync("access_token");
+ if (accessToken is null)
+ {
+ this.Error = "No access token available. Please log in again.";
+ return;
+ }
+
+ // Call the AgentService with the Bearer token
+ var client = this._httpClientFactory.CreateClient("AgentService");
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
+
+ var payload = JsonSerializer.Serialize(new { message = this.Message });
+ var content = new StringContent(payload, Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync(new Uri("/chat", UriKind.Relative), content);
+
+ if (response.IsSuccessStatusCode)
+ {
+ using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ this.Reply = json.RootElement.GetProperty("reply").GetString();
+ this.ReplyUser = json.RootElement.GetProperty("user").GetString();
+ }
+ else
+ {
+ this.Error = response.StatusCode switch
+ {
+ System.Net.HttpStatusCode.Unauthorized => "Authentication failed (401). Your session may have expired.",
+ System.Net.HttpStatusCode.Forbidden => "Access denied (403). Your account does not have the required 'agent.chat' scope.",
+ _ => $"AgentService returned {(int)response.StatusCode} {response.ReasonPhrase}."
+ };
+ }
+ }
+ catch (Exception ex)
+ {
+ this.Error = $"Failed to contact the AgentService: {ex.Message}";
+ }
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Index.cshtml b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Index.cshtml
new file mode 100644
index 0000000000..ab1d7cb1dc
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Index.cshtml
@@ -0,0 +1,18 @@
+@page
+@model AspNetAgentAuthorization.RazorWebClient.Pages.IndexModel
+@{
+ Layout = "_Layout";
+}
+
+Welcome
+This sample demonstrates securing an AI agent API with OAuth 2.0 / OpenID Connect.
+
+@if (User.Identity?.IsAuthenticated == true)
+{
+ You are logged in as @User.Identity.Name.
+ Go to Chat →
+}
+else
+{
+ Please log in to chat with the agent.
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Index.cshtml.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Index.cshtml.cs
new file mode 100644
index 0000000000..2547fb6fce
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Index.cshtml.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace AspNetAgentAuthorization.RazorWebClient.Pages;
+
+public class IndexModel : PageModel
+{
+ public void OnGet()
+ {
+ }
+
+ public IActionResult OnGetLogout()
+ {
+ return this.SignOut(
+ new AuthenticationProperties { RedirectUri = "/" },
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ OpenIdConnectDefaults.AuthenticationScheme);
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Shared/_Layout.cshtml b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Shared/_Layout.cshtml
new file mode 100644
index 0000000000..c44e993624
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/Shared/_Layout.cshtml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Auth Agent Chat
+
+
+
+
+
+ @RenderBody()
+
+
+
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/_ViewImports.cshtml b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/_ViewImports.cshtml
new file mode 100644
index 0000000000..71c71463de
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Pages/_ViewImports.cshtml
@@ -0,0 +1,3 @@
+@using Microsoft.AspNetCore.Authentication
+@namespace AspNetAgentAuthorization.RazorWebClient.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Program.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Program.cs
new file mode 100644
index 0000000000..67fb3063e6
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Program.cs
@@ -0,0 +1,142 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates an OIDC-authenticated Razor Pages web client
+// that calls a JWT-secured AI agent REST API.
+
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddRazorPages();
+
+// Persist data protection keys so antiforgery tokens survive container rebuilds
+builder.Services.AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo("/app/keys"));
+
+// ---------------------------------------------------------------------------
+// Authentication: Cookie + OpenID Connect (Keycloak)
+// ---------------------------------------------------------------------------
+string authority = builder.Configuration["Auth:Authority"]
+ ?? throw new InvalidOperationException("Auth:Authority is not configured.");
+
+// PublicKeycloakUrl is the browser-facing Keycloak base URL. When the
+// web-client runs inside Docker, Authority points to the internal hostname
+// (e.g. http://keycloak:8080) for backchannel discovery, while
+// PublicKeycloakUrl is what the browser can reach (e.g. http://localhost:5002).
+// When running outside Docker, Authority already IS the public URL and
+// PublicKeycloakUrl is not needed.
+string? publicKeycloakUrl = builder.Configuration["Auth:PublicKeycloakUrl"];
+
+// In Codespaces, override the public URLs with the tunnel endpoints.
+string? codespaceName = Environment.GetEnvironmentVariable("CODESPACE_NAME");
+string? codespaceDomain = Environment.GetEnvironmentVariable("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN");
+bool isCodespaces = !string.IsNullOrEmpty(codespaceName) && !string.IsNullOrEmpty(codespaceDomain);
+if (isCodespaces)
+{
+ publicKeycloakUrl = $"https://{codespaceName}-5002.{codespaceDomain}";
+}
+
+// Derive the internal base URL from Authority for URL rewriting.
+string internalKeycloakBase = new Uri(authority).GetLeftPart(UriPartial.Authority);
+
+builder.Services
+ .AddAuthentication(options =>
+ {
+ options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddOpenIdConnect(options =>
+ {
+ options.Authority = authority;
+ options.ClientId = builder.Configuration["Auth:ClientId"]
+ ?? throw new InvalidOperationException("Auth:ClientId is not configured.");
+
+ options.ResponseType = OpenIdConnectResponseType.Code;
+ options.SaveTokens = true;
+ options.GetClaimsFromUserInfoEndpoint = true;
+
+ // Request scopes so the access token includes them
+ options.Scope.Clear();
+ options.Scope.Add("openid");
+ options.Scope.Add("profile");
+ options.Scope.Add("email");
+ options.Scope.Add("agent.chat");
+ options.Scope.Add("expenses.view");
+ options.Scope.Add("expenses.approve");
+
+ // For local development with HTTP-only Keycloak
+ options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
+
+ // When the web-client is inside Docker, the backchannel Authority uses
+ // an internal hostname that differs from the browser-facing URL.
+ // Rewrite the authorization/logout endpoints so the browser is
+ // redirected to the public Keycloak URL, and disable issuer validation
+ // because the token issuer (public URL) won't match the discovery
+ // document issuer (internal URL).
+ if (publicKeycloakUrl is not null)
+ {
+#pragma warning disable CA5404 // Token issuer validation disabled: backchannel uses internal Docker hostname while tokens are issued via the public URL.
+ options.TokenValidationParameters.ValidateIssuer = false;
+#pragma warning restore CA5404
+
+ // The UserInfo endpoint is on the internal URL but the token
+ // issuer is the public URL — Keycloak rejects the mismatch.
+ // The ID token already contains all needed claims.
+ options.GetClaimsFromUserInfoEndpoint = false;
+
+ // In Codespaces the tunnel delivers with Host: localhost, so the
+ // auto-generated redirect_uri is wrong. Override it explicitly.
+ string? publicWebClientBase = isCodespaces
+ ? $"https://{codespaceName}-8080.{codespaceDomain}"
+ : null;
+
+ options.Events = new OpenIdConnectEvents
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress
+ .Replace(internalKeycloakBase, publicKeycloakUrl);
+ if (publicWebClientBase is not null)
+ {
+ context.ProtocolMessage.RedirectUri = $"{publicWebClientBase}/signin-oidc";
+ }
+
+ return Task.CompletedTask;
+ },
+ OnRedirectToIdentityProviderForSignOut = context =>
+ {
+ context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress
+ .Replace(internalKeycloakBase, publicKeycloakUrl);
+ if (publicWebClientBase is not null)
+ {
+ context.ProtocolMessage.PostLogoutRedirectUri = $"{publicWebClientBase}/signout-callback-oidc";
+ }
+
+ return Task.CompletedTask;
+ },
+ };
+ }
+ });
+
+// ---------------------------------------------------------------------------
+// HttpClient for calling the AgentService — attaches Bearer token
+// ---------------------------------------------------------------------------
+builder.Services.AddHttpClient("AgentService", client =>
+{
+ string baseUrl = builder.Configuration["AgentService:BaseUrl"] ?? "http://localhost:5001";
+ client.BaseAddress = new Uri(baseUrl);
+});
+
+WebApplication app = builder.Build();
+
+app.UseStaticFiles();
+app.UseRouting();
+app.UseAuthentication();
+app.UseAuthorization();
+app.MapRazorPages();
+
+await app.RunAsync();
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Properties/launchSettings.json b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Properties/launchSettings.json
new file mode 100644
index 0000000000..28c3cf0be6
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "RazorWebClient": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:58080;http://localhost:8080"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj
new file mode 100644
index 0000000000..d1c7fec19a
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ $(NoWarn);CS1591
+
+
+
+
+
+
+
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/appsettings.json b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/appsettings.json
new file mode 100644
index 0000000000..5372dad530
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Auth": {
+ "Authority": "http://localhost:5002/realms/dev",
+ "ClientId": "web-client"
+ },
+ "AgentService": {
+ "BaseUrl": "http://localhost:5001"
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Dockerfile b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Dockerfile
new file mode 100644
index 0000000000..69517af95d
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Dockerfile
@@ -0,0 +1,34 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /repo
+
+# Copy solution-level files for restore
+COPY Directory.Build.props Directory.Build.targets Directory.Packages.props global.json nuget.config ./
+COPY eng/ eng/
+COPY nuget/ nuget/
+COPY src/Shared/ src/Shared/
+COPY samples/Directory.Build.props samples/
+
+# Create sentinel file so $(RepoRoot) resolves correctly inside the container.
+# RepoRoot is the parent of the dir containing CODE_OF_CONDUCT.md,
+# and src projects import $(RepoRoot)/dotnet/nuget/nuget-package.props.
+RUN touch /CODE_OF_CONDUCT.md && mkdir -p /dotnet/nuget && cp /repo/nuget/* /dotnet/nuget/
+
+# Copy project files for restore
+COPY src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj src/Microsoft.Agents.AI.Abstractions/
+COPY src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj src/Microsoft.Agents.AI/
+COPY src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj src/Microsoft.Agents.AI.OpenAI/
+COPY samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj samples/05-end-to-end/AspNetAgentAuthorization/Service/
+
+RUN dotnet restore samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj -p:TargetFramework=net10.0 -p:TreatWarningsAsErrors=false
+
+# Copy everything and build
+COPY src/ src/
+COPY samples/05-end-to-end/AspNetAgentAuthorization/Service/ samples/05-end-to-end/AspNetAgentAuthorization/Service/
+RUN dotnet publish samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj -c Release -f net10.0 -o /app -p:TreatWarningsAsErrors=false
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0
+WORKDIR /app
+COPY --from=build /app .
+ENV ASPNETCORE_URLS=http://+:5001
+EXPOSE 5001
+ENTRYPOINT ["dotnet", "Service.dll"]
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/ExpenseService.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/ExpenseService.cs
new file mode 100644
index 0000000000..d02ab8d409
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/ExpenseService.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Concurrent;
+using System.ComponentModel;
+
+namespace AspNetAgentAuthorization.Service;
+
+///
+/// Represents an expense awaiting approval.
+///
+public sealed class Expense
+{
+ public int Id { get; init; }
+
+ public string Description { get; init; } = string.Empty;
+
+ public decimal Amount { get; init; }
+
+ public string Submitter { get; init; } = string.Empty;
+
+ public string Status { get; set; } = "Pending";
+
+ public string? ApprovedBy { get; set; }
+}
+
+///
+/// Manages expense approvals. Pre-seeded with demo data so there are
+/// expenses to review immediately. Uses to
+/// identify the caller and enforce scope-based permissions.
+///
+public sealed class ExpenseService
+{
+ /// Maximum amount (EUR) that can be approved.
+ private const decimal ApprovalLimit = 1000m;
+
+ private static readonly ConcurrentDictionary s_expenses = new(
+ new Dictionary
+ {
+ [1] = new() { Id = 1, Description = "Conference travel — Berlin", Amount = 850m, Submitter = "Alice" },
+ [2] = new() { Id = 2, Description = "Team dinner — Q4 celebration", Amount = 320m, Submitter = "Bob" },
+ [3] = new() { Id = 3, Description = "Cloud infrastructure — annual renewal", Amount = 4500m, Submitter = "Carol" },
+ [4] = new() { Id = 4, Description = "Office supplies — ergonomic keyboards", Amount = 675m, Submitter = "Dave" },
+ [5] = new() { Id = 5, Description = "Client gift baskets — holiday season", Amount = 980m, Submitter = "Eve" },
+ });
+
+ private readonly IUserContext _userContext;
+
+ public ExpenseService(IUserContext userContext)
+ {
+ this._userContext = userContext;
+ }
+
+ ///
+ /// Lists all pending expenses awaiting approval.
+ ///
+ [Description("Lists all pending expenses awaiting approval. Requires the expenses.view scope.")]
+ public string ListPendingExpenses()
+ {
+ if (!this._userContext.Scopes.Contains("expenses.view"))
+ {
+ return "Access denied. You do not have the expenses.view scope.";
+ }
+
+ var pending = s_expenses.Values
+ .Where(e => e.Status == "Pending")
+ .OrderBy(e => e.Id)
+ .ToList();
+
+ if (pending.Count == 0)
+ {
+ return "No pending expenses.";
+ }
+
+ return string.Join("\n", pending.Select(e =>
+ $"#{e.Id}: {e.Description} — €{e.Amount:N2} (submitted by {e.Submitter})"));
+ }
+
+ ///
+ /// Approves a pending expense by its ID.
+ ///
+ [Description("Approves a pending expense by its ID. Requires the expenses.approve scope.")]
+ public string ApproveExpense([Description("The ID of the expense to approve")] int expenseId)
+ {
+ if (!this._userContext.Scopes.Contains("expenses.approve"))
+ {
+ return "Access denied. You do not have the expenses.approve scope.";
+ }
+
+ if (!s_expenses.TryGetValue(expenseId, out var expense))
+ {
+ return $"Expense #{expenseId} not found.";
+ }
+
+ if (expense.Status != "Pending")
+ {
+ return $"Expense #{expenseId} has already been approved.";
+ }
+
+ if (expense.Amount > ApprovalLimit)
+ {
+ return $"Cannot approve expense #{expenseId} (€{expense.Amount:N2}). " +
+ $"Amount exceeds the €{ApprovalLimit:N2} approval limit.";
+ }
+
+ expense.Status = "Approved";
+ expense.ApprovedBy = this._userContext.DisplayName;
+
+ return $"Expense #{expenseId} (\"{expense.Description}\", €{expense.Amount:N2}) has been approved.";
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs
new file mode 100644
index 0000000000..b4a5d00a9a
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to authorize AI agent tools using OAuth 2.0
+// scopes. The /chat endpoint requires the "agent.chat" scope, and each tool
+// checks its own scope (expenses.view, expenses.approve) at runtime.
+
+using System.Security.Claims;
+using System.Text.Json.Serialization;
+using AspNetAgentAuthorization.Service;
+using Microsoft.Agents.AI;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.Extensions.AI;
+using OpenAI;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+// ---------------------------------------------------------------------------
+// Authentication: JWT Bearer tokens validated against the OIDC provider
+// ---------------------------------------------------------------------------
+builder.Services
+ .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(options =>
+ {
+ options.Authority = builder.Configuration["Auth:Authority"]
+ ?? throw new InvalidOperationException("Auth:Authority is not configured.");
+ options.Audience = builder.Configuration["Auth:Audience"]
+ ?? throw new InvalidOperationException("Auth:Audience is not configured.");
+
+ // For local development with HTTP-only Keycloak
+ options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
+
+ options.TokenValidationParameters.ValidateAudience = true;
+ options.TokenValidationParameters.ValidateLifetime = true;
+
+ // In Codespaces, tokens are issued with the public tunnel URL as
+ // issuer (Keycloak sees X-Forwarded-Host from the tunnel) but the
+ // agent-service discovers Keycloak via the internal Docker hostname.
+ // Disable issuer validation in development to handle this mismatch.
+ options.TokenValidationParameters.ValidateIssuer = !builder.Environment.IsDevelopment();
+ });
+
+// ---------------------------------------------------------------------------
+// Authorization: policy requiring the "agent.chat" scope
+// ---------------------------------------------------------------------------
+builder.Services.AddAuthorizationBuilder()
+ .AddPolicy("AgentChat", policy =>
+ policy.RequireAuthenticatedUser()
+ .RequireAssertion(context =>
+ {
+ // Keycloak puts scopes in the "scope" claim (space-delimited)
+ var scopeClaim = context.User.FindFirstValue("scope");
+ if (scopeClaim is not null)
+ {
+ var scopes = scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (scopes.Contains("agent.chat", StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }));
+
+// ---------------------------------------------------------------------------
+// Configure JSON serialization
+// ---------------------------------------------------------------------------
+builder.Services.ConfigureHttpJsonOptions(options =>
+ options.SerializerOptions.TypeInfoResolverChain.Add(SampleServiceSerializerContext.Default));
+
+// ---------------------------------------------------------------------------
+// Create the AI agent with expense approval tools, registered in DI
+// ---------------------------------------------------------------------------
+string apiKey = builder.Configuration["OPENAI_API_KEY"]
+ ?? throw new InvalidOperationException("Set the OPENAI_API_KEY environment variable.");
+string model = builder.Configuration["OPENAI_MODEL"] ?? "gpt-4.1-mini";
+
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped(sp =>
+{
+ var expenseService = sp.GetRequiredService();
+
+ return new OpenAIClient(apiKey)
+ .GetChatClient(model)
+ .AsIChatClient()
+ .AsAIAgent(
+ name: "ExpenseApprovalAgent",
+ instructions: "You are an expense approval assistant. You can list pending expenses "
+ + "and approve them if the user has the required permissions and approval limit. "
+ + "Keep responses concise.",
+ tools:
+ [
+ AIFunctionFactory.Create(expenseService.ListPendingExpenses),
+ AIFunctionFactory.Create(expenseService.ApproveExpense),
+ ]);
+});
+
+WebApplication app = builder.Build();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+// ---------------------------------------------------------------------------
+// POST /chat — requires the "agent.chat" scope
+// ---------------------------------------------------------------------------
+app.MapPost("/chat", [Authorize(Policy = "AgentChat")] async (ChatRequest request, IUserContext userContext, AIAgent agent) =>
+{
+ var response = await agent.RunAsync(request.Message);
+
+ return Results.Ok(new ChatResponse(response.Text, userContext.DisplayName));
+});
+
+await app.RunAsync();
+
+// ---------------------------------------------------------------------------
+// Request / Response models
+// ---------------------------------------------------------------------------
+internal sealed record ChatRequest(string Message);
+internal sealed record ChatResponse(string Reply, string User);
+
+[JsonSerializable(typeof(ChatRequest))]
+[JsonSerializable(typeof(ChatResponse))]
+internal sealed partial class SampleServiceSerializerContext : JsonSerializerContext;
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Properties/launchSettings.json b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Properties/launchSettings.json
new file mode 100644
index 0000000000..6366505896
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "Service": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:55001;http://localhost:5001"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj
new file mode 100644
index 0000000000..40b91fcd86
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ $(NoWarn);CS1591
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs
new file mode 100644
index 0000000000..34f4fe8956
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Security.Claims;
+
+namespace AspNetAgentAuthorization.Service;
+
+///
+/// Provides the authenticated user's identity for the current request.
+///
+public interface IUserContext
+{
+ /// Unique identifier for the current user (e.g. the OIDC "sub" claim).
+ string UserId { get; }
+
+ /// Login name for the current user.
+ string UserName { get; }
+
+ /// Human-readable display name (e.g. "Test User").
+ string DisplayName { get; }
+
+ /// OAuth scopes granted in the current access token.
+ IReadOnlySet Scopes { get; }
+}
+
+///
+/// Resolves the current user's identity from Keycloak-specific JWT claims.
+/// Keycloak uses sub for the user ID, preferred_username
+/// for the login name, given_name/family_name for the
+/// display name, and scope (space-delimited) for granted scopes.
+/// Registered as a scoped service so it is resolved once per request.
+///
+public sealed class KeycloakUserContext : IUserContext
+{
+ public string UserId { get; }
+
+ public string UserName { get; }
+
+ public string DisplayName { get; }
+
+ public IReadOnlySet Scopes { get; }
+
+ public KeycloakUserContext(IHttpContextAccessor httpContextAccessor)
+ {
+ ClaimsPrincipal? user = httpContextAccessor.HttpContext?.User;
+
+ this.UserId = user?.FindFirstValue(ClaimTypes.NameIdentifier)
+ ?? user?.FindFirstValue("sub")
+ ?? "anonymous";
+
+ this.UserName = user?.FindFirstValue("preferred_username")
+ ?? user?.FindFirstValue(ClaimTypes.Name)
+ ?? "unknown";
+
+ string? givenName = user?.FindFirstValue("given_name") ?? user?.FindFirstValue(ClaimTypes.GivenName);
+ string? familyName = user?.FindFirstValue("family_name") ?? user?.FindFirstValue(ClaimTypes.Surname);
+ this.DisplayName = (givenName, familyName) switch
+ {
+ (not null, not null) => $"{givenName} {familyName}",
+ (not null, null) => givenName,
+ (null, not null) => familyName,
+ _ => this.UserName,
+ };
+
+ string? scopeClaim = user?.FindFirstValue("scope");
+ this.Scopes = scopeClaim is not null
+ ? new HashSet(scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase)
+ : new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/appsettings.json b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/appsettings.json
new file mode 100644
index 0000000000..c5275372ad
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Auth": {
+ "Authority": "http://localhost:5002/realms/dev",
+ "Audience": "agent-service"
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml
new file mode 100644
index 0000000000..eb9e356e72
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml
@@ -0,0 +1,80 @@
+services:
+ keycloak:
+ image: quay.io/keycloak/keycloak:latest
+ container_name: auth-keycloak
+ environment:
+ - KC_BOOTSTRAP_ADMIN_USERNAME=admin
+ - KC_BOOTSTRAP_ADMIN_PASSWORD=admin
+ - KC_HOSTNAME_STRICT=false
+ - KC_PROXY_HEADERS=xforwarded
+ volumes:
+ - ./keycloak/dev-realm.json:/opt/keycloak/data/import/dev-realm.json
+ command: ["start-dev", "--import-realm"]
+ ports:
+ - "5002:8080"
+ healthcheck:
+ test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /realms/master HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '200'"]
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 30s
+
+ # One-shot init container that registers the Codespaces redirect URI
+ # with Keycloak after it becomes healthy. Auto-detects Codespaces via
+ # CODESPACE_NAME and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN env vars.
+ keycloak-init:
+ image: curlimages/curl:latest
+ container_name: auth-keycloak-init
+ environment:
+ - KEYCLOAK_URL=http://keycloak:8080
+ - CODESPACE_NAME=${CODESPACE_NAME:-}
+ - GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN=${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:-}
+ volumes:
+ - ./keycloak/setup-redirect-uris.sh:/setup-redirect-uris.sh:ro
+ entrypoint: ["sh", "/setup-redirect-uris.sh"]
+ depends_on:
+ keycloak:
+ condition: service_healthy
+
+ agent-service:
+ build:
+ context: ../../..
+ dockerfile: samples/05-end-to-end/AspNetAgentAuthorization/Service/Dockerfile
+ container_name: auth-agent-service
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - Auth__Authority=http://keycloak:8080/realms/dev
+ - Auth__Audience=agent-service
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
+ - OPENAI_MODEL=${OPENAI_MODEL:-gpt-4.1-mini}
+ ports:
+ - "5001:5001"
+ depends_on:
+ keycloak:
+ condition: service_healthy
+
+ web-client:
+ build:
+ context: ../../..
+ dockerfile: samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/Dockerfile
+ container_name: auth-web-client
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - Auth__Authority=http://keycloak:8080/realms/dev
+ - Auth__PublicKeycloakUrl=http://localhost:5002
+ - Auth__ClientId=web-client
+ - AgentService__BaseUrl=http://agent-service:5001
+ - CODESPACE_NAME=${CODESPACE_NAME:-}
+ - GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN=${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:-}
+ ports:
+ - "8080:8080"
+ volumes:
+ - web-client-keys:/app/keys
+ depends_on:
+ keycloak:
+ condition: service_healthy
+ agent-service:
+ condition: service_started
+
+volumes:
+ web-client-keys:
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/dev-realm.json b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/dev-realm.json
new file mode 100644
index 0000000000..41e8ce3038
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/dev-realm.json
@@ -0,0 +1,232 @@
+{
+ "realm": "dev",
+ "enabled": true,
+ "sslRequired": "none",
+ "registrationAllowed": false,
+ "roles": {
+ "realm": [
+ {
+ "name": "agent-chat-user",
+ "description": "Grants access to the agent.chat scope"
+ },
+ {
+ "name": "expenses-viewer",
+ "description": "Grants access to the expenses.view scope"
+ },
+ {
+ "name": "expenses-approver",
+ "description": "Grants access to the expenses.approve scope"
+ }
+ ]
+ },
+ "scopeMappings": [
+ {
+ "clientScope": "agent.chat",
+ "roles": ["agent-chat-user"]
+ },
+ {
+ "clientScope": "expenses.view",
+ "roles": ["expenses-viewer"]
+ },
+ {
+ "clientScope": "expenses.approve",
+ "roles": ["expenses-approver"]
+ }
+ ],
+ "clientScopes": [
+ {
+ "name": "openid",
+ "description": "OpenID Connect scope",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true"
+ },
+ "protocolMappers": [
+ {
+ "name": "sub",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-sub-mapper",
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "name": "profile",
+ "description": "OpenID Connect profile scope",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true"
+ },
+ "protocolMappers": [
+ {
+ "name": "preferred_username",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "config": {
+ "user.attribute": "username",
+ "claim.name": "preferred_username",
+ "jsonType.label": "String",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "name": "given_name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "config": {
+ "user.attribute": "firstName",
+ "claim.name": "given_name",
+ "jsonType.label": "String",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "name": "family_name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "config": {
+ "user.attribute": "lastName",
+ "claim.name": "family_name",
+ "jsonType.label": "String",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "name": "email",
+ "description": "OpenID Connect email scope",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true"
+ }
+ },
+ {
+ "name": "agent.chat",
+ "description": "Allows chatting with the agent",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true"
+ }
+ },
+ {
+ "name": "expenses.view",
+ "description": "Allows viewing pending expenses",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true"
+ }
+ },
+ {
+ "name": "expenses.approve",
+ "description": "Allows approving pending expenses",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "true"
+ }
+ },
+ {
+ "name": "agent-service-audience",
+ "description": "Adds the agent-service audience to access tokens",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "name": "agent-service-audience-mapper",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-mapper",
+ "config": {
+ "included.client.audience": "agent-service",
+ "id.token.claim": "false",
+ "access.token.claim": "true"
+ }
+ }
+ ]
+ }
+ ],
+ "clients": [
+ {
+ "clientId": "agent-service",
+ "enabled": true,
+ "publicClient": false,
+ "secret": "agent-service-secret",
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "standardFlowEnabled": false,
+ "protocol": "openid-connect"
+ },
+ {
+ "clientId": "web-client",
+ "enabled": true,
+ "publicClient": true,
+ "directAccessGrantsEnabled": true,
+ "standardFlowEnabled": true,
+ "fullScopeAllowed": false,
+ "protocol": "openid-connect",
+ "redirectUris": [
+ "http://localhost:8080/*"
+ ],
+ "webOrigins": [
+ "http://localhost:8080"
+ ],
+ "defaultClientScopes": [
+ "openid",
+ "profile",
+ "email",
+ "agent-service-audience"
+ ],
+ "optionalClientScopes": [
+ "agent.chat",
+ "expenses.view",
+ "expenses.approve"
+ ]
+ }
+ ],
+ "users": [
+ {
+ "username": "testuser",
+ "enabled": true,
+ "email": "testuser@example.com",
+ "firstName": "Test",
+ "lastName": "User",
+ "realmRoles": ["agent-chat-user", "expenses-viewer", "expenses-approver"],
+ "credentials": [
+ {
+ "type": "password",
+ "value": "password",
+ "temporary": false
+ }
+ ]
+ },
+ {
+ "username": "viewer",
+ "enabled": true,
+ "email": "viewer@example.com",
+ "firstName": "View",
+ "lastName": "Only",
+ "realmRoles": ["agent-chat-user", "expenses-viewer"],
+ "credentials": [
+ {
+ "type": "password",
+ "value": "password",
+ "temporary": false
+ }
+ ]
+ }
+ ]
+}
diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/setup-redirect-uris.sh b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/setup-redirect-uris.sh
new file mode 100755
index 0000000000..b49cfc4e80
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/keycloak/setup-redirect-uris.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+# Adds an extra redirect URI to the Keycloak web-client configuration.
+# Auto-detects GitHub Codespaces via CODESPACE_NAME and
+# GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN environment variables.
+
+set -e
+
+KEYCLOAK_URL="${KEYCLOAK_URL:-http://keycloak:8080}"
+
+# Auto-detect Codespaces
+if [ -n "$CODESPACE_NAME" ] && [ -n "$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" ]; then
+ WEBCLIENT_PUBLIC_URL="https://${CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
+fi
+
+if [ -z "$WEBCLIENT_PUBLIC_URL" ]; then
+ echo "Not running in Codespaces — skipping redirect URI setup."
+ exit 0
+fi
+
+echo "Configuring Keycloak redirect URIs for: $WEBCLIENT_PUBLIC_URL"
+
+# Get admin token
+TOKEN=$(curl -sf -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
+ -d "grant_type=password&client_id=admin-cli&username=admin&password=admin" \
+ | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
+
+if [ -z "$TOKEN" ]; then
+ echo "ERROR: Failed to get admin token" >&2
+ exit 1
+fi
+
+# Get web-client UUID
+CLIENT_UUID=$(curl -sf "$KEYCLOAK_URL/admin/realms/dev/clients?clientId=web-client" \
+ -H "Authorization: Bearer $TOKEN" \
+ | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
+
+if [ -z "$CLIENT_UUID" ]; then
+ echo "ERROR: Failed to find web-client UUID" >&2
+ exit 1
+fi
+# Update redirect URIs and web origins
+curl -sf -X PUT "$KEYCLOAK_URL/admin/realms/dev/clients/$CLIENT_UUID" \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"redirectUris\": [\"http://localhost:8080/*\", \"${WEBCLIENT_PUBLIC_URL}/*\"],
+ \"webOrigins\": [\"http://localhost:8080\", \"${WEBCLIENT_PUBLIC_URL}\"]
+ }"
+
+echo "Keycloak redirect URIs updated successfully."
From d932947ba5a692a34643e67acfacd7d63601e637 Mon Sep 17 00:00:00 2001
From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Date: Mon, 2 Mar 2026 11:29:32 -0800
Subject: [PATCH 22/36] Add Name and Description support for GroupChat workflow
builder (#4334)
---
.../03_AgentWorkflowPatterns/Program.cs | 2 +
.../GroupChatWorkflowBuilder.cs | 34 ++++++++++++++
.../AgentWorkflowBuilderTests.cs | 44 +++++++++++++++++++
3 files changed, 80 insertions(+)
diff --git a/dotnet/samples/03-workflows/_StartHere/03_AgentWorkflowPatterns/Program.cs b/dotnet/samples/03-workflows/_StartHere/03_AgentWorkflowPatterns/Program.cs
index ae8208e964..a562226740 100644
--- a/dotnet/samples/03-workflows/_StartHere/03_AgentWorkflowPatterns/Program.cs
+++ b/dotnet/samples/03-workflows/_StartHere/03_AgentWorkflowPatterns/Program.cs
@@ -72,6 +72,8 @@ public static class Program
await RunWorkflowAsync(
AgentWorkflowBuilder.CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 5 })
.AddParticipants(from lang in (string[])["French", "Spanish", "English"] select GetTranslationAgent(lang, client))
+ .WithName("Translation Round Robin Workflow")
+ .WithDescription("A workflow where three translation agents take turns responding in a round-robin fashion.")
.Build(),
[new(ChatRole.User, "Hello, world!")]);
break;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs
index 79a7b35498..66e4429e35 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs
@@ -16,6 +16,8 @@ public sealed class GroupChatWorkflowBuilder
{
private readonly Func, GroupChatManager> _managerFactory;
private readonly HashSet _participants = new(AIAgentIDEqualityComparer.Instance);
+ private string _name = string.Empty;
+ private string _description = string.Empty;
internal GroupChatWorkflowBuilder(Func, GroupChatManager> managerFactory) =>
this._managerFactory = managerFactory;
@@ -42,6 +44,28 @@ public sealed class GroupChatWorkflowBuilder
return this;
}
+ ///
+ /// Sets the human-readable name for the workflow.
+ ///
+ /// The name of the workflow.
+ /// This instance of the .
+ public GroupChatWorkflowBuilder WithName(string name)
+ {
+ this._name = name;
+ return this;
+ }
+
+ ///
+ /// Sets the description for the workflow.
+ ///
+ /// The description of what the workflow does.
+ /// This instance of the .
+ public GroupChatWorkflowBuilder WithDescription(string description)
+ {
+ this._description = description;
+ return this;
+ }
+
///
/// Builds a composed of agents that operate via group chat, with the next
/// agent to process messages selected by the group chat manager.
@@ -65,6 +89,16 @@ public sealed class GroupChatWorkflowBuilder
ExecutorBinding host = groupChatHostFactory.BindExecutor(nameof(GroupChatHost));
WorkflowBuilder builder = new(host);
+ if (!string.IsNullOrEmpty(this._name))
+ {
+ builder = builder.WithName(this._name);
+ }
+
+ if (!string.IsNullOrEmpty(this._description))
+ {
+ builder = builder.WithDescription(this._description);
+ }
+
foreach (var participant in agentMap.Values)
{
builder
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs
index 01ce7c3441..77d8d0a88d 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs
@@ -88,6 +88,50 @@ public class AgentWorkflowBuilderTests
Assert.Equal(int.MaxValue, manager.MaximumIterationCount);
}
+ [Fact]
+ public void BuildGroupChat_WithNameAndDescription_SetsWorkflowNameAndDescription()
+ {
+ const string WorkflowName = "Test Group Chat";
+ const string WorkflowDescription = "A test group chat workflow";
+
+ var workflow = AgentWorkflowBuilder
+ .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 })
+ .AddParticipants(new DoubleEchoAgent("agent1"), new DoubleEchoAgent("agent2"))
+ .WithName(WorkflowName)
+ .WithDescription(WorkflowDescription)
+ .Build();
+
+ Assert.Equal(WorkflowName, workflow.Name);
+ Assert.Equal(WorkflowDescription, workflow.Description);
+ }
+
+ [Fact]
+ public void BuildGroupChat_WithNameOnly_SetsWorkflowName()
+ {
+ const string WorkflowName = "Named Group Chat";
+
+ var workflow = AgentWorkflowBuilder
+ .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 })
+ .AddParticipants(new DoubleEchoAgent("agent1"))
+ .WithName(WorkflowName)
+ .Build();
+
+ Assert.Equal(WorkflowName, workflow.Name);
+ Assert.Null(workflow.Description);
+ }
+
+ [Fact]
+ public void BuildGroupChat_WithoutNameOrDescription_DefaultsToNull()
+ {
+ var workflow = AgentWorkflowBuilder
+ .CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 2 })
+ .AddParticipants(new DoubleEchoAgent("agent1"))
+ .Build();
+
+ Assert.Null(workflow.Name);
+ Assert.Null(workflow.Description);
+ }
+
[Theory]
[InlineData(1)]
[InlineData(2)]
From 3b4eed270fc070166f5e38610609df0a01dcc373 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Mon, 2 Mar 2026 19:30:32 +0000
Subject: [PATCH 23/36] .NET: Skip OffThread observability test (#4399)
* Skip flaky OffThread observability test
Temporarily skip CreatesWorkflowEndToEndActivities_WithCorrectName_OffThreadAsync
due to intermittent failures. Tracked in #4398.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../ObservabilityTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs
index af8a9d8e0d..be45f55104 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs
@@ -139,7 +139,7 @@ public sealed class ObservabilityTests : IDisposable
await this.TestWorkflowEndToEndActivitiesAsync("Default");
}
- [Fact]
+ [Fact(Skip = "Flaky test - temporarily disabled. Tracked in #12345")]
public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_OffThreadAsync()
{
await this.TestWorkflowEndToEndActivitiesAsync("OffThread");
From a442ee115dc9f27bb39cb0cfc7ce5cd8be37e931 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Mon, 2 Mar 2026 22:12:55 +0000
Subject: [PATCH 24/36] .NET: AzureAI Package - Skip tool validation when
UseProvidedChatClientAsIs is true (#4389)
* Skip tool validation when UseProvidedChatClientAsIs is true (#3855)
When GetAIAgentAsync is called with ChatClientAgentOptions.UseProvidedChatClientAsIs = true,
skip requireInvocableTools validation so users can handle function calls manually
via custom ChatClient middleware without needing to provide matching AIFunction tools.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify requireInvocableTools expression per review feedback
UseProvidedChatClientAsIs is a non-nullable bool, so use ! operator
instead of != true for clarity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Decouple tool matching from validation and add tool preservation test (#3855)
Always match provided AIFunctions to server-side function definitions
regardless of requireInvocableTools flag. Only throw when validation
is required and no match is found. This ensures UseProvidedChatClientAsIs
still preserves user-provided AIFunction tools instead of falling back
to the broken ResponseToolAITool wrapper.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../AzureAIProjectChatClientExtensions.cs | 20 +++---
...AzureAIProjectChatClientExtensionsTests.cs | 64 +++++++++++++++++++
2 files changed, 75 insertions(+), 9 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
index 027eea1bca..a190f4b154 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs
@@ -191,7 +191,7 @@ public static partial class AzureAIProjectChatClientExtensions
AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, options.Name, cancellationToken).ConfigureAwait(false);
var agentVersion = agentRecord.Versions.Latest;
- var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true);
+ var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: !options.UseProvidedChatClientAsIs);
return AsChatClientAgent(
aiProjectClient,
@@ -522,21 +522,23 @@ public static partial class AzureAIProjectChatClientExtensions
// Check function tools
foreach (ResponseTool responseTool in definitionTools)
{
- if (requireInvocableTools && responseTool is FunctionTool functionTool)
+ if (responseTool is FunctionTool functionTool)
{
// Check if a tool with the same type and name exists in the provided tools.
- // When invocable tools are required, match only AIFunction.
+ // Always prefer matching AIFunction when available, regardless of requireInvocableTools.
var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name);
- if (matchingTool is null)
- {
- (missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
- }
- else
+ if (matchingTool is not null)
{
(agentTools ??= []).Add(matchingTool!);
+ continue;
+ }
+
+ if (requireInvocableTools)
+ {
+ (missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
+ continue;
}
- continue;
}
(agentTools ??= []).Add(responseTool.AsAITool());
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
index 2f2e276ae9..a7b9c54aac 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
@@ -2375,6 +2375,70 @@ public sealed class AzureAIProjectChatClientExtensionsTests
Assert.NotNull(agent);
}
+ ///
+ /// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true skips tool validation
+ /// and does not throw even when server-side function tools exist without matching invocable tools.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_SkipsToolValidationAsync()
+ {
+ // Arrange
+ PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
+ definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
+
+ AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
+ var options = new ChatClientAgentOptions
+ {
+ Name = "test-agent",
+ ChatOptions = new ChatOptions { Instructions = "Test" },
+ UseProvidedChatClientAsIs = true
+ };
+
+ // Act - should not throw even without tools when UseProvidedChatClientAsIs is true
+ ChatClientAgent agent = await client.GetAIAgentAsync(options);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true still matches provided AIFunction tools
+ /// to server-side function definitions, instead of falling back to the ResponseToolAITool wrapper.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesProvidedToolsAsync()
+ {
+ // Arrange
+ PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
+ definition.Tools.Add(ResponseTool.CreateFunctionTool("my_function", BinaryData.FromString("{}"), strictModeEnabled: false));
+
+ AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
+
+ var providedTool = AIFunctionFactory.Create(() => "test", "my_function", "A test function");
+ var options = new ChatClientAgentOptions
+ {
+ Name = "test-agent",
+ UseProvidedChatClientAsIs = true,
+ ChatOptions = new ChatOptions
+ {
+ Instructions = "Test",
+ Tools = [providedTool]
+ },
+ };
+
+ // Act - UseProvidedChatClientAsIs is true, but provided AIFunctions should still be matched and preserved
+ ChatClientAgent agent = await client.GetAIAgentAsync(options);
+
+ // Assert
+ Assert.NotNull(agent);
+
+ // Verify the provided AIFunction was matched and preserved in ChatOptions.Tools (not replaced by AsAITool wrapper)
+ var chatOptions = agent.GetService();
+ Assert.NotNull(chatOptions);
+ Assert.NotNull(chatOptions!.Tools);
+ Assert.Contains(chatOptions.Tools, t => t is AIFunction af && af.Name == "my_function");
+ }
+
#endregion
#region Empty Version and ID Handling Tests
From 5276a6c371df560c4bdf7b24d518bf022d7c1dad Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 23:06:34 +0000
Subject: [PATCH 25/36] Bump rollup in
/python/samples/demos/ag_ui_workflow_handoff/frontend (#4284)
Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.59.0)
---
updated-dependencies:
- dependency-name: rollup
dependency-version: 4.59.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.../frontend/package-lock.json | 206 +++++++++---------
1 file changed, 103 insertions(+), 103 deletions(-)
diff --git a/python/samples/demos/ag_ui_workflow_handoff/frontend/package-lock.json b/python/samples/demos/ag_ui_workflow_handoff/frontend/package-lock.json
index bc75c569ff..991211fafd 100644
--- a/python/samples/demos/ag_ui_workflow_handoff/frontend/package-lock.json
+++ b/python/samples/demos/ag_ui_workflow_handoff/frontend/package-lock.json
@@ -802,9 +802,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
- "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -816,9 +816,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
- "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -830,9 +830,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
- "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -844,9 +844,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
- "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -858,9 +858,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
- "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -872,9 +872,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
- "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -886,9 +886,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
- "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -900,9 +900,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
- "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -914,9 +914,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
- "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -928,9 +928,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
- "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -942,9 +942,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
- "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -956,9 +956,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
- "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -970,9 +970,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
- "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -984,9 +984,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
- "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -998,9 +998,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
- "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -1012,9 +1012,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
- "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -1026,9 +1026,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
- "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -1040,9 +1040,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -1054,9 +1054,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
- "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -1068,9 +1068,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
- "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -1082,9 +1082,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
- "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -1096,9 +1096,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
- "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -1110,9 +1110,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
- "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -1124,9 +1124,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -1138,9 +1138,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
- "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1633,9 +1633,9 @@
}
},
"node_modules/rollup": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
- "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1649,31 +1649,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.57.1",
- "@rollup/rollup-android-arm64": "4.57.1",
- "@rollup/rollup-darwin-arm64": "4.57.1",
- "@rollup/rollup-darwin-x64": "4.57.1",
- "@rollup/rollup-freebsd-arm64": "4.57.1",
- "@rollup/rollup-freebsd-x64": "4.57.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
- "@rollup/rollup-linux-arm64-gnu": "4.57.1",
- "@rollup/rollup-linux-arm64-musl": "4.57.1",
- "@rollup/rollup-linux-loong64-gnu": "4.57.1",
- "@rollup/rollup-linux-loong64-musl": "4.57.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
- "@rollup/rollup-linux-ppc64-musl": "4.57.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
- "@rollup/rollup-linux-riscv64-musl": "4.57.1",
- "@rollup/rollup-linux-s390x-gnu": "4.57.1",
- "@rollup/rollup-linux-x64-gnu": "4.57.1",
- "@rollup/rollup-linux-x64-musl": "4.57.1",
- "@rollup/rollup-openbsd-x64": "4.57.1",
- "@rollup/rollup-openharmony-arm64": "4.57.1",
- "@rollup/rollup-win32-arm64-msvc": "4.57.1",
- "@rollup/rollup-win32-ia32-msvc": "4.57.1",
- "@rollup/rollup-win32-x64-gnu": "4.57.1",
- "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
From 2b5e55625605edae480282340e33cf33e2bbb1a7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 15:33:15 -0800
Subject: [PATCH 26/36] Bump rollup (#4386)
Bumps [rollup](https://github.com/rollup/rollup) from 4.52.4 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.4...v4.59.0)
---
updated-dependencies:
- dependency-name: rollup
dependency-version: 4.59.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.../frontend/package-lock.json | 227 +++++++++++-------
1 file changed, 136 insertions(+), 91 deletions(-)
diff --git a/python/samples/05-end-to-end/chatkit-integration/frontend/package-lock.json b/python/samples/05-end-to-end/chatkit-integration/frontend/package-lock.json
index 2a9ef09e64..5ab9ed8ed0 100644
--- a/python/samples/05-end-to-end/chatkit-integration/frontend/package-lock.json
+++ b/python/samples/05-end-to-end/chatkit-integration/frontend/package-lock.json
@@ -493,9 +493,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
- "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -507,9 +507,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
- "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -521,9 +521,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
- "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -535,9 +535,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
- "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -549,9 +549,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
- "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -563,9 +563,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
- "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -577,9 +577,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
- "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -591,9 +591,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
- "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -605,9 +605,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
- "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -619,9 +619,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
- "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -633,9 +633,23 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
- "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -647,9 +661,23 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
- "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -661,9 +689,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
- "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -675,9 +703,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
- "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -689,9 +717,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
- "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -703,9 +731,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
- "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -717,9 +745,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
- "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -730,10 +758,24 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
- "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -745,9 +787,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
- "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -759,9 +801,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
- "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -773,9 +815,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
- "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -787,9 +829,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
- "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1208,9 +1250,9 @@
}
},
"node_modules/rollup": {
- "version": "4.52.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
- "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1224,28 +1266,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.52.4",
- "@rollup/rollup-android-arm64": "4.52.4",
- "@rollup/rollup-darwin-arm64": "4.52.4",
- "@rollup/rollup-darwin-x64": "4.52.4",
- "@rollup/rollup-freebsd-arm64": "4.52.4",
- "@rollup/rollup-freebsd-x64": "4.52.4",
- "@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
- "@rollup/rollup-linux-arm-musleabihf": "4.52.4",
- "@rollup/rollup-linux-arm64-gnu": "4.52.4",
- "@rollup/rollup-linux-arm64-musl": "4.52.4",
- "@rollup/rollup-linux-loong64-gnu": "4.52.4",
- "@rollup/rollup-linux-ppc64-gnu": "4.52.4",
- "@rollup/rollup-linux-riscv64-gnu": "4.52.4",
- "@rollup/rollup-linux-riscv64-musl": "4.52.4",
- "@rollup/rollup-linux-s390x-gnu": "4.52.4",
- "@rollup/rollup-linux-x64-gnu": "4.52.4",
- "@rollup/rollup-linux-x64-musl": "4.52.4",
- "@rollup/rollup-openharmony-arm64": "4.52.4",
- "@rollup/rollup-win32-arm64-msvc": "4.52.4",
- "@rollup/rollup-win32-ia32-msvc": "4.52.4",
- "@rollup/rollup-win32-x64-gnu": "4.52.4",
- "@rollup/rollup-win32-x64-msvc": "4.52.4",
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
From 986e60a16544ca6670833dad570b6c9d176699ef Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 15:33:38 -0800
Subject: [PATCH 27/36] Bump ruff from 0.15.2 to 0.15.4 in /python (#4390)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.2 to 0.15.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.2...0.15.4)
---
updated-dependencies:
- dependency-name: ruff
dependency-version: 0.15.4
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
python/uv.lock | 41 ++++++++++++++++++++++-------------------
1 file changed, 22 insertions(+), 19 deletions(-)
diff --git a/python/uv.lock b/python/uv.lock
index 9f2e97a91e..872b4ec2c8 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -2843,6 +2843,9 @@ name = "jsonpath-ng"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" },
+]
[[package]]
name = "jsonschema"
@@ -5720,27 +5723,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.2"
+version = "0.15.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
- { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
- { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
- { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
- { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
- { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
- { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
- { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
- { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
- { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
- { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
- { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
- { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
- { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
- { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
- { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" },
+ { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" },
]
[[package]]
From 3c66820307b2ec0eb25d83fef3dade6f5e3f482f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 23:34:13 +0000
Subject: [PATCH 28/36] Bump prek from 0.3.3 to 0.3.4 in /python (#4391)
Bumps [prek](https://github.com/j178/prek) from 0.3.3 to 0.3.4.
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.3.3...v0.3.4)
---
updated-dependencies:
- dependency-name: prek
dependency-version: 0.3.4
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
python/uv.lock | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/python/uv.lock b/python/uv.lock
index 872b4ec2c8..7b92da4644 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -4602,26 +4602,26 @@ wheels = [
[[package]]
name = "prek"
-version = "0.3.3"
+version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bf/f1/7613dc8347a33e40fc5b79eec6bc7d458d8bbc339782333d8433b665f86f/prek-0.3.3.tar.gz", hash = "sha256:117bd46ebeb39def24298ce021ccc73edcf697b81856fcff36d762dd56093f6f", size = 343697, upload-time = "2026-02-15T13:33:28.723Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/51/2324eaad93a4b144853ca1c56da76f357d3a70c7b4fd6659e972d7bb8660/prek-0.3.4.tar.gz", hash = "sha256:56a74d02d8b7dfe3c774ecfcd8c1b4e5f1e1b84369043a8003e8e3a779fce72d", size = 356633, upload-time = "2026-02-28T03:47:13.452Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2d/8b/dce13d2a3065fd1e8ffce593a0e51c4a79c3cde9c9a15dc0acc8d9d1573d/prek-0.3.3-py3-none-linux_armv6l.whl", hash = "sha256:e8629cac4bdb131be8dc6e5a337f0f76073ad34a8305f3fe2bc1ab6201ede0a4", size = 4644636, upload-time = "2026-02-15T13:33:43.609Z" },
- { url = "https://files.pythonhosted.org/packages/01/30/06ab4dbe7ce02a8ce833e92deb1d9a8e85ae9d40e33d1959a2070b7494c6/prek-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4b9e819b9e4118e1e785047b1c8bd9aec7e4d836ed034cb58b7db5bcaaf49437", size = 4651410, upload-time = "2026-02-15T13:33:34.277Z" },
- { url = "https://files.pythonhosted.org/packages/d4/fc/da3bc5cb38471e7192eda06b7a26b7c24ef83e82da2c1dbc145f2bf33640/prek-0.3.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bf29db3b5657c083eb8444c25aadeeec5167dc492e9019e188f87932f01ea50a", size = 4273163, upload-time = "2026-02-15T13:33:42.106Z" },
- { url = "https://files.pythonhosted.org/packages/b4/74/47839395091e2937beced81a5dd2f8ea9c8239c853da8611aaf78ee21a8b/prek-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ae09736149815b26e64a9d350ca05692bab32c2afdf2939114d3211aaad68a3e", size = 4631808, upload-time = "2026-02-15T13:33:20.076Z" },
- { url = "https://files.pythonhosted.org/packages/e2/89/3f5ef6f7c928c017cb63b029349d6bc03598ab7f6979d4a770ce02575f82/prek-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:856c2b55c51703c366bb4ce81c6a91102b70573a9fc8637db2ac61c66e4565f9", size = 4548959, upload-time = "2026-02-15T13:33:36.325Z" },
- { url = "https://files.pythonhosted.org/packages/b2/18/80002c4c4475f90ca025f27739a016927a0e5d905c60612fc95da1c56ab7/prek-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3acdf13a018f685beaff0a71d4b0d2ccbab4eaa1aced6d08fd471c1a654183eb", size = 4862256, upload-time = "2026-02-15T13:33:37.754Z" },
- { url = "https://files.pythonhosted.org/packages/c5/25/648bf084c2468fa7cfcdbbe9e59956bbb31b81f36e113bc9107d80af26a7/prek-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f035667a8bd0a77b2bfa2b2e125da8cb1793949e9eeef0d8daab7f8ac8b57fe", size = 5404486, upload-time = "2026-02-15T13:33:39.239Z" },
- { url = "https://files.pythonhosted.org/packages/8b/43/261fb60a11712a327da345912bd8b338dc5a050199de800faafa278a6133/prek-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09b2ad14332eede441d977de08eb57fb3f61226ed5fd2ceb7aadf5afcdb6794", size = 4887513, upload-time = "2026-02-15T13:33:40.702Z" },
- { url = "https://files.pythonhosted.org/packages/c7/2c/581e757ee57ec6046b32e0ee25660fc734bc2622c319f57119c49c0cab58/prek-0.3.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c0c3ffac16e37a9daba43a7e8316778f5809b70254be138761a8b5b9ef0df28e", size = 4632336, upload-time = "2026-02-15T13:33:25.867Z" },
- { url = "https://files.pythonhosted.org/packages/d5/d8/aa276ce5d11b77882da4102ca0cb7161095831105043ae7979bbfdcc3dc4/prek-0.3.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a3dc7720b580c07c0386e17af2486a5b4bc2f6cc57034a288a614dcbc4abe555", size = 4679370, upload-time = "2026-02-15T13:33:22.247Z" },
- { url = "https://files.pythonhosted.org/packages/70/19/9d4fa7bde428e58d9f48a74290c08736d42aeb5690dcdccc7a713e34a449/prek-0.3.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60e0fa15da5020a03df2ee40268145ec5b88267ec2141a205317ad4df8c992d6", size = 4540316, upload-time = "2026-02-15T13:33:24.088Z" },
- { url = "https://files.pythonhosted.org/packages/25/b5/973cce29257e0b47b16cc9b4c162772ea01dbb7c080791ea0c068e106e05/prek-0.3.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:553515da9586d9624dc42db32b744fdb91cf62b053753037a0cadb3c2d8d82a2", size = 4724566, upload-time = "2026-02-15T13:33:29.832Z" },
- { url = "https://files.pythonhosted.org/packages/d6/8b/ad8b2658895a8ed2b0bc630bf38686fe38b7ff2c619c58953a80e4de3048/prek-0.3.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9512cf370e0d1496503463a4a65621480efb41b487841a9e9ff1661edf14b238", size = 4995072, upload-time = "2026-02-15T13:33:27.417Z" },
- { url = "https://files.pythonhosted.org/packages/fd/b7/0540c101c00882adb9d30319d22d8f879413598269ecc60235e41875efd4/prek-0.3.3-py3-none-win32.whl", hash = "sha256:b2b328c7c6dc14ccdc79785348589aa39850f47baff33d8f199f2dee80ff774c", size = 4293144, upload-time = "2026-02-15T13:33:46.013Z" },
- { url = "https://files.pythonhosted.org/packages/97/c7/e4f11da653093040efba2d835aa0995d78940aea30887287aeaebe34a545/prek-0.3.3-py3-none-win_amd64.whl", hash = "sha256:3d7d7acf7ca8db65ba0943c52326c898f84bab0b1c26a35c87e0d177f574ca5f", size = 4652761, upload-time = "2026-02-15T13:33:32.962Z" },
- { url = "https://files.pythonhosted.org/packages/11/e4/d99dec54c6a5fb2763488bff6078166383169a93f3af27d2edae88379a39/prek-0.3.3-py3-none-win_arm64.whl", hash = "sha256:8aa87ee7628cd74482c0dd6537a3def1f162b25cd642d78b1b35dd3e81817f60", size = 4367520, upload-time = "2026-02-15T13:33:31.664Z" },
+ { url = "https://files.pythonhosted.org/packages/09/20/1a964cb72582307c2f1dc7f583caab90f42810ad41551e5220592406a4c3/prek-0.3.4-py3-none-linux_armv6l.whl", hash = "sha256:c35192d6e23fe7406bd2f333d1c7dab1a4b34ab9289789f453170f33550aa74d", size = 4641915, upload-time = "2026-02-28T03:47:03.772Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/cb/4a21f37102bac37e415b61818344aa85de8d29a581253afa7db8c08d5a33/prek-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f784d78de72a8bbe58a5fe7bde787c364ae88f0aff5222c5c5c7287876c510a", size = 4649166, upload-time = "2026-02-28T03:47:06.164Z" },
+ { url = "https://files.pythonhosted.org/packages/85/9c/a7c0d117a098d57931428bdb60fcb796e0ebc0478c59288017a2e22eca96/prek-0.3.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50a43f522625e8c968e8c9992accf9e29017abad6c782d6d176b73145ad680b7", size = 4274422, upload-time = "2026-02-28T03:46:59.356Z" },
+ { url = "https://files.pythonhosted.org/packages/59/84/81d06df1724d09266df97599a02543d82fde7dfaefd192f09d9b2ccb092f/prek-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4bbb1d3912a88935f35c6ba4466b4242732e3e3a8c608623c708e83cea85de00", size = 4629873, upload-time = "2026-02-28T03:46:56.419Z" },
+ { url = "https://files.pythonhosted.org/packages/09/cd/bb0aefa25cfacd8dbced75b9a9d9945707707867fa5635fb69ae1bbc2d88/prek-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca4d4134db8f6e8de3c418317becdf428957e3cab271807f475318105fd46d04", size = 4552507, upload-time = "2026-02-28T03:47:05.004Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/c0/578a7af4861afb64ec81c03bfdcc1bb3341bb61f2fff8a094ecf13987a56/prek-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fb6395f6eb76133bb1e11fc718db8144522466cdc2e541d05e7813d1bbcae7d", size = 4865929, upload-time = "2026-02-28T03:47:09.231Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/48/f169406590028f7698ef2e1ff5bffd92ca05e017636c1163a2f5ef0f8275/prek-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae17813239ddcb4ae7b38418de4d49afff740f48f8e0556029c96f58e350412", size = 5390286, upload-time = "2026-02-28T03:47:10.796Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c5/98a73fec052059c3ae06ce105bef67caca42334c56d84e9ef75df72ba152/prek-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a621a690d9c127afc3d21c275030d364d1fbef3296c095068d3ae80a59546e", size = 4891028, upload-time = "2026-02-28T03:47:07.916Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b4/029966e35e59b59c142be7e1d2208ad261709ac1a66aa4a3ce33c5b9f91f/prek-0.3.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d978c31bc3b1f0b3d58895b7c6ac26f077e0ea846da54f46aeee4c7088b1b105", size = 4633986, upload-time = "2026-02-28T03:47:14.351Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/27/d122802555745b6940c99fcb41496001c192ddcdf56ec947ec10a0298e05/prek-0.3.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8e089a030f0a023c22a4bb2ec4ff3fcc153585d701cff67acbfca2f37e173ae", size = 4680722, upload-time = "2026-02-28T03:47:12.224Z" },
+ { url = "https://files.pythonhosted.org/packages/34/40/92318c96b3a67b4e62ed82741016ede34d97ea9579d3cc1332b167632222/prek-0.3.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8060c72b764f0b88112616763da9dd3a7c293e010f8520b74079893096160a2f", size = 4535623, upload-time = "2026-02-28T03:46:52.221Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f5/6b383d94e722637da4926b4f609d36fe432827bb6f035ad46ee02bde66b6/prek-0.3.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:65b23268456b5a763278d4e1ec532f2df33918f13ded85869a1ddff761eb9697", size = 4729879, upload-time = "2026-02-28T03:46:57.886Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f8/fdc705b807d813fd713ffa4f67f96741542ed1dafbb221206078c06f3df4/prek-0.3.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3975c61139c7b3200e38dc3955e050b0f2615701d3deb9715696a902e850509e", size = 5001569, upload-time = "2026-02-28T03:47:00.892Z" },
+ { url = "https://files.pythonhosted.org/packages/84/92/b007a41f58e8192a1e611a21b396ad870d51d7873b7af12068ebae7fc15f/prek-0.3.4-py3-none-win32.whl", hash = "sha256:37449ae82f4dc08b72e542401e3d7318f05d1163e87c31ab260a40f425d6516e", size = 4297057, upload-time = "2026-02-28T03:47:02.219Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dc/bcb02de9b11461e8e0c7d3c8fdf8cfa15ac6efe73472a4375549ba5defd2/prek-0.3.4-py3-none-win_amd64.whl", hash = "sha256:60e9aa86ca65de963510ae28c5d94b9d7a97bcbaa6e4cdb5bf5083ed4c45dc71", size = 4655174, upload-time = "2026-02-28T03:46:53.749Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/86/98f5598569f4cd3de7161e266fab6a8981e65555f79d4704810c1502ad0a/prek-0.3.4-py3-none-win_arm64.whl", hash = "sha256:486bdae8f4512d3b4f6eb61b83e5b7595da2adca385af4b2b7823c0ab38d1827", size = 4367817, upload-time = "2026-02-28T03:46:55.264Z" },
]
[[package]]
From aa0148a9becea51443e8d50775b8e37502cfab24 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 23:35:08 +0000
Subject: [PATCH 29/36] Bump poethepoet from 0.42.0 to 0.42.1 in /python
(#4392)
Bumps [poethepoet](https://github.com/nat-n/poethepoet) from 0.42.0 to 0.42.1.
- [Release notes](https://github.com/nat-n/poethepoet/releases)
- [Commits](https://github.com/nat-n/poethepoet/compare/v0.42.0...v0.42.1)
---
updated-dependencies:
- dependency-name: poethepoet
dependency-version: 0.42.1
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
python/uv.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/python/uv.lock b/python/uv.lock
index 7b92da4644..29bbe3af99 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -4518,16 +4518,16 @@ wheels = [
[[package]]
name = "poethepoet"
-version = "0.42.0"
+version = "0.42.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pastel", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4a/9a/4e81fafef2ba94e5c974b4701343d1f053a27575ab5133cbd264348925dd/poethepoet-0.42.0.tar.gz", hash = "sha256:c9a2828259e585e9ed152857602130ff339f7b1638879b80d4a23f25588be4f8", size = 91278, upload-time = "2026-02-22T14:24:50.967Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/9b/e717572686bbf23e17483389c1bf3a381ca2427c84c7e0af0cdc0f23fccc/poethepoet-0.42.1.tar.gz", hash = "sha256:205747e276062c2aaba8afd8a98838f8a3a0237b7ab94715fab8d82718aac14f", size = 93209, upload-time = "2026-02-26T22:57:50.883Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/3e/58041b7e4d49b69e859dc81c35e221cf02d91ed4dbb5a2f6cc4698a29f44/poethepoet-0.42.0-py3-none-any.whl", hash = "sha256:e43cc20d458ee5bfccaa4572bc5783bcb93991a7d2fcf8dadc9c43f1ebc9b277", size = 118091, upload-time = "2026-02-22T14:24:49.53Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/68/75fa0a5ef39718ea6ba7ab6a3d031fa93640e57585580cec85539540bb65/poethepoet-0.42.1-py3-none-any.whl", hash = "sha256:d8d1345a5ca521be9255e7c13bc2c4c8698ed5e5ac5e9e94890d239fcd423d0a", size = 119967, upload-time = "2026-02-26T22:57:49.467Z" },
]
[[package]]
From 6de5e57b20dcb930f92095f197cac796aa3a9987 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 23:35:26 +0000
Subject: [PATCH 30/36] Bump uv from 0.10.5 to 0.10.7 in /python (#4393)
Bumps [uv](https://github.com/astral-sh/uv) from 0.10.5 to 0.10.7.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.10.5...0.10.7)
---
updated-dependencies:
- dependency-name: uv
dependency-version: 0.10.7
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
python/uv.lock | 38 +++++++++++++++++++-------------------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/python/uv.lock b/python/uv.lock
index 29bbe3af99..fddcab4657 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -6624,27 +6624,27 @@ wheels = [
[[package]]
name = "uv"
-version = "0.10.5"
+version = "0.10.7"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/90/2f/472ff992c50e5947ef0570d291cfa3a70b423e5dcc6bee99b7a8e7b6da49/uv-0.10.5.tar.gz", hash = "sha256:c45de48b7fa6dd034de8515a7d129f85f4e74080b9f09a7bfc0bcce2798f8023", size = 3919437, upload-time = "2026-02-24T00:55:11.392Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/ec/b324a43b55fe59577505478a396cb1d2758487a2e2270c81ccfa4ac6c96d/uv-0.10.7.tar.gz", hash = "sha256:7c3b0133c2d6bd725d5a35ec5e109ebf0d75389943abe826f3d9ea6d6667a375", size = 3922193, upload-time = "2026-02-27T12:33:58.525Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/35/01/1521344a015f7fc01198f9d8560838adbeb9e80b835a23c25c712d8a8c08/uv-0.10.5-py3-none-linux_armv6l.whl", hash = "sha256:d1ccf2e7cf08b8a1477195da50476fb645bf20907072a39074f482049056aa5d", size = 22401966, upload-time = "2026-02-24T00:55:09.111Z" },
- { url = "https://files.pythonhosted.org/packages/3e/47/b4a4690f13d44f110ba7534a950a6ca63f61cc3d81c28f9c81afa9b74634/uv-0.10.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:63435e86321993dd5d90f440524f3f1b874b34aab30b7bf6752b48497117bfc4", size = 21504807, upload-time = "2026-02-24T00:55:18.55Z" },
- { url = "https://files.pythonhosted.org/packages/61/58/28725e2d223b36812f692123934c1cbd7a6bc5261d6cf0f3850889768c66/uv-0.10.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2cec424513140aa179d1c4decfcf86201497df7bc5674c13a20882d3b2837c7e", size = 20194774, upload-time = "2026-02-24T00:54:49.789Z" },
- { url = "https://files.pythonhosted.org/packages/6b/d4/87113bce59b9711e55995d2db66faffdb98952e371eab2d44fe4b0d79bf7/uv-0.10.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3aa708beef7fab912d115ba1ccaad383a7006dc1a8e5ecdd9656574188221a84", size = 22044475, upload-time = "2026-02-24T00:54:56.924Z" },
- { url = "https://files.pythonhosted.org/packages/7b/2c/af72b186786c4dd9a3d71d747cd0e02868b6eb7836b29c51e0d4cfe649de/uv-0.10.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:74c6d2d38160bbb2d596560f27875c3906c0e94e61c6279b5111d3f2d74dbcd9", size = 22038345, upload-time = "2026-02-24T00:54:59.245Z" },
- { url = "https://files.pythonhosted.org/packages/61/8f/573edcdffe160093ef640b34690f13a2c6f35e03674fe52207bd9f63f23c/uv-0.10.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3ff5bab65eb305d1cf024c5eb091b12f3d7b40e5a78409fb0afb937b2614001", size = 22006975, upload-time = "2026-02-24T00:55:28.954Z" },
- { url = "https://files.pythonhosted.org/packages/f0/28/9dbad27f80cc6b162f41c3becf154a1ba54177957ead4ae4faf3125b526f/uv-0.10.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd263e573a5259e6ce9854698e0c31e8ebdaa0a8d0701943db159854bbd6dcdf", size = 23326569, upload-time = "2026-02-24T00:55:33.966Z" },
- { url = "https://files.pythonhosted.org/packages/1d/a0/f5ee404b9601bfb03d36241637d0d2ff1089115e532bcd77de0d29a0a89b/uv-0.10.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:faaa30c94ffeda248c29b7185ce4d5809de4c54f2a1c16f0120d50564473d9b4", size = 24197070, upload-time = "2026-02-24T00:55:06.621Z" },
- { url = "https://files.pythonhosted.org/packages/dc/e8/c0c33168ca17f582727d33e629fa1673bc1e1c2411b174f2f78c1d16d287/uv-0.10.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49db2d27555d6f7c69422d2d5f79ebe2dc4ed6a859a698d015d48de51e16aaab", size = 23277854, upload-time = "2026-02-24T00:55:31.444Z" },
- { url = "https://files.pythonhosted.org/packages/8f/d9/4bb264bdb7f2e95efe09622cc6512288a842956bb4c2c3d6fe711eaef7df/uv-0.10.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8acf9be268ce2fc2c16117b5884f0724498d7191f8db2d12d8a7c7482652d38", size = 23252223, upload-time = "2026-02-24T00:55:16.256Z" },
- { url = "https://files.pythonhosted.org/packages/fc/ac/b669f622c0e978754083aad3d7916594828ad5c3b634cb8374b7a841e153/uv-0.10.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0fbd426d2c215098cd8e08dfa36ad0a313ebe5eb90107ab7b3b8d5563b9b0c03", size = 22124089, upload-time = "2026-02-24T00:55:20.916Z" },
- { url = "https://files.pythonhosted.org/packages/1f/0a/e9f44902757ec1723e8f1970463ce477ce11c79fa52a09001fbc8934128a/uv-0.10.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:24825579973a05b7d482f1bba5e1b6d687d8e6ddf0ca088ff893e94ab34943a2", size = 22828770, upload-time = "2026-02-24T00:55:26.571Z" },
- { url = "https://files.pythonhosted.org/packages/37/18/d69ba9636c560b771b96c08bcfb4424829cc53983d8c7b71e0d2f301e7fb/uv-0.10.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0338429ec4bb0b64620d05905a3fc1dc420df2a0e22b1a9b01dcc9e430067622", size = 22530138, upload-time = "2026-02-24T00:55:13.363Z" },
- { url = "https://files.pythonhosted.org/packages/92/72/15ef087c4a4ab1531d77b267345a2321301b09345fbe6419f8a8b94ffc3d/uv-0.10.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:515042b1f4a05396496a3db9ffc338b2f8f7bb39214fdbcb425b0462630f9270", size = 23448538, upload-time = "2026-02-24T00:54:53.364Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5c/b07bc4fd89fad1a0b7946d40469850552738613fcd678a4ecee5e892aa8c/uv-0.10.5-py3-none-win32.whl", hash = "sha256:b235b4a5f25fb3bb93b96aebb6a2623eda0c2f48a6471b172a89e10444aa3626", size = 21507185, upload-time = "2026-02-24T00:55:01.646Z" },
- { url = "https://files.pythonhosted.org/packages/43/31/c564541cd1a27001a245241e1ac82ef4132fb5d96cab13a4a19e91981eaf/uv-0.10.5-py3-none-win_amd64.whl", hash = "sha256:4924af9facedde12eba2190463d84a4940062a875322e29ef59c8f447951e5c7", size = 23945906, upload-time = "2026-02-24T00:55:04.065Z" },
- { url = "https://files.pythonhosted.org/packages/e0/f5/71fa52581b25d5aa8917b3d3956db9c3d1ed511d4785bb7c94bf02872160/uv-0.10.5-py3-none-win_arm64.whl", hash = "sha256:43445370bb0729917b9a61d18bc3aec4e55c12e86463e6c4536fafde4d4da9e0", size = 22343346, upload-time = "2026-02-24T00:55:23.699Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1b/decff24553325561850d70b75c737076e6fcbcfbf233011a27a33f06e4d9/uv-0.10.7-py3-none-linux_armv6l.whl", hash = "sha256:6a0af6c7a90fd2053edfa2c8ee719078ea906a2d9f4798d3fb3c03378726209a", size = 22497542, upload-time = "2026-02-27T12:33:39.425Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b5/51152c87921bc2576fecb982df4a02ac9cfd7fc934e28114a1232b99eed4/uv-0.10.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b7db0cab77232a7c8856062904fc3b9db22383f1dec7e97a9588fb6c8470f6a", size = 21558860, upload-time = "2026-02-27T12:34:03.362Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/15/8365dc2ded350a4ee5fcbbf9b15195cb2b45855114f2a154b5effb6fa791/uv-0.10.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d872d2ff9c9dfba989b5f05f599715bc0f19b94cd0dbf8ae4ad22f8879a66c8c", size = 20212775, upload-time = "2026-02-27T12:33:55.365Z" },
+ { url = "https://files.pythonhosted.org/packages/53/a0/ccf25e897f3907b5a6fd899007ff9a80b5bbf151b3a75a375881005611fd/uv-0.10.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d9b40d03693efda80a41e5d18ac997efdf1094b27fb75471c1a8f51a9ebeffb3", size = 22015584, upload-time = "2026-02-27T12:33:47.374Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/3a/5099747954e7774768572d30917bb6bda6b8d465d7a3c49c9bbf7af2a812/uv-0.10.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:e74fe4df9cf31fe84f20b84a0054874635077d31ce20e7de35ff0dd64d498d7b", size = 22100376, upload-time = "2026-02-27T12:34:06.169Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/1a/75897fd966b871803cf78019fa31757ced0d54af5ffd7f57bce8b01d64f3/uv-0.10.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c76659fc8bb618dd35cd83b2f479c6f880555a16630a454a251045c4c118ea4", size = 22105202, upload-time = "2026-02-27T12:34:16.972Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/1e/0b8caedd66ca911533e18fd051da79a213c792404138812c66043d529b9e/uv-0.10.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d160cceb9468024ca40dc57a180289dfd2024d98e42f2284b9ec44355723b0a", size = 23335601, upload-time = "2026-02-27T12:34:11.161Z" },
+ { url = "https://files.pythonhosted.org/packages/69/94/b741af277e39a92e0da07fe48c338eee1429c2607e7a192e41345208bb24/uv-0.10.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c775975d891cb60cf10f00953e61e643fcb9a9139e94c9ef5c805fe36e90477f", size = 24152851, upload-time = "2026-02-27T12:33:33.904Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b2/da351ccd02f0fb1aec5f992b886bea1374cce44276a78904348e2669dd78/uv-0.10.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a709e75583231cc1f39567fb3d8d9b4077ff94a64046eb242726300144ed1a4a", size = 23276444, upload-time = "2026-02-27T12:33:36.891Z" },
+ { url = "https://files.pythonhosted.org/packages/71/a9/2735cc9dc39457c9cf64d1ce2ba5a9a8ecbb103d0fb64b052bf33ba3d669/uv-0.10.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89de2504407dcf04aece914c6ca3b9d8e60cf9ff39a13031c1df1f7c040cea81", size = 23218464, upload-time = "2026-02-27T12:34:00.904Z" },
+ { url = "https://files.pythonhosted.org/packages/20/5f/5f204e9c3f04f5fc844d2f98d80a7de64b6b304af869644ab478d909f6ff/uv-0.10.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9945de1d11c4a5ad77e9c4f36f8b5f9e7c9c3c32999b8bc0e7e579145c3b641c", size = 22092562, upload-time = "2026-02-27T12:34:14.155Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/a4/16bebf106e3289a29cc1e1482d551c49bd220983e9b4bc5960142389ad3f/uv-0.10.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbe43527f478e2ffa420516aa465f82057763936bbea56f814fd054a9b7f961f", size = 22851312, upload-time = "2026-02-27T12:34:08.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/7a/953b1da589225d98ca8668412f665c3192f6deed2a0f4bb782b0df18f611/uv-0.10.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c0783f327631141501bdc5f31dd2b4c748df7e7f5dc5cdbfc0fbb82da86cc9ca", size = 22543775, upload-time = "2026-02-27T12:33:30.935Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/67/e133afdabf76e43989448be1c2ef607f13afc32aa1ee9f6897115dec8417/uv-0.10.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:eba438899010522812d3497af586e6eedc94fa2b0ced028f51812f0c10aafb30", size = 23431187, upload-time = "2026-02-27T12:33:42.131Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/40/6ffb58ec88a33d6cbe9a606966f9558807f37a50f7be7dc756824df2d04c/uv-0.10.7-py3-none-win32.whl", hash = "sha256:b56d1818aafb2701d92e94f552126fe71d30a13f28712d99345ef5cafc53d874", size = 21524397, upload-time = "2026-02-27T12:33:44.579Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/1f/74f4d625db838f716a555908d41777b6357bacc141ddef117a01855e5ef9/uv-0.10.7-py3-none-win_amd64.whl", hash = "sha256:ad0d0ddd9f5407ad8699e3b20fe6c18406cd606336743e246b16914801cfd8b0", size = 23999929, upload-time = "2026-02-27T12:33:49.839Z" },
+ { url = "https://files.pythonhosted.org/packages/48/4e/20cbfbcb1a0f48c5c1ca94f6baa0fa00754aafda365da9160c15e3b9c277/uv-0.10.7-py3-none-win_arm64.whl", hash = "sha256:edf732de80c1a9701180ef8c7a2fa926a995712e4a34ae8c025e090f797c2e0b", size = 22353084, upload-time = "2026-02-27T12:33:52.792Z" },
]
[[package]]
From d7abfcd44420cbd9eb5dc5df301bcf288ec04d24 Mon Sep 17 00:00:00 2001
From: Tao Chen
Date: Mon, 2 Mar 2026 15:36:18 -0800
Subject: [PATCH 31/36] Move sample validation script from samples/ to scripts/
(#4400)
---
.../workflows/python-sample-validation.yml | 40 +++++-----
.../sample_validation}/README.md | 18 ++---
.../sample_validation}/__init__.py | 8 +-
.../sample_validation}/__main__.py | 32 ++++----
.../sample_validation}/const.py | 0
.../create_dynamic_workflow_executor.py | 77 +++++++++++++------
.../sample_validation}/discovery.py | 8 +-
.../sample_validation}/models.py | 0
.../sample_validation}/report.py | 17 +++-
...un_dynamic_validation_workflow_executor.py | 25 ++++--
.../sample_validation}/workflow.py | 13 +++-
11 files changed, 152 insertions(+), 86 deletions(-)
rename python/{samples/_sample_validation => scripts/sample_validation}/README.md (94%)
rename python/{samples/_sample_validation => scripts/sample_validation}/__init__.py (63%)
rename python/{samples/_sample_validation => scripts/sample_validation}/__main__.py (75%)
rename python/{samples/_sample_validation => scripts/sample_validation}/const.py (100%)
rename python/{samples/_sample_validation => scripts/sample_validation}/create_dynamic_workflow_executor.py (82%)
rename python/{samples/_sample_validation => scripts/sample_validation}/discovery.py (94%)
rename python/{samples/_sample_validation => scripts/sample_validation}/models.py (100%)
rename python/{samples/_sample_validation => scripts/sample_validation}/report.py (88%)
rename python/{samples/_sample_validation => scripts/sample_validation}/run_dynamic_validation_workflow_executor.py (77%)
rename python/{samples/_sample_validation => scripts/sample_validation}/workflow.py (72%)
diff --git a/.github/workflows/python-sample-validation.yml b/.github/workflows/python-sample-validation.yml
index 2a5a0b6596..5f36af65cc 100644
--- a/.github/workflows/python-sample-validation.yml
+++ b/.github/workflows/python-sample-validation.yml
@@ -43,14 +43,14 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
+ cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-01-get-started
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
validate-02-agents:
name: Validate 02-agents
@@ -66,8 +66,8 @@ jobs:
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
- OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
- OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
+ OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
+ OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
# Observability
ENABLE_INSTRUMENTATION: "true"
defaults:
@@ -86,14 +86,14 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir 02-agents --save-report --report-name 02-agents
+ cd scripts && uv run python -m sample_validation --subdir 02-agents --save-report --report-name 02-agents
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-02-agents
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
validate-03-workflows:
name: Validate 03-workflows
@@ -123,14 +123,14 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
+ cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-03-workflows
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
validate-04-hosting:
name: Validate 04-hosting
@@ -162,14 +162,14 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir 04-hosting --save-report --report-name 04-hosting
+ cd scripts && uv run python -m sample_validation --subdir 04-hosting --save-report --report-name 04-hosting
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-04-hosting
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
validate-05-end-to-end:
name: Validate 05-end-to-end
@@ -206,14 +206,14 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
+ cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-05-end-to-end
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
validate-autogen-migration:
name: Validate autogen-migration
@@ -228,8 +228,8 @@ jobs:
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
- OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
- OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
+ OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
+ OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
working-directory: python
@@ -246,14 +246,14 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
+ cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-autogen-migration
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
validate-semantic-kernel-migration:
name: Validate semantic-kernel-migration
@@ -269,8 +269,8 @@ jobs:
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
- OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }}
- OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI_RESPONSES_MODEL_ID }}
+ OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
+ OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
# Copilot Studio
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
@@ -292,11 +292,11 @@ jobs:
- name: Run sample validation
run: |
- cd samples && uv run python -m _sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
+ cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
- name: Upload validation report
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-report-semantic-kernel-migration
- path: python/samples/_sample_validation/reports/
+ path: python/scripts/sample_validation/reports/
diff --git a/python/samples/_sample_validation/README.md b/python/scripts/sample_validation/README.md
similarity index 94%
rename from python/samples/_sample_validation/README.md
rename to python/scripts/sample_validation/README.md
index 4ed84b4c41..064d9752da 100644
--- a/python/samples/_sample_validation/README.md
+++ b/python/scripts/sample_validation/README.md
@@ -49,8 +49,8 @@ An AI-powered workflow system for validating Python samples by discovering them,
## File Structure
```
-samples/
-├── _sample_validation/
+scripts/
+├── sample_validation/
│ ├── __init__.py # Package exports
│ ├── README.md # This file
│ ├── models.py # Data classes
@@ -97,19 +97,19 @@ No required environment variables. Optional:
```bash
# Validate all samples
-uv run python -m _sample_validation
+uv run python -m sample_validation
# Validate specific subdirectory
-uv run python -m _sample_validation --subdir 03-workflows
+uv run python -m sample_validation --subdir 03-workflows
# Save reports to files
-uv run python -m _sample_validation --save-report --output-dir ./reports
+uv run python -m sample_validation --save-report --output-dir ./reports
```
### Configuration Options
```bash
-uv run python -m _sample_validation [OPTIONS]
+uv run python -m sample_validation [OPTIONS]
Options:
--subdir TEXT Subdirectory to validate (relative to samples/)
@@ -122,13 +122,13 @@ Options:
```bash
# Quick validation of a small directory
-uv run python -m _sample_validation --subdir 03-workflows/_start-here
+uv run python -m sample_validation --subdir 03-workflows/_start-here
# Limit parallel workers for large sample sets
-uv run python -m _sample_validation --subdir 02-agents --max-parallel-workers 8
+uv run python -m sample_validation --subdir 02-agents --max-parallel-workers 8
# Save report artifacts
-uv run python -m _sample_validation --save-report
+uv run python -m sample_validation --save-report
```
## How It Works
diff --git a/python/samples/_sample_validation/__init__.py b/python/scripts/sample_validation/__init__.py
similarity index 63%
rename from python/samples/_sample_validation/__init__.py
rename to python/scripts/sample_validation/__init__.py
index afa0f47291..450edafb9d 100644
--- a/python/samples/_sample_validation/__init__.py
+++ b/python/scripts/sample_validation/__init__.py
@@ -10,12 +10,12 @@ A workflow-based system for validating Python samples by:
4. Generating a validation report
Usage:
- uv run python -m _sample_validation
- uv run python -m _sample_validation --subdir 01-get-started
+ uv run python -m sample_validation
+ uv run python -m sample_validation --subdir 01-get-started
"""
-from _sample_validation.models import Report, RunResult, SampleInfo
-from _sample_validation.workflow import create_validation_workflow
+from sample_validation.models import Report, RunResult, SampleInfo
+from sample_validation.workflow import create_validation_workflow
__all__ = [
"SampleInfo",
diff --git a/python/samples/_sample_validation/__main__.py b/python/scripts/sample_validation/__main__.py
similarity index 75%
rename from python/samples/_sample_validation/__main__.py
rename to python/scripts/sample_validation/__main__.py
index 55d7df4b91..5d222b94b9 100644
--- a/python/samples/_sample_validation/__main__.py
+++ b/python/scripts/sample_validation/__main__.py
@@ -10,9 +10,9 @@ Validates all Python samples in the samples directory using a workflow that:
4. Generates a validation report
Usage:
- uv run python -m _sample_validation
- uv run python -m _sample_validation --subdir 03-workflows
- uv run python -m _sample_validation --output-dir ./reports
+ uv run python -m sample_validation
+ uv run python -m sample_validation --subdir 03-workflows
+ uv run python -m sample_validation --output-dir ./reports
"""
import argparse
@@ -25,9 +25,9 @@ from pathlib import Path
# Add the samples directory to the path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
-from _sample_validation.models import Report
-from _sample_validation.report import save_report
-from _sample_validation.workflow import ValidationConfig, create_validation_workflow
+from sample_validation.models import Report
+from sample_validation.report import save_report
+from sample_validation.workflow import ValidationConfig, create_validation_workflow
def parse_arguments() -> argparse.Namespace:
@@ -37,9 +37,9 @@ def parse_arguments() -> argparse.Namespace:
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
- uv run python -m _sample_validation # Validate all samples
- uv run python -m _sample_validation --subdir 03-workflows # Validate only workflows
- uv run python -m _sample_validation --output-dir ./reports # Save reports to custom dir
+ uv run python -m sample_validation # Validate all samples
+ uv run python -m sample_validation --subdir 03-workflows # Validate only workflows
+ uv run python -m sample_validation --output-dir ./reports # Save reports to custom dir
""",
)
@@ -52,8 +52,8 @@ Examples:
parser.add_argument(
"--output-dir",
type=str,
- default="./_sample_validation/reports",
- help="Directory to save validation reports (default: ./_sample_validation/reports)",
+ default="./sample_validation/reports",
+ help="Directory to save validation reports (default: ./sample_validation/reports)",
)
parser.add_argument(
@@ -83,8 +83,10 @@ async def main() -> int:
args = parse_arguments()
# Determine paths
- samples_dir = Path(__file__).parent.parent
- python_root = samples_dir.parent
+ # Script is at python/scripts/sample_validation/__main__.py
+ # python_root is python/, samples_dir is python/samples/
+ python_root = Path(__file__).parent.parent.parent
+ samples_dir = python_root / "samples"
print("=" * 80)
print("SAMPLE VALIDATION WORKFLOW")
@@ -93,7 +95,9 @@ async def main() -> int:
print(f"Python root: {python_root}")
if os.environ.get("GITHUB_COPILOT_MODEL"):
- print(f"Using GitHub Copilot model override: {os.environ['GITHUB_COPILOT_MODEL']}")
+ print(
+ f"Using GitHub Copilot model override: {os.environ['GITHUB_COPILOT_MODEL']}"
+ )
# Create validation config
config = ValidationConfig(
diff --git a/python/samples/_sample_validation/const.py b/python/scripts/sample_validation/const.py
similarity index 100%
rename from python/samples/_sample_validation/const.py
rename to python/scripts/sample_validation/const.py
diff --git a/python/samples/_sample_validation/create_dynamic_workflow_executor.py b/python/scripts/sample_validation/create_dynamic_workflow_executor.py
similarity index 82%
rename from python/samples/_sample_validation/create_dynamic_workflow_executor.py
rename to python/scripts/sample_validation/create_dynamic_workflow_executor.py
index bff720130d..69c5cc9a5e 100644
--- a/python/samples/_sample_validation/create_dynamic_workflow_executor.py
+++ b/python/scripts/sample_validation/create_dynamic_workflow_executor.py
@@ -4,16 +4,6 @@ import logging
from collections import deque
from dataclasses import dataclass
-from _sample_validation.const import WORKER_COMPLETED
-from _sample_validation.discovery import DiscoveryResult
-from _sample_validation.models import (
- ExecutionResult,
- RunResult,
- RunStatus,
- SampleInfo,
- ValidationConfig,
- WorkflowCreationResult,
-)
from agent_framework import (
Executor,
Message,
@@ -28,6 +18,17 @@ from copilot.types import PermissionRequest, PermissionRequestResult
from pydantic import BaseModel
from typing_extensions import Never
+from sample_validation.const import WORKER_COMPLETED
+from sample_validation.discovery import DiscoveryResult
+from sample_validation.models import (
+ ExecutionResult,
+ RunResult,
+ RunStatus,
+ SampleInfo,
+ ValidationConfig,
+ WorkflowCreationResult,
+)
+
logger = logging.getLogger(__name__)
@@ -89,10 +90,14 @@ def status_from_text(value: str) -> RunStatus:
return RunStatus.ERROR
-def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
+def prompt_permission(
+ request: PermissionRequest, context: dict[str, str]
+) -> PermissionRequestResult:
"""Permission handler that always approves."""
kind = request.get("kind", "unknown")
- logger.debug(f"[Permission Request: {kind}] ({context})Automatically approved for sample validation.")
+ logger.debug(
+ f"[Permission Request: {kind}] ({context})Automatically approved for sample validation."
+ )
return PermissionRequestResult(kind="approved")
@@ -108,12 +113,19 @@ class CustomAgentExecutor(Executor):
self.agent = agent
@handler
- async def handle_task(self, sample: SampleInfo, ctx: WorkflowContext[WorkerFreed | RunResult]) -> None:
+ async def handle_task(
+ self, sample: SampleInfo, ctx: WorkflowContext[WorkerFreed | RunResult]
+ ) -> None:
"""Execute one sample task and notify collector + coordinator."""
try:
- response = await self.agent.run([
- Message(role="user", text=f"Validate the following sample:\n\n{sample.relative_path}")
- ])
+ response = await self.agent.run(
+ [
+ Message(
+ role="user",
+ text=f"Validate the following sample:\n\n{sample.relative_path}",
+ )
+ ]
+ )
result_payload = parse_agent_json(response.text)
result = RunResult(
sample=sample,
@@ -146,7 +158,9 @@ class BatchCoordinatorExecutor(Executor):
self._pending: deque[SampleInfo] = deque()
self._inflight: set[str] = set()
- async def _assign_next(self, worker_id: str, ctx: WorkflowContext[SampleInfo | BatchCompletion]) -> None:
+ async def _assign_next(
+ self, worker_id: str, ctx: WorkflowContext[SampleInfo | BatchCompletion]
+ ) -> None:
if not self._pending:
# No more samples to assign
if not self._inflight:
@@ -161,7 +175,11 @@ class BatchCoordinatorExecutor(Executor):
await ctx.send_message(sample, target_id=worker_id)
@handler
- async def on_start(self, start: CoordinatorStart, ctx: WorkflowContext[SampleInfo | BatchCompletion]) -> None:
+ async def on_start(
+ self,
+ start: CoordinatorStart,
+ ctx: WorkflowContext[SampleInfo | BatchCompletion],
+ ) -> None:
"""Initialize queue and dispatch first wave of tasks."""
self._pending = deque(start.samples)
self._inflight.clear()
@@ -170,7 +188,9 @@ class BatchCoordinatorExecutor(Executor):
await self._assign_next(worker_id, ctx)
@handler
- async def on_worker_freed(self, freed: WorkerFreed, ctx: WorkflowContext[SampleInfo | BatchCompletion]) -> None:
+ async def on_worker_freed(
+ self, freed: WorkerFreed, ctx: WorkflowContext[SampleInfo | BatchCompletion]
+ ) -> None:
"""Dispatch next queued sample when a worker finishes."""
self._inflight.discard(freed.worker_id)
await self._assign_next(freed.worker_id, ctx)
@@ -184,7 +204,11 @@ class CollectorExecutor(Executor):
self._results: list[RunResult] = []
@handler
- async def on_all(self, batch_completion: BatchCompletion, ctx: WorkflowContext[Never, ExecutionResult]) -> None:
+ async def on_all(
+ self,
+ batch_completion: BatchCompletion,
+ ctx: WorkflowContext[Never, ExecutionResult],
+ ) -> None:
"""Receive all results at once and emit final output."""
await ctx.yield_output(ExecutionResult(results=self._results))
@@ -212,7 +236,9 @@ class CreateConcurrentValidationWorkflowExecutor(Executor):
print(f"\nCreating nested batched workflow for {sample_count} samples...")
if sample_count == 0:
- await ctx.send_message(WorkflowCreationResult(samples=[], workflow=None, agents=[]))
+ await ctx.send_message(
+ WorkflowCreationResult(samples=[], workflow=None, agents=[])
+ )
return
agents: list[GitHubCopilotAgent] = []
@@ -224,7 +250,10 @@ class CreateConcurrentValidationWorkflowExecutor(Executor):
id=agent_id,
name=agent_id,
instructions=AgentInstruction,
- default_options={"on_permission_request": prompt_permission, "timeout": 180}, # type: ignore
+ default_options={
+ "on_permission_request": prompt_permission,
+ "timeout": 180,
+ }, # type: ignore
)
agents.append(agent)
@@ -236,7 +265,9 @@ class CreateConcurrentValidationWorkflowExecutor(Executor):
)
collector = CollectorExecutor()
- nested_builder = WorkflowBuilder(start_executor=coordinator, output_executors=[collector])
+ nested_builder = WorkflowBuilder(
+ start_executor=coordinator, output_executors=[collector]
+ )
nested_builder.add_edge(coordinator, collector)
for worker in workers:
nested_builder.add_edge(coordinator, worker)
diff --git a/python/samples/_sample_validation/discovery.py b/python/scripts/sample_validation/discovery.py
similarity index 94%
rename from python/samples/_sample_validation/discovery.py
rename to python/scripts/sample_validation/discovery.py
index c71db32425..78eb1c9bfa 100644
--- a/python/samples/_sample_validation/discovery.py
+++ b/python/scripts/sample_validation/discovery.py
@@ -6,9 +6,10 @@ import ast
import os
from pathlib import Path
-from _sample_validation.models import DiscoveryResult, SampleInfo, ValidationConfig
from agent_framework import Executor, WorkflowContext, handler
+from sample_validation.models import DiscoveryResult, SampleInfo, ValidationConfig
+
def _is_main_entrypoint_guard(test: ast.expr) -> bool:
"""Check whether an expression is ``__name__ == '__main__'``."""
@@ -45,7 +46,10 @@ def _has_main_entrypoint_guard(path: Path) -> bool:
except Exception:
return False
- return any(isinstance(node, ast.If) and _is_main_entrypoint_guard(node.test) for node in tree.body)
+ return any(
+ isinstance(node, ast.If) and _is_main_entrypoint_guard(node.test)
+ for node in tree.body
+ )
def discover_samples(samples_dir: Path, subdir: str | None = None) -> list[SampleInfo]:
diff --git a/python/samples/_sample_validation/models.py b/python/scripts/sample_validation/models.py
similarity index 100%
rename from python/samples/_sample_validation/models.py
rename to python/scripts/sample_validation/models.py
diff --git a/python/samples/_sample_validation/report.py b/python/scripts/sample_validation/report.py
similarity index 88%
rename from python/samples/_sample_validation/report.py
rename to python/scripts/sample_validation/report.py
index 9d02d342d4..db8eddeed1 100644
--- a/python/samples/_sample_validation/report.py
+++ b/python/scripts/sample_validation/report.py
@@ -6,10 +6,11 @@ import json
from datetime import datetime
from pathlib import Path
-from _sample_validation.models import ExecutionResult, Report, RunResult, RunStatus
from agent_framework import Executor, WorkflowContext, handler
from typing_extensions import Never
+from sample_validation.models import ExecutionResult, Report, RunResult, RunStatus
+
def generate_report(results: list[RunResult]) -> Report:
"""
@@ -41,7 +42,9 @@ def generate_report(results: list[RunResult]) -> Report:
)
-def save_report(report: Report, output_dir: Path, name: str | None = None) -> tuple[Path, Path]:
+def save_report(
+ report: Report, output_dir: Path, name: str | None = None
+) -> tuple[Path, Path]:
"""
Save the report to markdown and JSON files.
@@ -81,7 +84,11 @@ def print_summary(report: Report) -> None:
print("SAMPLE VALIDATION SUMMARY")
print("=" * 80)
- if report.failure_count == 0 and report.timeout_count == 0 and report.error_count == 0:
+ if (
+ report.failure_count == 0
+ and report.timeout_count == 0
+ and report.error_count == 0
+ ):
print("[PASS] ALL SAMPLES PASSED!")
else:
print("[FAIL] SOME SAMPLES FAILED")
@@ -107,7 +114,9 @@ class GenerateReportExecutor(Executor):
super().__init__(id="generate_report")
@handler
- async def generate(self, execution: ExecutionResult, ctx: WorkflowContext[Never, Report]) -> None:
+ async def generate(
+ self, execution: ExecutionResult, ctx: WorkflowContext[Never, Report]
+ ) -> None:
"""Generate the validation report from fan-in results."""
print("\nGenerating report...")
diff --git a/python/samples/_sample_validation/run_dynamic_validation_workflow_executor.py b/python/scripts/sample_validation/run_dynamic_validation_workflow_executor.py
similarity index 77%
rename from python/samples/_sample_validation/run_dynamic_validation_workflow_executor.py
rename to python/scripts/sample_validation/run_dynamic_validation_workflow_executor.py
index c5e7c8616b..6f28dc9244 100644
--- a/python/samples/_sample_validation/run_dynamic_validation_workflow_executor.py
+++ b/python/scripts/sample_validation/run_dynamic_validation_workflow_executor.py
@@ -2,12 +2,19 @@
from collections.abc import Sequence
-from _sample_validation.const import WORKER_COMPLETED
-from _sample_validation.create_dynamic_workflow_executor import CoordinatorStart
-from _sample_validation.models import ExecutionResult, RunResult, RunStatus, SampleInfo, WorkflowCreationResult
from agent_framework import Executor, WorkflowContext, handler
from agent_framework.github import GitHubCopilotAgent
+from sample_validation.const import WORKER_COMPLETED
+from sample_validation.create_dynamic_workflow_executor import CoordinatorStart
+from sample_validation.models import (
+ ExecutionResult,
+ RunResult,
+ RunStatus,
+ SampleInfo,
+ WorkflowCreationResult,
+)
+
async def stop_agents(agents: Sequence[GitHubCopilotAgent]) -> None:
"""Stop all GitHub Copilot agents used by the nested workflow."""
@@ -25,7 +32,9 @@ class RunDynamicValidationWorkflowExecutor(Executor):
super().__init__(id="run_dynamic_workflow")
@handler
- async def run(self, creation: WorkflowCreationResult, ctx: WorkflowContext[ExecutionResult]) -> None:
+ async def run(
+ self, creation: WorkflowCreationResult, ctx: WorkflowContext[ExecutionResult]
+ ) -> None:
"""Run the nested workflow and emit execution results."""
if creation.workflow is None:
await ctx.send_message(ExecutionResult(results=[]))
@@ -37,10 +46,14 @@ class RunDynamicValidationWorkflowExecutor(Executor):
try:
remaining_sample_counts = len(creation.samples)
result: ExecutionResult | None = None
- async for event in creation.workflow.run(CoordinatorStart(samples=creation.samples), stream=True):
+ async for event in creation.workflow.run(
+ CoordinatorStart(samples=creation.samples), stream=True
+ ):
if event.type == "output" and isinstance(event.data, ExecutionResult):
result = event.data # type: ignore
- elif event.type == WORKER_COMPLETED and isinstance(event.data, SampleInfo): # type: ignore
+ elif event.type == WORKER_COMPLETED and isinstance(
+ event.data, SampleInfo
+ ): # type: ignore
remaining_sample_counts -= 1
print(
f"Completed validation for sample: {event.data.relative_path:<80} | "
diff --git a/python/samples/_sample_validation/workflow.py b/python/scripts/sample_validation/workflow.py
similarity index 72%
rename from python/samples/_sample_validation/workflow.py
rename to python/scripts/sample_validation/workflow.py
index 51cbd3d410..10187c069b 100644
--- a/python/samples/_sample_validation/workflow.py
+++ b/python/scripts/sample_validation/workflow.py
@@ -6,12 +6,17 @@ Sample Validation Workflow using Microsoft Agent Framework.
Workflow composition for sample validation.
"""
-from _sample_validation.create_dynamic_workflow_executor import CreateConcurrentValidationWorkflowExecutor
-from _sample_validation.discovery import DiscoverSamplesExecutor, ValidationConfig
-from _sample_validation.report import GenerateReportExecutor
-from _sample_validation.run_dynamic_validation_workflow_executor import RunDynamicValidationWorkflowExecutor
from agent_framework import Workflow, WorkflowBuilder
+from sample_validation.create_dynamic_workflow_executor import (
+ CreateConcurrentValidationWorkflowExecutor,
+)
+from sample_validation.discovery import DiscoverSamplesExecutor, ValidationConfig
+from sample_validation.report import GenerateReportExecutor
+from sample_validation.run_dynamic_validation_workflow_executor import (
+ RunDynamicValidationWorkflowExecutor,
+)
+
def create_validation_workflow(
config: ValidationConfig,
From ef8e18fb85ef06f3c8cd0c95e77f57373acdd672 Mon Sep 17 00:00:00 2001
From: "L. Elaine Dazzio"
Date: Mon, 2 Mar 2026 22:06:08 -0500
Subject: [PATCH 32/36] Python: fix(python): Use AgentResponse.value instead of
model_validate_json in HITL sample (#4405)
* fix(python): use AgentResponse.value instead of model_validate_json in HITL sample
Since the agent is configured with response_format=GuessOutput, the
AgentResponse already provides .value with the parsed Pydantic model.
Using .value is more idiomatic and avoids redundant JSON parsing.
Fixes #4396
* fix: add safety guard for AgentResponse.value being None
Address Copilot review feedback: .value is optional and may be None
if response_format isn't propagated through the streaming path.
Add an explicit None check with a clear error message.
---
.../guessing_game_with_human_input.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py b/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py
index 06e9a738f5..f764de6cb7 100644
--- a/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py
+++ b/python/samples/03-workflows/human-in-the-loop/guessing_game_with_human_input.py
@@ -102,12 +102,19 @@ class TurnManager(Executor):
"""Handle the agent's guess and request human guidance.
Steps:
- 1) Parse the agent's JSON into GuessOutput for robustness.
+ 1) Use .value to access the parsed structured output directly.
2) Request info with a HumanFeedbackRequest as the payload.
"""
- # Parse structured model output
- text = result.agent_response.text
- last_guess = GuessOutput.model_validate_json(text).guess
+ # Access the parsed structured model output via .value.
+ # Since the agent is configured with response_format=GuessOutput,
+ # .value returns the parsed GuessOutput instance directly.
+ agent_value = result.agent_response.value
+ if agent_value is None:
+ raise RuntimeError(
+ "AgentResponse.value is None. Ensure that the agent is invoked with "
+ "options={'response_format': GuessOutput} so structured output is available."
+ )
+ last_guess = agent_value.guess
# Craft a precise human prompt that defines higher and lower relative to the agent's guess.
prompt = (
From 869e51fdce5c27b0f617f3bc909ac8d3eebd3a29 Mon Sep 17 00:00:00 2001
From: "L. Elaine Dazzio"
Date: Mon, 2 Mar 2026 23:07:00 -0500
Subject: [PATCH 33/36] Python: fix(python): Handle thread.message.completed
event in Assistants API streaming (#4333)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: handle thread.message.completed event in Assistants API streaming
Previously, `thread.message.completed` events fell through to the
catch-all `else` branch and yielded empty `ChatResponseUpdate` objects,
silently discarding fully-resolved annotation data (file citations,
file paths, and their character-offset regions).
This commit adds a dedicated handler for `thread.message.completed`
that:
- Walks the completed ThreadMessage.content array
- Extracts text blocks with their fully-resolved annotations
- Maps FileCitationAnnotation and FilePathAnnotation to the
framework's Annotation type with proper TextSpanRegion data
- Yields a ChatResponseUpdate containing the complete text and
annotations
Fixes #4322
* test: add tests for thread.message.completed annotation handling
Tests cover:
- File citation annotation extraction
- File path annotation extraction
- Multiple annotations on a single text block
- Text-only messages (no annotations)
- Non-text blocks are skipped
- Mixed content blocks (text + image)
- Conversation ID propagation
* fix: address Copilot review - add quote field and log unrecognized annotations
- Include `quote` from `annotation.file_citation.quote` in
`additional_properties` for FileCitationAnnotation, preserving the
exact cited text snippet from the source file
- Add `else` clause to log unrecognized annotation types at debug level,
consistent with the pattern in `_responses_client.py`
- Add `import logging` and module-level logger
* test: add coverage for quote field and unrecognized annotation logging
- test_message_completed_with_file_citation_quote: verifies quote is
included in additional_properties
- test_message_completed_with_file_citation_no_quote: verifies quote
is omitted when None
- test_message_completed_unrecognized_annotation_logged: verifies
unknown annotation types are logged at debug level and skipped
* fix: address reviewer nits — logger name convention + annotation type string
Per @giles17's review:
- Use logging.getLogger('agent_framework.openai') to match module convention
- Simplify debug message to use annotation.type instead of type().__name__
* refactor: move message.completed tests into consolidated test file
Per @giles17's review: moved all tests from test_assistants_message_completed.py
into test_openai_assistants_client.py and deleted the standalone file.
* fix: resolve mypy no-redef and ruff RET504 lint errors
- Remove duplicate type annotation for 'ann' variable (no-redef)
- Return directly from fixture instead of unnecessary assignment (RET504)
* fix: rename annotation variable in completed block to fix mypy type conflict
The 'annotation' loop variable in thread.message.completed has type
FileCitationAnnotation | FilePathAnnotation, which conflicts with the
delta block's 'annotation' of type FileCitationDeltaAnnotation |
FilePathDeltaAnnotation. Renamed to 'completed_annotation' to avoid
mypy 'Incompatible types in assignment' error.
* fix: remove quote field from FileCitationAnnotation handling
---------
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
---
.../openai/_assistants_client.py | 71 +++++
.../openai/test_openai_assistants_client.py | 280 +++++++++++++++++-
2 files changed, 344 insertions(+), 7 deletions(-)
diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py
index dc05411a52..1c8aafc94e 100644
--- a/python/packages/core/agent_framework/openai/_assistants_client.py
+++ b/python/packages/core/agent_framework/openai/_assistants_client.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
+import logging
import sys
from collections.abc import (
AsyncIterable,
@@ -16,7 +17,9 @@ from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, cast
from openai import AsyncOpenAI
from openai.types.beta.threads import (
+ FileCitationAnnotation,
FileCitationDeltaAnnotation,
+ FilePathAnnotation,
FilePathDeltaAnnotation,
ImageURLContentBlockParam,
ImageURLParam,
@@ -26,6 +29,9 @@ from openai.types.beta.threads import (
TextContentBlockParam,
TextDeltaBlock,
)
+from openai.types.beta.threads import (
+ Message as ThreadMessage,
+)
from openai.types.beta.threads.run_create_params import AdditionalMessage
from openai.types.beta.threads.run_submit_tool_outputs_params import ToolOutput
from openai.types.beta.threads.runs import RunStep
@@ -72,6 +78,8 @@ else:
if TYPE_CHECKING:
from .._middleware import MiddlewareTypes
+logger = logging.getLogger("agent_framework.openai")
+
# region OpenAI Assistants Options TypedDict
@@ -610,6 +618,69 @@ class OpenAIAssistantsClient( # type: ignore[misc]
raw_representation=response.data,
response_id=response_id,
)
+ elif response.event == "thread.message.completed" and isinstance(response.data, ThreadMessage):
+ # Process completed message to extract fully resolved annotations.
+ # Delta events may carry partial/empty annotation data; the completed
+ # message contains the final text with all citation details populated.
+ completed_contents: list[Content] = []
+ for block in response.data.content:
+ if block.type != "text":
+ continue
+ text_content = Content.from_text(block.text.value)
+ if block.text.annotations:
+ text_content.annotations = []
+ for completed_annotation in block.text.annotations:
+ if isinstance(completed_annotation, FileCitationAnnotation):
+ props: dict[str, Any] = {
+ "text": completed_annotation.text,
+ }
+ ann = Annotation(
+ type="citation",
+ additional_properties=props,
+ raw_representation=completed_annotation,
+ )
+ if completed_annotation.file_citation and completed_annotation.file_citation.file_id:
+ ann["file_id"] = completed_annotation.file_citation.file_id
+ if completed_annotation.start_index is not None and completed_annotation.end_index is not None:
+ ann["annotated_regions"] = [
+ TextSpanRegion(
+ type="text_span",
+ start_index=completed_annotation.start_index,
+ end_index=completed_annotation.end_index,
+ )
+ ]
+ text_content.annotations.append(ann)
+ elif isinstance(completed_annotation, FilePathAnnotation):
+ ann = Annotation(
+ type="citation",
+ additional_properties={
+ "text": completed_annotation.text,
+ },
+ raw_representation=completed_annotation,
+ )
+ if completed_annotation.file_path and completed_annotation.file_path.file_id:
+ ann["file_id"] = completed_annotation.file_path.file_id
+ if completed_annotation.start_index is not None and completed_annotation.end_index is not None:
+ ann["annotated_regions"] = [
+ TextSpanRegion(
+ type="text_span",
+ start_index=completed_annotation.start_index,
+ end_index=completed_annotation.end_index,
+ )
+ ]
+ text_content.annotations.append(ann)
+ else:
+ logger.debug("Unparsed annotation type: %s", completed_annotation.type)
+ completed_contents.append(text_content)
+ if completed_contents:
+ yield ChatResponseUpdate(
+ role="assistant",
+ contents=completed_contents,
+ conversation_id=thread_id,
+ message_id=response_id,
+ raw_representation=response.data,
+ response_id=response_id,
+ )
elif response.event == "thread.run.requires_action" and isinstance(response.data, Run):
contents = self._parse_function_calls_from_assistants(response.data, response_id)
if contents:
diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py
index 8f39573006..1ce40eeba0 100644
--- a/python/packages/core/tests/openai/test_openai_assistants_client.py
+++ b/python/packages/core/tests/openai/test_openai_assistants_client.py
@@ -1,17 +1,12 @@
# Copyright (c) Microsoft. All rights reserved.
import json
+import logging
import os
from typing import Annotated, Any
-from unittest.mock import AsyncMock, MagicMock
+from unittest.mock import AsyncMock, MagicMock, patch
import pytest
-from openai.types.beta.threads import MessageDeltaEvent, Run, TextDeltaBlock
-from openai.types.beta.threads.file_citation_delta_annotation import FileCitationDeltaAnnotation
-from openai.types.beta.threads.file_path_delta_annotation import FilePathDeltaAnnotation
-from openai.types.beta.threads.runs import RunStep
-from pydantic import Field
-
from agent_framework import (
Agent,
AgentResponse,
@@ -25,6 +20,20 @@ from agent_framework import (
tool,
)
from agent_framework.openai import OpenAIAssistantsClient
+from openai.types.beta.threads import (
+ FileCitationAnnotation,
+ FilePathAnnotation,
+ MessageDeltaEvent,
+ Run,
+ TextDeltaBlock,
+)
+from openai.types.beta.threads import (
+ Message as ThreadMessage,
+)
+from openai.types.beta.threads.file_citation_delta_annotation import FileCitationDeltaAnnotation
+from openai.types.beta.threads.file_path_delta_annotation import FilePathDeltaAnnotation
+from openai.types.beta.threads.runs import RunStep
+from pydantic import Field
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"),
@@ -1566,3 +1575,260 @@ def test_with_callable_api_key() -> None:
assert client.model_id == "gpt-4o"
# OpenAI SDK now manages callable API keys internally
assert client.client is not None
+
+
+# region thread.message.completed helpers
+
+
+def _make_stream_event(event: str, data: Any) -> MagicMock:
+ """Create a mock stream event."""
+ mock = MagicMock()
+ mock.event = event
+ mock.data = data
+ return mock
+
+
+def _make_text_block(text_value: str, annotations: list | None = None) -> MagicMock:
+ """Create a mock TextContentBlock with optional annotations."""
+ block = MagicMock()
+ block.type = "text"
+ block.text = MagicMock()
+ block.text.value = text_value
+ block.text.annotations = annotations or []
+ return block
+
+
+def _make_image_block() -> MagicMock:
+ """Create a mock ImageContentBlock (non-text block)."""
+ block = MagicMock()
+ block.type = "image_file"
+ return block
+
+
+def _make_file_citation_annotation(
+ text: str = "【4:0†source】",
+ file_id: str = "file-abc123",
+ start_index: int = 10,
+ end_index: int = 24,
+) -> MagicMock:
+ """Create a mock FileCitationAnnotation."""
+ annotation = MagicMock(spec=FileCitationAnnotation)
+ annotation.text = text
+ annotation.start_index = start_index
+ annotation.end_index = end_index
+ annotation.file_citation = MagicMock()
+ annotation.file_citation.file_id = file_id
+ return annotation
+
+
+def _make_file_path_annotation(
+ text: str = "sandbox:/file.csv",
+ file_id: str = "file-xyz789",
+ start_index: int = 5,
+ end_index: int = 22,
+) -> MagicMock:
+ """Create a mock FilePathAnnotation."""
+ annotation = MagicMock(spec=FilePathAnnotation)
+ annotation.text = text
+ annotation.start_index = start_index
+ annotation.end_index = end_index
+ annotation.file_path = MagicMock()
+ annotation.file_path.file_id = file_id
+ return annotation
+
+
+def _make_unknown_annotation() -> MagicMock:
+ """Create a mock annotation of an unrecognized type."""
+ annotation = MagicMock()
+ annotation.__class__.__name__ = "FutureAnnotationType"
+ return annotation
+
+
+def _make_thread_message(content_blocks: list) -> MagicMock:
+ """Create a mock ThreadMessage."""
+ msg = MagicMock(spec=ThreadMessage)
+ msg.content = content_blocks
+ return msg
+
+
+async def _collect_updates(client, stream_events, thread_id="thread_123"):
+ """Helper to collect ChatResponseUpdate objects from _process_stream_events."""
+
+ class MockAsyncStream:
+ def __init__(self, events):
+ self._events = events
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, *args):
+ pass
+
+ def __aiter__(self):
+ return self
+
+ async def __anext__(self):
+ if not self._events:
+ raise StopAsyncIteration
+ return self._events.pop(0)
+
+ mock_stream = MockAsyncStream(list(stream_events))
+ results = []
+ async for update in client._process_stream_events(mock_stream, thread_id):
+ results.append(update)
+ return results
+
+
+# endregion
+
+
+class TestMessageCompletedAnnotations:
+ """Tests for thread.message.completed event handling."""
+
+ @pytest.fixture
+ def client(self):
+ """Create a client instance for testing."""
+ with patch.object(OpenAIAssistantsClient, "__init__", lambda self, **kw: None):
+ return object.__new__(OpenAIAssistantsClient)
+
+ @pytest.mark.asyncio
+ async def test_message_completed_with_file_citation(self, client):
+ """Verify file citation annotations are extracted from completed messages."""
+ citation = _make_file_citation_annotation(
+ text="【4:0†source】", file_id="file-abc123", start_index=10, end_index=24
+ )
+ text_block = _make_text_block("Some text with a citation【4:0†source】", [citation])
+ msg = _make_thread_message([text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events)
+
+ # Should yield exactly one update for the completed message
+ assert len(updates) == 1
+ update = updates[0]
+ assert update.role == "assistant"
+ assert len(update.contents) == 1
+
+ content = update.contents[0]
+ assert content.text == "Some text with a citation【4:0†source】"
+ assert content.annotations is not None
+ assert len(content.annotations) == 1
+
+ ann = content.annotations[0]
+ assert ann["type"] == "citation"
+ assert ann["file_id"] == "file-abc123"
+ assert ann["annotated_regions"][0]["start_index"] == 10
+ assert ann["annotated_regions"][0]["end_index"] == 24
+
+
+
+ @pytest.mark.asyncio
+ async def test_message_completed_with_file_path(self, client):
+ """Verify file path annotations are extracted from completed messages."""
+ file_path = _make_file_path_annotation(
+ text="sandbox:/output.csv", file_id="file-xyz789", start_index=0, end_index=19
+ )
+ text_block = _make_text_block("sandbox:/output.csv", [file_path])
+ msg = _make_thread_message([text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events)
+
+ assert len(updates) == 1
+ content = updates[0].contents[0]
+ assert content.annotations is not None
+ assert len(content.annotations) == 1
+
+ ann = content.annotations[0]
+ assert ann["type"] == "citation"
+ assert ann["file_id"] == "file-xyz789"
+ assert ann["annotated_regions"][0]["start_index"] == 0
+ assert ann["annotated_regions"][0]["end_index"] == 19
+
+ @pytest.mark.asyncio
+ async def test_message_completed_multiple_annotations(self, client):
+ """Verify multiple annotations on a single text block are all captured."""
+ cit1 = _make_file_citation_annotation(text="【1†src】", file_id="file-a", start_index=5, end_index=12)
+ cit2 = _make_file_citation_annotation(text="【2†src】", file_id="file-b", start_index=20, end_index=27)
+ text_block = _make_text_block("Hello【1†src】world【2†src】", [cit1, cit2])
+ msg = _make_thread_message([text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events)
+
+ assert len(updates) == 1
+ assert len(updates[0].contents[0].annotations) == 2
+ assert updates[0].contents[0].annotations[0]["file_id"] == "file-a"
+ assert updates[0].contents[0].annotations[1]["file_id"] == "file-b"
+
+ @pytest.mark.asyncio
+ async def test_message_completed_no_annotations(self, client):
+ """Verify text-only completed messages produce content without annotations."""
+ text_block = _make_text_block("Plain text response")
+ msg = _make_thread_message([text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events)
+
+ assert len(updates) == 1
+ content = updates[0].contents[0]
+ assert content.text == "Plain text response"
+ assert content.annotations is None or len(content.annotations) == 0
+
+ @pytest.mark.asyncio
+ async def test_message_completed_skips_non_text_blocks(self, client):
+ """Verify non-text content blocks (e.g., image_file) are skipped."""
+ image_block = _make_image_block()
+ msg = _make_thread_message([image_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events)
+
+ # No text blocks → no update yielded
+ assert len(updates) == 0
+
+ @pytest.mark.asyncio
+ async def test_message_completed_mixed_blocks(self, client):
+ """Verify only text blocks are processed in mixed-content messages."""
+ text_block = _make_text_block("Text content here")
+ image_block = _make_image_block()
+ msg = _make_thread_message([image_block, text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events)
+
+ assert len(updates) == 1
+ assert len(updates[0].contents) == 1
+ assert updates[0].contents[0].text == "Text content here"
+
+ @pytest.mark.asyncio
+ async def test_message_completed_conversation_id_preserved(self, client):
+ """Verify the thread_id is correctly propagated as conversation_id."""
+ text_block = _make_text_block("Response text")
+ msg = _make_thread_message([text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ updates = await _collect_updates(client, events, thread_id="thread_custom_456")
+
+ assert len(updates) == 1
+ assert updates[0].conversation_id == "thread_custom_456"
+
+ @pytest.mark.asyncio
+ async def test_message_completed_unrecognized_annotation_logged(self, client, caplog):
+ """Verify unrecognized annotation types are logged at debug level and skipped."""
+ unknown_ann = _make_unknown_annotation()
+ citation = _make_file_citation_annotation(text="【1†src】", file_id="file-a", start_index=0, end_index=7)
+ text_block = _make_text_block("Text【1†src】", [unknown_ann, citation])
+ msg = _make_thread_message([text_block])
+
+ events = [_make_stream_event("thread.message.completed", msg)]
+ with caplog.at_level(logging.DEBUG, logger="agent_framework.openai"):
+ updates = await _collect_updates(client, events)
+
+ # The known citation should still be processed
+ assert len(updates) == 1
+ assert len(updates[0].contents[0].annotations) == 1
+ assert updates[0].contents[0].annotations[0]["file_id"] == "file-a"
+
+ # The unrecognized annotation should have been logged
+ assert any("Unparsed annotation type" in record.message for record in caplog.records)
From debec5208cea036da6ef0b264a7d8bf42d43b757 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 08:51:38 +0000
Subject: [PATCH 34/36] Python: Add auto_retry.py sample for rate limit
handling (#4223)
* Initial plan
* Add auto_retry.py sample for rate limiting handling
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
* Update auto_retry sample to use class decorator for get_response retries
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
* Address review feedback on auto_retry sample header and wrapper usage
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
* Restore class-decorator retry sample and address reviewer feedback
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
---
python/samples/02-agents/auto_retry.py | 250 +++++++++++++++++++++++++
1 file changed, 250 insertions(+)
create mode 100644 python/samples/02-agents/auto_retry.py
diff --git a/python/samples/02-agents/auto_retry.py b/python/samples/02-agents/auto_retry.py
new file mode 100644
index 0000000000..7c985bd0c1
--- /dev/null
+++ b/python/samples/02-agents/auto_retry.py
@@ -0,0 +1,250 @@
+# /// script
+# requires-python = ">=3.10"
+# dependencies = [
+# "agent-framework",
+# "tenacity",
+# ]
+# ///
+# Run with any PEP 723 compatible runner, e.g.:
+# uv run samples/02-agents/auto_retry.py
+
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+import logging
+from collections.abc import Awaitable, Callable
+from typing import Any, TypeVar, cast
+
+from agent_framework import ChatContext, ChatMiddleware, SupportsChatGetResponse, chat_middleware
+from agent_framework.azure import AzureOpenAIChatClient
+from azure.identity import AzureCliCredential
+from dotenv import load_dotenv
+from openai import RateLimitError
+from tenacity import (
+ AsyncRetrying,
+ before_sleep_log,
+ retry,
+ retry_if_exception_type,
+ stop_after_attempt,
+ wait_exponential,
+)
+
+# Load environment variables from .env file
+load_dotenv()
+
+"""
+Auto-Retry Rate Limiting Sample
+
+Every model inference API enforces rate limits, so production agents need retry logic
+to handle 429 responses gracefully. This sample shows two ways to add automatic retry
+using the `tenacity` library, keeping your application code free of boilerplate.
+
+Approach 1 – Class decorator
+ Apply a class decorator to any client type implementing
+ SupportsChatGetResponse. The decorator patches get_response() with retry
+ behavior. Non-streaming responses are retried; streaming is returned as-is
+ (streaming retry requires more delicate handling).
+
+Approach 2 – Chat middleware
+ Register middleware on the agent that catches RateLimitError raised inside
+ call_next() and retries the entire request pipeline. Two styles are shown:
+ a) Class-based middleware (ChatMiddleware subclass)
+ b) Function-based middleware (@chat_middleware decorator)
+
+Both approaches use the same tenacity primitives:
+ - stop_after_attempt – cap the total number of tries
+ - wait_exponential – exponential back-off between retries
+ - retry_if_exception_type(RateLimitError) – only retry on 429 errors
+ - before_sleep_log – log each retry attempt at WARNING level
+"""
+
+logger = logging.getLogger(__name__)
+
+RETRY_ATTEMPTS = 3
+
+# =============================================================================
+# Approach 1: Class decorator
+# =============================================================================
+
+
+ChatClientT = TypeVar("ChatClientT", bound=SupportsChatGetResponse[Any])
+
+
+def with_rate_limit_retry(*, retry_attempts: int = RETRY_ATTEMPTS) -> Callable[[type[ChatClientT]], type[ChatClientT]]:
+ """Class decorator that adds non-streaming retry behavior to get_response()."""
+
+ def decorator(client_cls: type[ChatClientT]) -> type[ChatClientT]:
+ original_get_response = client_cls.get_response
+
+ def get_response_with_retry(self, *args, **kwargs): # type: ignore[no-untyped-def]
+ stream = kwargs.get("stream", False)
+
+ if stream:
+ # Streaming retry is more complex; fall back to the original behaviour.
+ return original_get_response(self, *args, **kwargs)
+
+ async def _with_retry():
+ async for attempt in AsyncRetrying(
+ stop=stop_after_attempt(retry_attempts),
+ wait=wait_exponential(multiplier=1, min=4, max=10),
+ retry=retry_if_exception_type(RateLimitError),
+ reraise=True,
+ before_sleep=before_sleep_log(logger, logging.WARNING),
+ ):
+ with attempt:
+ return await original_get_response(self, *args, **kwargs)
+ return None
+
+ return _with_retry()
+
+ client_cls.get_response = cast(Any, get_response_with_retry)
+ return client_cls
+
+ return decorator
+
+
+@with_rate_limit_retry()
+class RetryingAzureOpenAIChatClient(AzureOpenAIChatClient):
+ """Azure OpenAI Chat client with class-decorator-based retry behavior."""
+
+
+# =============================================================================
+# Approach 2a: Class-based chat middleware
+# =============================================================================
+
+
+class RateLimitRetryMiddleware(ChatMiddleware):
+ """Chat middleware that retries the full request pipeline on rate limit errors.
+
+ Register this middleware on an agent (or at the run level) to automatically
+ retry any call_next() invocation that raises RateLimitError.
+ """
+
+ def __init__(self, *, max_attempts: int = RETRY_ATTEMPTS) -> None:
+ """Initialize with the maximum number of retry attempts."""
+ self.max_attempts = max_attempts
+
+ async def process(
+ self,
+ context: ChatContext,
+ call_next: Callable[[], Awaitable[None]],
+ ) -> None:
+ """Retry call_next() on rate limit errors with exponential back-off."""
+ async for attempt in AsyncRetrying(
+ stop=stop_after_attempt(self.max_attempts),
+ wait=wait_exponential(multiplier=1, min=4, max=10),
+ retry=retry_if_exception_type(RateLimitError),
+ reraise=True,
+ before_sleep=before_sleep_log(logger, logging.WARNING),
+ ):
+ with attempt:
+ await call_next()
+
+
+# =============================================================================
+# Approach 2b: Function-based chat middleware
+# =============================================================================
+
+
+@chat_middleware
+async def rate_limit_retry_middleware(
+ context: ChatContext,
+ call_next: Callable[[], Awaitable[None]],
+) -> None:
+ """Function-based chat middleware that retries on rate limit errors.
+
+ Wrap call_next() with a tenacity @retry decorator so any RateLimitError
+ raised during model inference triggers an automatic retry with exponential
+ back-off.
+ """
+
+ @retry(
+ stop=stop_after_attempt(RETRY_ATTEMPTS),
+ wait=wait_exponential(multiplier=1, min=4, max=10),
+ retry=retry_if_exception_type(RateLimitError),
+ reraise=True,
+ before_sleep=before_sleep_log(logger, logging.WARNING),
+ )
+ async def _call_next_with_retry() -> None:
+ await call_next()
+
+ await _call_next_with_retry()
+
+
+# =============================================================================
+# Demo
+# =============================================================================
+
+
+async def class_decorator_example() -> None:
+ """Demonstrate Approach 1: class decorator on a chat client type."""
+ print("\n" + "=" * 60)
+ print("Approach 1: Class decorator (applied to client type)")
+ print("=" * 60)
+
+ # For authentication, run `az login` command in terminal or replace
+ # AzureCliCredential with your preferred authentication option.
+ agent = RetryingAzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
+ instructions="You are a helpful assistant.",
+ )
+
+ query = "Say hello!"
+ print(f"User: {query}")
+ result = await agent.run(query)
+ print(f"Agent: {result.text}")
+
+
+async def class_based_middleware_example() -> None:
+ """Demonstrate Approach 2a: class-based chat middleware."""
+ print("\n" + "=" * 60)
+ print("Approach 2a: Class-based chat middleware")
+ print("=" * 60)
+
+ # For authentication, run `az login` command in terminal or replace
+ # AzureCliCredential with your preferred authentication option.
+ agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
+ instructions="You are a helpful assistant.",
+ middleware=[RateLimitRetryMiddleware(max_attempts=3)],
+ )
+
+ query = "Say hello!"
+ print(f"User: {query}")
+ result = await agent.run(query)
+ print(f"Agent: {result.text}")
+
+
+async def function_based_middleware_example() -> None:
+ """Demonstrate Approach 2b: function-based chat middleware."""
+ print("\n" + "=" * 60)
+ print("Approach 2b: Function-based chat middleware")
+ print("=" * 60)
+
+ # For authentication, run `az login` command in terminal or replace
+ # AzureCliCredential with your preferred authentication option.
+ agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
+ instructions="You are a helpful assistant.",
+ middleware=[rate_limit_retry_middleware],
+ )
+
+ query = "Say hello!"
+ print(f"User: {query}")
+ result = await agent.run(query)
+ print(f"Agent: {result.text}")
+
+
+async def main() -> None:
+ """Run all auto-retry examples."""
+ print("=== Auto-Retry Rate Limiting Sample ===")
+ print(
+ "Demonstrates two approaches for automatic retry on rate limit (429) errors.\n"
+ "Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME (and optionally\n"
+ "AZURE_OPENAI_API_KEY) before running, or populate a .env file."
+ )
+
+ await class_decorator_example()
+ await class_based_middleware_example()
+ await function_based_middleware_example()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
From 2e9319359b5a20e3eae3d27471805754aba833c7 Mon Sep 17 00:00:00 2001
From: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Date: Tue, 3 Mar 2026 19:08:40 +0900
Subject: [PATCH 35/36] Python: Add regression tests for Entry JoinExecutor
Workflow.Inputs initialization (#4335)
* Python: Add regression tests for #3948 - Entry JoinExecutor initializes Workflow.Inputs
Add tests verifying that when workflow.run() is called with a dict or string
input, the Entry node (JoinExecutor with kind: 'Entry') correctly initializes
Workflow.Inputs via _ensure_state_initialized so that:
- Expressions like =inputs.age resolve to the correct value
- Conditions like =Local.age < 13 evaluate based on actual input (not blank/0)
- String inputs populate both inputs.input and System.LastMessage.Text
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Fix D420 and RUF070 lint errors across packages
* Revert _workflow.py yield-inside-context-manager changes
Moving yield inside `with _framework_event_origin()` blocks in the
async generator causes ContextVar token reset failures on Python 3.12
Windows. The token stays un-reset while the generator is suspended,
and async generator finalization in a different contextvars.Context
triggers ValueError, corrupting OpenTelemetry span state and causing
test_span_creation_and_attributes to see leaked spans.
Keep yields outside the context manager blocks to ensure tokens are
reset immediately before the generator suspends.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../tests/test_workflow_factory.py | 69 +++++++++++++++++++
1 file changed, 69 insertions(+)
diff --git a/python/packages/declarative/tests/test_workflow_factory.py b/python/packages/declarative/tests/test_workflow_factory.py
index 25c1249a50..f08f5993e5 100644
--- a/python/packages/declarative/tests/test_workflow_factory.py
+++ b/python/packages/declarative/tests/test_workflow_factory.py
@@ -159,6 +159,75 @@ actions:
_text_outputs = [str(o) for o in outputs if isinstance(o, str) or hasattr(o, "data")] # noqa: F841
assert any("Condition was true" in str(o) for o in outputs)
+ @pytest.mark.asyncio
+ async def test_entry_join_executor_initializes_workflow_inputs(self):
+ """Regression test for #3948: Entry JoinExecutor must initialize Workflow.Inputs.
+
+ When workflow.run() is called with a dict input, the Entry node (JoinExecutor
+ with kind: 'Entry') must call _ensure_state_initialized so that Workflow.Inputs
+ is populated. Without this, expressions like =inputs.age resolve to blank and
+ conditions like =Local.age < 13 always evaluate as true (blank treated as 0).
+ """
+ factory = WorkflowFactory()
+ workflow = factory.create_workflow_from_yaml("""
+name: entry-inputs-test
+actions:
+ - kind: SetValue
+ id: get_age
+ path: Local.age
+ value: =inputs.age
+ - kind: If
+ id: check_age
+ condition: =Local.age < 13
+ then:
+ - kind: SendActivity
+ activity:
+ text: child
+ else:
+ - kind: SendActivity
+ activity:
+ text: adult
+""")
+
+ # age=8 -> child branch
+ result_child = await workflow.run({"age": 8})
+ outputs_child = result_child.get_outputs()
+ assert any("child" in str(o) for o in outputs_child), f"Expected 'child' for age=8 but got: {outputs_child}"
+ assert not any("adult" in str(o) for o in outputs_child), (
+ f"Did not expect 'adult' for age=8 but got: {outputs_child}"
+ )
+
+ # age=25 -> adult branch (bug: blank treated as 0 made this always go to child)
+ result_adult = await workflow.run({"age": 25})
+ outputs_adult = result_adult.get_outputs()
+ assert any("adult" in str(o) for o in outputs_adult), f"Expected 'adult' for age=25 but got: {outputs_adult}"
+ assert not any("child" in str(o) for o in outputs_adult), (
+ f"Did not expect 'child' for age=25 but got: {outputs_adult}"
+ )
+
+ @pytest.mark.asyncio
+ async def test_entry_join_executor_initializes_workflow_inputs_string(self):
+ """Regression test for #3948: Entry JoinExecutor must initialize Workflow.Inputs for string input.
+
+ When workflow.run() is called with a string input, Workflow.Inputs.input and
+ System.LastMessage.Text should be set correctly.
+ """
+ factory = WorkflowFactory()
+ workflow = factory.create_workflow_from_yaml("""
+name: entry-string-inputs-test
+actions:
+ - kind: SetValue
+ path: Local.msg
+ value: =inputs.input
+ - kind: SendActivity
+ activity:
+ text: =Local.msg
+""")
+
+ result = await workflow.run("hello-world")
+ outputs = result.get_outputs()
+ assert any("hello-world" in str(o) for o in outputs), f"Expected 'hello-world' in outputs but got: {outputs}"
+
class TestWorkflowFactoryAgentRegistration:
"""Tests for agent registration."""
From 945933c3517c896dee137952af539070978a827d Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Tue, 3 Mar 2026 11:02:02 +0000
Subject: [PATCH 36/36] [BREAKING] Add response filter for store input in
*Providers (#4327)
* Add response filter for store input for *Providers
* Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Address feedback
* Apply suggestions from code review
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
---
.../01-get-started/04_memory/Program.cs | 1 -
.../Program.cs | 2 +-
.../Program.cs | 2 +-
.../Program.cs | 1 -
.../Program.cs | 4 +-
.../AIContextProvider.cs | 24 +++--
.../ChatHistoryProvider.cs | 18 ++--
.../InMemoryChatHistoryProvider.cs | 3 +-
.../InMemoryChatHistoryProviderOptions.cs | 14 ++-
.../MessageAIContextProvider.cs | 8 +-
.../CosmosChatHistoryProvider.cs | 24 +++--
.../FoundryMemoryProvider.cs | 2 +-
.../FoundryMemoryProviderOptions.cs | 11 ++-
.../Microsoft.Agents.AI.Mem0/Mem0Provider.cs | 2 +-
.../Mem0ProviderOptions.cs | 11 ++-
.../WorkflowChatHistoryProvider.cs | 1 -
.../Memory/ChatHistoryMemoryProvider.cs | 2 +-
.../ChatHistoryMemoryProviderOptions.cs | 10 +-
.../Microsoft.Agents.AI/TextSearchProvider.cs | 2 +-
.../TextSearchProviderOptions.cs | 11 ++-
.../AIContextProviderTests.cs | 98 ++++++++++++++++++-
.../ChatHistoryProviderTests.cs | 17 +++-
.../InMemoryChatHistoryProviderTests.cs | 2 +-
.../CosmosChatHistoryProviderTests.cs | 2 +-
.../Mem0ProviderTests.cs | 2 +-
.../ChatClient/ChatClientAgentOptionsTests.cs | 8 +-
.../ChatClient/ChatClientAgentTests.cs | 22 ++---
...hatClientAgent_BackgroundResponsesTests.cs | 16 +--
...tClientAgent_ChatHistoryManagementTests.cs | 8 +-
.../Data/TextSearchProviderTests.cs | 2 +-
.../Memory/ChatHistoryMemoryProviderTests.cs | 2 +-
31 files changed, 249 insertions(+), 83 deletions(-)
diff --git a/dotnet/samples/01-get-started/04_memory/Program.cs b/dotnet/samples/01-get-started/04_memory/Program.cs
index fa6940f5fd..3705e64f3a 100644
--- a/dotnet/samples/01-get-started/04_memory/Program.cs
+++ b/dotnet/samples/01-get-started/04_memory/Program.cs
@@ -92,7 +92,6 @@ namespace SampleApp
private readonly IChatClient _chatClient;
public UserInfoMemory(IChatClient chatClient, Func? stateInitializer = null)
- : base(null, null)
{
this._sessionState = new ProviderSessionState(
stateInitializer ?? (_ => new UserInfo()),
diff --git a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
index c04601d940..e1db6d3f4f 100644
--- a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
@@ -73,7 +73,7 @@ AIAgent agent = azureOpenAIClient
// We also want to maintain that exclusion here.
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
- StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
+ StorageInputRequestMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
}),
});
diff --git a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs
index 0c299a1445..0f65121c04 100644
--- a/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs
+++ b/dotnet/samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/Program.cs
@@ -80,7 +80,7 @@ AIAgent agent = azureOpenAIClient
// You may choose to persist the TextSearchProvider messages, if you want the search output to be provided to the model in future interactions as well.
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions()
{
- StorageInputMessageFilter = msgs => msgs.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider)
+ StorageInputRequestMessageFilter = msgs => msgs.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider)
})
});
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step04_3rdPartyChatHistoryStorage/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step04_3rdPartyChatHistoryStorage/Program.cs
index cbcf14157e..63fa5c0751 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step04_3rdPartyChatHistoryStorage/Program.cs
+++ b/dotnet/samples/02-agents/Agents/Agent_Step04_3rdPartyChatHistoryStorage/Program.cs
@@ -85,7 +85,6 @@ namespace SampleApp
VectorStore vectorStore,
Func? stateInitializer = null,
string? stateKey = null)
- : base(provideOutputMessageFilter: null, storeInputMessageFilter: null)
{
this._sessionState = new ProviderSessionState(
stateInitializer ?? (_ => new State(Guid.NewGuid().ToString("N"))),
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Program.cs
index a341abe8cd..e3913c9f0e 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Program.cs
+++ b/dotnet/samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Program.cs
@@ -49,11 +49,11 @@ AIAgent agent = new AzureOpenAIClient(
""" },
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
- // Use StorageInputMessageFilter to provide a custom filter for messages stored in chat history.
+ // Use StorageInputRequestMessageFilter to provide a custom filter for request messages stored in chat history.
// By default the chat history provider will store all messages, except for those that came from chat history in the first place.
// In this case, we want to also exclude messages that came from AI context providers.
// You may want to store these messages, depending on their content and your requirements.
- StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
+ StorageInputRequestMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.AIContextProvider && m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory)
}),
// Add multiple AI context providers: one that maintains a todo list and one that provides upcoming calendar entries.
// The agent will call each provider in sequence, accumulating context from each.
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
index 7ac4eed18c..82e5f2c360 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs
@@ -33,18 +33,23 @@ public abstract class AIContextProvider
{
private static IEnumerable DefaultExternalOnlyFilter(IEnumerable messages)
=> messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);
+ private static IEnumerable DefaultNoopFilter(IEnumerable messages)
+ => messages;
///
/// Initializes a new instance of the class.
///
/// An optional filter function to apply to input messages before providing context via . If not set, defaults to including only messages.
- /// An optional filter function to apply to request messages before storing context via . If not set, defaults to including only messages.
+ /// An optional filter function to apply to request messages before storing context via . If not set, defaults to including only messages.
+ /// An optional filter function to apply to response messages before storing context via . If not set, defaults to a no-op filter that includes all response messages.
protected AIContextProvider(
Func, IEnumerable>? provideInputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
{
this.ProvideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter;
- this.StoreInputMessageFilter = storeInputMessageFilter ?? DefaultExternalOnlyFilter;
+ this.StoreInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExternalOnlyFilter;
+ this.StoreInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter;
}
///
@@ -55,7 +60,12 @@ public abstract class AIContextProvider
///
/// Gets the filter function to apply to request messages before storing context via .
///
- protected Func, IEnumerable> StoreInputMessageFilter { get; }
+ protected Func, IEnumerable> StoreInputRequestMessageFilter { get; }
+
+ ///
+ /// Gets the filter function to apply to response messages before storing context via .
+ ///
+ protected Func, IEnumerable> StoreInputResponseMessageFilter { get; }
///
/// Gets the key used to store the provider state in the .
@@ -245,8 +255,10 @@ public abstract class AIContextProvider
///
///
/// The default implementation of this method skips execution for any invocation failures,
- /// filters the request messages using the configured store-input message filter
+ /// filters the request messages using the configured store-input request message filter
/// (which defaults to including only messages),
+ /// filters the response messages using the configured store-input response message filter
+ /// (which defaults to a no-op, so all response messages are processed),
/// and calls to process the invocation results.
/// For most scenarios, overriding is sufficient to process invocation results,
/// while still benefiting from the default error handling and filtering behavior.
@@ -261,7 +273,7 @@ public abstract class AIContextProvider
return default;
}
- var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputMessageFilter(context.RequestMessages), context.ResponseMessages!);
+ var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputRequestMessageFilter(context.RequestMessages), this.StoreInputResponseMessageFilter(context.ResponseMessages!));
return this.StoreAIContextAsync(subContext, cancellationToken);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs
index ad3f3aacfb..df9ff0069e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs
@@ -42,21 +42,27 @@ public abstract class ChatHistoryProvider
{
private static IEnumerable DefaultExcludeChatHistoryFilter(IEnumerable messages)
=> messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
+ private static IEnumerable DefaultNoopFilter(IEnumerable messages)
+ => messages;
private readonly Func, IEnumerable>? _provideOutputMessageFilter;
- private readonly Func, IEnumerable> _storeInputMessageFilter;
+ private readonly Func, IEnumerable> _storeInputRequestMessageFilter;
+ private readonly Func, IEnumerable> _storeInputResponseMessageFilter;
///
/// Initializes a new instance of the class.
///
/// An optional filter function to apply to messages when retrieving them from the chat history.
- /// An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to request messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to response messages before storing them in the chat history. If not set, defaults to a no-op filter that includes all response messages.
protected ChatHistoryProvider(
Func, IEnumerable>? provideOutputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
{
this._provideOutputMessageFilter = provideOutputMessageFilter;
- this._storeInputMessageFilter = storeInputMessageFilter ?? DefaultExcludeChatHistoryFilter;
+ this._storeInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExcludeChatHistoryFilter;
+ this._storeInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter;
}
///
@@ -216,7 +222,7 @@ public abstract class ChatHistoryProvider
/// To check if the invocation was successful, inspect the property.
///
///
- /// The default implementation of this method, skips execution for any invocation failures, filters messages using the optional storage input message filter
+ /// The default implementation of this method, skips execution for any invocation failures, filters messages using the optional storage input request and response message filters
/// and calls to store new chat history messages.
/// For most scenarios, overriding is sufficient to store chat history messages, while still benefiting from the default error handling and filtering behavior.
/// However, for scenarios that require more control over error handling or message filtering, overriding this method allows you to directly control the messages that are stored for the invocation.
@@ -229,7 +235,7 @@ public abstract class ChatHistoryProvider
return default;
}
- var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputMessageFilter(context.RequestMessages), context.ResponseMessages!);
+ var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputRequestMessageFilter(context.RequestMessages), this._storeInputResponseMessageFilter(context.ResponseMessages!));
return this.StoreChatHistoryAsync(subContext, cancellationToken);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs
index 12e935b23e..e09dd6b0a0 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs
@@ -38,7 +38,8 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options = null)
: base(
options?.ProvideOutputMessageFilter,
- options?.StorageInputMessageFilter)
+ options?.StorageInputRequestMessageFilter,
+ options?.StorageInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState(
options?.StateInitializer ?? (_ => new State()),
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs
index ba24f55ded..873619d484 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs
@@ -59,7 +59,19 @@ public sealed class InMemoryChatHistoryProviderOptions
/// Depending on your requirements, you could provide a different filter, that also excludes
/// messages from e.g. AI context providers.
///
- public Func, IEnumerable>? StorageInputMessageFilter { get; set; }
+ public Func, IEnumerable>? StorageInputRequestMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter function applied to response messages before they are added to storage
+ /// during .
+ ///
+ ///
+ /// When , no filtering is applied to response messages before they are stored.
+ /// If you want to avoid persisting certain messages (for example, those with
+ /// source type or produced by AI context providers),
+ /// provide a filter that returns only the messages you want to keep.
+ ///
+ public Func, IEnumerable>? StorageInputResponseMessageFilter { get; set; }
///
/// Gets or sets an optional filter function applied to messages produced by this provider
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs
index 24264e0e47..c5f367443c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs
@@ -34,11 +34,13 @@ public abstract class MessageAIContextProvider : AIContextProvider
/// Initializes a new instance of the class.
///
/// An optional filter function to apply to input messages before providing messages via . If not set, defaults to including only messages.
- /// An optional filter function to apply to request messages before storing messages via . If not set, defaults to including only messages.
+ /// An optional filter function to apply to request messages before storing messages via . If not set, defaults to including only messages.
+ /// An optional filter function to apply to response messages before storing messages via . If not set, defaults to including all response messages (no filtering).
protected MessageAIContextProvider(
Func, IEnumerable>? provideInputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
- : base(provideInputMessageFilter, storeInputMessageFilter)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
+ : base(provideInputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}
diff --git a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs
index f1670fbb84..afaa59ee53 100644
--- a/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs
@@ -87,7 +87,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
/// Whether this instance owns the CosmosClient and should dispose it.
/// An optional key to use for storing the state in the .
/// An optional filter function to apply to messages when retrieving them from the chat history.
- /// An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to request messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to response messages before storing them in the chat history. If not set, defaults to storing all response messages.
/// Thrown when or is .
/// Thrown when any string parameter is null or whitespace.
public CosmosChatHistoryProvider(
@@ -98,8 +99,9 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
bool ownsClient = false,
string? stateKey = null,
Func, IEnumerable>? provideOutputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
- : base(provideOutputMessageFilter, storeInputMessageFilter)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
+ : base(provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState(
Throw.IfNull(stateInitializer),
@@ -123,7 +125,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
/// A delegate that initializes the provider state on the first invocation.
/// An optional key to use for storing the state in the .
/// An optional filter function to apply to messages when retrieving them from the chat history.
- /// An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to request messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to response messages before storing them in the chat history. If not set, defaults to storing all response messages.
/// Thrown when any required parameter is null.
/// Thrown when any string parameter is null or whitespace.
public CosmosChatHistoryProvider(
@@ -133,8 +136,9 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
Func stateInitializer,
string? stateKey = null,
Func, IEnumerable>? provideOutputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
- : this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputMessageFilter)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
+ : this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}
@@ -148,7 +152,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
/// A delegate that initializes the provider state on the first invocation.
/// An optional key to use for storing the state in the .
/// An optional filter function to apply to messages when retrieving them from the chat history.
- /// An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to request messages before storing them in the chat history. If not set, defaults to excluding messages with source type .
+ /// An optional filter function to apply to response messages before storing them in the chat history. If not set, defaults to storing all response messages.
/// Thrown when any required parameter is null.
/// Thrown when any string parameter is null or whitespace.
public CosmosChatHistoryProvider(
@@ -159,8 +164,9 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
Func stateInitializer,
string? stateKey = null,
Func, IEnumerable>? provideOutputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
- : this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputMessageFilter)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
+ : this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
index 9ffeda3fb5..0f7041e834 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -59,7 +59,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider
Func stateInitializer,
FoundryMemoryProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
- : base(options?.SearchInputMessageFilter, options?.StorageInputMessageFilter)
+ : base(options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter)
{
Throw.IfNull(client);
Throw.IfNullOrWhitespace(memoryStoreName);
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
index 482e14db82..870fe1d271 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
@@ -63,5 +63,14 @@ public sealed class FoundryMemoryProviderOptions
/// When , the provider defaults to including only
/// messages.
///
- public Func, IEnumerable>? StorageInputMessageFilter { get; set; }
+ public Func, IEnumerable>? StorageInputRequestMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter function applied to response messages when determining which messages to
+ /// extract memories from during .
+ ///
+ ///
+ /// When , the provider does not filter response messages and includes all messages.
+ ///
+ public Func, IEnumerable>? StorageInputResponseMessageFilter { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
index 1924bc0da2..1e325b5683 100644
--- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
@@ -52,7 +52,7 @@ public sealed class Mem0Provider : MessageAIContextProvider
///
///
public Mem0Provider(HttpClient httpClient, Func stateInitializer, Mem0ProviderOptions? options = null, ILoggerFactory? loggerFactory = null)
- : base(options?.SearchInputMessageFilter, options?.StorageInputMessageFilter)
+ : base(options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState(
ValidateStateInitializer(Throw.IfNull(stateInitializer)),
diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs
index f7d14028d9..4a3a16712f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs
@@ -47,5 +47,14 @@ public sealed class Mem0ProviderOptions
/// When , the provider defaults to including only
/// messages.
///
- public Func, IEnumerable>? StorageInputMessageFilter { get; set; }
+ public Func, IEnumerable>? StorageInputRequestMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter function applied to response messages when determining which messages to
+ /// extract memories from during .
+ ///
+ ///
+ /// When , the provider applies no filtering and includes all response messages.
+ ///
+ public Func, IEnumerable>? StorageInputResponseMessageFilter { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowChatHistoryProvider.cs
index b9d5f3ae49..1fd42f923e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowChatHistoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowChatHistoryProvider.cs
@@ -22,7 +22,6 @@ internal sealed class WorkflowChatHistoryProvider : ChatHistoryProvider
/// and source generated serializers are required, or Native AOT / Trimming is required.
///
public WorkflowChatHistoryProvider(JsonSerializerOptions? jsonSerializerOptions = null)
- : base(provideOutputMessageFilter: null, storeInputMessageFilter: null)
{
this._sessionState = new ProviderSessionState(
_ => new StoreState(),
diff --git a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs
index 7905db74b8..cd59d1aaa3 100644
--- a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs
@@ -88,7 +88,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo
Func stateInitializer,
ChatHistoryMemoryProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
- : base(options?.SearchInputMessageFilter, options?.StorageInputMessageFilter)
+ : base(options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState(
Throw.IfNull(stateInitializer),
diff --git a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs
index 6c92a426f3..a9c5b93928 100644
--- a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs
@@ -75,8 +75,16 @@ public sealed class ChatHistoryMemoryProviderOptions
/// When , the provider defaults to including only
/// messages.
///
- public Func, IEnumerable>? StorageInputMessageFilter { get; set; }
+ public Func, IEnumerable>? StorageInputRequestMessageFilter { get; set; }
+ ///
+ /// Gets or sets an optional filter function applied to response messages when storing recent chat history
+ /// during .
+ ///
+ ///
+ /// When , the provider does not apply any filtering and includes all response messages.
+ ///
+ public Func, IEnumerable>? StorageInputResponseMessageFilter { get; set; }
///
/// Behavior choices for the provider.
///
diff --git a/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs b/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs
index dd62b0eb9b..df53729fce 100644
--- a/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs
@@ -61,7 +61,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider
Func>> searchAsync,
TextSearchProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
- : base(options?.SearchInputMessageFilter, options?.StorageInputMessageFilter)
+ : base(options?.SearchInputMessageFilter, options?.StorageInputRequestMessageFilter, options?.StorageInputResponseMessageFilter)
{
this._sessionState = new ProviderSessionState(
_ => new TextSearchProviderState(),
diff --git a/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs
index 837470b776..879e34121d 100644
--- a/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs
@@ -86,7 +86,16 @@ public sealed class TextSearchProviderOptions
/// When , the provider defaults to including only
/// messages.
///
- public Func, IEnumerable>? StorageInputMessageFilter { get; set; }
+ public Func, IEnumerable>? StorageInputRequestMessageFilter { get; set; }
+
+ ///
+ /// Gets or sets an optional filter function applied to response messages when updating the recent message
+ /// memory during .
+ ///
+ ///
+ /// When , the provider defaults to including all messages.
+ ///
+ public Func, IEnumerable>? StorageInputResponseMessageFilter { get; set; }
///
/// Gets or sets the list of types to filter recent messages to
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIContextProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIContextProviderTests.cs
index 811f9a3216..0e664d1ac9 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIContextProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIContextProviderTests.cs
@@ -543,7 +543,9 @@ public class AIContextProviderTests
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("External", storedRequest[0].Text);
- Assert.Same(responseMessages, provider.LastStoredContext.ResponseMessages);
+ var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
+ Assert.Single(storedResponse);
+ Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
@@ -565,13 +567,14 @@ public class AIContextProviderTests
{
// Arrange - filter that only keeps System messages
var provider = new TestAIContextProvider(
- storeInputMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System));
+ storeInputRequestMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System),
+ storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.Assistant));
var messages = new[]
{
new ChatMessage(ChatRole.User, "User msg"),
new ChatMessage(ChatRole.System, "System msg")
};
- var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, messages, [new ChatMessage(ChatRole.Assistant, "Response")]);
+ var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, messages, [new ChatMessage(ChatRole.Assistant, "Response"), new ChatMessage(ChatRole.Tool, "Response")]);
// Act
await provider.InvokedAsync(context);
@@ -581,6 +584,9 @@ public class AIContextProviderTests
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("System msg", storedRequest[0].Text);
+ var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
+ Assert.Single(storedResponse);
+ Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
@@ -605,6 +611,87 @@ public class AIContextProviderTests
Assert.Equal("External", storedRequest[0].Text);
}
+ [Fact]
+ public async Task InvokedCoreAsync_DefaultResponseFilterPassesAllResponseMessagesAsync()
+ {
+ // Arrange
+ var provider = new TestAIContextProvider();
+ var requestMessages = new[] { new ChatMessage(ChatRole.User, "Request") };
+ var externalResponse = new ChatMessage(ChatRole.Assistant, "ExternalResp");
+ var historyResponse = new ChatMessage(ChatRole.Assistant, "HistoryResp")
+ .WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
+ var contextResponse = new ChatMessage(ChatRole.Assistant, "ContextResp")
+ .WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "src");
+ var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, [externalResponse, historyResponse, contextResponse]);
+
+ // Act
+ await provider.InvokedAsync(context);
+
+ // Assert - default response filter is a noop, so all response messages are kept
+ Assert.NotNull(provider.LastStoredContext);
+ var storedResponse = provider.LastStoredContext!.ResponseMessages!.ToList();
+ Assert.Equal(3, storedResponse.Count);
+ Assert.Equal("ExternalResp", storedResponse[0].Text);
+ Assert.Equal("HistoryResp", storedResponse[1].Text);
+ Assert.Equal("ContextResp", storedResponse[2].Text);
+ }
+
+ [Fact]
+ public async Task InvokedCoreAsync_UsesCustomResponseFilterAsync()
+ {
+ // Arrange - response filter that only keeps Assistant messages with specific text
+ var provider = new TestAIContextProvider(
+ storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Text == "Keep"));
+ var requestMessages = new[] { new ChatMessage(ChatRole.User, "Request") };
+ var responseMessages = new[]
+ {
+ new ChatMessage(ChatRole.Assistant, "Keep"),
+ new ChatMessage(ChatRole.Assistant, "Drop")
+ };
+ var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, responseMessages);
+
+ // Act
+ await provider.InvokedAsync(context);
+
+ // Assert
+ Assert.NotNull(provider.LastStoredContext);
+ var storedResponse = provider.LastStoredContext!.ResponseMessages!.ToList();
+ Assert.Single(storedResponse);
+ Assert.Equal("Keep", storedResponse[0].Text);
+ }
+
+ [Fact]
+ public async Task InvokedCoreAsync_RequestAndResponseFiltersOperateIndependentlyAsync()
+ {
+ // Arrange - different filters for request and response
+ var provider = new TestAIContextProvider(
+ storeInputRequestMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System),
+ storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Text == "Resp1"));
+ var requestMessages = new[]
+ {
+ new ChatMessage(ChatRole.User, "User"),
+ new ChatMessage(ChatRole.System, "System")
+ };
+ var responseMessages = new[]
+ {
+ new ChatMessage(ChatRole.Assistant, "Resp1"),
+ new ChatMessage(ChatRole.Assistant, "Resp2")
+ };
+ var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, responseMessages);
+
+ // Act
+ await provider.InvokedAsync(context);
+
+ // Assert - request filter kept only System, response filter kept only Resp1
+ Assert.NotNull(provider.LastStoredContext);
+ var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
+ Assert.Single(storedRequest);
+ Assert.Equal("System", storedRequest[0].Text);
+ var storedResponse = provider.LastStoredContext!.ResponseMessages!.ToList();
+ Assert.Single(storedResponse);
+ Assert.Equal("Resp1", storedResponse[0].Text);
+ }
+
#endregion
private sealed class TestAIContextProvider : AIContextProvider
@@ -620,8 +707,9 @@ public class AIContextProviderTests
AIContext? provideContext = null,
bool captureFilteredContext = false,
Func, IEnumerable>? provideInputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
- : base(provideInputMessageFilter, storeInputMessageFilter)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
+ : base(provideInputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
this._provideContext = provideContext;
this._captureFilteredContext = captureFilteredContext;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatHistoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatHistoryProviderTests.cs
index 5df661f009..ed4e4823b3 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatHistoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/ChatHistoryProviderTests.cs
@@ -439,7 +439,9 @@ public class ChatHistoryProviderTests
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("External", storedRequest[0].Text);
- Assert.Same(responseMessages, provider.LastStoredContext.ResponseMessages);
+ var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
+ Assert.Single(storedResponse);
+ Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
@@ -461,13 +463,14 @@ public class ChatHistoryProviderTests
{
// Arrange - filter that only keeps System messages
var provider = new TestChatHistoryProvider(
- storeInputMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System));
+ storeInputRequestMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System),
+ storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.Assistant));
var messages = new[]
{
new ChatMessage(ChatRole.User, "User msg"),
new ChatMessage(ChatRole.System, "System msg")
};
- var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, [new ChatMessage(ChatRole.Assistant, "Response")]);
+ var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, [new ChatMessage(ChatRole.Assistant, "Response"), new ChatMessage(ChatRole.Tool, "Response")]);
// Act
await provider.InvokedAsync(context);
@@ -477,6 +480,9 @@ public class ChatHistoryProviderTests
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("System msg", storedRequest[0].Text);
+ var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
+ Assert.Single(storedResponse);
+ Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
@@ -529,8 +535,9 @@ public class ChatHistoryProviderTests
public TestChatHistoryProvider(
IEnumerable? provideMessages = null,
Func, IEnumerable>? provideOutputMessageFilter = null,
- Func, IEnumerable>? storeInputMessageFilter = null)
- : base(provideOutputMessageFilter, storeInputMessageFilter)
+ Func, IEnumerable>? storeInputRequestMessageFilter = null,
+ Func, IEnumerable>? storeInputResponseMessageFilter = null)
+ : base(provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
this._provideMessages = provideMessages;
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs
index ebe1131ab7..147ceaf195 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/InMemoryChatHistoryProviderTests.cs
@@ -418,7 +418,7 @@ public class InMemoryChatHistoryProviderTests
var session = CreateMockSession();
var provider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
- StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
+ StorageInputRequestMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
});
var requestMessages = new List
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs
index a790b19cdd..736bf7f026 100644
--- a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs
@@ -1004,7 +1004,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
s_testDatabaseId,
TestContainerId,
_ => new CosmosChatHistoryProvider.State(conversationId),
- storeInputMessageFilter: messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External));
+ storeInputRequestMessageFilter: messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External));
var requestMessages = new[]
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs
index 02e18f324e..9f9de9127b 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs
@@ -530,7 +530,7 @@ public sealed class Mem0ProviderTests : IDisposable
var mockSession = new TestAgentSession();
var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope), options: new Mem0ProviderOptions
{
- StorageInputMessageFilter = messages => messages // No filtering - store everything
+ StorageInputRequestMessageFilter = messages => messages // No filtering - store everything
});
var requestMessages = new List
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs
index 1798afb433..9f8894d5c2 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentOptionsTests.cs
@@ -119,8 +119,8 @@ public class ChatClientAgentOptionsTests
const string Description = "Test description";
var tools = new List { AIFunctionFactory.Create(() => "test") };
- var mockChatHistoryProvider = new Mock(null, null).Object;
- var mockAIContextProvider = new Mock(null, null).Object;
+ var mockChatHistoryProvider = new Mock(null, null, null).Object;
+ var mockAIContextProvider = new Mock(null, null, null).Object;
var original = new ChatClientAgentOptions()
{
@@ -161,8 +161,8 @@ public class ChatClientAgentOptionsTests
public void Clone_WithoutProvidingChatOptions_ClonesCorrectly()
{
// Arrange
- var mockChatHistoryProvider = new Mock(null, null).Object;
- var mockAIContextProvider = new Mock(null, null).Object;
+ var mockChatHistoryProvider = new Mock(null, null, null).Object;
+ var mockAIContextProvider = new Mock(null, null, null).Object;
var original = new ChatClientAgentOptions
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
index 12446c89c0..9713a91c2c 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
@@ -488,7 +488,7 @@ public partial class ChatClientAgentTests
})
.ReturnsAsync(new ChatResponse(responseMessages));
- var mockProvider = new Mock(null, null);
+ var mockProvider = new Mock(null, null, null);
mockProvider
.Protected()
.Setup>("InvokingCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
@@ -559,7 +559,7 @@ public partial class ChatClientAgentTests
It.IsAny()))
.Throws(new InvalidOperationException("downstream failure"));
- var mockProvider = new Mock(null, null);
+ var mockProvider = new Mock(null, null, null);
mockProvider
.Protected()
.Setup>("InvokingCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
@@ -617,7 +617,7 @@ public partial class ChatClientAgentTests
})
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- var mockProvider = new Mock(null, null);
+ var mockProvider = new Mock(null, null, null);
mockProvider
.Protected()
.Setup>("InvokingCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
@@ -677,7 +677,7 @@ public partial class ChatClientAgentTests
.ReturnsAsync(new ChatResponse(responseMessages));
// Provider 1: adds a system message and a tool
- var mockProvider1 = new Mock(null, null);
+ var mockProvider1 = new Mock(null, null, null);
mockProvider1.SetupGet(p => p.StateKey).Returns("Provider1");
mockProvider1
.Protected()
@@ -696,7 +696,7 @@ public partial class ChatClientAgentTests
// Provider 2: adds another system message and verifies it receives accumulated context from provider 1
AIContext? provider2ReceivedContext = null;
- var mockProvider2 = new Mock(null, null);
+ var mockProvider2 = new Mock(null, null, null);
mockProvider2.SetupGet(p => p.StateKey).Returns("Provider2");
mockProvider2
.Protected()
@@ -784,7 +784,7 @@ public partial class ChatClientAgentTests
It.IsAny()))
.ThrowsAsync(new InvalidOperationException("downstream failure"));
- var mockProvider1 = new Mock(null, null);
+ var mockProvider1 = new Mock(null, null, null);
mockProvider1.SetupGet(p => p.StateKey).Returns("Provider1");
mockProvider1
.Protected()
@@ -801,7 +801,7 @@ public partial class ChatClientAgentTests
.Setup("InvokedCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
.Returns(new ValueTask());
- var mockProvider2 = new Mock(null, null);
+ var mockProvider2 = new Mock(null, null, null);
mockProvider2.SetupGet(p => p.StateKey).Returns("Provider2");
mockProvider2
.Protected()
@@ -869,7 +869,7 @@ public partial class ChatClientAgentTests
})
.Returns(ToAsyncEnumerableAsync(responseUpdates));
- var mockProvider1 = new Mock(null, null);
+ var mockProvider1 = new Mock(null, null, null);
mockProvider1.SetupGet(p => p.StateKey).Returns("Provider1");
mockProvider1
.Protected()
@@ -886,7 +886,7 @@ public partial class ChatClientAgentTests
.Setup("InvokedCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
.Returns(new ValueTask());
- var mockProvider2 = new Mock(null, null);
+ var mockProvider2 = new Mock(null, null, null);
mockProvider2.SetupGet(p => p.StateKey).Returns("Provider2");
mockProvider2
.Protected()
@@ -1828,7 +1828,7 @@ public partial class ChatClientAgentTests
})
.Returns(ToAsyncEnumerableAsync(responseUpdates));
- var mockProvider = new Mock(null, null);
+ var mockProvider = new Mock(null, null, null);
mockProvider
.Protected()
.Setup>("InvokingCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
@@ -1907,7 +1907,7 @@ public partial class ChatClientAgentTests
It.IsAny()))
.Throws(new InvalidOperationException("downstream failure"));
- var mockProvider = new Mock(null, null);
+ var mockProvider = new Mock(null, null, null);
mockProvider
.Protected()
.Setup>("InvokingCoreAsync", ItExpr.IsAny(), ItExpr.IsAny())
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs
index 64835f2b2f..ebb1791dfd 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs
@@ -338,7 +338,7 @@ public class ChatClientAgent_BackgroundResponsesTests
List capturedMessages = [];
// Create a mock chat history provider that would normally provide messages
- var mockChatHistoryProvider = new Mock(null, null);
+ var mockChatHistoryProvider = new Mock(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKey).Returns("ChatHistoryProvider");
mockChatHistoryProvider
.Protected()
@@ -346,7 +346,7 @@ public class ChatClientAgent_BackgroundResponsesTests
.ReturnsAsync([new(ChatRole.User, "Message from chat history provider")]);
// Create a mock AI context provider that would normally provide context
- var mockContextProvider = new Mock(null, null);
+ var mockContextProvider = new Mock(null, null, null);
mockContextProvider.SetupGet(p => p.StateKey).Returns("Provider1");
mockContextProvider
.Protected()
@@ -407,7 +407,7 @@ public class ChatClientAgent_BackgroundResponsesTests
List capturedMessages = [];
// Create a mock chat history provider that would normally provide messages
- var mockChatHistoryProvider = new Mock(null, null);
+ var mockChatHistoryProvider = new Mock(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKey).Returns("ChatHistoryProvider");
mockChatHistoryProvider
.Protected()
@@ -415,7 +415,7 @@ public class ChatClientAgent_BackgroundResponsesTests
.ReturnsAsync([new(ChatRole.User, "Message from chat history provider")]);
// Create a mock AI context provider that would normally provide context
- var mockContextProvider = new Mock(null, null);
+ var mockContextProvider = new Mock(null, null, null);
mockContextProvider.SetupGet(p => p.StateKey).Returns("Provider1");
mockContextProvider
.Protected()
@@ -638,7 +638,7 @@ public class ChatClientAgent_BackgroundResponsesTests
.Returns(ToAsyncEnumerableAsync(returnUpdates));
List capturedMessagesAddedToProvider = [];
- var mockChatHistoryProvider = new Mock(null, null);
+ var mockChatHistoryProvider = new Mock(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKey).Returns("ChatHistoryProvider");
mockChatHistoryProvider
.Protected()
@@ -647,7 +647,7 @@ public class ChatClientAgent_BackgroundResponsesTests
.Returns(new ValueTask());
AIContextProvider.InvokedContext? capturedInvokedContext = null;
- var mockContextProvider = new Mock(null, null);
+ var mockContextProvider = new Mock(null, null, null);
mockContextProvider.SetupGet(p => p.StateKey).Returns("Provider1");
mockContextProvider
.Protected()
@@ -702,7 +702,7 @@ public class ChatClientAgent_BackgroundResponsesTests
.Returns(ToAsyncEnumerableAsync(Array.Empty()));
List capturedMessagesAddedToProvider = [];
- var mockChatHistoryProvider = new Mock(null, null);
+ var mockChatHistoryProvider = new Mock(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKey).Returns("ChatHistoryProvider");
mockChatHistoryProvider
.Protected()
@@ -711,7 +711,7 @@ public class ChatClientAgent_BackgroundResponsesTests
.Returns(new ValueTask());
AIContextProvider.InvokedContext? capturedInvokedContext = null;
- var mockContextProvider = new Mock(null, null);
+ var mockContextProvider = new Mock(null, null, null);
mockContextProvider.SetupGet(p => p.StateKey).Returns("Provider1");
mockContextProvider
.Protected()
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs
index 4d8326269a..59062cf49f 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatHistoryManagementTests.cs
@@ -185,7 +185,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
It.IsAny(),
It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
- Mock mockChatHistoryProvider = new(null, null);
+ Mock mockChatHistoryProvider = new(null, null, null);
mockChatHistoryProvider
.Protected()
.Setup>>("InvokingCoreAsync", ItExpr.IsAny