mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-featurecollections-messagestore
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+25
@@ -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
|
||||
|
||||
|
||||
+20
@@ -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).
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
+1
@@ -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|
|
||||
|
||||
+15
@@ -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>
|
||||
+24
@@ -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);
|
||||
}
|
||||
+43
@@ -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
|
||||
|
||||
+15
@@ -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>
|
||||
+59
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -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
|
||||
|
||||
+15
@@ -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>
|
||||
+37
@@ -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);
|
||||
}
|
||||
+47
@@ -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.
|
||||
|
||||
+4
-4
@@ -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")]
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
+20
@@ -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>
|
||||
+26
@@ -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));
|
||||
+30
@@ -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));
|
||||
+105
@@ -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;
|
||||
}
|
||||
+37
@@ -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));
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
@@ -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());
|
||||
}
|
||||
}
|
||||
+107
@@ -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
|
||||
{
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+290
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+257
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -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();
|
||||
|
||||
+1
-1
@@ -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 });
|
||||
|
||||
+1
-1
@@ -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 });
|
||||
|
||||
+1
-1
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
+12
-10
@@ -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
|
||||
+4
-4
@@ -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"]
|
||||
+36
-38
@@ -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:
|
||||
|
||||
@@ -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__",
|
||||
]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user