From 9a47620f645142e55a1b5c3da56db57e96f896f5 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:41:49 +0000 Subject: [PATCH 1/6] .NET: Update Hosted Samples References to latest beta.11 (#4853) * Bump HostedAgents samples to AgentFramework beta.11 and pass credential to UseFoundryTools Update all 8 HostedAgents samples: - Azure.AI.AgentServer.AgentFramework -> 1.0.0-beta.11 - Microsoft.Agents.AI.OpenAI -> 1.0.0-rc4 - Microsoft.Agents.AI/AzureAI/Workflows -> 1.0.0-rc4 - Azure.AI.Projects -> 2.0.0-beta.1 - Fix Workflow.AsAgent() -> AsAIAgent() in FoundryMultiAgent - Pass credential to UseFoundryTools in AgentWithTools (resolves #56802) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove AgentWithTools sample (UseFoundryTools no longer supported) Remove the AgentWithTools hosted agent sample as the UseFoundryTools backend is no longer supported. Updated HostedAgents README and solution file to remove all references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix AgentWithHostedMCP: downgrade Azure.AI.OpenAI to 2.8.0-beta.1 for rc4 compatibility Azure.AI.OpenAI 2.9.0-beta.1 has breaking changes (GetResponsesClient no longer accepts deployment name, ResponsesClient.Model removed) that are incompatible with Microsoft.Agents.AI.OpenAI rc4. Pin to 2.8.0-beta.1 and use GetResponsesClient(deploymentName).AsAIAgent() pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 1 - .../AgentThreadAndHITL.csproj | 4 +- .../AgentWithHostedMCP.csproj | 9 ++- .../AgentWithHostedMCP/Program.cs | 3 +- .../AgentWithLocalTools.csproj | 6 +- .../AgentWithTextSearchRag.csproj | 6 +- .../AgentWithTools/AgentWithTools.csproj | 68 ------------------- .../HostedAgents/AgentWithTools/Dockerfile | 20 ------ .../HostedAgents/AgentWithTools/Program.cs | 46 ------------- .../HostedAgents/AgentWithTools/README.md | 45 ------------ .../HostedAgents/AgentWithTools/agent.yaml | 31 --------- .../AgentWithTools/run-requests.http | 30 -------- .../AgentsInWorkflows.csproj | 4 +- .../FoundryMultiAgent.csproj | 12 ++-- .../HostedAgents/FoundryMultiAgent/Program.cs | 2 +- .../FoundrySingleAgent.csproj | 8 +-- .../05-end-to-end/HostedAgents/README.md | 33 +-------- 17 files changed, 29 insertions(+), 299 deletions(-) delete mode 100644 dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj delete mode 100644 dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Dockerfile delete mode 100644 dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Program.cs delete mode 100644 dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/README.md delete mode 100644 dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/agent.yaml delete mode 100644 dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/run-requests.http diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 576d2c5c54..af7ca9f0be 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -309,7 +309,6 @@ - diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj b/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj index 1afc7a7cec..a56157fe9d 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj @@ -36,10 +36,10 @@ - + - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj index c0e14e74b8..4e46f10c11 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj @@ -1,4 +1,4 @@ - + Exe @@ -35,11 +35,10 @@ - - + + - - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs index 8559269dff..b7b610b663 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs +++ b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/Program.cs @@ -31,8 +31,7 @@ AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAd AIAgent agent = new AzureOpenAIClient( new Uri(endpoint), new DefaultAzureCredential()) - .GetResponsesClient() - .AsIChatClient(deploymentName) + .GetResponsesClient(deploymentName) .AsAIAgent( instructions: "You answer questions by searching the Microsoft Learn content only.", name: "MicrosoftLearnAgent", diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj index ccda3156e5..b7970f8c5f 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj @@ -36,11 +36,11 @@ - - + + - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj index 19e5015912..7789abd315 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj @@ -1,4 +1,4 @@ - + Exe @@ -35,10 +35,10 @@ - + - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj deleted file mode 100644 index 19e5015912..0000000000 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj +++ /dev/null @@ -1,68 +0,0 @@ - - - - Exe - net10.0 - - enable - enable - - - false - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Dockerfile b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Dockerfile deleted file mode 100644 index c9f39f9574..0000000000 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build the application -FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build -WORKDIR /src - -# Copy files from the current directory on the host to the working directory in the container -COPY . . - -RUN dotnet restore -RUN dotnet build -c Release --no-restore -RUN dotnet publish -c Release --no-build -o /app -f net10.0 - -# Run the application -FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final -WORKDIR /app - -# Copy everything needed to run the app from the "build" stage. -COPY --from=build /app . - -EXPOSE 8088 -ENTRYPOINT ["dotnet", "AgentWithTools.dll"] diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Program.cs deleted file mode 100644 index f564a0d8d3..0000000000 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/Program.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates how to use Foundry tools (MCP and code interpreter) -// with an AI agent hosted using the Azure AI AgentServer SDK. - -using Azure.AI.AgentServer.AgentFramework.Extensions; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; -string toolConnectionId = Environment.GetEnvironmentVariable("MCP_TOOL_CONNECTION_ID") ?? throw new InvalidOperationException("MCP_TOOL_CONNECTION_ID is not set."); - -// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. -// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid -// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -DefaultAzureCredential credential = new(); - -IChatClient chatClient = new AzureOpenAIClient(new Uri(openAiEndpoint), credential) - .GetChatClient(deploymentName) - .AsIChatClient() - .AsBuilder() - .UseFoundryTools(new { type = "mcp", project_connection_id = toolConnectionId }, new { type = "code_interpreter" }) - .UseOpenTelemetry(sourceName: "Agents", configure: (cfg) => cfg.EnableSensitiveData = true) - .Build(); - -AIAgent agent = chatClient.AsAIAgent( - name: "AgentWithTools", - instructions: @"You are a helpful assistant with access to tools for fetching Microsoft documentation. - - IMPORTANT: When the user asks about Microsoft Learn articles or documentation: - 1. You MUST use the microsoft_docs_fetch tool to retrieve the actual content - 2. Do NOT rely on your training data - 3. Always fetch the latest information from the provided URL - - Available tools: - - microsoft_docs_fetch: Fetches and converts Microsoft Learn documentation - - microsoft_docs_search: Searches Microsoft/Azure documentation - - microsoft_code_sample_search: Searches for code examples") - .AsBuilder() - .UseOpenTelemetry(sourceName: "Agents", configure: (cfg) => cfg.EnableSensitiveData = true) - .Build(); - -await agent.RunAIAgentAsync(telemetrySourceName: "Agents"); diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/README.md b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/README.md deleted file mode 100644 index 55333f9940..0000000000 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# What this sample demonstrates - -This sample demonstrates how to use Foundry tools with an AI agent via the `UseFoundryTools` extension. The agent is configured with two tool types: an MCP (Model Context Protocol) connection for fetching Microsoft Learn documentation and a code interpreter for running code when needed. - -Key features: - -- Configuring Foundry tools using `UseFoundryTools` with MCP and code interpreter -- Connecting to an external MCP tool via a Foundry project connection -- Using `DefaultAzureCredential` for Azure authentication -- OpenTelemetry instrumentation for both the chat client and agent - -> For common prerequisites and setup instructions, see the [Hosted Agent Samples README](../README.md). - -## Prerequisites - -In addition to the common prerequisites: - -1. An **Azure AI Foundry project** with a chat model deployed (e.g., `gpt-5.2`, `gpt-4o-mini`) -2. The **Azure AI Developer** role assigned on the Foundry resource (includes the `agents/write` data action required by `UseFoundryTools`) -3. An **MCP tool connection** configured in your Foundry project pointing to `https://learn.microsoft.com/api/mcp` - -## Environment Variables - -In addition to the common environment variables in the root README: - -```powershell -# Your Azure AI Foundry project endpoint (required by UseFoundryTools) -$env:AZURE_AI_PROJECT_ENDPOINT="https://your-resource.services.ai.azure.com/api/projects/your-project" - -# Chat model deployment name (defaults to gpt-4o-mini if not set) -$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" - -# The MCP tool connection name (just the name, not the full ARM resource ID) -$env:MCP_TOOL_CONNECTION_ID="SampleMCPTool" -``` - -## How It Works - -1. An `AzureOpenAIClient` is created with `DefaultAzureCredential` and used to get a chat client -2. The chat client is wrapped with `UseFoundryTools` which registers two Foundry tool types: - - **MCP connection**: Connects to an external MCP server (Microsoft Learn) via the project connection name, providing documentation fetch and search capabilities - - **Code interpreter**: Allows the agent to execute code snippets when needed -3. `UseFoundryTools` resolves the connection using `AZURE_AI_PROJECT_ENDPOINT` internally -4. A `ChatClientAgent` is created with instructions guiding it to use the MCP tools for documentation queries -5. The agent is hosted using `RunAIAgentAsync` which exposes the OpenAI Responses-compatible API endpoint diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/agent.yaml b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/agent.yaml deleted file mode 100644 index 5d2b1f8d8d..0000000000 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/agent.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: AgentWithTools -displayName: "Agent with Tools" -description: > - An AI agent that uses Foundry tools (MCP and code interpreter) with Azure OpenAI. - The agent can fetch Microsoft Learn documentation and run code when needed. -metadata: - authors: - - Microsoft Agent Framework Team - tags: - - Azure AI AgentServer - - Microsoft Agent Framework - - Tools - - MCP - - Code Interpreter -template: - kind: hosted - name: AgentWithTools - protocols: - - protocol: responses - version: v1 - environment_variables: - - name: AZURE_OPENAI_ENDPOINT - value: ${AZURE_OPENAI_ENDPOINT} - - name: AZURE_OPENAI_DEPLOYMENT_NAME - value: gpt-4o-mini - - name: MCP_TOOL_CONNECTION_ID - value: ${MCP_TOOL_CONNECTION_ID} -resources: - - name: "gpt-4o-mini" - kind: model - id: gpt-4o-mini diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/run-requests.http b/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/run-requests.http deleted file mode 100644 index 22a37ff54e..0000000000 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentWithTools/run-requests.http +++ /dev/null @@ -1,30 +0,0 @@ -@host = http://localhost:8088 -@endpoint = {{host}}/responses - -### Health Check -GET {{host}}/readiness - -### Simple string input -POST {{endpoint}} -Content-Type: application/json -{ - "input": "Please use the microsoft_docs_fetch tool to fetch and summarize the Microsoft Learn article at https://learn.microsoft.com/azure/ai-services/openai/overview" -} - -### Explicit input -POST {{endpoint}} -Content-Type: application/json -{ - "input": [ - { - "type": "message", - "role": "user", - "content": [ - { - "type": "input_text", - "text": "Please use the microsoft_docs_fetch tool to fetch and summarize the Microsoft Learn article at https://learn.microsoft.com/azure/ai-services/openai/overview" - } - ] - } - ] -} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj index 3b3af40664..7789abd315 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj @@ -35,10 +35,10 @@ - + - + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj index b2fb41ac5e..e8c7a434b0 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj @@ -1,4 +1,4 @@ - + Exe net10.0 @@ -33,12 +33,12 @@ - - + + - - - + + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs index 6947c85e3f..8d21eef20a 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs @@ -41,7 +41,7 @@ try .Build(); Console.WriteLine("Starting Writer-Reviewer Workflow Agent Server on http://localhost:8088"); - await workflow.AsAgent().RunAIAgentAsync(); + await workflow.AsAIAgent().RunAIAgentAsync(); } finally { diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj index 756f3d30ee..70df458d90 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj @@ -33,11 +33,11 @@ - - + + - - + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/README.md b/dotnet/samples/05-end-to-end/HostedAgents/README.md index a36a9bddd1..919aa4b580 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/README.md +++ b/dotnet/samples/05-end-to-end/HostedAgents/README.md @@ -6,7 +6,6 @@ These samples demonstrate how to build and host AI agents using the [Azure AI Ag | Sample | Description | |--------|-------------| -| [`AgentWithTools`](./AgentWithTools/) | Foundry tools (MCP + code interpreter) via `UseFoundryTools` | | [`AgentWithLocalTools`](./AgentWithLocalTools/) | Local C# function tool execution (Seattle hotel search) | | [`AgentThreadAndHITL`](./AgentThreadAndHITL/) | Human-in-the-loop with `ApprovalRequiredAIFunction` and thread persistence | | [`AgentWithHostedMCP`](./AgentWithHostedMCP/) | Hosted MCP server tool (Microsoft Learn search) | @@ -40,19 +39,18 @@ Most samples require one or more of these environment variables: |----------|---------|-------------| | `AZURE_OPENAI_ENDPOINT` | Most samples | Your Azure OpenAI resource endpoint URL | | `AZURE_OPENAI_DEPLOYMENT_NAME` | Most samples | Chat model deployment name (defaults to `gpt-4o-mini`) | -| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Azure AI Foundry project endpoint | -| `MCP_TOOL_CONNECTION_ID` | AgentWithTools | Foundry MCP tool connection name | +| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Azure AI Foundry project endpoint | | `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Chat model deployment name (defaults to `gpt-4o-mini`) | See each sample's README for the specific variables required. ## Azure AI Foundry Setup (for samples that use Foundry) -Some samples (`AgentWithTools`, `AgentWithLocalTools`) connect to an Azure AI Foundry project. If you're using these samples, you'll need additional setup. +Some samples (`AgentWithLocalTools`, `FoundrySingleAgent`, `FoundryMultiAgent`) connect to an Azure AI Foundry project. If you're using these samples, you'll need additional setup. ### Azure AI Developer Role -The `UseFoundryTools` extension requires the **Azure AI Developer** role on the Cognitive Services resource. Even if you created the project, you may not have this role by default. +Some Foundry operations require the **Azure AI Developer** role on the Cognitive Services resource. Even if you created the project, you may not have this role by default. ```powershell az role assignment create ` @@ -65,23 +63,6 @@ az role assignment create ` For more details on permissions, see [Azure AI Foundry Permissions](https://aka.ms/FoundryPermissions). -### Creating an MCP Tool Connection - -The `AgentWithTools` sample requires an MCP tool connection configured in your Foundry project: - -1. Go to the [Azure AI Foundry portal](https://ai.azure.com) -2. Navigate to your project -3. Go to **Connected resources** → **+ New connection** → **Model Context Protocol tool** -4. Fill in: - - **Name**: `SampleMCPTool` (or any name you prefer) - - **Remote MCP Server endpoint**: `https://learn.microsoft.com/api/mcp` - - **Authentication**: `Unauthenticated` -5. Click **Connect** - -The connection **name** (e.g., `SampleMCPTool`) is used as the `MCP_TOOL_CONNECTION_ID` environment variable. - -> **Important**: Use only the connection **name**, not the full ARM resource ID. - ## Running a Sample Each sample runs as a standalone hosted agent on `http://localhost:8088/`: @@ -110,14 +91,6 @@ Each sample includes a `Dockerfile` and `agent.yaml` for deployment. To deploy y Assign the **Azure AI Developer** role to your user. See [Azure AI Developer Role](#azure-ai-developer-role) above. -### `Project connection ... was not found` - -Make sure `MCP_TOOL_CONNECTION_ID` contains only the connection **name** (e.g., `SampleMCPTool`), not the full ARM resource ID path. - -### `AZURE_AI_PROJECT_ENDPOINT must be set` - -The `UseFoundryTools` extension requires `AZURE_AI_PROJECT_ENDPOINT`. Set it to your Foundry project endpoint (e.g., `https://your-resource.services.ai.azure.com/api/projects/your-project`). - ### Multi-framework error when running `dotnet run` If you see "Your project targets multiple frameworks", specify the framework: From 5070c67d0e368383527541cbd6220fa3934e260f Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:06:07 +0000 Subject: [PATCH 2/6] Mark constructors of InvokingContext and InvokedContext as experimental (#4851) --- .../Microsoft.Agents.AI.Abstractions/AIContextProvider.cs | 8 ++++++++ .../ChatHistoryProvider.cs | 6 ++++++ .../MessageAIContextProvider.cs | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs index 9c1286c9b9..641825d1dc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -147,6 +149,7 @@ public abstract class AIContextProvider // Create a filtered context for ProvideAIContextAsync, filtering input messages // to exclude non-external messages (e.g. chat history, other AI context provider messages). +#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var filteredContext = new InvokingContext( context.Agent, context.Session, @@ -156,6 +159,7 @@ public abstract class AIContextProvider Messages = inputContext.Messages is not null ? this.ProvideInputMessageFilter(inputContext.Messages) : null, Tools = inputContext.Tools }); +#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var provided = await this.ProvideAIContextAsync(filteredContext, cancellationToken).ConfigureAwait(false); @@ -294,7 +298,9 @@ public abstract class AIContextProvider return default; } +#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputRequestMessageFilter(context.RequestMessages), this.StoreInputResponseMessageFilter(context.ResponseMessages!)); +#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return this.StoreAIContextAsync(subContext, cancellationToken); } @@ -372,6 +378,7 @@ public abstract class AIContextProvider /// The session associated with the agent invocation. /// The AI context to be used by the agent for this invocation. /// or is . + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokingContext( AIAgent agent, AgentSession? session, @@ -431,6 +438,7 @@ public abstract class AIContextProvider /// that were used by the agent for this invocation. /// The response messages generated during this invocation. /// , , or is . + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokedContext( AIAgent agent, AgentSession? session, diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs index f4f198df97..a2aa8e1ed5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -250,7 +252,9 @@ public abstract class ChatHistoryProvider return default; } +#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputRequestMessageFilter(context.RequestMessages), this._storeInputResponseMessageFilter(context.ResponseMessages!)); +#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return this.StoreChatHistoryAsync(subContext, cancellationToken); } @@ -340,6 +344,7 @@ public abstract class ChatHistoryProvider /// The session associated with the agent invocation. /// The messages to be used by the agent for this invocation. /// is . + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokingContext( AIAgent agent, AgentSession? session, @@ -399,6 +404,7 @@ public abstract class ChatHistoryProvider /// that were used by the agent for this invocation. /// The response messages generated during this invocation. /// , , or is . + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokedContext( AIAgent agent, AgentSession? session, diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs index c5f367443c..041e621341 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/MessageAIContextProvider.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -49,12 +51,14 @@ public abstract class MessageAIContextProvider : AIContextProvider { // Call ProvideMessagesAsync directly to return only additional messages. // The base AIContextProvider.InvokingCoreAsync handles merging with the original input and stamping. +#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return new AIContext { Messages = await this.ProvideMessagesAsync( new InvokingContext(context.Agent, context.Session, context.AIContext.Messages ?? []), cancellationToken).ConfigureAwait(false) }; +#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } /// @@ -109,10 +113,12 @@ public abstract class MessageAIContextProvider : AIContextProvider // Create a filtered context for ProvideMessagesAsync, filtering input messages // to exclude non-external messages (e.g. chat history, other AI context provider messages). +#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var filteredContext = new InvokingContext( context.Agent, context.Session, this.ProvideInputMessageFilter(inputMessages)); +#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var providedMessages = await this.ProvideMessagesAsync(filteredContext, cancellationToken).ConfigureAwait(false); @@ -163,6 +169,7 @@ public abstract class MessageAIContextProvider : AIContextProvider /// The session associated with the agent invocation. /// The messages to be used by the agent for this invocation. /// or is . + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public InvokingContext( AIAgent agent, AgentSession? session, From cb96347c9595139b0a79343a5f31fd6eb58cd086 Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:47:12 -0700 Subject: [PATCH 3/6] Python: Fix PydanticSchemaGenerationError when using `from __future__ import annotations` with @tool (#4822) * Fix PydanticSchemaGenerationError with PEP 563 annotations in @tool _resolve_input_model used raw param.annotation from inspect.signature(), which returns string annotations when 'from __future__ import annotations' is active (PEP 563). This caused Pydantic's create_model to fail for complex types like Optional[int] or FunctionInvocationContext. Use typing.get_type_hints() to resolve annotations to actual types before passing them to create_model, matching the approach already used by _discover_injected_parameters. Fixes #4809 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply pre-commit auto-fixes * Remove reproduction report and unused test imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): strengthen PEP 563 regression tests per review feedback (#4809) - Verify type correctness in schema assertions (not just key presence) - Fix ctx annotation to FunctionInvocationContext | None for type consistency - Add test for Optional[CustomType] pattern (original bug trigger) - Add test for get_type_hints() fallback with unresolvable forward refs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply pre-commit auto-fixes * Address review feedback for #4809: Python: [Bug]: PydanticSchemaGenerationError in FunctionInvocationContext --------- Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_tools.py | 8 +- .../core/test_tools_future_annotations.py | 134 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 python/packages/core/tests/core/test_tools_future_annotations.py diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index cf7384588f..f7bc3f0e15 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -466,9 +466,15 @@ class FunctionTool(SerializationMixin): if func is None: return create_model(f"{self.name}_input") sig = inspect.signature(func) + try: + type_hints = typing.get_type_hints(func, include_extras=True) + except Exception: + type_hints = {} fields: dict[str, Any] = { pname: ( - _parse_annotation(param.annotation) if param.annotation is not inspect.Parameter.empty else str, + _parse_annotation(type_hints.get(pname, param.annotation)) + if type_hints.get(pname, param.annotation) is not inspect.Parameter.empty + else str, param.default if param.default is not inspect.Parameter.empty else ..., ) for pname, param in sig.parameters.items() diff --git a/python/packages/core/tests/core/test_tools_future_annotations.py b/python/packages/core/tests/core/test_tools_future_annotations.py new file mode 100644 index 0000000000..1c9649dcb9 --- /dev/null +++ b/python/packages/core/tests/core/test_tools_future_annotations.py @@ -0,0 +1,134 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for @tool with PEP 563 (from __future__ import annotations). + +When ``from __future__ import annotations`` is active, all annotations +become strings. _resolve_input_model must resolve them via +typing.get_type_hints() before passing them to Pydantic's create_model. +""" + +from __future__ import annotations + +from pydantic import BaseModel + +from agent_framework import tool +from agent_framework._middleware import FunctionInvocationContext + + +class SearchConfig(BaseModel): + max_results: int = 10 + + +def test_tool_with_context_parameter(): + """FunctionInvocationContext parameter is excluded from schema under PEP 563.""" + + @tool + def get_weather(location: str, ctx: FunctionInvocationContext) -> str: + """Get the weather for a given location.""" + return f"Weather in {location}" + + params = get_weather.parameters() + assert "ctx" not in params.get("properties", {}) + assert "location" in params["properties"] + + +def test_tool_with_context_parameter_first(): + """FunctionInvocationContext as the first parameter is excluded under PEP 563.""" + + @tool + def get_weather(ctx: FunctionInvocationContext, location: str) -> str: + """Get the weather for a given location.""" + return f"Weather in {location}" + + params = get_weather.parameters() + assert "ctx" not in params.get("properties", {}) + assert "location" in params["properties"] + + +def test_tool_with_optional_param(): + """Optional[int] is resolved to the actual type, not left as a string.""" + + @tool + def search(query: str, limit: int | None = None) -> str: + """Search for something.""" + return query + + params = search.parameters() + assert params["properties"]["query"]["type"] == "string" + limit_schema = params["properties"]["limit"] + limit_types = {t["type"] for t in limit_schema["anyOf"]} + assert limit_types == {"integer", "null"} + + +def test_tool_with_optional_param_and_context(): + """Optional param + FunctionInvocationContext both work under PEP 563.""" + + @tool + def search(query: str, limit: int | None = None, ctx: FunctionInvocationContext | None = None) -> str: + """Search for something.""" + return query + + params = search.parameters() + assert params["properties"]["query"]["type"] == "string" + limit_schema = params["properties"]["limit"] + limit_types = {t["type"] for t in limit_schema["anyOf"]} + assert limit_types == {"integer", "null"} + assert "ctx" not in params.get("properties", {}) + + +def test_tool_with_optional_custom_type(): + """Optional[CustomType] is resolved under PEP 563 (original bug pattern).""" + + @tool + def search(query: str, config: SearchConfig | None = None) -> str: + """Search for something.""" + return query + + params = search.parameters() + assert params["properties"]["query"]["type"] == "string" + config_schema = params["properties"]["config"] + config_types = [t.get("type") for t in config_schema["anyOf"]] + assert "null" in config_types + + +def test_tool_with_unresolvable_forward_ref(): + """Fallback to raw annotations when get_type_hints() fails.""" + import types + + # Build a function in an isolated namespace so get_type_hints() cannot resolve + # the forward reference, exercising the except-branch fallback. + ns: dict = {} + exec( + "def greet(name: str = 'world') -> str:\n '''Greet someone.'''\n return f'Hello {name}'\n", + ns, + ) + func = ns["greet"] + # Place the function in a throwaway module so get_type_hints() will fail on + # any non-builtin forward ref while still having a valid __module__. + mod = types.ModuleType("_phantom") + func.__module__ = mod.__name__ + + t = tool(func) + params = t.parameters() + assert params["properties"]["name"]["type"] == "string" + + +async def test_tool_invoke_with_context(): + """Full invocation with FunctionInvocationContext under PEP 563.""" + + @tool + def get_weather(location: str, ctx: FunctionInvocationContext) -> str: + """Get the weather for a given location.""" + user = ctx.kwargs.get("user", "anon") + return f"Weather in {location} for {user}" + + params = get_weather.parameters() + assert "ctx" not in params.get("properties", {}) + + context = FunctionInvocationContext( + function=get_weather, + arguments=get_weather.input_model(location="Seattle"), + kwargs={"user": "test_user"}, + ) + result = await get_weather.invoke(context=context) + assert result[0].text == "Weather in Seattle for test_user" From 01aaf2baea418ee45e4a7737916b543b8f3ad5ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:55:19 -0700 Subject: [PATCH 4/6] Bump actions/download-artifact from 7 to 8 (#4372) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/python-test-coverage-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-test-coverage-report.yml b/.github/workflows/python-test-coverage-report.yml index f5f5f8eb03..dbe5b9e9c0 100644 --- a/.github/workflows/python-test-coverage-report.yml +++ b/.github/workflows/python-test-coverage-report.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Download coverage report - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} run-id: ${{ github.event.workflow_run.id }} From cc85bbc2dce749bb49fc2c11d9e39f0df8ccaf97 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:06:32 +0000 Subject: [PATCH 5/6] .NET: Re-enable AzureAI.Persistent packaging and integration tests (#4769) (#4865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Azure.AI.Agents.Persistent 1.2.0-beta.10 now targets ME.AI 10.4.0+, resolving the compatibility issue that required disabling this package. - Remove IsPackable=false from the csproj - Re-enable all 6 integration test classes (IntegrationDisabled → Integration) - Remove outdated compatibility warning from README.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...Microsoft.Agents.AI.AzureAI.Persistent.csproj | 3 +-- .../README.md | 16 ---------------- ...reAIAgentsChatClientAgentRunStreamingTests.cs | 5 +---- .../AzureAIAgentsChatClientAgentRunTests.cs | 5 +---- .../AzureAIAgentsPersistentCreateTests.cs | 5 +---- .../AzureAIAgentsPersistentRunStreamingTests.cs | 5 +---- .../AzureAIAgentsPersistentRunTests.cs | 5 +---- ...AIAgentsPersistentStructuredOutputRunTests.cs | 5 +---- 8 files changed, 7 insertions(+), 42 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj index bb02f3065a..8e14047919 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj @@ -20,8 +20,7 @@ Microsoft Agent Framework AzureAI Persistent Agents Provides Microsoft Agent Framework support for Azure AI Persistent Agents. - - false + diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/README.md b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/README.md index a01debc5ca..0513ad5d99 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/README.md +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/README.md @@ -1,19 +1,3 @@ # Microsoft.Agents.AI.AzureAI.Persistent Provides integration between the Microsoft Agent Framework and Azure AI Agents Persistent (`Azure.AI.Agents.Persistent`). - -## ⚠️ Known Compatibility Limitation - -The underlying `Azure.AI.Agents.Persistent` package (currently 1.2.0-beta.9) targets `Microsoft.Extensions.AI.Abstractions` 10.1.x and references types that were renamed in 10.4.0 (e.g., `McpServerToolApprovalResponseContent` → `ToolApprovalResponseContent`). This causes `TypeLoadException` at runtime when used with ME.AI 10.4.0+. - -**Compatible versions:** - -| Package | Compatible Version | -|---|---| -| `Azure.AI.Agents.Persistent` | 1.2.0-beta.9 (targets ME.AI 10.1.x) | -| `Microsoft.Extensions.AI.Abstractions` | ≤ 10.3.0 | -| `OpenAI` | ≤ 2.8.0 | - -**Resolution:** An updated version of `Azure.AI.Agents.Persistent` targeting ME.AI 10.4.0+ is expected in 1.2.0-beta.10. The upstream fix is tracked in [Azure/azure-sdk-for-net#56929](https://github.com/Azure/azure-sdk-for-net/pull/56929). - -**Tracking issue:** [microsoft/agent-framework#4769](https://github.com/microsoft/agent-framework/issues/4769) diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunStreamingTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunStreamingTests.cs index b4a581d81e..487dcd6878 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunStreamingTests.cs @@ -4,10 +4,7 @@ using AgentConformance.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests; -// Disabled: Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent -// which was removed in ME.AI 10.4.0. Re-enable once Persistent targets ME.AI 10.4.0+ (expected in 1.2.0-beta.10). -// Tracking: https://github.com/microsoft/agent-framework/issues/4769 -[Trait("Category", "IntegrationDisabled")] +[Trait("Category", "Integration")] public class AzureAIAgentsChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new()) { } diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunTests.cs index 8529dac6b4..79a7c24c69 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsChatClientAgentRunTests.cs @@ -4,10 +4,7 @@ using AgentConformance.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests; -// Disabled: Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent -// which was removed in ME.AI 10.4.0. Re-enable once Persistent targets ME.AI 10.4.0+ (expected in 1.2.0-beta.10). -// Tracking: https://github.com/microsoft/agent-framework/issues/4769 -[Trait("Category", "IntegrationDisabled")] +[Trait("Category", "Integration")] public class AzureAIAgentsChatClientAgentRunTests() : ChatClientAgentRunTests(() => new()) { } diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs index de9a41e1a0..971bf57cbc 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs @@ -14,10 +14,7 @@ using Shared.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests; -// Disabled: Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent -// which was removed in ME.AI 10.4.0. Re-enable once Persistent targets ME.AI 10.4.0+ (expected in 1.2.0-beta.10). -// Tracking: https://github.com/microsoft/agent-framework/issues/4769 -[Trait("Category", "IntegrationDisabled")] +[Trait("Category", "Integration")] public class AzureAIAgentsPersistentCreateTests { private const string SkipCodeInterpreterReason = "Azure AI Code Interpreter intermittently fails to execute uploaded files in CI"; diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunStreamingTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunStreamingTests.cs index 87f1cfbb70..1ba0ec991e 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunStreamingTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunStreamingTests.cs @@ -4,10 +4,7 @@ using AgentConformance.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests; -// Disabled: Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent -// which was removed in ME.AI 10.4.0. Re-enable once Persistent targets ME.AI 10.4.0+ (expected in 1.2.0-beta.10). -// Tracking: https://github.com/microsoft/agent-framework/issues/4769 -[Trait("Category", "IntegrationDisabled")] +[Trait("Category", "Integration")] public class AzureAIAgentsPersistentRunStreamingTests() : RunStreamingTests(() => new()) { } diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunTests.cs index fd4b92e4b9..446b27b817 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentRunTests.cs @@ -4,10 +4,7 @@ using AgentConformance.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests; -// Disabled: Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent -// which was removed in ME.AI 10.4.0. Re-enable once Persistent targets ME.AI 10.4.0+ (expected in 1.2.0-beta.10). -// Tracking: https://github.com/microsoft/agent-framework/issues/4769 -[Trait("Category", "IntegrationDisabled")] +[Trait("Category", "Integration")] public class AzureAIAgentsPersistentRunTests() : RunTests(() => new()) { } diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs index 34663c29e4..6ec4620ce3 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs @@ -5,10 +5,7 @@ using AgentConformance.IntegrationTests; namespace AzureAIAgentsPersistent.IntegrationTests; -// Disabled: Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent -// which was removed in ME.AI 10.4.0. Re-enable once Persistent targets ME.AI 10.4.0+ (expected in 1.2.0-beta.10). -// Tracking: https://github.com/microsoft/agent-framework/issues/4769 -[Trait("Category", "IntegrationDisabled")] +[Trait("Category", "Integration")] public class AzureAIAgentsPersistentStructuredOutputRunTests() : StructuredOutputRunTests(() => new()) { private const string SkipReason = "Fails intermittently on the build agent/CI"; From 2c000b032d0e64d6500bcd55cd0ebe08684c6b73 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:12:55 +0000 Subject: [PATCH 6/6] .NET: Update AIContextProviders to use Microsoft.Extensions.Compliance.Redaction (#4854) * Update providers to use Microsoft.Extensions.Compliance.Redaction * Fix formatting. * Fix readme --- dotnet/Directory.Packages.props | 1 + dotnet/agent-framework-dotnet.slnx | 4 ++ dotnet/eng/MSBuild/Shared.props | 3 + .../FoundryMemoryProvider.cs | 7 ++- .../FoundryMemoryProviderOptions.cs | 15 +++++ .../Microsoft.Agents.AI.FoundryMemory.csproj | 2 + .../Microsoft.Agents.AI.Mem0/Mem0Provider.cs | 7 ++- .../Mem0ProviderOptions.cs | 15 +++++ .../Microsoft.Agents.AI.Mem0.csproj | 5 ++ .../Memory/ChatHistoryMemoryProvider.cs | 7 ++- .../ChatHistoryMemoryProviderOptions.cs | 15 +++++ .../Microsoft.Agents.AI.csproj | 2 + .../Microsoft.Agents.AI/TextSearchProvider.cs | 9 ++- .../TextSearchProviderOptions.cs | 21 +++++++ dotnet/src/Shared/Redaction/README.md | 30 +++++++++ .../src/Shared/Redaction/ReplacingRedactor.cs | 35 +++++++++++ .../Mem0ProviderTests.cs | 29 ++++++--- .../Data/TextSearchProviderTests.cs | 62 ++++++++++++++++++- .../Memory/ChatHistoryMemoryProviderTests.cs | 48 ++++++++------ 19 files changed, 277 insertions(+), 40 deletions(-) create mode 100644 dotnet/src/Shared/Redaction/README.md create mode 100644 dotnet/src/Shared/Redaction/ReplacingRedactor.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index fa54be567c..ef1a882465 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -70,6 +70,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index af7ca9f0be..1d923076ee 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -452,6 +452,10 @@ + + + + diff --git a/dotnet/eng/MSBuild/Shared.props b/dotnet/eng/MSBuild/Shared.props index 94ac5b417b..1a3feb4c4f 100644 --- a/dotnet/eng/MSBuild/Shared.props +++ b/dotnet/eng/MSBuild/Shared.props @@ -29,4 +29,7 @@ + + + diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs index 35baa055d1..6f9f37518e 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.Projects; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; @@ -37,7 +38,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider private readonly string _memoryStoreName; private readonly int _maxMemories; private readonly int _updateDelay; - private readonly bool _enableSensitiveTelemetryData; + private readonly Redactor _redactor; private readonly AIProjectClient _client; private readonly ILogger? _logger; @@ -79,7 +80,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider this._memoryStoreName = memoryStoreName; this._maxMemories = effectiveOptions.MaxMemories; this._updateDelay = effectiveOptions.UpdateDelay; - this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData; + this._redactor = effectiveOptions.EnableSensitiveTelemetryData ? NullRedactor.Instance : (effectiveOptions.Redactor ?? new ReplacingRedactor("")); } /// @@ -416,7 +417,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider private static bool IsAllowedRole(ChatRole role) => role == ChatRole.User || role == ChatRole.Assistant || role == ChatRole.System; - private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : ""; + private string SanitizeLogData(string? data) => this._redactor.Redact(data); /// /// Represents the state of a stored in the . diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs index 870fe1d271..cf4fb5ab15 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; namespace Microsoft.Agents.AI.FoundryMemory; @@ -37,8 +38,22 @@ public sealed class FoundryMemoryProviderOptions /// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs. /// /// Defaults to . + /// + /// When set to , sensitive data is passed through to logs unchanged and any + /// configured is ignored. This property takes precedence over . + /// public bool EnableSensitiveTelemetryData { get; set; } + /// + /// Gets or sets a custom used to redact sensitive data in log output. + /// + /// + /// When (the default), sensitive data is replaced with a placeholder. + /// When set, this redactor is used to transform sensitive values before they are logged. + /// Ignored when is . + /// + public Redactor? Redactor { get; set; } + /// /// Gets or sets the key used to store the provider state in the session's . /// diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj index a1b8f85ae8..7abc3d0bcc 100644 --- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj +++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj @@ -8,6 +8,7 @@ true true + true true true @@ -20,6 +21,7 @@ + diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs index d7c54e2114..8be799ac1a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; @@ -51,7 +52,7 @@ public sealed class Mem0Provider : MessageAIContextProvider private readonly ProviderSessionState _sessionState; private IReadOnlyList? _stateKeys; private readonly string _contextPrompt; - private readonly bool _enableSensitiveTelemetryData; + private readonly Redactor _redactor; private readonly Mem0Client _client; private readonly ILogger? _logger; @@ -91,7 +92,7 @@ public sealed class Mem0Provider : MessageAIContextProvider this._client = new Mem0Client(httpClient); this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt; - this._enableSensitiveTelemetryData = options?.EnableSensitiveTelemetryData ?? false; + this._redactor = options?.EnableSensitiveTelemetryData == true ? NullRedactor.Instance : (options?.Redactor ?? new ReplacingRedactor("")); } /// @@ -297,5 +298,5 @@ public sealed class Mem0Provider : MessageAIContextProvider public Mem0ProviderScope SearchScope { get; } } - private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : ""; + private string SanitizeLogData(string? data) => this._redactor.Redact(data); } diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs index 4a3a16712f..2c09bf9a7d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; namespace Microsoft.Agents.AI.Mem0; @@ -21,8 +22,22 @@ public sealed class Mem0ProviderOptions /// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs. /// /// Defaults to . + /// + /// When set to , sensitive data is passed through to logs unchanged and any + /// configured is ignored. This property takes precedence over . + /// public bool EnableSensitiveTelemetryData { get; set; } + /// + /// Gets or sets a custom used to redact sensitive data in log output. + /// + /// + /// When (the default), sensitive data is replaced with a placeholder. + /// When set, this redactor is used to transform sensitive values before they are logged. + /// Ignored when is . + /// + public Redactor? Redactor { get; set; } + /// /// Gets or sets the key used to store the provider state in the session's . /// diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj b/dotnet/src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj index 52bcdda165..9612b3d4b0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj @@ -6,6 +6,7 @@ true + true true @@ -23,6 +24,10 @@ + + + + Microsoft Agent Framework - Mem0 integration diff --git a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs index 6881f7303f..2bc8408a26 100644 --- a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Logging; using Microsoft.Extensions.VectorData; using Microsoft.Shared.Diagnostics; @@ -80,7 +81,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo private readonly VectorStoreCollection> _collection; private readonly int _maxResults; private readonly string _contextPrompt; - private readonly bool _enableSensitiveTelemetryData; + private readonly Redactor _redactor; private readonly ChatHistoryMemoryProviderOptions.SearchBehavior _searchTime; private readonly string _toolName; private readonly string _toolDescription; @@ -118,7 +119,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo options ??= new ChatHistoryMemoryProviderOptions(); this._maxResults = options.MaxResults.HasValue ? Throw.IfLessThanOrEqual(options.MaxResults.Value, 0) : DefaultMaxResults; this._contextPrompt = options.ContextPrompt ?? DefaultContextPrompt; - this._enableSensitiveTelemetryData = options.EnableSensitiveTelemetryData; + this._redactor = options.EnableSensitiveTelemetryData ? NullRedactor.Instance : (options.Redactor ?? new ReplacingRedactor("")); this._searchTime = options.SearchTime; this._logger = loggerFactory?.CreateLogger(); this._toolName = options.FunctionToolName ?? DefaultFunctionToolName; @@ -485,7 +486,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo GC.SuppressFinalize(this); } - private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : ""; + private string SanitizeLogData(string? data) => this._redactor.Redact(data); /// /// Rebinds a filter expression's body to use the specified shared parameter, diff --git a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs index a9c5b93928..db1731a54d 100644 --- a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProviderOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; namespace Microsoft.Agents.AI; @@ -46,8 +47,22 @@ public sealed class ChatHistoryMemoryProviderOptions /// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs. /// /// Defaults to . + /// + /// When set to , sensitive data is passed through to logs unchanged and any + /// configured is ignored. This property takes precedence over . + /// public bool EnableSensitiveTelemetryData { get; set; } + /// + /// Gets or sets a custom used to redact sensitive data in log output. + /// + /// + /// When (the default), sensitive data is replaced with a placeholder. + /// When set, this redactor is used to transform sensitive values before they are logged. + /// Ignored when is . + /// + public Redactor? Redactor { get; set; } + /// /// Gets or sets the key used to store provider state in the . /// diff --git a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj index 93b228d29e..70da404a61 100644 --- a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj +++ b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj @@ -8,6 +8,7 @@ true true + true true true true @@ -22,6 +23,7 @@ + diff --git a/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs b/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs index e389b02294..7a48243ef6 100644 --- a/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/TextSearchProvider.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; @@ -62,6 +63,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider private readonly string _contextPrompt; private readonly string _citationsPrompt; private readonly Func, string>? _contextFormatter; + private readonly Redactor _redactor; /// /// Initializes a new instance of the class. @@ -89,6 +91,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt; this._citationsPrompt = options?.CitationsPrompt ?? DefaultCitationsPrompt; this._contextFormatter = options?.ContextFormatter; + this._redactor = options?.EnableSensitiveTelemetryData == true ? NullRedactor.Instance : (options?.Redactor ?? new ReplacingRedactor("")); // Create the on-demand search tool (only used if behavior is OnDemandFunctionCalling) this._tools = @@ -180,7 +183,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider if (this._logger?.IsEnabled(LogLevel.Trace) is true) { - this._logger.LogTrace("TextSearchProvider: Search Results\nInput:{Input}\nOutput:{MessageText}", input, formatted); + this._logger.LogTrace("TextSearchProvider: Search Results\nInput:{Input}\nOutput:{MessageText}", this.SanitizeLogData(input), this.SanitizeLogData(formatted)); } return [new ChatMessage(ChatRole.User, formatted)]; @@ -249,7 +252,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider if (this._logger.IsEnabled(LogLevel.Trace)) { - this._logger.LogTrace("TextSearchProvider Input:{UserQuestion}\nOutput:{MessageText}", userQuestion, outputText); + this._logger.LogTrace("TextSearchProvider Input:{UserQuestion}\nOutput:{MessageText}", this.SanitizeLogData(userQuestion), this.SanitizeLogData(outputText)); } } @@ -325,6 +328,8 @@ public sealed class TextSearchProvider : MessageAIContextProvider public object? RawRepresentation { get; set; } } + private string SanitizeLogData(string? data) => this._redactor.Redact(data); + /// /// Represents the per-session state of a stored in the . /// diff --git a/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs index 879e34121d..9c6845ff21 100644 --- a/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/TextSearchProviderOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Compliance.Redaction; namespace Microsoft.Agents.AI; @@ -117,6 +118,26 @@ public sealed class TextSearchProviderOptions /// public List? RecentMessageRolesIncluded { get; set; } + /// + /// Gets or sets a value indicating whether sensitive data such as user queries and search results may appear in logs. + /// + /// Defaults to . + /// + /// When set to , sensitive data is passed through to logs unchanged and any + /// configured is ignored. This property takes precedence over . + /// + public bool EnableSensitiveTelemetryData { get; set; } + + /// + /// Gets or sets a custom used to redact sensitive data in log output. + /// + /// + /// When (the default), sensitive data is replaced with a placeholder. + /// When set, this redactor is used to transform sensitive values before they are logged. + /// Ignored when is . + /// + public Redactor? Redactor { get; set; } + /// /// Behavior choices for the provider. /// diff --git a/dotnet/src/Shared/Redaction/README.md b/dotnet/src/Shared/Redaction/README.md new file mode 100644 index 0000000000..01683d0a54 --- /dev/null +++ b/dotnet/src/Shared/Redaction/README.md @@ -0,0 +1,30 @@ +# Redaction + +Log data redaction utilities built on `Microsoft.Extensions.Compliance.Redaction.Redactor`. + +Provides `ReplacingRedactor`, an internal `Redactor` implementation that replaces +any input with a fixed replacement string (e.g. `""`). + +To use this in your project, add the following to your `.csproj` file: + +```xml + + true + +``` + +You will also need to add a package reference to `Microsoft.Extensions.Compliance.Abstractions`: + +```xml + + + +``` + +And finally, this also depends on the shared Throw class, so when using redaction, InjectSharedThrow should also be enabled: + +```xml + + true + +``` \ No newline at end of file diff --git a/dotnet/src/Shared/Redaction/ReplacingRedactor.cs b/dotnet/src/Shared/Redaction/ReplacingRedactor.cs new file mode 100644 index 0000000000..3e97e7318e --- /dev/null +++ b/dotnet/src/Shared/Redaction/ReplacingRedactor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// A that replaces the entire input with a fixed replacement string. +/// +internal sealed class ReplacingRedactor : Redactor +{ + private readonly string _replacementText; + + /// + /// Initializes a new instance of the class. + /// + /// The text to substitute for any input value. + /// Thrown when is . + public ReplacingRedactor(string replacementText) + { + this._replacementText = Throw.IfNull(replacementText); + } + + /// + public override int GetRedactedLength(ReadOnlySpan input) => this._replacementText.Length; + + /// + public override int Redact(ReadOnlySpan source, Span destination) + { + this._replacementText.AsSpan().CopyTo(destination); + return this._replacementText.Length; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs index 3374270861..a959faa515 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs @@ -148,11 +148,15 @@ public sealed class Mem0ProviderTests : IDisposable } [Theory] - [InlineData(false, false, 4)] - [InlineData(true, false, 4)] - [InlineData(false, true, 2)] - [InlineData(true, true, 2)] - public async Task InvokingAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations) + [InlineData(false, false, false, 4)] + [InlineData(false, false, true, 4)] + [InlineData(true, false, false, 4)] + [InlineData(true, false, true, 4)] + [InlineData(false, true, false, 2)] + [InlineData(false, true, true, 2)] + [InlineData(true, true, false, 2)] + [InlineData(true, true, true, 2)] + public async Task InvokingAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool requestThrows, bool useCustomRedactor, int expectedLogInvocations) { // Arrange if (requestThrows) @@ -171,7 +175,11 @@ public sealed class Mem0ProviderTests : IDisposable ThreadId = "session", UserId = "user" }; - var options = new Mem0ProviderOptions { EnableSensitiveTelemetryData = enableSensitiveTelemetryData }; + var options = new Mem0ProviderOptions + { + EnableSensitiveTelemetryData = enableSensitiveTelemetryData, + Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null + }; var mockSession = new TestAgentSession(); var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope), options: options, loggerFactory: this._loggerFactoryMock.Object); @@ -180,7 +188,8 @@ public sealed class Mem0ProviderTests : IDisposable // Act await sut.InvokingAsync(invokingContext, CancellationToken.None); - // Assert + // Assert — EnableSensitiveTelemetryData takes precedence over Redactor + string expectedRedaction = enableSensitiveTelemetryData ? "user" : (useCustomRedactor ? "***" : ""); Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count); foreach (var logInvocation in this._loggerMock.Invocations) { @@ -191,18 +200,18 @@ public sealed class Mem0ProviderTests : IDisposable var state = Assert.IsType>>(logInvocation.Arguments[2], exactMatch: false); var userIdValue = state.First(kvp => kvp.Key == "UserId").Value; - Assert.Equal(enableSensitiveTelemetryData ? "user" : "", userIdValue); + Assert.Equal(expectedRedaction, userIdValue); var inputValue = state.FirstOrDefault(kvp => kvp.Key == "Input").Value; if (inputValue != null) { - Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : "", inputValue); + Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : expectedRedaction, inputValue); } var messageTextValue = state.FirstOrDefault(kvp => kvp.Key == "MessageText").Value; if (messageTextValue != null) { - Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : "", messageTextValue); + Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : expectedRedaction, messageTextValue); } } } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs index a782993f6a..12bc57a3ee 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs @@ -85,7 +85,8 @@ public sealed class TextSearchProviderTests { SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, ContextPrompt = overrideContextPrompt, - CitationsPrompt = overrideCitationsPrompt + CitationsPrompt = overrideCitationsPrompt, + EnableSensitiveTelemetryData = true }; var provider = new TextSearchProvider(SearchDelegateAsync, options, withLogging ? this._loggerFactoryMock.Object : null); @@ -164,6 +165,65 @@ public sealed class TextSearchProviderTests } } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task InvokingAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool useCustomRedactor) + { + // Arrange + List results = + [ + new() { SourceName = "Doc1", SourceLink = "http://example.com/doc1", Text = "Content of Doc1" } + ]; + + Task> SearchDelegateAsync(string input, CancellationToken ct) + { + return Task.FromResult>(results); + } + + var options = new TextSearchProviderOptions + { + SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, + EnableSensitiveTelemetryData = enableSensitiveTelemetryData, + Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null + }; + var provider = new TextSearchProvider(SearchDelegateAsync, options, this._loggerFactoryMock.Object); + + var invokingContext = new AIContextProvider.InvokingContext( + s_mockAgent, + new TestAgentSession(), + new AIContext { Messages = new List { new(ChatRole.User, "Sample user question?") } }); + + // Act + await provider.InvokingAsync(invokingContext, CancellationToken.None); + + // Assert — EnableSensitiveTelemetryData takes precedence over Redactor + var traceInvocation = this._loggerMock.Invocations + .Where(i => i.Method.Name == nameof(ILogger.Log)) + .FirstOrDefault(i => (LogLevel)i.Arguments[0]! == LogLevel.Trace); + Assert.NotNull(traceInvocation); + + var state = Assert.IsType>>(traceInvocation.Arguments[2], exactMatch: false); + var inputValue = state.First(kvp => kvp.Key == "Input").Value; + var messageTextValue = state.First(kvp => kvp.Key == "MessageText").Value; + + if (enableSensitiveTelemetryData) + { + // EnableSensitiveTelemetryData=true: raw data passes through regardless of Redactor + Assert.Equal("Sample user question?", inputValue); + Assert.Contains("Content of Doc1", messageTextValue?.ToString()!); + } + else + { + // EnableSensitiveTelemetryData=false: custom redactor or default placeholder + string expectedRedaction = useCustomRedactor ? "***" : ""; + Assert.Equal(expectedRedaction, inputValue); + Assert.Equal(expectedRedaction, messageTextValue); + } + } + [Theory] [InlineData(null, null, "Search", "Allows searching for additional information to help answer the user question.")] [InlineData("CustomSearch", "CustomDescription", "CustomSearch", "CustomDescription")] diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs index 35c7f780b4..43cabebaed 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs @@ -270,16 +270,21 @@ public class ChatHistoryMemoryProviderTests } [Theory] - [InlineData(false, false, 0)] - [InlineData(true, false, 0)] - [InlineData(false, true, 2)] - [InlineData(true, true, 2)] - public async Task InvokedAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations) + [InlineData(false, false, false, 0)] + [InlineData(false, false, true, 0)] + [InlineData(true, false, false, 0)] + [InlineData(true, false, true, 0)] + [InlineData(false, true, false, 2)] + [InlineData(false, true, true, 2)] + [InlineData(true, true, false, 2)] + [InlineData(true, true, true, 2)] + public async Task InvokedAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool requestThrows, bool useCustomRedactor, int expectedLogInvocations) { // Arrange var options = new ChatHistoryMemoryProviderOptions { - EnableSensitiveTelemetryData = enableSensitiveTelemetryData + EnableSensitiveTelemetryData = enableSensitiveTelemetryData, + Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null }; if (requestThrows) @@ -309,7 +314,7 @@ public class ChatHistoryMemoryProviderTests // Act await provider.InvokedAsync(invokedContext, CancellationToken.None); - // Assert + // Assert — EnableSensitiveTelemetryData takes precedence over Redactor Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count); foreach (var logInvocation in this._loggerMock.Invocations) { @@ -320,7 +325,8 @@ public class ChatHistoryMemoryProviderTests var state = Assert.IsType>>(logInvocation.Arguments[2], exactMatch: false); var userIdValue = state.First(kvp => kvp.Key == "UserId").Value; - Assert.Equal(enableSensitiveTelemetryData ? "user1" : "", userIdValue); + string expectedRedaction = enableSensitiveTelemetryData ? "user1" : (useCustomRedactor ? "***" : ""); + Assert.Equal(expectedRedaction, userIdValue); } } @@ -526,17 +532,22 @@ public class ChatHistoryMemoryProviderTests } [Theory] - [InlineData(false, false, 2)] - [InlineData(true, false, 2)] - [InlineData(false, true, 2)] - [InlineData(true, true, 2)] - public async Task InvokingAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations) + [InlineData(false, false, false, 2)] + [InlineData(false, false, true, 2)] + [InlineData(true, false, false, 2)] + [InlineData(true, false, true, 2)] + [InlineData(false, true, false, 2)] + [InlineData(false, true, true, 2)] + [InlineData(true, true, false, 2)] + [InlineData(true, true, true, 2)] + public async Task InvokingAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool requestThrows, bool useCustomRedactor, int expectedLogInvocations) { // Arrange var options = new ChatHistoryMemoryProviderOptions { SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke, - EnableSensitiveTelemetryData = enableSensitiveTelemetryData + EnableSensitiveTelemetryData = enableSensitiveTelemetryData, + Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null }; var scope = new ChatHistoryMemoryProviderScope @@ -578,7 +589,8 @@ public class ChatHistoryMemoryProviderTests // Act await provider.InvokingAsync(invokingContext, CancellationToken.None); - // Assert + // Assert — EnableSensitiveTelemetryData takes precedence over Redactor + string expectedRedaction = enableSensitiveTelemetryData ? "user1" : (useCustomRedactor ? "***" : ""); Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count); foreach (var logInvocation in this._loggerMock.Invocations) { @@ -589,18 +601,18 @@ public class ChatHistoryMemoryProviderTests var state = Assert.IsType>>(logInvocation.Arguments[2], exactMatch: false); var userIdValue = state.First(kvp => kvp.Key == "UserId").Value; - Assert.Equal(enableSensitiveTelemetryData ? "user1" : "", userIdValue); + Assert.Equal(expectedRedaction, userIdValue); var inputValue = state.FirstOrDefault(kvp => kvp.Key == "Input").Value; if (inputValue != null) { - Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : "", inputValue); + Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : expectedRedaction, inputValue); } var messageTextValue = state.FirstOrDefault(kvp => kvp.Key == "MessageText").Value; if (messageTextValue != null) { - Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : "", messageTextValue); + Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : expectedRedaction, messageTextValue); } } }