Merge branch 'main' into feature-featurecollections-messagestore

This commit is contained in:
westey
2025-11-25 13:38:21 +00:00
committed by GitHub
Unverified
121 changed files with 7844 additions and 4174 deletions
+3 -2
View File
@@ -1,10 +1,11 @@
{
"name": "C# (.NET)",
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0",
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
"features": {
"ghcr.io/devcontainers/features/dotnet:2.4.0": {},
"ghcr.io/devcontainers/features/powershell:1.5.1": {},
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {},
"ghcr.io/devcontainers/features/docker-in-docker:2.12.4": {}
},
"workspaceFolder": "/workspaces/agent-framework/dotnet/",
"customizations": {
+9 -7
View File
@@ -11,6 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="10.2.1" />
<PackageVersion Include="Anthropic.Foundry" Version="0.0.2" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
@@ -18,7 +20,7 @@
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
@@ -26,7 +28,7 @@
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
@@ -51,10 +53,10 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.0-preview.1.25559.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
@@ -95,7 +97,7 @@
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.6" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.6.0" />
<PackageVersion Include="OpenAI" Version="2.7.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<!-- Workflows -->
@@ -169,4 +171,4 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
</Project>
+13 -1
View File
@@ -40,10 +40,12 @@
<Folder Name="/Samples/GettingStarted/A2A/">
<File Path="samples/GettingStarted/A2A/README.md" />
<Project Path="samples/GettingStarted/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
<Project Path="samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentProviders/">
<File Path="samples/GettingStarted/AgentProviders/README.md" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
@@ -81,6 +83,12 @@
<File Path="samples/GettingStarted/DevUI/README.md" />
<Project Path="samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithAnthropic/">
<File Path="samples/GettingStarted/AgentWithAnthropic/README.md" />
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
<Project Path="samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithMemory/">
<File Path="samples/GettingStarted/AgentWithMemory/README.md" />
<Project Path="samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
@@ -311,6 +319,7 @@
<File Path="src/Shared/Demos/SampleEnvironment.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/IntegrationTests/">
<File Path="src/Shared/IntegrationTests/AnthropicConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/AzureAIConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/Mem0Configuration.cs" />
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
@@ -335,6 +344,7 @@
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
@@ -357,6 +367,7 @@
<Folder Name="/Tests/" />
<Folder Name="/Tests/IntegrationTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
@@ -373,6 +384,7 @@
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
@@ -389,4 +401,4 @@
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
</Folder>
</Solution>
</Solution>
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A AI agent.
using A2A;
using Microsoft.Agents.AI;
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
// Initialize an A2ACardResolver to get an A2A agent card.
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
// Get the agent card
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent agent = agentCard.GetAIAgent();
AgentThread thread = agent.GetNewThread();
// Start the initial run with a long-running task.
AgentRunResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", thread);
// Poll until the response is complete.
while (response.ContinuationToken is { } token)
{
// Wait before polling again.
await Task.Delay(TimeSpan.FromSeconds(2));
// Continue with the token.
response = await agent.RunAsync(thread, options: new AgentRunOptions { ContinuationToken = token });
}
// Display the result
Console.WriteLine(response);
@@ -0,0 +1,25 @@
# Polling for A2A Agent Task Completion
This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A AI agent, following the background responses pattern.
The sample:
- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
- Sends a request to the agent that may take time to complete
- Polls the agent at regular intervals using continuation tokens until a final response is received
- Displays the final result
This pattern is useful when an AI model cannot complete a complex task in a single response and needs multiple rounds of processing.
# Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10.0 SDK or later
- An A2A agent server running and accessible via HTTP
Set the following environment variable:
```powershell
$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
```
@@ -14,6 +14,7 @@ See the README.md for each sample for the prerequisites for that sample.
|Sample|Description|
|---|---|
|[A2A Agent As Function Tools](./A2AAgent_AsFunctionTools/)|This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent, and register these function tools with another AI agent so it can leverage the A2A agent's skills.|
|[A2A Agent Polling For Task Completion](./A2AAgent_PollingForTaskCompletion/)|This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A agent.|
## Running the samples from the console
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);IDE0059</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use an AI agent with Anthropic as the backend.
using System.ClientModel;
using System.Net.Http.Headers;
using Anthropic;
using Anthropic.Core;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI;
using Sample;
var deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_DEPLOYMENT_NAME") ?? "claude-haiku-4-5";
// The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri
// ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions
var resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
AnthropicClient? client = (resource is null)
? new AnthropicClient() { APIKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API
: (apiKey is not null)
? new AnthropicFoundryClient(resource, new ApiKeyCredential(apiKey)) // If an apiKey is provided, use Foundry with ApiKey authentication
: new AnthropicFoundryClient(resource, new AzureCliCredential()); // Otherwise, use Foundry with Azure Client authentication
AIAgent agent = client.CreateAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
namespace Sample
{
/// <summary>
/// Provides methods for invoking the Azure hosted Anthropic api.
/// </summary>
public class AnthropicFoundryClient : AnthropicClient
{
private readonly TokenCredential _tokenCredential;
private readonly string _resourceName;
/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// </summary>
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
/// <param name="tokenCredential">The credential provider. Use any specialization of <see cref="TokenCredential"/> to get your access token in supported environments.</param>
/// <param name="options">Set of <see cref="Anthropic.Core.ClientOptions"/> client option configurations</param>
/// <exception cref="ArgumentNullException">Resource is null</exception>
/// <exception cref="ArgumentNullException">TokenCredential is null</exception>
/// <remarks>
/// Any <see cref="Anthropic.Core.ClientOptions"/> APIKey or Bearer token provided will be ignored in favor of the <see cref="TokenCredential"/> provided in the constructor
/// </remarks>
public AnthropicFoundryClient(string resourceName, TokenCredential tokenCredential, Anthropic.Core.ClientOptions? options = null) : base(options ?? new())
{
this._resourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
this._tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
this.BaseUrl = new Uri($"https://{this._resourceName}.services.ai.azure.com/anthropic", UriKind.Absolute);
}
/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// </summary>
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
/// <param name="apiKeyCredential">The api key.</param>
/// <param name="options">Set of <see cref="Anthropic.Core.ClientOptions"/> client option configurations</param>
/// <exception cref="ArgumentNullException">Resource is null</exception>
/// <exception cref="ArgumentNullException">Api key is null</exception>
/// <remarks>
/// Any <see cref="Anthropic.Core.ClientOptions"/> APIKey or Bearer token provided will be ignored in favor of the <see cref="ApiKeyCredential"/> provided in the constructor
/// </remarks>
public AnthropicFoundryClient(string resourceName, ApiKeyCredential apiKeyCredential, Anthropic.Core.ClientOptions? options = null) :
this(resourceName, apiKeyCredential is null
? throw new ArgumentNullException(nameof(apiKeyCredential))
: DelegatedTokenCredential.Create((_, _) =>
{
apiKeyCredential.Deconstruct(out string dangerousCredential);
return new AccessToken(dangerousCredential, DateTimeOffset.MaxValue);
}),
options)
{ }
public override IAnthropicClient WithOptions(Func<Anthropic.Core.ClientOptions, Anthropic.Core.ClientOptions> modifier)
=> this;
protected override ValueTask BeforeSend<T>(
HttpRequest<T> request,
HttpRequestMessage requestMessage,
CancellationToken cancellationToken
)
{
var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);
return default;
}
}
}
@@ -0,0 +1,53 @@
# Creating an AIAgent with Anthropic
This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service.
The sample supports three deployment scenarios:
1. **Anthropic Public API** - Direct connection to Anthropic's public API
2. **Azure Foundry with API Key** - Anthropic models deployed through Azure Foundry using API key authentication
3. **Azure Foundry with Azure CLI** - Anthropic models deployed through Azure Foundry using Azure CLI credentials
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
### For Anthropic Public API
- Anthropic API key
Set the following environment variables:
```powershell
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
$env:ANTHROPIC_DEPLOYMENT_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
```
### For Azure Foundry with API Key
- Azure Foundry service endpoint and deployment configured
- Anthropic API key
Set the following environment variables:
```powershell
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure Foundry resource name (subdomain before .services.ai.azure.com)
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
$env:ANTHROPIC_DEPLOYMENT_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
```
### For Azure Foundry with Azure CLI
- Azure Foundry service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
Set the following environment variables:
```powershell
$env:ANTHROPIC_RESOURCE="your-foundry-resource-name" # Replace with your Azure Foundry resource name (subdomain before .services.ai.azure.com)
$env:ANTHROPIC_DEPLOYMENT_NAME="claude-haiku-4-5" # Optional, defaults to claude-haiku-4-5
```
**Note**: When using Azure Foundry with Azure CLI, make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
@@ -3,6 +3,7 @@
// This sample shows how to create and use a simple AI agent with OpenAI Chat Completion as the backend.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
@@ -15,6 +15,7 @@ See the README.md for each sample for the prerequisites for that sample.
|Sample|Description|
|---|---|
|[Creating an AIAgent with A2A](./Agent_With_A2A/)|This sample demonstrates how to create AIAgent for an existing A2A agent.|
|[Creating an AIAgent with Anthropic](./Agent_With_Anthropic/)|This sample demonstrates how to create an AIAgent using Anthropic Claude models as the underlying inference service|
|[Creating an AIAgent with Foundry Agents using Azure.AI.Agents.Persistent](./Agent_With_AzureAIAgentsPersistent/)|This sample demonstrates how to create a Foundry Persistent agent and expose it as an AIAgent using the Azure.AI.Agents.Persistent SDK|
|[Creating an AIAgent with Foundry Agents using Azure.AI.Project](./Agent_With_AzureAIProject/)|This sample demonstrates how to create an Foundry Project agent and expose it as an AIAgent using the Azure.AI.Project SDK|
|[Creating an AIAgent with AzureFoundry Model](./Agent_With_AzureFoundryModel/)|This sample demonstrates how to use any model deployed to Azure Foundry to create an AIAgent|
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with Anthropic as the backend.
using Anthropic;
using Anthropic.Core;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5";
AIAgent agent = new AnthropicClient(new ClientOptions { APIKey = apiKey })
.CreateAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
var response = await agent.RunAsync("Tell me a joke about a pirate.");
Console.WriteLine(response);
// Invoke the agent with streaming support.
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
{
Console.WriteLine(update);
}
@@ -0,0 +1,43 @@
# Running a simple agent with Anthropic
This sample demonstrates how to create and run a basic agent with Anthropic Claude models.
## What this sample demonstrates
- Creating an AI agent with Anthropic Claude
- Running a simple agent with instructions
- Managing agent lifecycle
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Anthropic API key configured
**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
Set the following environment variables:
```powershell
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model
```
## Run the sample
Navigate to the AgentWithAnthropic sample directory and run:
```powershell
cd dotnet\samples\GettingStarted\AgentWithAnthropic
dotnet run --project .\Agent_Anthropic_Step01_Running
```
## Expected behavior
The sample will:
1. Create an agent with Anthropic Claude
2. Run the agent with a simple prompt
3. Display the agent's response
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use an AI agent with reasoning capabilities.
using Anthropic;
using Anthropic.Core;
using Anthropic.Models.Messages;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5";
var maxTokens = 4096;
var thinkingTokens = 2048;
var agent = new AnthropicClient(new ClientOptions { APIKey = apiKey })
.CreateAIAgent(
model: model,
clientFactory: (chatClient) => chatClient
.AsBuilder()
.ConfigureOptions(
options => options.RawRepresentationFactory = (_) => new MessageCreateParams()
{
Model = options.ModelId ?? model,
MaxTokens = options.MaxOutputTokens ?? maxTokens,
Messages = [],
Thinking = new ThinkingConfigParam(new ThinkingConfigEnabled(budgetTokens: thinkingTokens))
})
.Build());
Console.WriteLine("1. Non-streaming:");
var response = await agent.RunAsync("Solve this problem step by step: If a train travels 60 miles per hour and needs to cover 180 miles, how long will the journey take? Show your reasoning.");
Console.WriteLine("#### Start Thinking ####");
Console.WriteLine($"\e[92m{string.Join("\n", response.Messages.SelectMany(m => m.Contents.OfType<TextReasoningContent>().Select(c => c.Text)))}\e[0m");
Console.WriteLine("#### End Thinking ####");
Console.WriteLine("\n#### Final Answer ####");
Console.WriteLine(response.Text);
Console.WriteLine("Token usage:");
Console.WriteLine($"Input: {response.Usage?.InputTokenCount}, Output: {response.Usage?.OutputTokenCount}, {string.Join(", ", response.Usage?.AdditionalCounts ?? [])}");
Console.WriteLine();
Console.WriteLine("2. Streaming");
await foreach (var update in agent.RunStreamingAsync("Explain the theory of relativity in simple terms."))
{
foreach (var item in update.Contents)
{
if (item is TextReasoningContent reasoningContent)
{
Console.WriteLine($"\e[92m{reasoningContent.Text}\e[0m");
}
else if (item is TextContent textContent)
{
Console.WriteLine(textContent.Text);
}
}
}
@@ -0,0 +1,46 @@
# Using reasoning with Anthropic agents
This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents.
## What this sample demonstrates
- Creating an AI agent with Anthropic Claude extended thinking
- Using reasoning capabilities for complex problem solving
- Extracting thinking and response content from agent output
- Managing agent lifecycle
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Anthropic API key configured
- Access to Anthropic Claude models with extended thinking support
**Note**: This sample uses Anthropic Claude models with extended thinking. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
Set the following environment variables:
```powershell
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model
```
## Run the sample
Navigate to the AgentWithAnthropic sample directory and run:
```powershell
cd dotnet\samples\GettingStarted\AgentWithAnthropic
dotnet run --project .\Agent_Anthropic_Step02_Reasoning
```
## Expected behavior
The sample will:
1. Create an agent with Anthropic Claude extended thinking enabled
2. Run the agent with a complex reasoning prompt
3. Display the agent's thinking process
4. Display the agent's final response
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use an agent with function tools.
// It shows both non-streaming and streaming agent interactions using weather-related tools.
using System.ComponentModel;
using Anthropic;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5";
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
const string AssistantInstructions = "You are a helpful assistant that can get weather information.";
const string AssistantName = "WeatherAssistant";
// Define the agent with function tools.
AITool tool = AIFunctionFactory.Create(GetWeather);
// Get anthropic client to create agents.
AIAgent agent = new AnthropicClient { APIKey = apiKey }
.CreateAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentThread thread = agent.GetNewThread();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
thread = agent.GetNewThread();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
}
@@ -0,0 +1,47 @@
# Using Function Tools with Anthropic agents
This sample demonstrates how to use function tools with Anthropic Claude agents, allowing agents to call custom functions to retrieve information.
## What this sample demonstrates
- Creating function tools using AIFunctionFactory
- Passing function tools to an Anthropic Claude agent
- Running agents with function tools (text output)
- Running agents with function tools (streaming output)
- Managing agent lifecycle
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Anthropic API key configured
**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
Set the following environment variables:
```powershell
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
$env:ANTHROPIC_MODEL="your-anthropic-model" # Replace with your Anthropic model
```
## Run the sample
Navigate to the AgentWithAnthropic sample directory and run:
```powershell
cd dotnet\samples\GettingStarted\AgentWithAnthropic
dotnet run --project .\Agent_Anthropic_Step03_UsingFunctionTools
```
## Expected behavior
The sample will:
1. Create an agent named "WeatherAssistant" with a GetWeather function tool
2. Run the agent with a text prompt asking about weather
3. The agent will invoke the GetWeather function tool to retrieve weather information
4. Run the agent again with streaming to display the response as it's generated
5. Clean up resources by deleting the agent
@@ -0,0 +1,72 @@
# Getting started with agents using Anthropic
The getting started with agents using Anthropic samples demonstrate the fundamental concepts and functionalities
of single agents using Anthropic as the AI provider.
These samples use Anthropic Claude models as the AI provider and use ChatCompletion as the type of service.
For other samples that demonstrate how to create and configure each type of agent that come with the agent framework,
see the [How to create an agent for each provider](../AgentProviders/README.md) samples.
## Getting started with agents using Anthropic prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 8.0 SDK or later
- Anthropic API key configured
- User has access to Anthropic Claude models
**Note**: These samples use Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/).
## Using Anthropic with Azure Foundry
To use Anthropic with Azure Foundry, you can check the sample [AgentProviders/Agent_With_Anthropic](../AgentProviders/Agent_With_Anthropic/README.md) for more details.
## Samples
|Sample|Description|
|---|---|
|[Running a simple agent](./Agent_Anthropic_Step01_Running/)|This sample demonstrates how to create and run a basic agent with Anthropic Claude|
|[Using reasoning with an agent](./Agent_Anthropic_Step02_Reasoning/)|This sample demonstrates how to use extended thinking/reasoning capabilities with Anthropic Claude agents|
|[Using function tools with an agent](./Agent_Anthropic_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with an Anthropic Claude agent|
## Running the samples from the console
To run the samples, navigate to the desired sample directory, e.g.
```powershell
cd Agent_Anthropic_Step01_Running
```
Set the following environment variables:
```powershell
$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key
```
If the variables are not set, you will be prompted for the values when running the samples.
Execute the following command to build the sample:
```powershell
dotnet build
```
Execute the following command to run the sample:
```powershell
dotnet run --no-build
```
Or just build and run in one step:
```powershell
dotnet run
```
## Running the samples from Visual Studio
Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
You will be prompted for any required environment variables if they are not already set.
@@ -44,7 +44,7 @@ while (response.ContinuationToken is not null)
await Task.Delay(TimeSpan.FromSeconds(10));
RestoreAgentState(agent, out thread, out object? continuationToken);
RestoreAgentState(agent, out thread, out ResponseContinuationToken? continuationToken);
options.ContinuationToken = continuationToken;
response = await agent.RunAsync(thread, options);
@@ -52,19 +52,19 @@ while (response.ContinuationToken is not null)
Console.WriteLine(response.Text);
void PersistAgentState(AgentThread thread, object? continuationToken)
void PersistAgentState(AgentThread thread, ResponseContinuationToken? continuationToken)
{
stateStore["thread"] = thread.Serialize();
stateStore["continuationToken"] = JsonSerializer.SerializeToElement(continuationToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
}
void RestoreAgentState(AIAgent agent, out AgentThread thread, out object? continuationToken)
void RestoreAgentState(AIAgent agent, out AgentThread thread, out ResponseContinuationToken? continuationToken)
{
JsonElement serializedThread = stateStore["thread"] ?? throw new InvalidOperationException("No serialized thread found in state store.");
JsonElement? serializedToken = stateStore["continuationToken"];
thread = agent.DeserializeThread(serializedThread);
continuationToken = serializedToken?.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
continuationToken = (ResponseContinuationToken?)serializedToken?.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
}
[Description("Researches relevant space facts and scientific information for writing a science fiction novel")]
+1
View File
@@ -15,5 +15,6 @@ of the agent framework.
|[A2A](./A2A/README.md)|Getting started with A2A (Agent-to-Agent) specific features|
|[Agent Open Telemetry](./AgentOpenTelemetry/README.md)|Getting started with OpenTelemetry for agents|
|[Agent With OpenAI exchange types](./AgentWithOpenAI/README.md)|Using OpenAI exchange types with agents|
|[Agent With Anthropic](./AgentWithAnthropic/README.md)|Getting started with agents using Anthropic Claude|
|[Workflow](./Workflows/README.md)|Getting started with Workflow|
|[Model Context Protocol](./ModelContextProtocol/README.md)|Getting started with Model Context Protocol|
+161 -33
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
@@ -79,26 +80,32 @@ internal sealed class A2AAgent : AIAgent
{
_ = Throw.IfNull(messages);
var a2aMessage = messages.ToA2AMessage();
thread ??= this.GetNewThread();
if (thread is not A2AAgentThread typedThread)
{
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
}
// Linking the message to the existing conversation, if any.
a2aMessage.ContextId = typedThread.ContextId;
this._logger.LogA2AAgentInvokingAgent(nameof(RunAsync), this.Id, this.Name);
var a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
A2AResponse? a2aResponse = null;
if (GetContinuationToken(messages, options) is { } token)
{
a2aResponse = await this._a2aClient.GetTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
else
{
var a2aMessage = CreateA2AMessage(typedThread, messages);
a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
}
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
if (a2aResponse is AgentMessage message)
{
UpdateThreadConversationId(typedThread, message.ContextId);
UpdateThread(typedThread, message.ContextId);
return new AgentRunResponse
{
@@ -106,21 +113,30 @@ internal sealed class A2AAgent : AIAgent
ResponseId = message.MessageId,
RawRepresentation = message,
Messages = [message.ToChatMessage()],
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
}
if (a2aResponse is AgentTask agentTask)
{
UpdateThreadConversationId(typedThread, agentTask.ContextId);
UpdateThread(typedThread, agentTask.ContextId, agentTask.Id);
return new AgentRunResponse
var response = new AgentRunResponse
{
AgentId = this.Id,
ResponseId = agentTask.Id,
RawRepresentation = agentTask,
Messages = agentTask.ToChatMessages(),
AdditionalProperties = agentTask.Metadata.ToAdditionalProperties(),
Messages = agentTask.ToChatMessages() ?? [],
ContinuationToken = CreateContinuationToken(agentTask.Id, agentTask.Status.State),
AdditionalProperties = agentTask.Metadata?.ToAdditionalProperties(),
};
if (agentTask.ToChatMessages() is { Count: > 0 } taskMessages)
{
response.Messages = taskMessages;
}
return response;
}
throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
@@ -131,43 +147,67 @@ internal sealed class A2AAgent : AIAgent
{
_ = Throw.IfNull(messages);
var a2aMessage = messages.ToA2AMessage();
thread ??= this.GetNewThread();
if (thread is not A2AAgentThread typedThread)
{
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
}
// Linking the message to the existing conversation, if any.
a2aMessage.ContextId = typedThread.ContextId;
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
var a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
ConfiguredCancelableAsyncEnumerable<SseItem<A2AEvent>> a2aSseEvents;
if (options?.ContinuationToken is not null)
{
// Task stream resumption is not well defined in the A2A v2.* specification, leaving it to the agent implementations.
// The v3.0 specification improves this by defining task stream reconnection that allows obtaining the same stream
// from the beginning, but it does not define stream resumption from a specific point in the stream.
// Therefore, the code should be updated once the A2A .NET library supports the A2A v3.0 specification,
// and AF has the necessary model to allow consumers to know whether they need to resume the stream and add new updates to
// the existing ones or reconnect the stream and obtain all updates again.
// For more details, see the following issue: https://github.com/microsoft/agent-framework/issues/1764
throw new InvalidOperationException("Reconnecting to task streams using continuation tokens is not supported yet.");
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
var a2aMessage = CreateA2AMessage(typedThread, messages);
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
string? contextId = null;
string? taskId = null;
await foreach (var sseEvent in a2aSseEvents)
{
if (sseEvent.Data is not AgentMessage message)
if (sseEvent.Data is AgentMessage message)
{
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {sseEvent.Data?.GetType().FullName ?? "null"}");
contextId = message.ContextId;
yield return this.ConvertToAgentResponseUpdate(message);
}
UpdateThreadConversationId(typedThread, message.ContextId);
yield return new AgentRunResponseUpdate
else if (sseEvent.Data is AgentTask task)
{
AgentId = this.Id,
ResponseId = message.MessageId,
RawRepresentation = message,
Role = ChatRole.Assistant,
MessageId = message.MessageId,
Contents = [.. message.Parts.Select(part => part.ToAIContent()).OfType<AIContent>()],
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
};
contextId = task.ContextId;
taskId = task.Id;
yield return this.ConvertToAgentResponseUpdate(task);
}
else if (sseEvent.Data is TaskUpdateEvent taskUpdateEvent)
{
contextId = taskUpdateEvent.ContextId;
taskId = taskUpdateEvent.TaskId;
yield return this.ConvertToAgentResponseUpdate(taskUpdateEvent);
}
else
{
throw new NotSupportedException($"Only message, task, task update events are supported from A2A agents. Received: {sseEvent.Data.GetType().FullName ?? "null"}");
}
}
UpdateThread(typedThread, contextId, taskId);
}
/// <inheritdoc/>
@@ -182,7 +222,7 @@ internal sealed class A2AAgent : AIAgent
/// <inheritdoc/>
public override string? Description => this._description ?? base.Description;
private static void UpdateThreadConversationId(A2AAgentThread? thread, string? contextId)
private static void UpdateThread(A2AAgentThread? thread, string? contextId, string? taskId = null)
{
if (thread is null)
{
@@ -199,5 +239,93 @@ internal sealed class A2AAgent : AIAgent
// Assign a server-generated context Id to the thread if it's not already set.
thread.ContextId ??= contextId;
thread.TaskId = taskId;
}
private static AgentMessage CreateA2AMessage(A2AAgentThread typedThread, IEnumerable<ChatMessage> messages)
{
var a2aMessage = messages.ToA2AMessage();
// Linking the message to the existing conversation, if any.
// See: https://github.com/a2aproject/A2A/blob/main/docs/topics/life-of-a-task.md#group-related-interactions
a2aMessage.ContextId = typedThread.ContextId;
// Link the message as a follow-up to an existing task, if any.
// See: https://github.com/a2aproject/A2A/blob/main/docs/topics/life-of-a-task.md#task-refinements
a2aMessage.ReferenceTaskIds = typedThread.TaskId is null ? null : [typedThread.TaskId];
return a2aMessage;
}
private static A2AContinuationToken? GetContinuationToken(IEnumerable<ChatMessage> messages, AgentRunOptions? options = null)
{
if (options?.ContinuationToken is ResponseContinuationToken token)
{
if (messages.Any())
{
throw new InvalidOperationException("Messages are not allowed when continuing a background response using a continuation token.");
}
return A2AContinuationToken.FromToken(token);
}
return null;
}
private static A2AContinuationToken? CreateContinuationToken(string taskId, TaskState state)
{
if (state == TaskState.Submitted || state == TaskState.Working)
{
return new A2AContinuationToken(taskId);
}
return null;
}
private AgentRunResponseUpdate ConvertToAgentResponseUpdate(AgentMessage message)
{
return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = message.MessageId,
RawRepresentation = message,
Role = ChatRole.Assistant,
MessageId = message.MessageId,
Contents = message.Parts.ConvertAll(part => part.ToAIContent()),
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
}
private AgentRunResponseUpdate ConvertToAgentResponseUpdate(AgentTask task)
{
return new AgentRunResponseUpdate
{
AgentId = this.Id,
ResponseId = task.Id,
RawRepresentation = task,
Role = ChatRole.Assistant,
Contents = task.ToAIContents(),
AdditionalProperties = task.Metadata?.ToAdditionalProperties(),
};
}
private AgentRunResponseUpdate ConvertToAgentResponseUpdate(TaskUpdateEvent taskUpdateEvent)
{
AgentRunResponseUpdate responseUpdate = new()
{
AgentId = this.Id,
ResponseId = taskUpdateEvent.TaskId,
RawRepresentation = taskUpdateEvent,
Role = ChatRole.Assistant,
AdditionalProperties = taskUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
};
if (taskUpdateEvent is TaskArtifactUpdateEvent artifactUpdateEvent)
{
responseUpdate.Contents = artifactUpdateEvent.Artifact.ToAIContents();
responseUpdate.RawRepresentation = artifactUpdateEvent;
}
return responseUpdate;
}
}
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
namespace Microsoft.Agents.AI.A2A;
@@ -7,22 +8,59 @@ namespace Microsoft.Agents.AI.A2A;
/// <summary>
/// Thread for A2A based agents.
/// </summary>
public sealed class A2AAgentThread : ServiceIdAgentThread
public sealed class A2AAgentThread : AgentThread
{
internal A2AAgentThread()
{
}
internal A2AAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null) : base(serializedThreadState, jsonSerializerOptions)
internal A2AAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
{
if (serializedThreadState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState));
}
var state = serializedThreadState.Deserialize(
A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(A2AAgentThreadState))) as A2AAgentThreadState;
if (state?.ContextId is string contextId)
{
this.ContextId = contextId;
}
if (state?.TaskId is string taskId)
{
this.TaskId = taskId;
}
}
/// <summary>
/// Gets the ID for the current conversation with the A2A agent.
/// </summary>
public string? ContextId
public string? ContextId { get; internal set; }
/// <summary>
/// Gets the ID for the task the agent is currently working on.
/// </summary>
public string? TaskId { get; internal set; }
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
get { return this.ServiceThreadId; }
internal set { this.ServiceThreadId = value; }
var state = new A2AAgentThreadState
{
ContextId = this.ContextId,
TaskId = this.TaskId
};
return JsonSerializer.SerializeToElement(state, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(A2AAgentThreadState)));
}
internal sealed class A2AAgentThreadState
{
public string? ContextId { get; set; }
public string? TaskId { get; set; }
}
}
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.A2A;
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
internal class A2AContinuationToken : ResponseContinuationToken
{
internal A2AContinuationToken(string taskId)
{
_ = Throw.IfNullOrEmpty(taskId);
this.TaskId = taskId;
}
internal string TaskId { get; }
internal static A2AContinuationToken FromToken(ResponseContinuationToken token)
{
if (token is A2AContinuationToken longRunContinuationToken)
{
return longRunContinuationToken;
}
ReadOnlyMemory<byte> data = token.ToBytes();
if (data.Length == 0)
{
Throw.ArgumentException(nameof(token), "Failed to create A2AContinuationToken from provided token because it does not contain any data.");
}
Utf8JsonReader reader = new(data.Span);
string taskId = null!;
reader.Read();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
string propertyName = reader.GetString() ?? throw new JsonException("Failed to read property name from continuation token.");
switch (propertyName)
{
case "taskId":
reader.Read();
taskId = reader.GetString()!;
break;
default:
throw new JsonException($"Unrecognized property '{propertyName}'.");
}
}
return new(taskId);
}
public override ReadOnlyMemory<byte> ToBytes()
{
using MemoryStream stream = new();
using Utf8JsonWriter writer = new(stream);
writer.WriteStartObject();
writer.WriteString("taskId", this.TaskId);
writer.WriteEndObject();
writer.Flush();
stream.Position = 0;
return stream.ToArray();
}
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.A2A;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides utility methods and configurations for JSON serialization operations for A2A agent types.
/// </summary>
public static partial class A2AJsonUtilities
{
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for JSON serialization operations of A2A agent types.
/// </summary>
/// <remarks>
/// <para>
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// includes source generated contracts for A2A agent types.
/// </para>
/// <para>
/// It additionally turns on the following settings:
/// <list type="number">
/// <item><description>Enables <see cref="JsonSerializerDefaults.Web"/> defaults.</description></item>
/// <item><description>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</description></item>
/// <item><description>Enables <see cref="JsonNumberHandling.AllowReadingFromString"/> as the default number handling for number types.</description></item>
/// <item><description>
/// Enables <see cref="JavaScriptEncoder.UnsafeRelaxedJsonEscaping"/> when escaping JSON strings.
/// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, such as HTML and XML.
/// </description></item>
/// </list>
/// </para>
/// </remarks>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
/// <summary>
/// Creates and configures the default JSON serialization options for agent abstraction types.
/// </summary>
/// <returns>The configured options.</returns>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
private static JsonSerializerOptions CreateDefaultOptions()
{
// Copy the configuration from the source generated context.
JsonSerializerOptions options = new(JsonContext.Default.Options)
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as AIJsonUtilities
};
// Chain in the resolvers from both AIJsonUtilities and our source generated context.
// We want AIJsonUtilities first to ensure any M.E.AI types are handled via its resolver.
options.TypeInfoResolverChain.Clear();
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
// If reflection-based serialization is enabled by default, this includes
// the default type info resolver that utilizes reflection, but we need to manually
// apply the same converter AIJsonUtilities adds for string-based enum serialization,
// as that's not propagated as part of the resolver.
if (JsonSerializer.IsReflectionEnabledByDefault)
{
options.Converters.Add(new JsonStringEnumConverter());
}
options.MakeReadOnly();
return options;
}
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
// A2A agent types
[JsonSerializable(typeof(A2AAgentThread.A2AAgentThreadState))]
[ExcludeFromCodeCoverage]
private sealed partial class JsonContext : JsonSerializerContext;
}
@@ -11,20 +11,37 @@ namespace A2A;
/// </summary>
internal static class A2AAgentTaskExtensions
{
internal static IList<ChatMessage> ToChatMessages(this AgentTask agentTask)
internal static IList<ChatMessage>? ToChatMessages(this AgentTask agentTask)
{
_ = Throw.IfNull(agentTask);
List<ChatMessage> messages = [];
List<ChatMessage>? messages = null;
if (agentTask.Artifacts is not null)
if (agentTask?.Artifacts is { Count: > 0 })
{
foreach (var artifact in agentTask.Artifacts)
{
messages.Add(artifact.ToChatMessage());
(messages ??= []).Add(artifact.ToChatMessage());
}
}
return messages;
}
internal static IList<AIContent>? ToAIContents(this AgentTask agentTask)
{
_ = Throw.IfNull(agentTask);
List<AIContent>? aiContents = null;
if (agentTask.Artifacts is not null)
{
foreach (var artifact in agentTask.Artifacts)
{
(aiContents ??= []).AddRange(artifact.ToAIContents());
}
}
return aiContents;
}
}
@@ -12,21 +12,15 @@ internal static class A2AArtifactExtensions
{
internal static ChatMessage ToChatMessage(this Artifact artifact)
{
List<AIContent>? aiContents = null;
foreach (var part in artifact.Parts)
{
var content = part.ToAIContent();
if (content is not null)
{
(aiContents ??= []).Add(content);
}
}
return new ChatMessage(ChatRole.Assistant, aiContents)
return new ChatMessage(ChatRole.Assistant, artifact.ToAIContents())
{
AdditionalProperties = artifact.Metadata.ToAdditionalProperties(),
RawRepresentation = artifact,
};
}
internal static List<AIContent> ToAIContents(this Artifact artifact)
{
return artifact.Parts.ConvertAll(part => part.ToAIContent());
}
}
@@ -2,12 +2,14 @@
<PropertyGroup>
<VersionSuffix>preview</VersionSuffix>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
</PropertyGroup>
<ItemGroup>
@@ -50,7 +50,7 @@ public class AgentRunOptions
/// can be polled for completion by obtaining the token from the <see cref="AgentRunResponse.ContinuationToken"/> property
/// and passing it via this property on subsequent calls to <see cref="AIAgent.RunAsync(AgentThread?, AgentRunOptions?, System.Threading.CancellationToken)"/>.
/// </remarks>
public object? ContinuationToken { get; set; }
public ResponseContinuationToken? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the background responses are allowed.
@@ -175,7 +175,7 @@ public class AgentRunResponse
/// to poll for completion.
/// </para>
/// </remarks>
public object? ContinuationToken { get; set; }
public ResponseContinuationToken? ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the timestamp indicating when this response was created.
@@ -159,7 +159,7 @@ public class AgentRunResponseUpdate
/// to resume streaming from the point of interruption.
/// </para>
/// </remarks>
public object? ContinuationToken { get; set; }
public ResponseContinuationToken? ContinuationToken { get; set; }
/// <inheritdoc/>
public override string ToString() => this.Text;
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
namespace Anthropic.Services;
/// <summary>
/// Provides extension methods for the <see cref="IBetaService"/> class.
/// </summary>
public static class AnthropicBetaServiceExtensions
{
/// <summary>
/// Specifies the default maximum number of tokens allowed for processing operations.
/// </summary>
public static int DefaultMaxTokens { get; set; } = 4096;
/// <summary>
/// Creates a new AI agent using the specified model and options.
/// </summary>
/// <param name="betaService">The Anthropic beta service.</param>
/// <param name="model">The model to use for chat completions.</param>
/// <param name="instructions">The instructions for the AI agent.</param>
/// <param name="name">The name of the AI agent.</param>
/// <param name="description">The description of the AI agent.</param>
/// <param name="tools">The tools available to the AI agent.</param>
/// <param name="defaultMaxTokens">The default maximum tokens for chat completions. Defaults to <see cref="DefaultMaxTokens"/> if not provided.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>The created <see cref="ChatClientAgent"/> AI agent.</returns>
public static ChatClientAgent CreateAIAgent(
this IBetaService betaService,
string model,
string? instructions = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
int? defaultMaxTokens = null,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
{
var options = new ChatClientAgentOptions
{
Instructions = instructions,
Name = name,
Description = description,
};
if (tools is { Count: > 0 })
{
options.ChatOptions = new ChatOptions { Tools = tools };
}
var chatClient = betaService.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, options, loggerFactory, services);
}
/// <summary>
/// Creates an AI agent from an <see cref="IBetaService"/> using the Anthropic Chat Completion API.
/// </summary>
/// <param name="betaService">The Anthropic <see cref="IBetaService"/> to use for the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the Anthropic Chat Completion service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="betaService"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent CreateAIAgent(
this IBetaService betaService,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(betaService);
Throw.IfNull(options);
var chatClient = betaService.AsIChatClient();
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, options, loggerFactory, services);
}
}
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
namespace Anthropic;
/// <summary>
/// Provides extension methods for the <see cref="IAnthropicClient"/> class.
/// </summary>
public static class AnthropicClientExtensions
{
/// <summary>
/// Specifies the default maximum number of tokens allowed for processing operations.
/// </summary>
public static int DefaultMaxTokens { get; set; } = 4096;
/// <summary>
/// Creates a new AI agent using the specified model and options.
/// </summary>
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent..</param>
/// <param name="model">The model to use for chat completions.</param>
/// <param name="instructions">The instructions for the AI agent.</param>
/// <param name="name">The name of the AI agent.</param>
/// <param name="description">The description of the AI agent.</param>
/// <param name="tools">The tools available to the AI agent.</param>
/// <param name="defaultMaxTokens">The default maximum tokens for chat completions. Defaults to <see cref="DefaultMaxTokens"/> if not provided.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>The created <see cref="ChatClientAgent"/> AI agent.</returns>
public static ChatClientAgent CreateAIAgent(
this IAnthropicClient client,
string model,
string? instructions = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
int? defaultMaxTokens = null,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
{
var options = new ChatClientAgentOptions
{
Instructions = instructions,
Name = name,
Description = description,
};
if (tools is { Count: > 0 })
{
options.ChatOptions = new ChatOptions { Tools = tools };
}
var chatClient = client.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, options, loggerFactory, services);
}
/// <summary>
/// Creates an AI agent from an <see cref="IAnthropicClient"/> using the Anthropic Chat Completion API.
/// </summary>
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent..</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <param name="services">An optional <see cref="IServiceProvider"/> to use for resolving services required by the <see cref="AIFunction"/> instances being invoked.</param>
/// <returns>An <see cref="ChatClientAgent"/> instance backed by the Anthropic Chat Completion service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent CreateAIAgent(
this IAnthropicClient client,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(client);
Throw.IfNull(options);
var chatClient = client.AsIChatClient();
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, options, loggerFactory, services);
}
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CA1812
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Anthropic;
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
internal sealed partial class AnthropicClientJsonContext : JsonSerializerContext;
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionSuffix>preview</VersionSuffix>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Anthropic" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Anthropic Agents</Title>
<Description>Provides Microsoft Agent Framework support for Anthropic Agents.</Description>
</PropertyGroup>
</Project>
@@ -146,6 +146,9 @@ internal sealed partial class IdGenerator
const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
if (random is not null)
{
#if NET10_0_OR_GREATER
return random.GetString(Chars, stringLength);
#else
// Use deterministic random generation when seed is provided
return string.Create(stringLength, random, static (destination, random) =>
{
@@ -154,6 +157,7 @@ internal sealed partial class IdGenerator
destination[i] = Chars[random.Next(Chars.Length)];
}
});
#endif
}
// Use cryptographically secure random generation when no seed is provided
@@ -329,7 +329,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// <summary>
/// Creates a new agent thread instance using an existing <see cref="ChatMessageStore"/> to continue a conversation.
/// </summary>
/// <param name="chatMessageStore">The chat history of the existing conversation to continue.</param>
/// <param name="chatMessageStore">The <see cref="ChatMessageStore"/> instance to use for managing the conversation's message history.</param>
/// <returns>
/// A new <see cref="AgentThread"/> instance configured to work with the provided <paramref name="chatMessageStore"/>.
/// </returns>
@@ -350,7 +350,7 @@ public sealed partial class ChatClientAgent : AIAgent
public AgentThread GetNewThread(ChatMessageStore chatMessageStore)
=> new ChatClientAgentThread()
{
MessageStore = chatMessageStore,
MessageStore = Throw.IfNull(chatMessageStore),
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null })
};
@@ -735,8 +735,8 @@ public sealed partial class ChatClientAgent : AIAgent
else
{
// If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and
// the thread has no MessageStore yet, and we have a custom messages store, we should update the thread
// with the custom MessageStore so that it has somewhere to store the chat history.
// the thread has no MessageStore yet, we should update the thread with the custom MessageStore or
// default InMemoryMessageStore so that it has somewhere to store the chat history.
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }) ?? new InMemoryChatMessageStore();
}
}
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Shared.IntegrationTests;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
#pragma warning disable CA1812 // Internal class that is apparently never instantiated.
internal sealed class AnthropicConfiguration
{
public string? ServiceId { get; set; }
public string ChatModelId { get; set; }
public string ChatReasoningModelId { get; set; }
public string ApiKey { get; set; }
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllChatClientRunStreaming(Func<AnthropicChatCompletionFixture> func) : ChatClientAgentRunStreamingTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: false));
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllChatClientAgentRun(Func<AnthropicChatCompletionFixture> func) : ChatClientAgentRunTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
=> base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
=> base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
}
public class AnthropicBetaChatCompletionChatClientAgentRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionChatClientAgentReasoningRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionChatClientAgentRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionChatClientAgentReasoningRunTests()
: SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: false));
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using Anthropic;
using Anthropic.Models.Beta.Messages;
using Anthropic.Models.Messages;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public class AnthropicChatCompletionFixture : IChatClientAgentFixture
{
// All tests for Anthropic are intended to be ran locally as the CI pipeline for Anthropic is not setup.
internal const string SkipReason = "Integrations tests for local execution only";
private static readonly AnthropicConfiguration s_config = TestConfiguration.LoadSection<AnthropicConfiguration>();
private readonly bool _useReasoningModel;
private readonly bool _useBeta;
private ChatClientAgent _agent = null!;
public AnthropicChatCompletionFixture(bool useReasoningChatModel, bool useBeta)
{
this._useReasoningModel = useReasoningChatModel;
this._useBeta = useBeta;
}
public AIAgent Agent => this._agent;
public IChatClient ChatClient => this._agent.ChatClient;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
{
var typedThread = (ChatClientAgentThread)thread;
return typedThread.MessageStore is null ? [] : (await typedThread.MessageStore.GetMessagesAsync()).ToList();
}
public Task<ChatClientAgent> CreateChatClientAgentAsync(
string name = "HelpfulAssistant",
string instructions = "You are a helpful assistant.",
IList<AITool>? aiTools = null)
{
var anthropicClient = new AnthropicClient() { APIKey = s_config.ApiKey };
IChatClient? chatClient = this._useBeta
? anthropicClient
.Beta
.AsIChatClient()
.AsBuilder()
.ConfigureOptions(options
=> options.RawRepresentationFactory = _
=> new Anthropic.Models.Beta.Messages.MessageCreateParams()
{
Model = options.ModelId ?? (this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId),
MaxTokens = options.MaxOutputTokens ?? 4096,
Messages = [],
Thinking = this._useReasoningModel
? new BetaThinkingConfigParam(new BetaThinkingConfigEnabled(2048))
: new BetaThinkingConfigParam(new BetaThinkingConfigDisabled())
}).Build()
: anthropicClient
.AsIChatClient()
.AsBuilder()
.ConfigureOptions(options
=> options.RawRepresentationFactory = _
=> new Anthropic.Models.Messages.MessageCreateParams()
{
Model = options.ModelId ?? (this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId),
MaxTokens = options.MaxOutputTokens ?? 4096,
Messages = [],
Thinking = this._useReasoningModel
? new ThinkingConfigParam(new ThinkingConfigEnabled(2048))
: new ThinkingConfigParam(new ThinkingConfigDisabled())
}).Build();
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
Instructions = instructions,
ChatOptions = new() { Tools = aiTools }
}));
}
public Task DeleteAgentAsync(ChatClientAgent agent) =>
// Chat Completion does not require/support deleting agents, so this is a no-op.
Task.CompletedTask;
public Task DeleteThreadAsync(AgentThread thread) =>
// Chat Completion does not require/support deleting threads, so this is a no-op.
Task.CompletedTask;
public async Task InitializeAsync() =>
this._agent = await this.CreateChatClientAgentAsync();
public Task DisposeAsync() =>
Task.CompletedTask;
}
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllRunStreaming(Func<AnthropicChatCompletionFixture> func) : RunStreamingTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync();
}
public class AnthropicBetaChatCompletionRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionReasoningRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionReasoningRunStreamingTests()
: SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: false));
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
namespace AnthropicChatCompletion.IntegrationTests;
public abstract class SkipAllRun(Func<AnthropicChatCompletionFixture> func) : RunTests<AnthropicChatCompletionFixture>(func)
{
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync();
[Fact(Skip = AnthropicChatCompletionFixture.SkipReason)]
public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync();
}
public class AnthropicBetaChatCompletionRunTests()
: SkipAllRun(() => new(useReasoningChatModel: false, useBeta: true));
public class AnthropicBetaChatCompletionReasoningRunTests()
: SkipAllRun(() => new(useReasoningChatModel: true, useBeta: true));
public class AnthropicChatCompletionRunTests()
: SkipAllRun(() => new(useReasoningChatModel: false, useBeta: false));
public class AnthropicChatCompletionReasoningRunTests()
: SkipAllRun(() => new(useReasoningChatModel: true, useBeta: false));
@@ -28,7 +28,7 @@ public class AIProjectClientCreateTests
public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync(string createMechanism)
{
// Arrange.
const string AgentName = "IntegrationTestAgent";
string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("IntegrationTestAgent");
const string AgentDescription = "An agent created during integration tests";
const string AgentInstructions = "You are an integration test agent";
@@ -86,7 +86,7 @@ public class AIProjectClientCreateTests
public async Task CreateAgent_CreatesAgentWithVectorStoresAsync(string createMechanism)
{
// Arrange.
const string AgentName = "VectorStoreAgent";
string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("VectorStoreAgent");
const string AgentInstructions = """
You are a helpful agent that can help fetch data from files you know about.
Use the File Search Tool to look up codes for words.
@@ -159,7 +159,7 @@ public class AIProjectClientCreateTests
public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism)
{
// Arrange.
const string AgentName = "CodeInterpreterAgent";
string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("CodeInterpreterAgent");
const string AgentInstructions = """
You are a helpful coding agent. A Python file is provided. Use the Code Interpreter Tool to run the file
and report the SECRET_NUMBER value it prints. Respond only with the number.
@@ -229,7 +229,7 @@ public class AIProjectClientCreateTests
public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync(string createMechanism)
{
// Arrange.
const string AgentName = "WeatherAgent";
string AgentName = AIProjectClientFixture.GenerateUniqueAgentName("WeatherAgent");
const string AgentInstructions = "You are a helpful weather assistant. Always call the GetWeather function to answer questions about weather.";
static string GetWeather(string location) => $"The weather in {location} is sunny with a high of 23C.";
@@ -114,7 +114,7 @@ public class AIProjectClientFixture : IChatClientAgentFixture
return await this._client.CreateAIAgentAsync(GenerateUniqueAgentName(name), model: s_config.DeploymentName, instructions: instructions, tools: aiTools);
}
private static string GenerateUniqueAgentName(string baseName) =>
public static string GenerateUniqueAgentName(string baseName) =>
$"{baseName}-{Guid.NewGuid().ToString("N").Substring(0, 8)}";
public Task DeleteAgentAsync(ChatClientAgent agent) =>
@@ -385,6 +385,7 @@ public sealed class A2AAgentTests : IDisposable
// Act & Assert
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages))
{
// Just iterate through to trigger the logic
}
}
@@ -414,15 +415,422 @@ public sealed class A2AAgentTests : IDisposable
Assert.Equal("https://example.com/file.pdf", ((FilePart)message.Parts[1]).File.Uri?.ToString());
}
[Fact]
public async Task RunAsync_WithContinuationTokenAndMessages_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => this._agent.RunAsync(inputMessages, null, options));
}
[Fact]
public async Task RunAsync_WithContinuationToken_CallsGetTaskAsyncAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-123",
ContextId = "context-123"
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
// Act
await this._agent.RunAsync([], options: options);
// Assert
Assert.Equal("tasks/get", this._handler.CapturedJsonRpcRequest?.Method);
Assert.Equal("task-123", this._handler.CapturedTaskIdParams?.Id);
}
[Fact]
public async Task RunAsync_WithTaskInThreadAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response to task" }]
};
var thread = (A2AAgentThread)this._agent.GetNewThread();
thread.TaskId = "task-123";
var inputMessage = new ChatMessage(ChatRole.User, "Please make the background transparent");
// Act
await this._agent.RunAsync(inputMessage, thread);
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
}
[Fact]
public async Task RunAsync_WithAgentTask_UpdatesThreadTaskIdAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
};
var thread = this._agent.GetNewThread();
// Act
await this._agent.RunAsync("Start a task", thread);
// Assert
var a2aThread = (A2AAgentThread)thread;
Assert.Equal("task-456", a2aThread.TaskId);
}
[Fact]
public async Task RunAsync_WithAgentTaskResponse_ReturnsTaskResponseCorrectlyAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-789",
ContextId = "context-456",
Status = new() { State = TaskState.Submitted },
Metadata = new Dictionary<string, JsonElement>
{
{ "key1", JsonSerializer.SerializeToElement("value1") },
{ "count", JsonSerializer.SerializeToElement(42) }
}
};
var thread = this._agent.GetNewThread();
// Act
var result = await this._agent.RunAsync("Start a long-running task", thread);
// Assert - verify task is converted correctly
Assert.NotNull(result);
Assert.Equal(this._agent.Id, result.AgentId);
Assert.Equal("task-789", result.ResponseId);
Assert.NotNull(result.RawRepresentation);
Assert.IsType<AgentTask>(result.RawRepresentation);
Assert.Equal("task-789", ((AgentTask)result.RawRepresentation).Id);
// Assert - verify continuation token is set for submitted task
Assert.NotNull(result.ContinuationToken);
Assert.IsType<A2AContinuationToken>(result.ContinuationToken);
Assert.Equal("task-789", ((A2AContinuationToken)result.ContinuationToken).TaskId);
// Assert - verify thread is updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal("context-456", a2aThread.ContextId);
Assert.Equal("task-789", a2aThread.TaskId);
// Assert - verify metadata is preserved
Assert.NotNull(result.AdditionalProperties);
Assert.NotNull(result.AdditionalProperties["key1"]);
Assert.Equal("value1", ((JsonElement)result.AdditionalProperties["key1"]!).GetString());
Assert.NotNull(result.AdditionalProperties["count"]);
Assert.Equal(42, ((JsonElement)result.AdditionalProperties["count"]!).GetInt32());
}
[Theory]
[InlineData(TaskState.Submitted)]
[InlineData(TaskState.Working)]
[InlineData(TaskState.Completed)]
[InlineData(TaskState.Failed)]
[InlineData(TaskState.Canceled)]
public async Task RunAsync_WithVariousTaskStates_ReturnsCorrectTokenAsync(TaskState taskState)
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-123",
ContextId = "context-123",
Status = new() { State = taskState }
};
// Act
var result = await this._agent.RunAsync("Test message");
// Assert
if (taskState == TaskState.Submitted || taskState == TaskState.Working)
{
Assert.NotNull(result.ContinuationToken);
}
else
{
Assert.Null(result.ContinuationToken);
}
}
[Fact]
public async Task RunStreamingAsync_WithContinuationTokenAndMessages_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options))
{
// Just iterate through to trigger the exception
}
});
}
[Fact]
public async Task RunStreamingAsync_WithTaskInThreadAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentMessage
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response to task" }]
};
var thread = (A2AAgentThread)this._agent.GetNewThread();
thread.TaskId = "task-123";
// Act
await foreach (var _ in this._agent.RunStreamingAsync("Please make the background transparent", thread))
{
// Just iterate through to trigger the logic
}
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
}
[Fact]
public async Task RunStreamingAsync_WithAgentTask_UpdatesThreadTaskIdAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentTask
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
};
var thread = this._agent.GetNewThread();
// Act
await foreach (var _ in this._agent.RunStreamingAsync("Start a task", thread))
{
// Just iterate through to trigger the logic
}
// Assert
var a2aThread = (A2AAgentThread)thread;
Assert.Equal("task-456", a2aThread.TaskId);
}
[Fact]
public async Task RunStreamingAsync_WithAgentMessage_YieldsResponseUpdateAsync()
{
// Arrange
const string MessageId = "msg-123";
const string ContextId = "ctx-456";
const string MessageText = "Hello from agent!";
this._handler.StreamingResponseToReturn = new AgentMessage
{
MessageId = MessageId,
Role = MessageRole.Agent,
ContextId = ContextId,
Parts =
[
new TextPart { Text = MessageText }
]
};
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Test message"))
{
updates.Add(update);
}
// Assert - one update should be yielded
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(MessageId, update0.MessageId);
Assert.Equal(MessageId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.Equal(MessageText, update0.Text);
Assert.IsType<AgentMessage>(update0.RawRepresentation);
Assert.Equal(MessageId, ((AgentMessage)update0.RawRepresentation!).MessageId);
}
[Fact]
public async Task RunStreamingAsync_WithAgentTask_YieldsResponseUpdateAsync()
{
// Arrange
const string TaskId = "task-789";
const string ContextId = "ctx-012";
this._handler.StreamingResponseToReturn = new AgentTask
{
Id = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Submitted },
Artifacts = [
new()
{
ArtifactId = "art-123",
Parts = [new TextPart { Text = "Task artifact content" }]
}
]
};
var thread = this._agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Start long-running task", thread))
{
updates.Add(update);
}
// Assert - one update should be yielded from artifact
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(TaskId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.IsType<AgentTask>(update0.RawRepresentation);
Assert.Equal(TaskId, ((AgentTask)update0.RawRepresentation!).Id);
// Assert - thread should be updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal(ContextId, a2aThread.ContextId);
Assert.Equal(TaskId, a2aThread.TaskId);
}
[Fact]
public async Task RunStreamingAsync_WithTaskStatusUpdateEvent_YieldsResponseUpdateAsync()
{
// Arrange
const string TaskId = "task-status-123";
const string ContextId = "ctx-status-456";
this._handler.StreamingResponseToReturn = new TaskStatusUpdateEvent
{
TaskId = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Working }
};
var thread = this._agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Check task status", thread))
{
updates.Add(update);
}
// Assert - one update should be yielded
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(TaskId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.IsType<TaskStatusUpdateEvent>(update0.RawRepresentation);
// Assert - thread should be updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal(ContextId, a2aThread.ContextId);
Assert.Equal(TaskId, a2aThread.TaskId);
}
[Fact]
public async Task RunStreamingAsync_WithTaskArtifactUpdateEvent_YieldsResponseUpdateAsync()
{
// Arrange
const string TaskId = "task-artifact-123";
const string ContextId = "ctx-artifact-456";
const string ArtifactContent = "Task artifact data";
this._handler.StreamingResponseToReturn = new TaskArtifactUpdateEvent
{
TaskId = TaskId,
ContextId = ContextId,
Artifact = new()
{
ArtifactId = "artifact-789",
Parts = [new TextPart { Text = ArtifactContent }]
}
};
var thread = this._agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Process artifact", thread))
{
updates.Add(update);
}
// Assert - one update should be yielded
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(TaskId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.IsType<TaskArtifactUpdateEvent>(update0.RawRepresentation);
// Assert - artifact content should be in the update
Assert.NotEmpty(update0.Contents);
Assert.Equal(ArtifactContent, update0.Text);
// Assert - thread should be updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal(ContextId, a2aThread.ContextId);
Assert.Equal(TaskId, a2aThread.TaskId);
}
public void Dispose()
{
this._handler.Dispose();
this._httpClient.Dispose();
}
internal sealed class A2AClientHttpMessageHandlerStub : HttpMessageHandler
{
public JsonRpcRequest? CapturedJsonRpcRequest { get; set; }
public MessageSendParams? CapturedMessageSendParams { get; set; }
public TaskIdParams? CapturedTaskIdParams { get; set; }
public A2AEvent? ResponseToReturn { get; set; }
public A2AEvent? StreamingResponseToReturn { get; set; }
@@ -434,9 +842,19 @@ public sealed class A2AAgentTests : IDisposable
var content = await request.Content!.ReadAsStringAsync();
#pragma warning restore CA2016
var jsonRpcRequest = JsonSerializer.Deserialize<JsonRpcRequest>(content)!;
this.CapturedJsonRpcRequest = JsonSerializer.Deserialize<JsonRpcRequest>(content);
this.CapturedMessageSendParams = jsonRpcRequest.Params?.Deserialize<MessageSendParams>();
try
{
this.CapturedMessageSendParams = this.CapturedJsonRpcRequest?.Params?.Deserialize<MessageSendParams>();
}
catch { /* Ignore deserialization errors for non-MessageSendParams requests */ }
try
{
this.CapturedTaskIdParams = this.CapturedJsonRpcRequest?.Params?.Deserialize<TaskIdParams>();
}
catch { /* Ignore deserialization errors for non-TaskIdParams requests */ }
// Return the pre-configured non-streaming response
if (this.ResponseToReturn is not null)
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AAgentThread"/> class.
/// </summary>
public sealed class A2AAgentThreadTests
{
[Fact]
public void Constructor_RoundTrip_SerializationPreservesState()
{
// Arrange
const string ContextId = "context-rt-001";
const string TaskId = "task-rt-002";
A2AAgentThread originalThread = new() { ContextId = ContextId, TaskId = TaskId };
// Act
JsonElement serialized = originalThread.Serialize();
A2AAgentThread deserializedThread = new(serialized);
// Assert
Assert.Equal(originalThread.ContextId, deserializedThread.ContextId);
Assert.Equal(originalThread.TaskId, deserializedThread.TaskId);
}
}
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AContinuationToken"/> class.
/// </summary>
public sealed class A2AContinuationTokenTests
{
[Fact]
public void Constructor_WithValidTaskId_InitializesTaskIdProperty()
{
// Arrange
const string TaskId = "task-123";
// Act
var token = new A2AContinuationToken(TaskId);
// Assert
Assert.Equal(TaskId, token.TaskId);
}
[Fact]
public void ToBytes_WithValidToken_SerializesToJsonBytes()
{
// Arrange
const string TaskId = "task-456";
var token = new A2AContinuationToken(TaskId);
// Act
var bytes = token.ToBytes();
// Assert
Assert.NotEqual(0, bytes.Length);
var jsonString = System.Text.Encoding.UTF8.GetString(bytes.ToArray());
using var jsonDoc = JsonDocument.Parse(jsonString);
var root = jsonDoc.RootElement;
Assert.True(root.TryGetProperty("taskId", out var taskIdElement));
Assert.Equal(TaskId, taskIdElement.GetString());
}
[Fact]
public void FromToken_WithA2AContinuationToken_ReturnsSameInstance()
{
// Arrange
const string TaskId = "task-direct";
var originalToken = new A2AContinuationToken(TaskId);
// Act
var resultToken = A2AContinuationToken.FromToken(originalToken);
// Assert
Assert.Same(originalToken, resultToken);
Assert.Equal(TaskId, resultToken.TaskId);
}
[Fact]
public void FromToken_WithSerializedToken_DeserializesCorrectly()
{
// Arrange
const string TaskId = "task-deserialized";
var originalToken = new A2AContinuationToken(TaskId);
var serialized = originalToken.ToBytes();
// Create a mock token wrapper to pass to FromToken
var mockToken = new MockResponseContinuationToken(serialized);
// Act
var resultToken = A2AContinuationToken.FromToken(mockToken);
// Assert
Assert.Equal(TaskId, resultToken.TaskId);
Assert.IsType<A2AContinuationToken>(resultToken);
}
[Fact]
public void FromToken_RoundTrip_PreservesTaskId()
{
// Arrange
const string TaskId = "task-roundtrip-123";
var originalToken = new A2AContinuationToken(TaskId);
var serialized = originalToken.ToBytes();
var mockToken = new MockResponseContinuationToken(serialized);
// Act
var deserializedToken = A2AContinuationToken.FromToken(mockToken);
var reserialized = deserializedToken.ToBytes();
var mockToken2 = new MockResponseContinuationToken(reserialized);
var deserializedAgain = A2AContinuationToken.FromToken(mockToken2);
// Assert
Assert.Equal(TaskId, deserializedAgain.TaskId);
}
[Fact]
public void FromToken_WithEmptyData_ThrowsArgumentException()
{
// Arrange
var emptyToken = new MockResponseContinuationToken(ReadOnlyMemory<byte>.Empty);
// Act & Assert
Assert.Throws<ArgumentException>(() => A2AContinuationToken.FromToken(emptyToken));
}
[Fact]
public void FromToken_WithMissingTaskIdProperty_ThrowsException()
{
// Arrange
var jsonWithoutTaskId = System.Text.Encoding.UTF8.GetBytes("{ \"someOtherProperty\": \"value\" }").AsMemory();
var mockToken = new MockResponseContinuationToken(jsonWithoutTaskId);
// Act & Assert
Assert.Throws<JsonException>(() => A2AContinuationToken.FromToken(mockToken));
}
[Fact]
public void FromToken_WithValidTaskId_ParsesTaskIdCorrectly()
{
// Arrange
const string TaskId = "task-multi-prop";
var json = System.Text.Encoding.UTF8.GetBytes($"{{ \"taskId\": \"{TaskId}\" }}").AsMemory();
var mockToken = new MockResponseContinuationToken(json);
// Act
var resultToken = A2AContinuationToken.FromToken(mockToken);
// Assert
Assert.Equal(TaskId, resultToken.TaskId);
}
/// <summary>
/// Mock implementation of ResponseContinuationToken for testing.
/// </summary>
private sealed class MockResponseContinuationToken : ResponseContinuationToken
{
private readonly ReadOnlyMemory<byte> _data;
public MockResponseContinuationToken(ReadOnlyMemory<byte> data)
{
this._data = data;
}
public override ReadOnlyMemory<byte> ToBytes()
{
return this._data;
}
}
}
@@ -0,0 +1,169 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AAgentTaskExtensions"/> class.
/// </summary>
public sealed class A2AAgentTaskExtensionsTests
{
[Fact]
public void ToChatMessages_WithNullAgentTask_ThrowsArgumentNullException()
{
// Arrange
AgentTask agentTask = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => agentTask.ToChatMessages());
}
[Fact]
public void ToAIContents_WithNullAgentTask_ThrowsArgumentNullException()
{
// Arrange
AgentTask agentTask = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => agentTask.ToAIContents());
}
[Fact]
public void ToChatMessages_WithEmptyArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<ChatMessage>? result = agentTask.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessages_WithNullArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = null,
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<ChatMessage>? result = agentTask.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToAIContents_WithEmptyArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<AIContent>? result = agentTask.ToAIContents();
// Assert
Assert.Null(result);
}
[Fact]
public void ToAIContents_WithNullArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = null,
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<AIContent>? result = agentTask.ToAIContents();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessages_WithValidArtifact_ReturnsChatMessages()
{
// Arrange
var artifact = new Artifact
{
Parts = [new TextPart { Text = "response" }],
};
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [artifact],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<ChatMessage>? result = agentTask.ToChatMessages();
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.All(result, msg => Assert.Equal(ChatRole.Assistant, msg.Role));
Assert.Equal("response", result[0].Contents[0].ToString());
}
[Fact]
public void ToAIContents_WithMultipleArtifacts_FlattenAllContents()
{
// Arrange
var artifact1 = new Artifact
{
Parts = [new TextPart { Text = "content1" }],
};
var artifact2 = new Artifact
{
Parts =
[
new TextPart { Text = "content2" },
new TextPart { Text = "content3" }
],
};
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [artifact1, artifact2],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<AIContent>? result = agentTask.ToAIContents();
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Equal(3, result.Count);
Assert.Equal("content1", result[0].ToString());
Assert.Equal("content2", result[1].ToString());
Assert.Equal("content3", result[2].ToString());
}
}
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AArtifactExtensions"/> class.
/// </summary>
public sealed class A2AArtifactExtensionsTests
{
[Fact]
public void ToChatMessage_WithMultiplePartsMetadataAndRawRepresentation_ReturnsCorrectChatMessage()
{
// Arrange
var artifact = new Artifact
{
ArtifactId = "artifact-comprehensive",
Name = "comprehensive-artifact",
Parts =
[
new TextPart { Text = "First part" },
new TextPart { Text = "Second part" },
new TextPart { Text = "Third part" }
],
Metadata = new Dictionary<string, JsonElement>
{
{ "key1", JsonSerializer.SerializeToElement("value1") },
{ "key2", JsonSerializer.SerializeToElement(42) }
}
};
// Act
var result = artifact.ToChatMessage();
// Assert - Verify multiple parts
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Equal(3, result.Contents.Count);
Assert.All(result.Contents, content => Assert.IsType<TextContent>(content));
Assert.Equal("First part", ((TextContent)result.Contents[0]).Text);
Assert.Equal("Second part", ((TextContent)result.Contents[1]).Text);
Assert.Equal("Third part", ((TextContent)result.Contents[2]).Text);
// Assert - Verify metadata conversion to AdditionalProperties
Assert.NotNull(result.AdditionalProperties);
Assert.Equal(2, result.AdditionalProperties.Count);
Assert.True(result.AdditionalProperties.ContainsKey("key1"));
Assert.True(result.AdditionalProperties.ContainsKey("key2"));
// Assert - Verify RawRepresentation is set to artifact
Assert.NotNull(result.RawRepresentation);
Assert.Same(artifact, result.RawRepresentation);
}
[Fact]
public void ToAIContents_WithMultipleParts_ReturnsCorrectList()
{
// Arrange
var artifact = new Artifact
{
ArtifactId = "artifact-ai-multi",
Name = "test",
Parts = new List<Part>
{
new TextPart { Text = "Part 1" },
new TextPart { Text = "Part 2" },
new TextPart { Text = "Part 3" }
},
Metadata = null
};
// Act
var result = artifact.ToAIContents();
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Count);
Assert.All(result, content => Assert.IsType<TextContent>(content));
Assert.Equal("Part 1", ((TextContent)result[0]).Text);
Assert.Equal("Part 2", ((TextContent)result[1]).Text);
Assert.Equal("Part 3", ((TextContent)result[2]).Text);
}
[Fact]
public void ToAIContents_WithEmptyParts_ReturnsEmptyList()
{
// Arrange
var artifact = new Artifact
{
ArtifactId = "artifact-empty",
Name = "test",
Parts = new List<Part>(),
Metadata = null
};
// Act
var result = artifact.ToAIContents();
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
}
@@ -17,7 +17,7 @@ public class AgentRunOptionsTests
// Arrange
var options = new AgentRunOptions
{
ContinuationToken = new object(),
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
AllowBackgroundResponses = true,
AdditionalProperties = new AdditionalPropertiesDictionary
{
@@ -42,7 +42,7 @@ public class AgentRunResponseUpdateTests
RawRepresentation = new object(),
ResponseId = "responseId",
Role = ChatRole.Assistant,
ContinuationToken = new object(),
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
AgentRunResponseUpdate response = new(chatResponseUpdate);
@@ -0,0 +1,290 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0052 // Remove unread private members
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Core;
using Anthropic.Services;
using Microsoft.Extensions.AI;
using Moq;
using IBetaMessageService = Anthropic.Services.Beta.IMessageService;
using IMessageService = Anthropic.Services.IMessageService;
namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions;
/// <summary>
/// Unit tests for the AnthropicClientExtensions class.
/// </summary>
public sealed class AnthropicBetaServiceExtensionsTests
{
/// <summary>
/// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory.
/// </summary>
[Fact]
public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
var testChatClient = new TestChatClient(chatClient.Beta.AsIChatClient());
// Act
var agent = chatClient.Beta.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
name: "Test Agent",
description: "Test description",
clientFactory: (innerClient) => testChatClient);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Test description", agent.Description);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly.
/// </summary>
[Fact]
public void CreateAIAgent_WithClientFactoryUsingAsBuilder_AppliesFactoryCorrectly()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
TestChatClient? testChatClient = null;
// Act
var agent = chatClient.Beta.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
clientFactory: (innerClient) =>
innerClient.AsBuilder().Use((innerClient) => testChatClient = new TestChatClient(innerClient)).Build());
// Assert
Assert.NotNull(agent);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory.
/// </summary>
[Fact]
public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
var testChatClient = new TestChatClient(chatClient.Beta.AsIChatClient());
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
};
// Act
var agent = chatClient.Beta.CreateAIAgent(
options,
clientFactory: (innerClient) => testChatClient);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Test description", agent.Description);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent without clientFactory works normally.
/// </summary>
[Fact]
public void CreateAIAgent_WithoutClientFactory_WorksNormally()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
// Act
var agent = chatClient.Beta.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
name: "Test Agent");
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that no TestChatClient is available since no factory was provided
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.Null(retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent with null clientFactory works normally.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullClientFactory_WorksNormally()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
// Act
var agent = chatClient.Beta.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
name: "Test Agent",
clientFactory: null);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that no TestChatClient is available since no factory was provided
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.Null(retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent throws ArgumentNullException when client is null.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException()
{
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
((IBetaService)null!).CreateAIAgent("test-model"));
Assert.Equal("betaService", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgent with options throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
chatClient.Beta.CreateAIAgent((ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Test custom chat client that can be used to verify clientFactory functionality.
/// </summary>
private sealed class TestChatClient : IChatClient
{
private readonly IChatClient _innerClient;
public TestChatClient(IChatClient innerClient)
{
this._innerClient = innerClient;
}
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> this._innerClient.GetResponseAsync(messages, options, cancellationToken);
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var update in this._innerClient.GetStreamingResponseAsync(messages, options, cancellationToken))
{
yield return update;
}
}
public object? GetService(Type serviceType, object? serviceKey = null)
{
// Return this instance when requested
if (serviceType == typeof(TestChatClient))
{
return this;
}
return this._innerClient.GetService(serviceType, serviceKey);
}
public void Dispose() => this._innerClient.Dispose();
}
/// <summary>
/// Creates a test ChatClient implementation for testing.
/// </summary>
private sealed class TestAnthropicChatClient : IAnthropicClient
{
public TestAnthropicChatClient()
{
this.BetaService = new TestBetaService(this);
}
public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public Uri BaseUrl { get => new("http://localhost"); init => throw new NotImplementedException(); }
public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public string? APIKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public IMessageService Messages => throw new NotImplementedException();
public IModelService Models => throw new NotImplementedException();
public IBetaService Beta => this.BetaService;
public IBetaService BetaService { get; }
IMessageService IAnthropicClient.Messages => new Mock<IMessageService>().Object;
public Task<HttpResponse> Execute<T>(HttpRequest<T> request, CancellationToken cancellationToken = default) where T : ParamsBase
{
throw new NotImplementedException();
}
public IAnthropicClient WithOptions(Func<ClientOptions, ClientOptions> modifier)
{
throw new NotImplementedException();
}
private sealed class TestBetaService : IBetaService
{
private readonly IAnthropicClient _client;
public TestBetaService(IAnthropicClient client)
{
this._client = client;
}
public global::Anthropic.Services.Beta.IModelService Models => throw new NotImplementedException();
public global::Anthropic.Services.Beta.IFileService Files => throw new NotImplementedException();
public global::Anthropic.Services.Beta.ISkillService Skills => throw new NotImplementedException();
public IBetaMessageService Messages => new Mock<IBetaMessageService>().Object;
public IBetaService WithOptions(Func<ClientOptions, ClientOptions> modifier)
{
throw new NotImplementedException();
}
}
}
}
@@ -0,0 +1,257 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Core;
using Anthropic.Services;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions;
/// <summary>
/// Unit tests for the AnthropicClientExtensions class.
/// </summary>
public sealed class AnthropicClientExtensionsTests
{
/// <summary>
/// Test custom chat client that can be used to verify clientFactory functionality.
/// </summary>
private sealed class TestChatClient : IChatClient
{
private readonly IChatClient _innerClient;
public TestChatClient(IChatClient innerClient)
{
this._innerClient = innerClient;
}
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> this._innerClient.GetResponseAsync(messages, options, cancellationToken);
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var update in this._innerClient.GetStreamingResponseAsync(messages, options, cancellationToken))
{
yield return update;
}
}
public object? GetService(Type serviceType, object? serviceKey = null)
{
// Return this instance when requested
if (serviceType == typeof(TestChatClient))
{
return this;
}
return this._innerClient.GetService(serviceType, serviceKey);
}
public void Dispose() => this._innerClient.Dispose();
}
/// <summary>
/// Creates a test ChatClient implementation for testing.
/// </summary>
private sealed class TestAnthropicChatClient : IAnthropicClient
{
public TestAnthropicChatClient()
{
}
public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public Uri BaseUrl { get => new("http://localhost"); init => throw new NotImplementedException(); }
public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public string? APIKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); }
public IMessageService Messages => throw new NotImplementedException();
public IModelService Models => throw new NotImplementedException();
public IBetaService Beta => throw new NotImplementedException();
public Task<HttpResponse> Execute<T>(HttpRequest<T> request, CancellationToken cancellationToken = default) where T : ParamsBase
{
throw new NotImplementedException();
}
public IAnthropicClient WithOptions(Func<ClientOptions, ClientOptions> modifier)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory.
/// </summary>
[Fact]
public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
var testChatClient = new TestChatClient(chatClient.AsIChatClient());
// Act
var agent = chatClient.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
name: "Test Agent",
description: "Test description",
clientFactory: (innerClient) => testChatClient);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Test description", agent.Description);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly.
/// </summary>
[Fact]
public void CreateAIAgent_WithClientFactoryUsingAsBuilder_AppliesFactoryCorrectly()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
TestChatClient? testChatClient = null;
// Act
var agent = chatClient.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
clientFactory: (innerClient) =>
innerClient.AsBuilder().Use((innerClient) => testChatClient = new TestChatClient(innerClient)).Build());
// Assert
Assert.NotNull(agent);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory.
/// </summary>
[Fact]
public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
var testChatClient = new TestChatClient(chatClient.AsIChatClient());
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
};
// Act
var agent = chatClient.CreateAIAgent(
options,
clientFactory: (innerClient) => testChatClient);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Test description", agent.Description);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent without clientFactory works normally.
/// </summary>
[Fact]
public void CreateAIAgent_WithoutClientFactory_WorksNormally()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
// Act
var agent = chatClient.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
name: "Test Agent");
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that no TestChatClient is available since no factory was provided
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.Null(retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent with null clientFactory works normally.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullClientFactory_WorksNormally()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
// Act
var agent = chatClient.CreateAIAgent(
model: "test-model",
instructions: "Test instructions",
name: "Test Agent",
clientFactory: null);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that no TestChatClient is available since no factory was provided
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.Null(retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent throws ArgumentNullException when client is null.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException()
{
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
((TestAnthropicChatClient)null!).CreateAIAgent("test-model"));
Assert.Equal("client", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgent with options throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
{
// Arrange
var chatClient = new TestAnthropicChatClient();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
chatClient.CreateAIAgent((ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Anthropic\Microsoft.Agents.AI.Anthropic.csproj" />
</ItemGroup>
</Project>
@@ -524,7 +524,7 @@ public partial class ChatClientAgentTests
}
/// <summary>
/// Verify that RunAsync throws when a ChatMessageStore Factory is provided but when the chat client returns a conversation id.
/// Verify that RunAsync throws when a ChatMessageStore Factory is provided and the chat client returns a conversation id.
/// </summary>
[Fact]
public async Task RunAsyncThrowsWhenChatMessageStoreFactoryProvidedAndConversationIdReturnedByChatClientAsync()
@@ -2141,7 +2141,7 @@ public partial class ChatClientAgentTests
public async Task RunAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
{
// Arrange
object continuationToken = new();
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
@@ -2190,8 +2190,8 @@ public partial class ChatClientAgentTests
public async Task RunAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
{
// Arrange
object continuationToken1 = new();
object continuationToken2 = new();
var continuationToken1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
var continuationToken2 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
@@ -2237,7 +2237,7 @@ public partial class ChatClientAgentTests
new ChatResponseUpdate(role: ChatRole.Assistant, content: "at?"),
];
object continuationToken = new();
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
@@ -2294,8 +2294,8 @@ public partial class ChatClientAgentTests
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh"),
];
object continuationToken1 = new();
object continuationToken2 = new();
var continuationToken1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
var continuationToken2 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
@@ -2335,7 +2335,7 @@ public partial class ChatClientAgentTests
public async Task RunAsyncPropagatesContinuationTokenFromChatResponseToAgentRunResponseAsync()
{
// Arrange
object continuationToken = new();
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
@@ -2360,7 +2360,7 @@ public partial class ChatClientAgentTests
public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync()
{
// Arrange
object token1 = new();
var token1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatResponseUpdate[] expectedUpdates =
[
new ChatResponseUpdate(ChatRole.Assistant, "pa") { ContinuationToken = token1 },
@@ -2400,7 +2400,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { ContinuationToken = new() };
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
@@ -2424,7 +2424,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { ContinuationToken = new() };
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
@@ -2487,7 +2487,7 @@ public partial class ChatClientAgentTests
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new() { ContinuationToken = new() };
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
// Act
await agent.RunAsync([], thread, options: runOptions);
@@ -2549,7 +2549,7 @@ public partial class ChatClientAgentTests
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new() { ContinuationToken = new() };
AgentRunOptions runOptions = new() { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) };
// Act
await agent.RunStreamingAsync([], thread, options: runOptions).ToListAsync();
@@ -25,7 +25,7 @@ internal static class Step10EntryPoint
foreach (string input in inputs)
{
AgentRunResponse response;
object? continuationToken = null;
ResponseContinuationToken? continuationToken = null;
do
{
response = await hostAgent.RunAsync(input, thread, new AgentRunOptions { ContinuationToken = continuationToken });
@@ -37,7 +37,7 @@ internal static class Step11EntryPoint
foreach (string input in inputs)
{
AgentRunResponse response;
object? continuationToken = null;
ResponseContinuationToken? continuationToken = null;
do
{
response = await hostAgent.RunAsync(input, thread, new AgentRunOptions { ContinuationToken = continuationToken });
@@ -73,7 +73,7 @@ internal static class Step12EntryPoint
foreach (string input in inputs)
{
AgentRunResponse response;
object? continuationToken = null;
ResponseContinuationToken? continuationToken = null;
do
{
response = await hostAgent.RunAsync(input, thread, new AgentRunOptions { ContinuationToken = continuationToken });
+1
View File
@@ -27,6 +27,7 @@
"aiplatform",
"azuredocindex",
"azuredocs",
"azurefunctions",
"boto",
"contentvector",
"contoso",
@@ -3,7 +3,7 @@
Please install this package via pip:
```bash
pip install agent-framework-aisearch --pre
pip install agent-framework-azure-ai-search --pre
```
## Azure AI Search Integration
@@ -1,15 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure AI Search Context Provider for Agent Framework.
This module provides context providers for Azure AI Search integration with two modes:
- Agentic: Recommended for most scenarios. Uses Knowledge Bases for query planning and
multi-hop reasoning. Slightly slower with more token consumption, but more accurate.
- Semantic: Fast hybrid search (vector + keyword) with semantic ranker. Best for simple
queries where speed is critical.
See: https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720
"""
import sys
from collections.abc import Awaitable, Callable, MutableSequence
@@ -111,6 +101,18 @@ if sys.version_info >= (3, 12):
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
"""Azure AI Search Context Provider for Agent Framework.
This module provides context providers for Azure AI Search integration with two modes:
- Agentic: Recommended for most scenarios. Uses Knowledge Bases for query planning and
multi-hop reasoning. Slightly slower with more token consumption, but more accurate.
- Semantic: Fast hybrid search (vector + keyword) with semantic ranker. Best for simple
queries where speed is critical.
See: https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/foundry-iq-boost-response-relevance-by-36-with-agentic-retrieval/4470720
"""
# Module-level constants
logger = get_logger("agent_framework.azure")
_DEFAULT_AGENTIC_MESSAGE_HISTORY_COUNT = 10
@@ -1,5 +1,5 @@
[project]
name = "agent-framework-aisearch"
name = "agent-framework-azure-ai-search"
description = "Azure AI Search integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
@@ -76,15 +76,15 @@ disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.bandit]
targets = ["agent_framework_aisearch"]
targets = ["agent_framework_azure_ai_search"]
exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_aisearch"
test = "pytest --cov=agent_framework_aisearch --cov-report=term-missing:skip-covered tests"
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search"
test = "pytest --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
@@ -6,13 +6,11 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import ChatMessage, Context, Role
from agent_framework.azure import AzureAISearchContextProvider
from agent_framework.azure import AzureAISearchContextProvider, AzureAISearchSettings
from agent_framework.exceptions import ServiceInitializationError
from azure.core.credentials import AzureKeyCredential
from azure.core.exceptions import ResourceNotFoundError
from agent_framework_aisearch import AzureAISearchSettings
@pytest.fixture
def mock_search_client() -> AsyncMock:
@@ -246,7 +244,7 @@ class TestSemanticSearch:
"""Test semantic search functionality."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_semantic_search_basic(
self, mock_search_class: MagicMock, sample_messages: list[ChatMessage]
) -> None:
@@ -275,7 +273,7 @@ class TestSemanticSearch:
assert "Test document content" in context.messages[1].text
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_semantic_search_empty_query(self, mock_search_class: MagicMock) -> None:
"""Test that empty queries return empty context."""
mock_search_client = AsyncMock()
@@ -295,7 +293,7 @@ class TestSemanticSearch:
assert len(context.messages) == 0
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_semantic_search_with_vector_query(
self, mock_search_class: MagicMock, sample_messages: list[ChatMessage]
) -> None:
@@ -332,8 +330,8 @@ class TestKnowledgeBaseSetup:
"""Test Knowledge Base setup for agentic mode."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_ensure_knowledge_base_creates_when_not_exists(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -369,8 +367,8 @@ class TestKnowledgeBaseSetup:
mock_index_client.create_or_update_knowledge_base.assert_called_once()
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_ensure_knowledge_base_skips_when_exists(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -406,7 +404,7 @@ class TestContextProviderLifecycle:
"""Test context provider lifecycle methods."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_context_manager(self, mock_search_class: MagicMock) -> None:
"""Test that provider can be used as async context manager."""
mock_search_client = AsyncMock()
@@ -422,9 +420,9 @@ class TestContextProviderLifecycle:
assert isinstance(provider, AzureAISearchContextProvider)
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_context_manager_agentic_cleanup(
self, mock_search_class: MagicMock, mock_index_class: MagicMock, mock_retrieval_class: MagicMock
) -> None:
@@ -470,7 +468,7 @@ class TestMessageFiltering:
"""Test message filtering functionality."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_filters_non_user_assistant_messages(self, mock_search_class: MagicMock) -> None:
"""Test that only USER and ASSISTANT messages are processed."""
# Setup mock
@@ -502,7 +500,7 @@ class TestMessageFiltering:
mock_search_client.search.assert_called_once()
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_filters_empty_messages(self, mock_search_class: MagicMock) -> None:
"""Test that empty/whitespace messages are filtered out."""
mock_search_client = AsyncMock()
@@ -532,7 +530,7 @@ class TestCitations:
"""Test citation functionality."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_citations_included_in_semantic_search(self, mock_search_class: MagicMock) -> None:
"""Test that citations are included in semantic search results."""
# Setup mock with document ID
@@ -564,9 +562,9 @@ class TestAgenticSearch:
"""Test agentic search functionality."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_agentic_search_basic(
self,
mock_search_class: MagicMock,
@@ -593,7 +591,7 @@ class TestAgenticSearch:
mock_content = MagicMock()
mock_content.text = "Agentic search result"
# Make it pass isinstance check
from agent_framework_aisearch._search_provider import _agentic_retrieval_available
from agent_framework_azure_ai_search._search_provider import _agentic_retrieval_available
if _agentic_retrieval_available:
from azure.search.documents.knowledgebases.models import KnowledgeBaseMessageTextContent
@@ -623,9 +621,9 @@ class TestAgenticSearch:
assert len(context.messages) >= 1
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_agentic_search_no_results(
self,
mock_search_class: MagicMock,
@@ -670,9 +668,9 @@ class TestAgenticSearch:
assert len(context.messages) >= 1
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.KnowledgeBaseRetrievalClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_agentic_search_with_medium_reasoning(
self,
mock_search_class: MagicMock,
@@ -696,7 +694,7 @@ class TestAgenticSearch:
mock_message = MagicMock()
mock_content = MagicMock()
mock_content.text = "Medium reasoning result"
from agent_framework_aisearch._search_provider import _agentic_retrieval_available
from agent_framework_azure_ai_search._search_provider import _agentic_retrieval_available
if _agentic_retrieval_available:
from azure.search.documents.knowledgebases.models import KnowledgeBaseMessageTextContent
@@ -730,8 +728,8 @@ class TestVectorFieldAutoDiscovery:
"""Test vector field auto-discovery functionality."""
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_auto_discovers_single_vector_field(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -795,8 +793,8 @@ class TestVectorFieldAutoDiscovery:
assert is_vector_3 is False
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_no_false_positives_on_string_fields(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -839,8 +837,8 @@ class TestVectorFieldAutoDiscovery:
assert provider._auto_discovered_vector_field is True
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_multiple_vector_fields_without_vectorizer(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -884,8 +882,8 @@ class TestVectorFieldAutoDiscovery:
assert provider._auto_discovered_vector_field is True
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_multiple_vectorizable_fields(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -941,8 +939,8 @@ class TestVectorFieldAutoDiscovery:
assert provider._auto_discovered_vector_field is True
@pytest.mark.asyncio
@patch("agent_framework_aisearch._search_provider.SearchIndexClient")
@patch("agent_framework_aisearch._search_provider.SearchClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchIndexClient")
@patch("agent_framework_azure_ai_search._search_provider.SearchClient")
async def test_single_vectorizable_field_detected(
self, mock_search_class: MagicMock, mock_index_class: MagicMock
) -> None:
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import MutableSequence
from collections.abc import Mapping, MutableSequence
from typing import Any, ClassVar, TypeVar
from agent_framework import (
@@ -14,7 +14,7 @@ from agent_framework import (
use_chat_middleware,
use_function_invocation,
)
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError
from agent_framework.observability import use_observability
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
from azure.ai.projects.aio import AIProjectClient
@@ -22,7 +22,9 @@ from azure.ai.projects.models import (
MCPTool,
PromptAgentDefinition,
PromptAgentDefinitionText,
ResponseTextFormatConfigurationJsonObject,
ResponseTextFormatConfigurationJsonSchema,
ResponseTextFormatConfigurationText,
)
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.exceptions import ResourceNotFoundError
@@ -188,6 +190,40 @@ class AzureAIClient(OpenAIBaseResponsesClient):
"""Close the project_client."""
await self._close_client_if_needed()
def _create_text_format_config(
self, response_format: Any
) -> (
ResponseTextFormatConfigurationJsonSchema
| ResponseTextFormatConfigurationJsonObject
| ResponseTextFormatConfigurationText
):
"""Convert response_format into Azure text format configuration."""
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
return ResponseTextFormatConfigurationJsonSchema(
name=response_format.__name__,
schema=response_format.model_json_schema(),
)
if isinstance(response_format, Mapping):
format_config = self._convert_response_format(response_format)
format_type = format_config.get("type")
if format_type == "json_schema":
config_kwargs: dict[str, Any] = {
"name": format_config.get("name") or "response",
"schema": format_config["schema"],
}
if "strict" in format_config:
config_kwargs["strict"] = format_config["strict"]
if "description" in format_config:
config_kwargs["description"] = format_config["description"]
return ResponseTextFormatConfigurationJsonSchema(**config_kwargs)
if format_type == "json_object":
return ResponseTextFormatConfigurationJsonObject()
if format_type == "text":
return ResponseTextFormatConfigurationText()
raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.")
async def _get_agent_reference_or_create(
self, run_options: dict[str, Any], messages_instructions: str | None
) -> dict[str, str]:
@@ -228,12 +264,7 @@ class AzureAIClient(OpenAIBaseResponsesClient):
if "response_format" in run_options:
response_format = run_options["response_format"]
args["text"] = PromptAgentDefinitionText(
format=ResponseTextFormatConfigurationJsonSchema(
name=response_format.__name__,
schema=response_format.model_json_schema(),
)
)
args["text"] = PromptAgentDefinitionText(format=self._create_text_format_config(response_format))
# Combine instructions from messages and options
combined_instructions = [
@@ -561,6 +561,56 @@ async def test_azure_ai_client_agent_creation_with_response_format(
assert "description" in schema["properties"]
async def test_azure_ai_client_agent_creation_with_mapping_response_format(
mock_project_client: MagicMock,
) -> None:
"""Test agent creation when response_format is provided as a mapping."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
mock_agent = MagicMock()
mock_agent.name = "test-agent"
mock_agent.version = "1.0"
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
runtime_schema = {
"title": "WeatherDigest",
"type": "object",
"properties": {
"location": {"type": "string"},
"conditions": {"type": "string"},
"temperature_c": {"type": "number"},
"advisory": {"type": "string"},
},
"required": ["location", "conditions", "temperature_c", "advisory"],
"additionalProperties": False,
}
run_options = {
"model": "test-model",
"response_format": {
"type": "json_schema",
"json_schema": {
"name": runtime_schema["title"],
"strict": True,
"schema": runtime_schema,
},
},
}
await client._get_agent_reference_or_create(run_options, None) # type: ignore
call_args = mock_project_client.agents.create_version.call_args
created_definition = call_args[1]["definition"]
assert hasattr(created_definition, "text")
assert created_definition.text is not None
format_config = created_definition.text.format
assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
assert format_config.name == runtime_schema["title"]
assert format_config.schema == runtime_schema
assert format_config.strict is True
async def test_azure_ai_client_prepare_options_excludes_response_format(
mock_project_client: MagicMock,
) -> None:
@@ -4,12 +4,15 @@ import logging
from .exceptions import AgentFrameworkException
logging.basicConfig(
format="[%(asctime)s - %(pathname)s:%(lineno)d - %(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
__all__ = ["get_logger", "setup_logging"]
__all__ = ["get_logger"]
def setup_logging() -> None:
"""Setup the logging configuration for the agent framework."""
logging.basicConfig(
format="[%(asctime)s - %(pathname)s:%(lineno)d - %(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def get_logger(name: str = "agent_framework") -> logging.Logger:
+20 -8
View File
@@ -278,18 +278,30 @@ def _get_input_model_from_mcp_tool(tool: types.Tool) -> type[BaseModel]:
python_type = resolve_type(prop_details)
description = prop_details.get("description", "")
# Build field kwargs (description, array items schema, etc.)
field_kwargs: dict[str, Any] = {}
if description:
field_kwargs["description"] = description
# Preserve array items schema if present
if prop_details.get("type") == "array" and "items" in prop_details:
items_schema = prop_details["items"]
if items_schema and items_schema != {}:
field_kwargs["json_schema_extra"] = {"items": items_schema}
# Create field definition for create_model
if prop_name in required:
field_definitions[prop_name] = (
(python_type, Field(description=description)) if description else (python_type, ...)
)
if field_kwargs:
field_definitions[prop_name] = (python_type, Field(**field_kwargs))
else:
field_definitions[prop_name] = (python_type, ...)
else:
default_value = prop_details.get("default", None)
field_definitions[prop_name] = (
(python_type, Field(default=default_value, description=description))
if description
else (python_type, default_value)
)
field_kwargs["default"] = default_value
if field_kwargs and any(k != "default" for k in field_kwargs):
field_definitions[prop_name] = (python_type, Field(**field_kwargs))
else:
field_definitions[prop_name] = (python_type, default_value)
return create_model(f"{tool.name}_input", **field_definitions)
@@ -64,13 +64,15 @@ from ._group_chat import (
)
from ._handoff import HandoffBuilder, HandoffUserInputRequest
from ._magentic import (
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MAGENTIC_EVENT_TYPE_AGENT_DELTA,
MAGENTIC_EVENT_TYPE_ORCHESTRATOR,
ORCH_MSG_KIND_INSTRUCTION,
ORCH_MSG_KIND_NOTICE,
ORCH_MSG_KIND_TASK_LEDGER,
ORCH_MSG_KIND_USER_TASK,
MagenticBuilder,
MagenticContext,
MagenticFinalResultEvent,
MagenticManagerBase,
MagenticOrchestratorMessageEvent,
MagenticPlanReviewDecision,
MagenticPlanReviewReply,
MagenticPlanReviewRequest,
@@ -104,6 +106,12 @@ __all__ = [
"DEFAULT_MANAGER_INSTRUCTIONS",
"DEFAULT_MANAGER_STRUCTURED_OUTPUT_PROMPT",
"DEFAULT_MAX_ITERATIONS",
"MAGENTIC_EVENT_TYPE_AGENT_DELTA",
"MAGENTIC_EVENT_TYPE_ORCHESTRATOR",
"ORCH_MSG_KIND_INSTRUCTION",
"ORCH_MSG_KIND_NOTICE",
"ORCH_MSG_KIND_TASK_LEDGER",
"ORCH_MSG_KIND_USER_TASK",
"AgentExecutor",
"AgentExecutorRequest",
"AgentExecutorResponse",
@@ -132,13 +140,9 @@ __all__ = [
"HandoffUserInputRequest",
"InMemoryCheckpointStorage",
"InProcRunnerContext",
"MagenticAgentDeltaEvent",
"MagenticAgentMessageEvent",
"MagenticBuilder",
"MagenticContext",
"MagenticFinalResultEvent",
"MagenticManagerBase",
"MagenticOrchestratorMessageEvent",
"MagenticPlanReviewDecision",
"MagenticPlanReviewReply",
"MagenticPlanReviewRequest",
@@ -1,185 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from ._agent import WorkflowAgent
from ._agent_executor import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
)
from ._checkpoint import (
CheckpointStorage,
FileCheckpointStorage,
InMemoryCheckpointStorage,
WorkflowCheckpoint,
)
from ._checkpoint_summary import WorkflowCheckpointSummary, get_checkpoint_summary
from ._concurrent import ConcurrentBuilder
from ._const import DEFAULT_MAX_ITERATIONS
from ._edge import (
Case,
Default,
Edge,
FanInEdgeGroup,
FanOutEdgeGroup,
SingleEdgeGroup,
SwitchCaseEdgeGroup,
SwitchCaseEdgeGroupCase,
SwitchCaseEdgeGroupDefault,
)
from ._edge_runner import create_edge_runner
from ._events import (
AgentRunEvent,
AgentRunUpdateEvent,
ExecutorCompletedEvent,
ExecutorEvent,
ExecutorFailedEvent,
ExecutorInvokedEvent,
RequestInfoEvent,
SuperStepCompletedEvent,
SuperStepStartedEvent,
WorkflowErrorDetails,
WorkflowEvent,
WorkflowEventSource,
WorkflowFailedEvent,
WorkflowLifecycleEvent,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStartedEvent,
WorkflowStatusEvent,
)
from ._executor import (
Executor,
handler,
)
from ._function_executor import FunctionExecutor, executor
from ._group_chat import (
DEFAULT_MANAGER_INSTRUCTIONS,
DEFAULT_MANAGER_STRUCTURED_OUTPUT_PROMPT,
GroupChatBuilder,
GroupChatDirective,
GroupChatStateSnapshot,
)
from ._handoff import HandoffBuilder, HandoffUserInputRequest
from ._magentic import (
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticBuilder,
MagenticContext,
MagenticFinalResultEvent,
MagenticManagerBase,
MagenticOrchestratorMessageEvent,
MagenticPlanReviewDecision,
MagenticPlanReviewReply,
MagenticPlanReviewRequest,
StandardMagenticManager,
)
from ._orchestration_state import OrchestrationState
from ._request_info_mixin import response_handler
from ._runner import Runner
from ._runner_context import (
InProcRunnerContext,
Message,
RunnerContext,
)
from ._sequential import SequentialBuilder
from ._shared_state import SharedState
from ._validation import (
EdgeDuplicationError,
GraphConnectivityError,
TypeCompatibilityError,
ValidationTypeEnum,
WorkflowValidationError,
validate_workflow_graph,
)
from ._viz import WorkflowViz
from ._workflow import Workflow, WorkflowRunResult
from ._workflow_builder import WorkflowBuilder
from ._workflow_context import WorkflowContext
from ._workflow_executor import SubWorkflowRequestMessage, SubWorkflowResponseMessage, WorkflowExecutor
__all__ = [
"DEFAULT_MANAGER_INSTRUCTIONS",
"DEFAULT_MANAGER_STRUCTURED_OUTPUT_PROMPT",
"DEFAULT_MAX_ITERATIONS",
"AgentExecutor",
"AgentExecutorRequest",
"AgentExecutorResponse",
"AgentRunEvent",
"AgentRunUpdateEvent",
"Case",
"CheckpointStorage",
"ConcurrentBuilder",
"Default",
"Edge",
"EdgeDuplicationError",
"Executor",
"ExecutorCompletedEvent",
"ExecutorEvent",
"ExecutorFailedEvent",
"ExecutorInvokedEvent",
"FanInEdgeGroup",
"FanOutEdgeGroup",
"FileCheckpointStorage",
"FunctionExecutor",
"GraphConnectivityError",
"GroupChatBuilder",
"GroupChatDirective",
"GroupChatStateSnapshot",
"HandoffBuilder",
"HandoffUserInputRequest",
"InMemoryCheckpointStorage",
"InProcRunnerContext",
"MagenticAgentDeltaEvent",
"MagenticAgentMessageEvent",
"MagenticBuilder",
"MagenticContext",
"MagenticFinalResultEvent",
"MagenticManagerBase",
"MagenticOrchestratorMessageEvent",
"MagenticPlanReviewDecision",
"MagenticPlanReviewReply",
"MagenticPlanReviewRequest",
"Message",
"OrchestrationState",
"RequestInfoEvent",
"Runner",
"RunnerContext",
"SequentialBuilder",
"SharedState",
"SingleEdgeGroup",
"StandardMagenticManager",
"SubWorkflowRequestMessage",
"SubWorkflowResponseMessage",
"SuperStepCompletedEvent",
"SuperStepStartedEvent",
"SwitchCaseEdgeGroup",
"SwitchCaseEdgeGroupCase",
"SwitchCaseEdgeGroupDefault",
"TypeCompatibilityError",
"ValidationTypeEnum",
"Workflow",
"WorkflowAgent",
"WorkflowBuilder",
"WorkflowCheckpoint",
"WorkflowCheckpointSummary",
"WorkflowContext",
"WorkflowErrorDetails",
"WorkflowEvent",
"WorkflowEventSource",
"WorkflowExecutor",
"WorkflowFailedEvent",
"WorkflowLifecycleEvent",
"WorkflowOutputEvent",
"WorkflowRunResult",
"WorkflowRunState",
"WorkflowStartedEvent",
"WorkflowStatusEvent",
"WorkflowValidationError",
"WorkflowViz",
"create_edge_runner",
"executor",
"get_checkpoint_summary",
"handler",
"response_handler",
"validate_workflow_graph",
]
@@ -236,8 +236,9 @@ class WorkflowAgent(BaseAgent):
) -> AgentRunResponseUpdate | None:
"""Convert a workflow event to an AgentRunResponseUpdate.
Only AgentRunUpdateEvent and RequestInfoEvent are processed and the rest
are not relevant. Returns None if the event is not relevant.
Only AgentRunUpdateEvent and RequestInfoEvent are processed.
Other workflow events are ignored as they are workflow-internal and should
have corresponding AgentRunUpdateEvent emissions if relevant to agent consumers.
"""
match event:
case AgentRunUpdateEvent(data=update):
@@ -271,9 +272,8 @@ class WorkflowAgent(BaseAgent):
created_at=datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
)
case _:
# Ignore non-agent workflow events
# Ignore workflow-internal events
pass
# We only care about the above two events and discard the rest.
return None
def _extract_function_responses(self, input_messages: list[ChatMessage]) -> dict[str, Any]:
@@ -10,7 +10,7 @@ from abc import ABC, abstractmethod
from collections.abc import AsyncIterable, Sequence
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Protocol, TypeVar, Union, cast
from typing import Any, TypeVar, cast
from uuid import uuid4
from agent_framework import (
@@ -19,15 +19,13 @@ from agent_framework import (
AgentRunResponseUpdate,
ChatClientProtocol,
ChatMessage,
FunctionCallContent,
FunctionResultContent,
Role,
)
from ._base_group_chat_orchestrator import BaseGroupChatOrchestrator
from ._checkpoint import CheckpointStorage, WorkflowCheckpoint
from ._const import EXECUTOR_STATE_KEY
from ._events import WorkflowEvent
from ._events import AgentRunUpdateEvent, WorkflowEvent
from ._executor import Executor, handler
from ._group_chat import (
GroupChatBuilder,
@@ -104,64 +102,13 @@ def _message_from_payload(payload: Any) -> ChatMessage:
raise TypeError("Unable to reconstruct ChatMessage from payload")
# region Unified callback API (developer-facing)
# region Magentic event metadata constants
# Event type identifiers for magentic_event_type in additional_properties
MAGENTIC_EVENT_TYPE_ORCHESTRATOR = "orchestrator_message"
MAGENTIC_EVENT_TYPE_AGENT_DELTA = "agent_delta"
@dataclass
class MagenticOrchestratorMessageEvent(WorkflowEvent):
orchestrator_id: str = ""
message: ChatMessage | None = None
kind: str = ""
def __post_init__(self) -> None:
super().__init__(data=self.message)
@dataclass
class MagenticAgentDeltaEvent(WorkflowEvent):
agent_id: str | None = None
text: str | None = None
function_call_id: str | None = None
function_call_name: str | None = None
function_call_arguments: Any | None = None
function_result_id: str | None = None
function_result: Any | None = None
role: Role | None = None
def __post_init__(self) -> None:
super().__init__(data=self.text)
@dataclass
class MagenticAgentMessageEvent(WorkflowEvent):
agent_id: str = ""
message: ChatMessage | None = None
def __post_init__(self) -> None:
super().__init__(data=self.message)
@dataclass
class MagenticFinalResultEvent(WorkflowEvent):
message: ChatMessage | None = None
def __post_init__(self) -> None:
super().__init__(data=self.message)
MagenticCallbackEvent = Union[
MagenticOrchestratorMessageEvent,
MagenticAgentDeltaEvent,
MagenticAgentMessageEvent,
MagenticFinalResultEvent,
]
class CallbackSink(Protocol):
async def __call__(self, event: MagenticCallbackEvent) -> None: ...
# endregion Unified callback API
# endregion Magentic event metadata constants
# region Magentic One Prompts
@@ -1020,9 +967,8 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator):
) -> None:
"""Emit orchestrator message to the workflow event stream.
Orchestrator messages flow through the unified workflow event stream as
MagenticOrchestratorMessageEvent instances. Consumers should subscribe to
these events via workflow.run_stream().
Emits an AgentRunUpdateEvent (for agent wrapper consumers) with metadata indicating
the orchestrator event type.
Args:
ctx: Workflow context for adding events to the stream
@@ -1031,15 +977,24 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator):
Example:
async for event in workflow.run_stream("task"):
if isinstance(event, MagenticOrchestratorMessageEvent):
print(f"Orchestrator {event.kind}: {event.message.text}")
if isinstance(event, AgentRunUpdateEvent):
props = event.data.additional_properties if event.data else None
if props and props.get("magentic_event_type") == "orchestrator_message":
kind = props.get("orchestrator_message_kind", "")
print(f"Orchestrator {kind}: {event.data.text}")
"""
event = MagenticOrchestratorMessageEvent(
orchestrator_id=self.id,
message=message,
kind=kind,
# Emit AgentRunUpdateEvent with metadata
update = AgentRunResponseUpdate(
text=message.text,
role=message.role,
author_name=self._get_author_name(),
additional_properties={
"magentic_event_type": MAGENTIC_EVENT_TYPE_ORCHESTRATOR,
"orchestrator_message_kind": kind,
"orchestrator_id": self.id,
},
)
await ctx.add_event(event)
await ctx.add_event(AgentRunUpdateEvent(executor_id=self.id, data=update))
@override
async def on_checkpoint_save(self) -> dict[str, Any]:
@@ -1524,7 +1479,6 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator):
# Emit a completed event for the workflow
await context.yield_output(final_answer)
await context.add_event(MagenticFinalResultEvent(message=final_answer))
async def _check_within_limits_or_complete(
self,
@@ -1556,7 +1510,6 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator):
# Yield the partial result and signal completion
await context.yield_output(partial_result)
await context.add_event(MagenticFinalResultEvent(message=partial_result))
return False
return True
@@ -1733,45 +1686,23 @@ class MagenticAgentExecutor(Executor):
ctx: WorkflowContext[Any, Any],
update: AgentRunResponseUpdate,
) -> None:
contents = list(getattr(update, "contents", []) or [])
chunk = getattr(update, "text", None)
if not chunk:
chunk = "".join(getattr(item, "text", "") for item in contents if hasattr(item, "text"))
if chunk:
await ctx.add_event(
MagenticAgentDeltaEvent(
agent_id=self._agent_id,
text=chunk or None,
role=getattr(update, "role", None),
)
)
for item in contents:
if isinstance(item, FunctionCallContent):
await ctx.add_event(
MagenticAgentDeltaEvent(
agent_id=self._agent_id,
function_call_id=getattr(item, "call_id", None),
function_call_name=getattr(item, "name", None),
function_call_arguments=getattr(item, "arguments", None),
role=getattr(update, "role", None),
)
)
elif isinstance(item, FunctionResultContent):
await ctx.add_event(
MagenticAgentDeltaEvent(
agent_id=self._agent_id,
function_result_id=getattr(item, "call_id", None),
function_result=getattr(item, "result", None),
role=getattr(update, "role", None),
)
)
# Add metadata to identify this as an agent streaming update
if update.additional_properties is None:
update.additional_properties = {}
update.additional_properties["magentic_event_type"] = MAGENTIC_EVENT_TYPE_AGENT_DELTA
update.additional_properties["agent_id"] = self._agent_id
# Emit AgentRunUpdateEvent with the agent response update
await ctx.add_event(AgentRunUpdateEvent(executor_id=self._agent_id, data=update))
async def _emit_agent_message_event(
self,
ctx: WorkflowContext[Any, Any],
message: ChatMessage,
) -> None:
await ctx.add_event(MagenticAgentMessageEvent(agent_id=self._agent_id, message=message))
# Agent message completion is already communicated via streaming updates
# No additional event needed
pass
async def _invoke_agent(
self,
@@ -3,20 +3,20 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_a2a"
PACKAGE_EXTRA = "a2a"
IMPORT_PATH = "agent_framework_a2a"
PACKAGE_NAME = "agent-framework-a2a"
_IMPORTS = ["__version__", "A2AAgent"]
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -1,5 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_a2a import A2AAgent, __version__
from agent_framework_a2a import (
A2AAgent,
__version__,
)
__all__ = ["A2AAgent", "__version__"]
__all__ = [
"A2AAgent",
"__version__",
]
@@ -3,8 +3,8 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_ag_ui"
PACKAGE_EXTRA = "ag-ui"
IMPORT_PATH = "agent_framework_ag_ui"
PACKAGE_NAME = "agent-framework-ag-ui"
_IMPORTS = [
"__version__",
"AgentFrameworkAgent",
@@ -23,12 +23,12 @@ _IMPORTS = [
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -3,20 +3,20 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_anthropic"
PACKAGE_EXTRA = "anthropic"
IMPORT_PATH = "agent_framework_anthropic"
PACKAGE_NAME = "agent-framework-anthropic"
_IMPORTS = ["__version__", "AnthropicClient"]
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -1,5 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_anthropic import AnthropicClient, __version__
from agent_framework_anthropic import (
AnthropicClient,
__version__,
)
__all__ = ["AnthropicClient", "__version__"]
__all__ = [
"AnthropicClient",
"__version__",
]
@@ -1,36 +1,35 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AgentCallbackContext": ("agent_framework_azurefunctions", "azurefunctions"),
"AgentFunctionApp": ("agent_framework_azurefunctions", "azurefunctions"),
"AgentResponseCallbackProtocol": ("agent_framework_azurefunctions", "azurefunctions"),
"AzureAIAgentClient": ("agent_framework_azure_ai", "azure-ai"),
"AzureAIClient": ("agent_framework_azure_ai", "azure-ai"),
"AzureAISearchContextProvider": ("agent_framework_aisearch", "aisearch"),
"AzureAISearchSettings": ("agent_framework_aisearch", "aisearch"),
"AzureOpenAIAssistantsClient": ("agent_framework.azure._assistants_client", "core"),
"AzureOpenAIChatClient": ("agent_framework.azure._chat_client", "core"),
"AzureAISettings": ("agent_framework_azure_ai", "azure-ai"),
"AzureOpenAISettings": ("agent_framework.azure._shared", "core"),
"AzureOpenAIResponsesClient": ("agent_framework.azure._responses_client", "core"),
"DurableAIAgent": ("agent_framework_azurefunctions", "azurefunctions"),
"get_entra_auth_token": ("agent_framework.azure._entra_id_authentication", "core"),
"AgentCallbackContext": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"),
"AgentFunctionApp": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"),
"AgentResponseCallbackProtocol": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"),
"AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureAIClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureAISearchContextProvider": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"),
"AzureAISearchSettings": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"),
"AzureAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureOpenAIAssistantsClient": ("agent_framework.azure._assistants_client", "agent-framework-core"),
"AzureOpenAIChatClient": ("agent_framework.azure._chat_client", "agent-framework-core"),
"AzureOpenAIResponsesClient": ("agent_framework.azure._responses_client", "agent-framework-core"),
"AzureOpenAISettings": ("agent_framework.azure._shared", "agent-framework-core"),
"DurableAIAgent": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"),
"get_entra_auth_token": ("agent_framework.azure._entra_id_authentication", "agent-framework-core"),
}
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
package_name, package_extra = _IMPORTS[name]
import_path, package_name = _IMPORTS[name]
try:
return getattr(importlib.import_module(package_name), name)
return getattr(importlib.import_module(import_path), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"please use `pip install agent-framework-{package_extra}`, "
"or update your requirements.txt or pyproject.toml file."
f"The package {package_name} is required to use `{name}`. "
f"Please use `pip install {package_name}`, or update your requirements.txt or pyproject.toml file."
) from exc
raise AttributeError(f"Module `azure` has no attribute {name}.")
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAISettings
from agent_framework_azure_ai_search import AzureAISearchContextProvider, AzureAISearchSettings
from agent_framework_azurefunctions import (
AgentCallbackContext,
AgentFunctionApp,
@@ -20,6 +21,8 @@ __all__ = [
"AgentResponseCallbackProtocol",
"AzureAIAgentClient",
"AzureAIClient",
"AzureAISearchContextProvider",
"AzureAISearchSettings",
"AzureAISettings",
"AzureOpenAIAssistantsClient",
"AzureOpenAIChatClient",
@@ -3,20 +3,20 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_chatkit"
PACKAGE_EXTRA = "chatkit"
IMPORT_PATH = "agent_framework_chatkit"
PACKAGE_NAME = "agent-framework-chatkit"
_IMPORTS = ["__version__", "ThreadItemConverter", "simple_to_agent_input", "stream_agent_response"]
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -7,4 +7,9 @@ from agent_framework_chatkit import (
stream_agent_response,
)
__all__ = ["ThreadItemConverter", "__version__", "simple_to_agent_input", "stream_agent_response"]
__all__ = [
"ThreadItemConverter",
"__version__",
"simple_to_agent_input",
"stream_agent_response",
]
@@ -3,8 +3,8 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_devui"
PACKAGE_EXTRA = "devui"
IMPORT_PATH = "agent_framework_devui"
PACKAGE_NAME = "agent-framework-devui"
_IMPORTS = [
"AgentFrameworkRequest",
"DevServer",
@@ -22,12 +22,12 @@ _IMPORTS = [
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -1,4 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_devui import (
AgentFrameworkRequest,
DevServer,
@@ -3,20 +3,20 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_mem0"
PACKAGE_EXTRA = "mem0"
IMPORT_PATH = "agent_framework_mem0"
PACKAGE_NAME = "agent-framework-mem0"
_IMPORTS = ["__version__", "Mem0Provider"]
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -1,5 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_mem0 import Mem0Provider, __version__
from agent_framework_mem0 import (
Mem0Provider,
__version__,
)
__all__ = ["Mem0Provider", "__version__"]
__all__ = [
"Mem0Provider",
"__version__",
]
@@ -3,35 +3,33 @@
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, list[str]]] = {
"CopilotStudioAgent": ("agent_framework_copilotstudio", ["microsoft-copilotstudio", "copilotstudio"]),
"__version__": ("agent_framework_copilotstudio", ["microsoft-copilotstudio", "copilotstudio"]),
"acquire_token": ("agent_framework_copilotstudio", ["microsoft-copilotstudio", "copilotstudio"]),
# Purview (Graph Data Security & Governance) integration exports
"PurviewPolicyMiddleware": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewChatPolicyMiddleware": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewSettings": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewAppLocation": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewLocationType": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewAuthenticationError": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewPaymentRequiredError": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewRateLimitError": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewRequestError": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"PurviewServiceError": ("agent_framework_purview", ["microsoft-purview", "purview"]),
"CacheProvider": ("agent_framework_purview", ["microsoft-purview", "purview"]),
_IMPORTS: dict[str, tuple[str, str]] = {
"CopilotStudioAgent": ("agent_framework_copilotstudio", "agent-framework-copilotstudio"),
"__version__": ("agent_framework_copilotstudio", "agent-framework-copilotstudio"),
"acquire_token": ("agent_framework_copilotstudio", "agent-framework-copilotstudio"),
"PurviewPolicyMiddleware": ("agent_framework_purview", "agent-framework-purview"),
"PurviewChatPolicyMiddleware": ("agent_framework_purview", "agent-framework-purview"),
"PurviewSettings": ("agent_framework_purview", "agent-framework-purview"),
"PurviewAppLocation": ("agent_framework_purview", "agent-framework-purview"),
"PurviewLocationType": ("agent_framework_purview", "agent-framework-purview"),
"PurviewAuthenticationError": ("agent_framework_purview", "agent-framework-purview"),
"PurviewPaymentRequiredError": ("agent_framework_purview", "agent-framework-purview"),
"PurviewRateLimitError": ("agent_framework_purview", "agent-framework-purview"),
"PurviewRequestError": ("agent_framework_purview", "agent-framework-purview"),
"PurviewServiceError": ("agent_framework_purview", "agent-framework-purview"),
"CacheProvider": ("agent_framework_purview", "agent-framework-purview"),
}
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
package_name, package_extra = _IMPORTS[name]
import_path, package_name = _IMPORTS[name]
try:
return getattr(importlib.import_module(package_name), name)
return getattr(importlib.import_module(import_path), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The {' or '.join(package_extra)} extra is not installed, "
f"please use `pip install agent-framework-{package_extra[0]}`, "
"or update your requirements.txt or pyproject.toml file."
f"The package {package_name} is required to use `{name}`. "
f"Please use `pip install {package_name}`, or update your requirements.txt or pyproject.toml file."
) from exc
raise AttributeError(f"Module `microsoft` has no attribute {name}.")
@@ -1,6 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_copilotstudio import CopilotStudioAgent, __version__, acquire_token
from agent_framework_copilotstudio import (
CopilotStudioAgent,
__version__,
acquire_token,
)
from agent_framework_purview import (
CacheProvider,
PurviewAppLocation,
@@ -91,18 +91,21 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
) -> ChatResponse:
client = await self.ensure_client()
run_options = await self.prepare_options(messages, chat_options)
response_format = run_options.pop("response_format", None)
text_config = run_options.pop("text", None)
text_format, text_config = self._prepare_text_config(response_format=response_format, text_config=text_config)
if text_config:
run_options["text"] = text_config
try:
response_format = run_options.pop("response_format", None)
if not response_format:
if not text_format:
response = await client.responses.create(
stream=False,
**run_options,
)
chat_options.conversation_id = self.get_conversation_id(response, chat_options.store)
return self._create_response_content(response, chat_options=chat_options)
# create call does not support response_format, so we need to handle it via parse call
parsed_response: ParsedResponse[BaseModel] = await client.responses.parse(
text_format=response_format,
text_format=text_format,
stream=False,
**run_options,
)
@@ -134,9 +137,13 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
client = await self.ensure_client()
run_options = await self.prepare_options(messages, chat_options)
function_call_ids: dict[int, tuple[str, str]] = {} # output_index: (call_id, name)
response_format = run_options.pop("response_format", None)
text_config = run_options.pop("text", None)
text_format, text_config = self._prepare_text_config(response_format=response_format, text_config=text_config)
if text_config:
run_options["text"] = text_config
try:
response_format = run_options.pop("response_format", None)
if not response_format:
if not text_format:
response = await client.responses.create(
stream=True,
**run_options,
@@ -147,9 +154,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
)
yield update
return
# create call does not support response_format, so we need to handle it via stream call
async with client.responses.stream(
text_format=response_format,
text_format=text_format,
**run_options,
) as response:
async for chunk in response:
@@ -173,6 +179,71 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
inner_exception=ex,
) from ex
def _prepare_text_config(
self,
*,
response_format: Any,
text_config: MutableMapping[str, Any] | None,
) -> tuple[type[BaseModel] | None, dict[str, Any] | None]:
"""Normalize response_format into Responses text configuration and parse target."""
prepared_text = dict(text_config) if isinstance(text_config, MutableMapping) else None
if text_config is not None and not isinstance(text_config, MutableMapping):
raise ServiceInvalidRequestError("text must be a mapping when provided.")
if response_format is None:
return None, prepared_text
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
if prepared_text and "format" in prepared_text:
raise ServiceInvalidRequestError("response_format cannot be combined with explicit text.format.")
return response_format, prepared_text
if isinstance(response_format, Mapping):
format_config = self._convert_response_format(response_format)
if prepared_text is None:
prepared_text = {}
elif "format" in prepared_text and prepared_text["format"] != format_config:
raise ServiceInvalidRequestError("Conflicting response_format definitions detected.")
prepared_text["format"] = format_config
return None, prepared_text
raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.")
def _convert_response_format(self, response_format: Mapping[str, Any]) -> dict[str, Any]:
"""Convert Chat style response_format into Responses text format config."""
if "format" in response_format and isinstance(response_format["format"], Mapping):
return dict(response_format["format"])
format_type = response_format.get("type")
if format_type == "json_schema":
schema_section = response_format.get("json_schema", response_format)
if not isinstance(schema_section, Mapping):
raise ServiceInvalidRequestError("json_schema response_format must be a mapping.")
schema = schema_section.get("schema")
if schema is None:
raise ServiceInvalidRequestError("json_schema response_format requires a schema.")
name = (
schema_section.get("name")
or schema_section.get("title")
or (schema.get("title") if isinstance(schema, Mapping) else None)
or "response"
)
format_config: dict[str, Any] = {
"type": "json_schema",
"name": name,
"schema": schema,
}
if "strict" in schema_section:
format_config["strict"] = schema_section["strict"]
if "description" in schema_section and schema_section["description"] is not None:
format_config["description"] = schema_section["description"]
return format_config
if format_type in {"json_object", "text"}:
return {"type": format_type}
raise ServiceInvalidRequestError("Unsupported response_format provided for Responses client.")
def get_conversation_id(
self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None
) -> str | None:
@@ -518,9 +589,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
args: dict[str, Any] = {
"call_id": content.call_id,
"type": "function_call_output",
"output": prepare_function_call_results(content.result),
}
if content.result:
args["output"] = prepare_function_call_results(content.result)
return args
case FunctionApprovalRequestContent():
return {
@@ -46,9 +46,7 @@ RESPONSE_TYPE = Union[
OPTION_TYPE = Union[ChatOptions, dict[str, Any]]
__all__ = [
"OpenAISettings",
]
__all__ = ["OpenAISettings"]
def _check_openai_version_for_callable_api_key() -> None:
@@ -3,20 +3,20 @@
import importlib
from typing import Any
PACKAGE_NAME = "agent_framework_redis"
PACKAGE_EXTRA = "redis"
IMPORT_PATH = "agent_framework_redis"
PACKAGE_NAME = "agent-framework-redis"
_IMPORTS = ["__version__", "RedisProvider", "RedisChatMessageStore"]
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(PACKAGE_NAME), name)
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_EXTRA}' extra is not installed, please do `pip install agent-framework-{PACKAGE_EXTRA}`"
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {PACKAGE_NAME} has no attribute {name}.")
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
@@ -1,5 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_redis import RedisChatMessageStore, RedisProvider, __version__
from agent_framework_redis import (
RedisChatMessageStore,
RedisProvider,
__version__,
)
__all__ = ["RedisChatMessageStore", "RedisProvider", "__version__"]
__all__ = [
"RedisChatMessageStore",
"RedisProvider",
"__version__",
]
+1 -1
View File
@@ -43,7 +43,7 @@ dependencies = [
all = [
"agent-framework-a2a",
"agent-framework-ag-ui",
"agent-framework-aisearch",
"agent-framework-azure-ai-search",
"agent-framework-anthropic",
"agent-framework-azure-ai",
"agent-framework-azurefunctions",
@@ -483,6 +483,36 @@ def test_get_input_model_from_mcp_tool_with_ref_schema():
assert dumped == {"params": {"customer_id": 251}}
def test_get_input_model_from_mcp_tool_with_simple_array():
"""Test array with simple items schema (items schema should be preserved in json_schema_extra)."""
tool = types.Tool(
name="simple_array_tool",
description="Tool with simple array",
inputSchema={
"type": "object",
"properties": {
"tags": {
"type": "array",
"description": "List of tags",
"items": {"type": "string"}, # Simple string array
}
},
"required": ["tags"],
},
)
model = _get_input_model_from_mcp_tool(tool)
# Create an instance
instance = model(tags=["tag1", "tag2", "tag3"])
assert instance.tags == ["tag1", "tag2", "tag3"]
# Verify JSON schema still preserves items for simple types
json_schema = model.model_json_schema()
tags_property = json_schema["properties"]["tags"]
assert "items" in tags_property
assert tags_property["items"]["type"] == "string"
def test_get_input_model_from_mcp_prompt():
"""Test creation of input model from MCP prompt."""
prompt = types.Prompt(

Some files were not shown because too many files have changed in this diff Show More