Merge branch 'main' into javiercn/mapagui-hosting-overloads

This commit is contained in:
Javier Calvarro Nelson
2025-12-05 13:46:45 +01:00
committed by GitHub
242 changed files with 13019 additions and 6985 deletions
+4 -3
View File
@@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="10.4.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.0.2" />
<PackageVersion Include="Anthropic" Version="11.0.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
<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)" />
@@ -21,7 +21,7 @@
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.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.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
@@ -46,6 +46,7 @@
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.0" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
+2
View File
@@ -104,6 +104,8 @@
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
</Folder>
<Folder Name="/Samples/Purview/" />
<Folder Name="/Samples/Purview/AgentWithPurview/">
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251125.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251125.1</PackageVersion>
<GitTag>1.0.0-preview.251125.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251204.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251204.1</PackageVersion>
<GitTag>1.0.0-preview.251204.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -14,11 +14,6 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<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.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
@@ -15,11 +15,6 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<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" />
@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Anthropic.Foundry" />
</ItemGroup>
<ItemGroup>
@@ -2,10 +2,9 @@
// This sample shows how to create and use an AI agent with Anthropic as the backend.
using System.ClientModel;
using System.Net.Http.Headers;
using Anthropic;
using Anthropic.Core;
using Anthropic.Foundry;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -15,8 +14,8 @@ var deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_DEPLOYMENT_NA
// The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri
// ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions
var resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
string? resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
@@ -24,8 +23,8 @@ 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
? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication
: new AnthropicFoundryClient(new AnthropicAzureTokenCredential(new AzureCliCredential(), resource)); // Otherwise, use Foundry with Azure Client authentication
AIAgent agent = client.CreateAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName);
@@ -35,67 +34,41 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
namespace Sample
{
/// <summary>
/// Provides methods for invoking the Azure hosted Anthropic api.
/// Provides methods for invoking the Azure hosted Anthropic models using <see cref="TokenCredential"/> types.
/// </summary>
public class AnthropicFoundryClient : AnthropicClient
public sealed class AnthropicAzureTokenCredential : IAnthropicFoundryCredentials
{
private readonly TokenCredential _tokenCredential;
private readonly string _resourceName;
private readonly Lock _lock = new();
private AccessToken? _cachedAccessToken;
/// <inheritdoc/>
public string ResourceName { get; }
/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// Creates a new instance of the <see cref="AnthropicAzureTokenCredential"/>.
/// </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())
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
internal AnthropicAzureTokenCredential(TokenCredential tokenCredential, string resourceName)
{
this._resourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
this.ResourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
this._tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
this.BaseUrl = new Uri($"https://{this._resourceName}.services.ai.azure.com/anthropic", UriKind.Absolute);
}
/// <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
)
/// <inheritdoc/>
public void Apply(HttpRequestMessage requestMessage)
{
var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken);
lock (this._lock)
{
// Add a 5-minute buffer to avoid using tokens that are about to expire
if (this._cachedAccessToken is null || this._cachedAccessToken.Value.ExpiresOn <= DateTimeOffset.Now.AddMinutes(5))
{
this._cachedAccessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), CancellationToken.None);
}
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);
return default;
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", this._cachedAccessToken.Value.Token);
}
}
}
@@ -2,11 +2,11 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);IDE0059</NoWarn>
<NoWarn>$(NoWarn);IDE0059;NU1510</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -14,6 +14,10 @@
<PackageReference Include="Mscc.GenerativeAI.Microsoft" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<PackageReference Include="System.Net.Security" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
@@ -32,7 +32,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are good at telling jokes.",
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
AIContextProviderFactory = (ctx) => new ChatHistoryMemoryProvider(
vectorStore,
@@ -30,7 +30,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions()
{
Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details.",
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
// If each thread should have its own Mem0 scope, you can create a new id per thread here:
// ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
@@ -33,7 +33,7 @@ ChatClient chatClient = new AzureOpenAIClient(
// and its storage to that user id.
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
Instructions = "You are a friendly assistant. Always address the user by their name.",
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
AIContextProviderFactory = ctx => new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions)
});
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.
using OpenAI;
using OpenAI.Chat;
string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
string model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
// Create a ChatClient directly from OpenAIClient
ChatClient chatClient = new OpenAIClient(apiKey).GetChatClient(model);
// Create an agent directly from the ChatClient using OpenAIChatClientAgent
OpenAIChatClientAgent agent = new(chatClient, instructions: "You are good at telling jokes.", name: "Joker");
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
// Invoke the agent and output the text result.
ChatCompletion chatCompletion = await agent.RunAsync([chatMessage]);
Console.WriteLine(chatCompletion.Content.Last().Text);
// Invoke the agent with streaming support.
IAsyncEnumerable<StreamingChatCompletionUpdate> completionUpdates = agent.RunStreamingAsync([chatMessage]);
await foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
{
if (completionUpdate.ContentUpdate.Count > 0)
{
Console.WriteLine(completionUpdate.ContentUpdate[0].Text);
}
}
@@ -0,0 +1,22 @@
# Creating an Agent from a ChatClient
This sample demonstrates how to create an AI agent directly from an `OpenAI.Chat.ChatClient` instance using the `OpenAIChatClientAgent` class.
## What This Sample Shows
- **Direct ChatClient Creation**: Shows how to create an `OpenAI.Chat.ChatClient` from `OpenAI.OpenAIClient` and then use it to instantiate an agent
- **OpenAIChatClientAgent**: Demonstrates using the OpenAI SDK primitives instead of the ones from Microsoft.Extensions.AI and Microsoft.Agents.AI abstractions
- **Full Agent Capabilities**: Shows both regular and streaming invocation of the agent
## Running the Sample
1. Set the required environment variables:
```bash
set OPENAI_API_KEY=your_api_key_here
set OPENAI_MODEL=gpt-4o-mini
```
2. Run the sample:
```bash
dotnet run
```
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an OpenAIResponseClient instance.
using OpenAI;
using OpenAI.Responses;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
// Create an OpenAIResponseClient directly from OpenAIClient
OpenAIResponseClient responseClient = new OpenAIClient(apiKey).GetOpenAIResponseClient(model);
// Create an agent directly from the OpenAIResponseClient using OpenAIResponseClientAgent
OpenAIResponseClientAgent agent = new(responseClient, instructions: "You are good at telling jokes.", name: "Joker");
ResponseItem userMessage = ResponseItem.CreateUserMessageItem("Tell me a joke about a pirate.");
// Invoke the agent and output the text result.
OpenAIResponse response = await agent.RunAsync([userMessage]);
Console.WriteLine(response.GetOutputText());
// Invoke the agent with streaming support.
IAsyncEnumerable<StreamingResponseUpdate> responseUpdates = agent.RunStreamingAsync([userMessage]);
await foreach (StreamingResponseUpdate responseUpdate in responseUpdates)
{
if (responseUpdate is StreamingResponseOutputTextDeltaUpdate textUpdate)
{
Console.WriteLine(textUpdate.Delta);
}
}
@@ -0,0 +1,22 @@
# Creating an Agent from an OpenAIResponseClient
This sample demonstrates how to create an AI agent directly from an `OpenAI.Responses.OpenAIResponseClient` instance using the `OpenAIResponseClientAgent` class.
## What This Sample Shows
- **Direct OpenAIResponseClient Creation**: Shows how to create an `OpenAI.Responses.OpenAIResponseClient` from `OpenAI.OpenAIClient` and then use it to instantiate an agent
- **OpenAIResponseClientAgent**: Demonstrates using the OpenAI SDK primitives instead of the ones from Microsoft.Extensions.AI and Microsoft.Agents.AI abstractions
- **Full Agent Capabilities**: Shows both regular and streaming invocation of the agent
## Running the Sample
1. Set the required environment variables:
```bash
set OPENAI_API_KEY=your_api_key_here
set OPENAI_MODEL=gpt-4o-mini
```
2. Run the sample:
```bash
dotnet run
```
@@ -10,5 +10,7 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|Sample|Description|
|---|---|
|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent instructions with native OpenAI SDK types.|
|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent with native OpenAI SDK types. Shows both regular and streaming invocation of the agent.|
|[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.|
|[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.|
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
@@ -8,7 +8,6 @@
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Data;
using Microsoft.Agents.AI.Samples;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
@@ -62,7 +61,7 @@ AIAgent agent = azureOpenAIClient
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -7,7 +7,6 @@
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Data;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.Qdrant;
@@ -71,7 +70,7 @@ AIAgent agent = azureOpenAIClient
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief.",
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -9,7 +9,6 @@
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Data;
using Microsoft.Extensions.AI;
using OpenAI;
@@ -29,7 +28,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -22,7 +22,7 @@ ChatClient chatClient = new AzureOpenAIClient(
.GetChatClient(deploymentName);
// Create the ChatClientAgent with the specified name and instructions.
ChatClientAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant."));
ChatClientAgent agent = chatClient.CreateAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
// Set PersonInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke the agent with some unstructured input.
AgentRunResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>("Please provide information about John Smith, who is a 35-year-old software engineer.");
@@ -34,12 +34,10 @@ Console.WriteLine($"Age: {response.Result.Age}");
Console.WriteLine($"Occupation: {response.Result.Occupation}");
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
ChatClientAgent agentWithPersonInfo = chatClient.CreateAIAgent(new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant.")
ChatClientAgent agentWithPersonInfo = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new()
{
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
Name = "HelpfulAssistant",
ChatOptions = new() { Instructions = "You are a helpful assistant.", ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>() }
});
// Invoke the agent with some unstructured input while streaming, to extract the structured information from.
@@ -28,7 +28,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are good at telling jokes.",
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = ctx =>
{
@@ -18,8 +18,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Add agent options to the service collection.
builder.Services.AddSingleton(
new ChatClientAgentOptions(instructions: "You are good at telling jokes.", name: "Joker"));
builder.Services.AddSingleton(new ChatClientAgentOptions() { Name = "Joker", ChatOptions = new() { Instructions = "You are good at telling jokes." } });
// Add a chat client to the service collection.
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
@@ -16,10 +16,6 @@
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
</ItemGroup>
@@ -21,7 +21,7 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are good at telling jokes.",
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
});
@@ -24,10 +24,12 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential(
// Create ChatClientAgent directly
ChatClientAgent agent = await aiProjectClient.CreateAIAgentAsync(
model: deploymentName,
new ChatClientAgentOptions(name: AssistantName, instructions: AssistantInstructions)
new ChatClientAgentOptions()
{
Name = AssistantName,
ChatOptions = new()
{
Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
@@ -44,10 +46,12 @@ Console.WriteLine($"Occupation: {response.Result.Occupation}");
// Create the ChatClientAgent with the specified name, instructions, and expected structured output the agent should produce.
ChatClientAgent agentWithPersonInfo = aiProjectClient.CreateAIAgent(
model: deploymentName,
new ChatClientAgentOptions(name: AssistantName, instructions: AssistantInstructions)
new ChatClientAgentOptions()
{
Name = AssistantName,
ChatOptions = new()
{
Instructions = AssistantInstructions,
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<PersonInfo>()
}
});
@@ -17,10 +17,6 @@
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
@@ -34,9 +34,9 @@ AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
options: new()
{
Name = "MicrosoftLearnAgent",
Instructions = "You answer questions by searching the Microsoft Learn content only.",
ChatOptions = new()
{
Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = [mcpTool]
},
});
@@ -67,9 +67,9 @@ AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAs
options: new()
{
Name = "MicrosoftLearnAgentWithApproval",
Instructions = "You answer questions by searching the Microsoft Learn content only.",
ChatOptions = new()
{
Instructions = "You answer questions by searching the Microsoft Learn content only.",
Tools = [mcpToolWithApproval]
},
});
@@ -118,10 +118,11 @@ internal sealed class SloganWriterExecutor : Executor
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public SloganWriterExecutor(string id, IChatClient chatClient) : base(id)
{
ChatClientAgentOptions agentOptions = new(instructions: "You are a professional slogan writer. You will be given a task to create a slogan.")
ChatClientAgentOptions agentOptions = new()
{
ChatOptions = new()
{
Instructions = "You are a professional slogan writer. You will be given a task to create a slogan.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<SloganResult>()
}
};
@@ -193,10 +194,11 @@ internal sealed class FeedbackExecutor : Executor<SloganResult>
/// <param name="chatClient">The chat client to use for the AI agent.</param>
public FeedbackExecutor(string id, IChatClient chatClient) : base(id)
{
ChatClientAgentOptions agentOptions = new(instructions: "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.")
ChatClientAgentOptions agentOptions = new()
{
ChatOptions = new()
{
Instructions = "You are a professional editor. You will be given a slogan and the task it is meant to accomplish.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<FeedbackResult>()
}
};
@@ -85,10 +85,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for spam detection</returns>
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are a spam detection assistant that identifies spam emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
}
});
@@ -98,10 +99,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
}
});
@@ -100,10 +100,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for spam detection</returns>
private static ChatClientAgent GetSpamDetectionAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are a spam detection assistant that identifies spam emails. Be less confident in your assessments.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<DetectionResult>()
}
});
@@ -113,10 +114,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
}
});
@@ -140,10 +140,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email analysis</returns>
private static ChatClientAgent GetEmailAnalysisAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are a spam detection assistant that identifies spam emails.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are a spam detection assistant that identifies spam emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<AnalysisResult>()
}
});
@@ -153,10 +154,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email assistance</returns>
private static ChatClientAgent GetEmailAssistantAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an email assistant that helps users draft responses to emails with professionalism.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are an email assistant that helps users draft responses to emails with professionalism.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailResponse>()
}
});
@@ -166,10 +168,11 @@ public static class Program
/// </summary>
/// <returns>A ChatClientAgent configured for email summarization</returns>
private static ChatClientAgent GetEmailSummaryAgent(IChatClient chatClient) =>
new(chatClient, new ChatClientAgentOptions(instructions: "You are an assistant that helps users summarize emails.")
new(chatClient, new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = "You are an assistant that helps users summarize emails.",
ResponseFormat = ChatResponseFormat.ForJsonSchema<EmailSummary>()
}
});
@@ -285,19 +285,19 @@ internal sealed class CriticExecutor : Executor<ChatMessage, CriticDecision>
this._agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "Critic",
Instructions = """
You are a constructive critic. Review the content and provide specific feedback.
Always try to provide actionable suggestions for improvement and strive to identify improvement points.
Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
Provide your decision as structured output with:
- approved: true if content is good, false if revisions needed
- feedback: specific improvements needed (empty if approved)
Be concise but specific in your feedback.
""",
ChatOptions = new()
{
Instructions = """
You are a constructive critic. Review the content and provide specific feedback.
Always try to provide actionable suggestions for improvement and strive to identify improvement points.
Only approve if the content is high quality, clear, and meets the original requirements and you see no improvement points.
Provide your decision as structured output with:
- approved: true if content is good, false if revisions needed
- feedback: specific improvements needed (empty if approved)
Be concise but specific in your feedback.
""",
ResponseFormat = ChatResponseFormat.ForJsonSchema<CriticDecision>()
}
});
@@ -33,9 +33,9 @@ public class WeatherForecastAgent : DelegatingAIAgent
new ChatClientAgentOptions()
{
Name = AgentName,
Instructions = AgentInstructions,
ChatOptions = new ChatOptions()
{
Instructions = AgentInstructions,
Tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))],
// We want the agent to return structured output in a known format
// so that we can easily create adaptive cards from the response.
@@ -14,8 +14,6 @@
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<PropertyGroup>
@@ -23,7 +23,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Net.Http.Json" />
<PackageReference Include="System.Threading.Channels" />
</ItemGroup>
@@ -32,7 +32,7 @@ internal sealed class RunAgentInput
[JsonPropertyName("context")]
public AGUIContextItem[] Context { get; set; } = [];
[JsonPropertyName("forwardedProperties")]
[JsonPropertyName("forwardedProps")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonElement ForwardedProperties { get; set; }
}
@@ -124,7 +124,7 @@ public abstract class AIContextProvider
/// that will be used. Context providers can use this information to determine what additional context
/// should be provided for the invocation.
/// </remarks>
public class InvokingContext
public sealed class InvokingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
@@ -153,7 +153,7 @@ public abstract class AIContextProvider
/// request messages that were used and the response messages that were generated. It also indicates
/// whether the invocation succeeded or failed.
/// </remarks>
public class InvokedContext
public sealed class InvokedContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
@@ -45,14 +45,20 @@ public static class AnthropicBetaServiceExtensions
{
var options = new ChatClientAgentOptions
{
Instructions = instructions,
Name = name,
Description = description,
};
if (!string.IsNullOrWhiteSpace(instructions))
{
options.ChatOptions ??= new();
options.ChatOptions.Instructions = instructions;
}
if (tools is { Count: > 0 })
{
options.ChatOptions = new ChatOptions { Tools = tools };
options.ChatOptions ??= new();
options.ChatOptions.Tools = tools;
}
var chatClient = betaService.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
@@ -45,14 +45,20 @@ public static class AnthropicClientExtensions
{
var options = new ChatClientAgentOptions
{
Instructions = instructions,
Name = name,
Description = description,
};
if (!string.IsNullOrWhiteSpace(instructions))
{
options.ChatOptions ??= new();
options.ChatOptions.Instructions = instructions;
}
if (tools is { Count: > 0 })
{
options.ChatOptions = new ChatOptions { Tools = tools };
options.ChatOptions ??= new();
options.ChatOptions.Tools = tools;
}
var chatClient = client.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens);
@@ -67,12 +67,17 @@ public static class PersistentAgentsClientExtensions
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(persistentAgentMetadata.Instructions) && chatOptions?.Instructions is null)
{
chatOptions ??= new ChatOptions();
chatOptions.Instructions = persistentAgentMetadata.Instructions;
}
return new ChatClientAgent(chatClient, options: new()
{
Id = persistentAgentMetadata.Id,
Name = persistentAgentMetadata.Name,
Description = persistentAgentMetadata.Description,
Instructions = persistentAgentMetadata.Instructions,
ChatOptions = chatOptions
}, services: services);
}
@@ -207,12 +212,17 @@ public static class PersistentAgentsClientExtensions
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(persistentAgentMetadata.Instructions) && options.ChatOptions?.Instructions is null)
{
options.ChatOptions ??= new ChatOptions();
options.ChatOptions.Instructions = persistentAgentMetadata.Instructions;
}
var agentOptions = new ChatClientAgentOptions()
{
Id = persistentAgentMetadata.Id,
Name = options.Name ?? persistentAgentMetadata.Name,
Description = options.Description ?? persistentAgentMetadata.Description,
Instructions = options.Instructions ?? persistentAgentMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
@@ -453,7 +463,7 @@ public static class PersistentAgentsClientExtensions
model: model,
name: options.Name,
description: options.Description,
instructions: options.Instructions,
instructions: options.ChatOptions?.Instructions,
tools: toolDefinitionsAndResources.ToolDefinitions,
toolResources: toolDefinitionsAndResources.ToolResources,
temperature: null,
@@ -513,7 +523,7 @@ public static class PersistentAgentsClientExtensions
model: model,
name: options.Name,
description: options.Description,
instructions: options.Instructions,
instructions: options.ChatOptions?.Instructions,
tools: toolDefinitionsAndResources.ToolDefinitions,
toolResources: toolDefinitionsAndResources.ToolResources,
temperature: null,
@@ -393,7 +393,7 @@ public static partial class AzureAIProjectChatClientExtensions
PromptAgentDefinition agentDefinition = new(model)
{
Instructions = options.Instructions,
Instructions = options.ChatOptions?.Instructions,
Temperature = options.ChatOptions?.Temperature,
TopP = options.ChatOptions?.TopP,
TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) }
@@ -459,7 +459,7 @@ public static partial class AzureAIProjectChatClientExtensions
PromptAgentDefinition agentDefinition = new(model)
{
Instructions = options.Instructions,
Instructions = options.ChatOptions?.Instructions,
Temperature = options.ChatOptions?.Temperature,
TopP = options.ChatOptions?.TopP,
TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) }
@@ -822,10 +822,9 @@ public static partial class AzureAIProjectChatClientExtensions
if (agentDefinition is PromptAgentDefinition promptAgentDefinition)
{
agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new();
agentOptions.Instructions = promptAgentDefinition.Instructions;
agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
agentOptions.ChatOptions.Temperature = promptAgentDefinition.Temperature;
agentOptions.ChatOptions.TopP = promptAgentDefinition.TopP;
agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
}
if (agentTools is { Count: > 0 })
@@ -38,7 +38,6 @@ public sealed class ChatClientPromptAgentFactory : PromptAgentFactory
{
Name = promptAgent.Name,
Description = promptAgent.Description,
Instructions = promptAgent.Instructions?.ToTemplateString(),
ChatOptions = promptAgent.GetChatOptions(this.Engine, this._functions),
};
@@ -35,7 +35,7 @@ public static class PromptAgentExtensions
return new ChatOptions()
{
Instructions = promptAgent.ResponseInstructions?.ToTemplateString(),
Instructions = promptAgent.Instructions?.ToTemplateString(),
Temperature = (float?)modelOptions?.Temperature?.Eval(engine),
MaxOutputTokens = (int?)modelOptions?.MaxOutputTokens?.Eval(engine),
TopP = (float?)modelOptions?.TopP?.Eval(engine),
@@ -14,8 +14,6 @@
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
@@ -18,10 +18,6 @@
<PackageReference Include="A2A" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
@@ -25,16 +25,15 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Microsoft.Agents.AI.AGUI\Shared\**\*.cs" LinkBase="Shared" />
<Compile Remove="ServerSentEventsResult.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '10.0'))" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests" />
@@ -26,8 +26,6 @@
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<ItemGroup>
@@ -77,12 +77,17 @@ public static class OpenAIAssistantClientExtensions
chatClient = clientFactory(chatClient);
}
if (!string.IsNullOrWhiteSpace(assistantMetadata.Instructions) && chatOptions?.Instructions is null)
{
chatOptions ??= new ChatOptions();
chatOptions.Instructions = assistantMetadata.Instructions;
}
return new ChatClientAgent(chatClient, options: new()
{
Id = assistantMetadata.Id,
Name = assistantMetadata.Name,
Description = assistantMetadata.Description,
Instructions = assistantMetadata.Instructions,
ChatOptions = chatOptions
}, services: services);
}
@@ -215,12 +220,17 @@ public static class OpenAIAssistantClientExtensions
chatClient = clientFactory(chatClient);
}
if (string.IsNullOrWhiteSpace(options.ChatOptions?.Instructions) && !string.IsNullOrWhiteSpace(assistantMetadata.Instructions))
{
options.ChatOptions ??= new ChatOptions();
options.ChatOptions.Instructions = assistantMetadata.Instructions;
}
var mergedOptions = new ChatClientAgentOptions()
{
Id = assistantMetadata.Id,
Name = options.Name ?? assistantMetadata.Name,
Description = options.Description ?? assistantMetadata.Description,
Instructions = options.Instructions ?? assistantMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
@@ -339,10 +349,10 @@ public static class OpenAIAssistantClientExtensions
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Tools = tools,
Instructions = instructions
}
},
clientFactory,
@@ -377,7 +387,7 @@ public static class OpenAIAssistantClientExtensions
{
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
Instructions = options.ChatOptions?.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
@@ -443,10 +453,10 @@ public static class OpenAIAssistantClientExtensions
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Tools = tools,
Instructions = instructions,
}
},
clientFactory,
@@ -484,7 +494,7 @@ public static class OpenAIAssistantClientExtensions
{
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
Instructions = options.ChatOptions?.Instructions,
};
// Convert AITools to ToolDefinitions and ToolResources
@@ -47,9 +47,9 @@ public static class OpenAIChatClientExtensions
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Instructions = instructions,
Tools = tools,
}
},
@@ -50,9 +50,9 @@ public static class OpenAIResponseClientExtensions
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
ChatOptions = tools is null && string.IsNullOrWhiteSpace(instructions) ? null : new ChatOptions()
{
Instructions = instructions,
Tools = tools,
}
},
@@ -32,7 +32,7 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = new ChatOptions() { Instructions = instructions },
}, loggerFactory)
{
}
@@ -32,7 +32,7 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = new ChatOptions() { Instructions = instructions },
}, loggerFactory)
{
}
@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Data;
namespace Microsoft.Agents.AI;
@@ -59,13 +59,13 @@ public sealed partial class ChatClientAgent : AIAgent
chatClient,
new ChatClientAgentOptions
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions
ChatOptions = (tools is null && string.IsNullOrWhiteSpace(instructions)) ? null : new ChatOptions
{
Tools = tools,
}
Instructions = instructions
},
Name = name,
Description = description
},
loggerFactory,
services)
@@ -141,7 +141,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// These instructions are typically provided to the AI model as system messages to establish
/// the context and expected behavior for the agent's responses.
/// </remarks>
public string? Instructions => this._agentOptions?.Instructions;
public string? Instructions => this._agentOptions?.ChatOptions?.Instructions;
/// <summary>
/// Gets of the default <see cref="ChatOptions"/> used by the agent.
@@ -492,7 +492,6 @@ public sealed partial class ChatClientAgent : AIAgent
requestChatOptions.AllowMultipleToolCalls ??= this._agentOptions.ChatOptions.AllowMultipleToolCalls;
requestChatOptions.ConversationId ??= this._agentOptions.ChatOptions.ConversationId;
requestChatOptions.FrequencyPenalty ??= this._agentOptions.ChatOptions.FrequencyPenalty;
requestChatOptions.Instructions ??= this._agentOptions.ChatOptions.Instructions;
requestChatOptions.MaxOutputTokens ??= this._agentOptions.ChatOptions.MaxOutputTokens;
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
@@ -503,6 +502,13 @@ public sealed partial class ChatClientAgent : AIAgent
requestChatOptions.TopK ??= this._agentOptions.ChatOptions.TopK;
requestChatOptions.ToolMode ??= this._agentOptions.ChatOptions.ToolMode;
// Merge instructions by concatenating them if both are present.
requestChatOptions.Instructions = !string.IsNullOrWhiteSpace(requestChatOptions.Instructions) && !string.IsNullOrWhiteSpace(this.Instructions)
? $"{this.Instructions}\n{requestChatOptions.Instructions}"
: (!string.IsNullOrWhiteSpace(requestChatOptions.Instructions)
? requestChatOptions.Instructions
: this.Instructions);
// Merge only the additional properties from the agent if they are not already set in the request options.
if (requestChatOptions.AdditionalProperties is not null && this._agentOptions.ChatOptions.AdditionalProperties is not null)
{
@@ -685,12 +691,6 @@ public sealed partial class ChatClientAgent : AIAgent
""");
}
if (!string.IsNullOrWhiteSpace(this.Instructions))
{
chatOptions ??= new();
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? this.Instructions : $"{this.Instructions}\n{chatOptions.Instructions}";
}
// Only create or update ChatOptions if we have an id on the thread and we don't have the same one already in ChatOptions.
if (!string.IsNullOrWhiteSpace(typedThread.ConversationId) && typedThread.ConversationId != chatOptions?.ConversationId)
{
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.AI;
@@ -15,37 +14,8 @@ namespace Microsoft.Agents.AI;
/// identifier, display name, operational instructions, and a descriptive summary. It can be used to store and transfer
/// agent-related metadata within a chat application.
/// </remarks>
public class ChatClientAgentOptions
public sealed class ChatClientAgentOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgentOptions"/> class.
/// </summary>
public ChatClientAgentOptions()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgentOptions"/> class with the specified parameters.
/// </summary>
/// <remarks>If <paramref name="tools"/> is provided, a new <see cref="ChatOptions"/> instance is created
/// with the specified instructions and tools.</remarks>
/// <param name="instructions">The instructions or guidelines for the chat client agent. Can be <see langword="null"/> if not specified.</param>
/// <param name="name">The name of the chat client agent. Can be <see langword="null"/> if not specified.</param>
/// <param name="description">The description of the chat client agent. Can be <see langword="null"/> if not specified.</param>
/// <param name="tools">A list of <see cref="AITool"/> instances available to the chat client agent. Can be <see langword="null"/> if no
/// tools are specified.</param>
public ChatClientAgentOptions(string? instructions, string? name = null, string? description = null, IList<AITool>? tools = null)
{
this.Name = name;
this.Instructions = instructions;
this.Description = description;
if (tools is not null)
{
(this.ChatOptions ??= new()).Tools = tools;
}
}
/// <summary>
/// Gets or sets the agent id.
/// </summary>
@@ -56,11 +26,6 @@ public class ChatClientAgentOptions
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the agent instructions.
/// </summary>
public string? Instructions { get; set; }
/// <summary>
/// Gets or sets the agent description.
/// </summary>
@@ -106,7 +71,6 @@ public class ChatClientAgentOptions
{
Id = this.Id,
Name = this.Name,
Instructions = this.Instructions,
Description = this.Description,
ChatOptions = this.ChatOptions?.Clone(),
ChatMessageStoreFactory = this.ChatMessageStoreFactory,
@@ -11,7 +11,7 @@ using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Data;
namespace Microsoft.Agents.AI;
/// <summary>
/// A text search context provider that performs a search over external knowledge
@@ -4,7 +4,7 @@ using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Data;
namespace Microsoft.Agents.AI;
/// <summary>
/// Options controlling the behavior of <see cref="TextSearchProvider"/>.
@@ -13,8 +13,6 @@
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
@@ -84,8 +84,7 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
Instructions = instructions,
ChatOptions = new() { Tools = aiTools }
ChatOptions = new() { Instructions = instructions, Tools = aiTools }
}));
}
@@ -37,16 +37,20 @@ public class AIProjectClientCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync(
model: s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
options: new ChatClientAgentOptions()
{
Name = AgentName,
Description = AgentDescription,
ChatOptions = new() { Instructions = AgentInstructions }
}),
"CreateWithChatClientAgentOptionsSync" => this._client.CreateAIAgent(
model: s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
options: new ChatClientAgentOptions()
{
Name = AgentName,
Description = AgentDescription,
ChatOptions = new() { Instructions = AgentInstructions }
}),
"CreateWithFoundryOptionsAsync" => await this._client.CreateAIAgentAsync(
name: AgentName,
creationOptions: new AgentVersionCreationOptions(new PromptAgentDefinition(s_config.DeploymentName) { Instructions = AgentInstructions }) { Description = AgentDescription }),
@@ -239,16 +243,18 @@ public class AIProjectClientCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._client.CreateAIAgentAsync(
model: s_config.DeploymentName,
options: new ChatClientAgentOptions(
name: AgentName,
instructions: AgentInstructions,
tools: [weatherFunction])),
options: new ChatClientAgentOptions()
{
Name = AgentName,
ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] }
}),
"CreateWithChatClientAgentOptionsSync" => this._client.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
name: AgentName,
instructions: AgentInstructions,
tools: [weatherFunction])),
options: new ChatClientAgentOptions()
{
Name = AgentName,
ChatOptions = new() { Instructions = AgentInstructions, Tools = [weatherFunction] }
}),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
@@ -34,16 +35,20 @@ public class AzureAIAgentsPersistentCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
options: new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = AgentInstructions },
Name = AgentName,
Description = AgentDescription
}),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
options: new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = AgentInstructions },
Name = AgentName,
Description = AgentDescription
}),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -104,19 +109,32 @@ public class AzureAIAgentsPersistentCreateTests
);
var vectorStoreMetadata = await this._persistentAgentsClient.VectorStores.CreateVectorStoreAsync([uploadedAgentFile.Id], name: "WordCodeLookup_VectorStore");
// Wait for vector store indexing to complete before using it
await this.WaitForVectorStoreReadyAsync(this._persistentAgentsClient, vectorStoreMetadata.Value.Id);
// Act.
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]
}
}),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }]
}
}),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -179,15 +197,24 @@ public class AzureAIAgentsPersistentCreateTests
// Hosted tool path (tools supplied via ChatClientAgentOptions)
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]
}
}),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
// Foundry (definitions + resources provided directly)
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }]
}
}),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
@@ -232,14 +259,24 @@ public class AzureAIAgentsPersistentCreateTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
@@ -259,4 +296,42 @@ public class AzureAIAgentsPersistentCreateTests
await this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
}
/// <summary>
/// Waits for a vector store to complete indexing by polling its status.
/// </summary>
/// <param name="client">The persistent agents client.</param>
/// <param name="vectorStoreId">The ID of the vector store.</param>
/// <param name="maxWaitSeconds">Maximum time to wait in seconds (default: 30).</param>
/// <returns>A task that completes when the vector store is ready or throws on timeout/failure.</returns>
private async Task WaitForVectorStoreReadyAsync(
PersistentAgentsClient client,
string vectorStoreId,
int maxWaitSeconds = 30)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < maxWaitSeconds)
{
PersistentAgentsVectorStore vectorStore = await client.VectorStores.GetVectorStoreAsync(vectorStoreId);
if (vectorStore.Status == VectorStoreStatus.Completed)
{
if (vectorStore.FileCounts.Failed > 0)
{
throw new InvalidOperationException("Vector store indexing failed for some files");
}
return;
}
if (vectorStore.Status == VectorStoreStatus.Expired)
{
throw new InvalidOperationException("Vector store has expired");
}
await Task.Delay(1000);
}
throw new TimeoutException($"Vector store did not complete indexing within {maxWaitSeconds}s");
}
}
@@ -1,10 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
</ItemGroup>
@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="System.Net.ServerSentEvents" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
@@ -13,8 +13,6 @@
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
@@ -91,7 +91,7 @@ public sealed class AnthropicBetaServiceExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -158,7 +158,7 @@ public sealed class AnthropicClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -310,7 +310,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -337,7 +337,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -386,7 +386,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -413,7 +413,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -557,7 +557,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -584,7 +584,7 @@ public sealed class PersistentAgentsClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -752,7 +752,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -775,7 +775,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
TestChatClient? testChatClient = null;
@@ -803,7 +803,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -826,7 +826,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
TestChatClient? testChatClient = null;
@@ -1575,8 +1575,8 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Custom instructions",
Description = "Custom description"
Description = "Custom description",
ChatOptions = new ChatOptions { Instructions = "Custom instructions" }
};
// Act
@@ -1610,8 +1610,7 @@ public sealed class AzureAIProjectChatClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test",
ChatOptions = new ChatOptions { Tools = tools }
ChatOptions = new ChatOptions { Instructions = "Test", Tools = tools }
};
// Act
@@ -49,8 +49,7 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
ChatOptions = new() { ConversationId = "conv_12345" }
ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_12345" }
});
// Act
@@ -99,7 +98,7 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
ChatOptions = new() { Instructions = "Test instructions" },
});
// Act
@@ -148,8 +147,7 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
ChatOptions = new() { ConversationId = "conv_should_not_use_default" }
ChatOptions = new() { Instructions = "Test instructions", ConversationId = "conv_should_not_use_default" }
});
// Act
@@ -198,7 +196,7 @@ public class AzureAIProjectChatClientTests
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
ChatOptions = new() { Instructions = "Test instructions" },
});
// Act
@@ -70,7 +70,7 @@ public sealed class ChatClientAgentFactoryTests
Assert.IsType<ChatClientAgent>(agent);
var chatClientAgent = agent as ChatClientAgent;
Assert.NotNull(chatClientAgent?.ChatOptions);
Assert.Equal("Provide detailed and accurate responses.", chatClientAgent?.ChatOptions?.Instructions);
Assert.Equal("You are a helpful assistant.", chatClientAgent?.ChatOptions?.Instructions);
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.Temperature);
Assert.Equal(0.7F, chatClientAgent?.ChatOptions?.FrequencyPenalty);
Assert.Equal(1024, chatClientAgent?.ChatOptions?.MaxOutputTokens);
@@ -0,0 +1,358 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests;
public sealed class ForwardedPropertiesTests : IAsyncDisposable
{
private WebApplication? _app;
private HttpClient? _client;
[Fact]
public async Task ForwardedProps_AreParsedAndPassedToAgent_WhenProvidedInRequestAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
// Create request JSON with forwardedProps (per AG-UI protocol spec)
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test forwarded props" }],
"forwardedProps": { "customProp": "customValue", "sessionId": "test-session-123" }
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
fakeAgent.ReceivedForwardedProperties.GetProperty("customProp").GetString().Should().Be("customValue");
fakeAgent.ReceivedForwardedProperties.GetProperty("sessionId").GetString().Should().Be("test-session-123");
}
[Fact]
public async Task ForwardedProps_WithNestedObjects_AreCorrectlyParsedAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test nested props" }],
"forwardedProps": {
"user": { "id": "user-1", "name": "Test User" },
"metadata": { "version": "1.0", "feature": "test" }
}
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
JsonElement user = fakeAgent.ReceivedForwardedProperties.GetProperty("user");
user.GetProperty("id").GetString().Should().Be("user-1");
user.GetProperty("name").GetString().Should().Be("Test User");
JsonElement metadata = fakeAgent.ReceivedForwardedProperties.GetProperty("metadata");
metadata.GetProperty("version").GetString().Should().Be("1.0");
metadata.GetProperty("feature").GetString().Should().Be("test");
}
[Fact]
public async Task ForwardedProps_WithArrays_AreCorrectlyParsedAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test array props" }],
"forwardedProps": {
"tags": ["tag1", "tag2", "tag3"],
"scores": [1, 2, 3, 4, 5]
}
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
JsonElement tags = fakeAgent.ReceivedForwardedProperties.GetProperty("tags");
tags.GetArrayLength().Should().Be(3);
tags[0].GetString().Should().Be("tag1");
JsonElement scores = fakeAgent.ReceivedForwardedProperties.GetProperty("scores");
scores.GetArrayLength().Should().Be(5);
scores[2].GetInt32().Should().Be(3);
}
[Fact]
public async Task ForwardedProps_WhenEmpty_DoesNotCauseErrorsAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test empty props" }],
"forwardedProps": {}
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
}
[Fact]
public async Task ForwardedProps_WhenNotProvided_AgentStillWorksAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test no props" }]
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Undefined);
}
[Fact]
public async Task ForwardedProps_ReturnsValidSSEResponse_WithTextDeltaEventsAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test response" }],
"forwardedProps": { "customProp": "value" }
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
response.EnsureSuccessStatusCode();
Stream stream = await response.Content.ReadAsStreamAsync();
List<SseItem<string>> events = [];
await foreach (SseItem<string> item in SseParser.Create(stream).EnumerateAsync())
{
events.Add(item);
}
// Assert
events.Should().NotBeEmpty();
// SSE events have EventType = "message" and the actual type is in the JSON data
// Should have run_started event
events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"RUN_STARTED\""));
// Should have text_message_start event
events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"TEXT_MESSAGE_START\""));
// Should have text_message_content event with the response text
events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"TEXT_MESSAGE_CONTENT\""));
// Should have run_finished event
events.Should().Contain(e => e.Data != null && e.Data.Contains("\"type\":\"RUN_FINISHED\""));
}
[Fact]
public async Task ForwardedProps_WithMixedTypes_AreCorrectlyParsedAsync()
{
// Arrange
FakeForwardedPropsAgent fakeAgent = new();
await this.SetupTestServerAsync(fakeAgent);
const string RequestJson = """
{
"threadId": "thread-123",
"runId": "run-456",
"messages": [{ "id": "msg-1", "role": "user", "content": "test mixed types" }],
"forwardedProps": {
"stringProp": "text",
"numberProp": 42,
"boolProp": true,
"nullProp": null,
"arrayProp": [1, "two", false],
"objectProp": { "nested": "value" }
}
}
""";
using StringContent content = new(RequestJson, Encoding.UTF8, "application/json");
// Act
HttpResponseMessage response = await this._client!.PostAsync(new Uri("/agent", UriKind.Relative), content);
// Assert
response.IsSuccessStatusCode.Should().BeTrue();
fakeAgent.ReceivedForwardedProperties.ValueKind.Should().Be(JsonValueKind.Object);
fakeAgent.ReceivedForwardedProperties.GetProperty("stringProp").GetString().Should().Be("text");
fakeAgent.ReceivedForwardedProperties.GetProperty("numberProp").GetInt32().Should().Be(42);
fakeAgent.ReceivedForwardedProperties.GetProperty("boolProp").GetBoolean().Should().BeTrue();
fakeAgent.ReceivedForwardedProperties.GetProperty("nullProp").ValueKind.Should().Be(JsonValueKind.Null);
fakeAgent.ReceivedForwardedProperties.GetProperty("arrayProp").GetArrayLength().Should().Be(3);
fakeAgent.ReceivedForwardedProperties.GetProperty("objectProp").GetProperty("nested").GetString().Should().Be("value");
}
private async Task SetupTestServerAsync(FakeForwardedPropsAgent fakeAgent)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Services.AddAGUI();
builder.WebHost.UseTestServer();
this._app = builder.Build();
this._app.MapAGUI("/agent", fakeAgent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
this._client = testServer.CreateClient();
}
public async ValueTask DisposeAsync()
{
this._client?.Dispose();
if (this._app != null)
{
await this._app.DisposeAsync();
}
}
}
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated in tests")]
internal sealed class FakeForwardedPropsAgent : AIAgent
{
public FakeForwardedPropsAgent()
{
}
public override string? Description => "Agent for forwarded properties testing";
public JsonElement ReceivedForwardedProperties { get; private set; }
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Extract forwarded properties from ChatOptions.AdditionalProperties (set by AG-UI hosting layer)
if (options is ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } &&
properties.TryGetValue("ag_ui_forwarded_properties", out object? propsObj) &&
propsObj is JsonElement forwardedProps)
{
this.ReceivedForwardedProperties = forwardedProps;
}
// Always return a text response
string messageId = Guid.NewGuid().ToString("N");
yield return new AgentRunResponseUpdate
{
MessageId = messageId,
Role = ChatRole.Assistant,
Contents = [new TextContent("Forwarded props processed")]
};
await Task.CompletedTask;
}
public override AgentThread GetNewThread() => new FakeInMemoryAgentThread();
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
private sealed class FakeInMemoryAgentThread : InMemoryAgentThread
{
public FakeInMemoryAgentThread()
: base()
{
}
public FakeInMemoryAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThread, jsonSerializerOptions)
{
}
}
public override object? GetService(Type serviceType, object? serviceKey = null) => null;
}
@@ -20,10 +20,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
@@ -9,8 +9,6 @@
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
@@ -8,7 +8,6 @@
<ItemGroup>
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
@@ -92,7 +92,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -223,7 +223,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -250,7 +250,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -299,7 +299,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -326,7 +326,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
ChatOptions = new() { Instructions = "Override Instructions" }
};
// Act
@@ -498,7 +498,7 @@ public sealed class OpenAIAssistantClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -130,7 +130,7 @@ public sealed class OpenAIChatClientExtensionsTests
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -208,7 +208,7 @@ public sealed class OpenAIResponseClientExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
@@ -19,7 +19,6 @@ public class ChatClientAgentOptionsTests
// Assert
Assert.Null(options.Name);
Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
Assert.Null(options.ChatMessageStoreFactory);
@@ -27,90 +26,44 @@ public class ChatClientAgentOptionsTests
}
[Fact]
public void ParameterizedConstructor_WithNullValues_SetsPropertiesCorrectly()
public void Constructor_WithNullValues_SetsPropertiesCorrectly()
{
// Act
var options = new ChatClientAgentOptions(
instructions: null,
name: null,
description: null,
tools: null);
var options = new ChatClientAgentOptions() { Name = null, Description = null, ChatOptions = new() { Tools = null, Instructions = null } };
// Assert
Assert.Null(options.Name);
Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
Assert.Null(options.AIContextProviderFactory);
Assert.Null(options.ChatMessageStoreFactory);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Null(options.ChatOptions.Tools);
}
[Fact]
public void ParameterizedConstructor_WithInstructionsOnly_SetsChatOptionsWithInstructions()
{
// Arrange
const string Instructions = "Test instructions";
// Act
var options = new ChatClientAgentOptions(
instructions: Instructions,
name: null,
description: null,
tools: null);
// Assert
Assert.Null(options.Name);
Assert.Equal(Instructions, options.Instructions);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
}
[Fact]
public void ParameterizedConstructor_WithToolsOnly_SetsChatOptionsWithTools()
public void Constructor_WithToolsOnly_SetsChatOptionsWithTools()
{
// Arrange
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions(
instructions: null,
name: null,
description: null,
tools: tools);
var options = new ChatClientAgentOptions()
{
Name = null,
Description = null,
ChatOptions = new() { Tools = tools }
};
// Assert
Assert.Null(options.Name);
Assert.Null(options.Instructions);
Assert.Null(options.Description);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Same(tools, options.ChatOptions.Tools);
AssertSameTools(tools, options.ChatOptions.Tools);
}
[Fact]
public void ParameterizedConstructor_WithInstructionsAndTools_SetsChatOptionsWithBoth()
{
// Arrange
const string Instructions = "Test instructions";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions(
instructions: Instructions,
name: null,
description: null,
tools: tools);
// Assert
Assert.Null(options.Name);
Assert.Equal(Instructions, options.Instructions);
Assert.Null(options.Description);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Same(tools, options.ChatOptions.Tools);
}
[Fact]
public void ParameterizedConstructor_WithAllParameters_SetsAllPropertiesCorrectly()
public void Constructor_WithAllParameters_SetsAllPropertiesCorrectly()
{
// Arrange
const string Instructions = "Test instructions";
@@ -119,38 +72,37 @@ public class ChatClientAgentOptionsTests
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions(
instructions: Instructions,
name: Name,
description: Description,
tools: tools);
var options = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
ChatOptions = new() { Tools = tools, Instructions = Instructions }
};
// Assert
Assert.Equal(Name, options.Name);
Assert.Equal(Instructions, options.Instructions);
Assert.Equal(Instructions, options.ChatOptions.Instructions);
Assert.Equal(Description, options.Description);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Same(tools, options.ChatOptions.Tools);
AssertSameTools(tools, options.ChatOptions.Tools);
}
[Fact]
public void ParameterizedConstructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
public void Constructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
{
// Arrange
const string Name = "Test name";
const string Description = "Test description";
// Act
var options = new ChatClientAgentOptions(
instructions: null,
name: Name,
description: Description,
tools: null);
var options = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
};
// Assert
Assert.Equal(Name, options.Name);
Assert.Null(options.Instructions);
Assert.Equal(Description, options.Description);
Assert.Null(options.ChatOptions);
}
@@ -159,7 +111,6 @@ public class ChatClientAgentOptionsTests
public void Clone_CreatesDeepCopyWithSameValues()
{
// Arrange
const string Instructions = "Test instructions";
const string Name = "Test name";
const string Description = "Test description";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
@@ -171,8 +122,11 @@ public class ChatClientAgentOptionsTests
ChatClientAgentOptions.AIContextProviderFactoryContext ctx) =>
new Mock<AIContextProvider>().Object;
var original = new ChatClientAgentOptions(Instructions, Name, Description, tools)
var original = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
ChatOptions = new() { Tools = tools },
Id = "test-id",
ChatMessageStoreFactory = ChatMessageStoreFactory,
AIContextProviderFactory = AIContextProviderFactory
@@ -185,7 +139,6 @@ public class ChatClientAgentOptionsTests
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
Assert.Equal(original.Instructions, clone.Instructions);
Assert.Equal(original.Description, clone.Description);
Assert.Same(original.ChatMessageStoreFactory, clone.ChatMessageStoreFactory);
Assert.Same(original.AIContextProviderFactory, clone.AIContextProviderFactory);
@@ -197,14 +150,13 @@ public class ChatClientAgentOptionsTests
}
[Fact]
public void Clone_WithNullChatOptions_ClonesCorrectly()
public void Clone_WithoutProvidingChatOptions_ClonesCorrectly()
{
// Arrange
var original = new ChatClientAgentOptions
{
Id = "test-id",
Name = "Test name",
Instructions = "Test instructions",
Description = "Test description"
};
@@ -215,10 +167,19 @@ public class ChatClientAgentOptionsTests
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
Assert.Equal(original.Instructions, clone.Instructions);
Assert.Equal(original.Description, clone.Description);
Assert.Null(clone.ChatOptions);
Assert.Null(original.ChatOptions);
Assert.Null(clone.ChatMessageStoreFactory);
Assert.Null(clone.AIContextProviderFactory);
}
private static void AssertSameTools(IList<AITool>? expected, IList<AITool>? actual)
{
var index = 0;
foreach (var tool in expected ?? [])
{
Assert.Same(tool, actual?[index]);
index++;
}
}
}
@@ -31,7 +31,7 @@ public partial class ChatClientAgentTests
Id = "test-agent-id",
Name = "test name",
Description = "test description",
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
});
// Assert
@@ -65,7 +65,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent =
new(mockService.Object, options: new()
{
Instructions = "test instructions"
ChatOptions = new() { Instructions = "base instructions" },
});
// Act
@@ -99,7 +99,7 @@ public partial class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => agent.RunAsync((IReadOnlyCollection<ChatMessage>)null!));
@@ -120,7 +120,7 @@ public partial class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Act
await agent.RunAsync([new(ChatRole.User, "test")], options: new ChatClientAgentRunOptions(chatOptions));
@@ -181,7 +181,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions" } });
var runOptions = new AgentRunOptions();
// Act
@@ -212,7 +212,7 @@ public partial class ChatClientAgentTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse(responseMessages));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions", Name = authorName });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" }, Name = authorName });
// Act
var result = await agent.RunAsync([new(ChatRole.User, "test")]);
@@ -239,7 +239,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Create a thread using the agent's GetNewThread method
var thread = agent.GetNewThread();
@@ -270,7 +270,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = null });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = null } });
// Act
await agent.RunAsync([new(ChatRole.User, "test message")]);
@@ -300,7 +300,7 @@ public partial class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Act
await agent.RunAsync([]);
@@ -326,7 +326,7 @@ public partial class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -346,7 +346,7 @@ public partial class ChatClientAgentTests
var chatOptions = new ChatOptions { ConversationId = "ConvId" };
Mock<IChatClient> mockService = new();
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ThreadId" };
@@ -369,7 +369,7 @@ public partial class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100 && opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -394,7 +394,7 @@ public partial class ChatClientAgentTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
@@ -415,7 +415,7 @@ public partial class ChatClientAgentTests
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new();
// Act
@@ -442,7 +442,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -473,7 +473,7 @@ public partial class ChatClientAgentTests
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
});
// Act
@@ -508,7 +508,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -539,7 +539,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -592,7 +592,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
@@ -654,7 +654,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(requestMessages));
@@ -700,7 +700,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await agent.RunAsync([new(ChatRole.User, "user message")]);
@@ -907,7 +907,7 @@ public partial class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var metadata = new ChatClientAgentOptions { Instructions = "You are a helpful assistant" };
var metadata = new ChatClientAgentOptions { ChatOptions = new() { Instructions = "You are a helpful assistant" } };
ChatClientAgent agent = new(chatClient, metadata);
// Act & Assert
@@ -936,7 +936,7 @@ public partial class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
var metadata = new ChatClientAgentOptions { Instructions = null };
var metadata = new ChatClientAgentOptions { ChatOptions = new() { Instructions = null } };
ChatClientAgent agent = new(chatClient, metadata);
// Act & Assert
@@ -967,10 +967,10 @@ public partial class ChatClientAgentTests
}
/// <summary>
/// Verify that ChatOptions property returns null when no params are provided that require a ChatOptions instance.
/// Verify that ChatOptions is created with instructions when instructions are provided and no tools are provided.
/// </summary>
[Fact]
public void ChatOptionsReturnsNullWhenConstructorToolsNotProvided()
public void ChatOptionsCreatedWithInstructionsEvenWhenConstructorToolsNotProvided()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
@@ -980,7 +980,8 @@ public partial class ChatClientAgentTests
Assert.Equal("TestInstructions", agent.Instructions);
Assert.Equal("TestName", agent.Name);
Assert.Equal("TestDescription", agent.Description);
Assert.Null(agent.ChatOptions);
Assert.NotNull(agent.ChatOptions);
Assert.Equal("TestInstructions", agent.ChatOptions.Instructions);
}
#endregion
@@ -1071,7 +1072,7 @@ public partial class ChatClientAgentTests
public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
{
// Arrange
var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f };
var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, Instructions = "test instructions" };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
@@ -1085,7 +1086,6 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1114,7 +1114,7 @@ public partial class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new("test instructions"));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
@@ -1167,6 +1167,7 @@ public partial class ChatClientAgentTests
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
@@ -1204,7 +1205,6 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1263,6 +1263,7 @@ public partial class ChatClientAgentTests
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
@@ -1283,7 +1284,6 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1312,6 +1312,7 @@ public partial class ChatClientAgentTests
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
@@ -1333,7 +1334,6 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1360,6 +1360,7 @@ public partial class ChatClientAgentTests
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
RawRepresentationFactory = _ => agentSetting
};
var requestChatOptions = new ChatOptions
@@ -1380,7 +1381,6 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1436,7 +1436,7 @@ public partial class ChatClientAgentTests
TopK = 50,
PresencePenalty = 0.1f,
FrequencyPenalty = 0.2f,
Instructions = "test instructions\nrequest instructions",
Instructions = "agent instructions\nrequest instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
@@ -1459,7 +1459,6 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
@@ -1509,7 +1508,7 @@ public partial class ChatClientAgentTests
{
Id = "test-agent-id",
Name = "TestAgent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1532,7 +1531,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1556,7 +1555,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1582,7 +1581,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1606,7 +1605,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1632,7 +1631,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1661,7 +1660,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1694,7 +1693,7 @@ public partial class ChatClientAgentTests
{
Id = "test-agent-id",
Name = "TestAgent",
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1721,7 +1720,7 @@ public partial class ChatClientAgentTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1756,12 +1755,12 @@ public partial class ChatClientAgentTests
var chatClientAgent1 = new ChatClientAgent(mockChatClient1.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions 1"
ChatOptions = new() { Instructions = "Test instructions 1" }
});
var chatClientAgent2 = new ChatClientAgent(mockChatClient2.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions 2"
ChatOptions = new() { Instructions = "Test instructions 2" }
});
// Act
@@ -1796,7 +1795,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1820,7 +1819,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act
@@ -1845,7 +1844,7 @@ public partial class ChatClientAgentTests
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act - Request IChatClient with a service key (base.GetService will return null due to serviceKey)
@@ -1870,7 +1869,7 @@ public partial class ChatClientAgentTests
mockChatClient.Setup(c => c.GetService(typeof(string), "some-key")).Returns("test-result");
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions"
ChatOptions = new() { Instructions = "Test instructions" }
});
// Act - Request string with a service key (base.GetService will return null due to serviceKey)
@@ -1911,7 +1910,7 @@ public partial class ChatClientAgentTests
ChatClientAgent agent =
new(mockService.Object, options: new()
{
Instructions = "test instructions"
ChatOptions = new() { Instructions = "test instructions" }
});
// Act
@@ -1958,7 +1957,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -1996,7 +1995,7 @@ public partial class ChatClientAgentTests
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
@@ -2049,7 +2048,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
@@ -2112,7 +2111,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
// Act
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
@@ -20,7 +20,7 @@ public class ChatClientAgent_DeserializeThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions",
ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = _ =>
{
factoryCalled = true;
@@ -53,7 +53,7 @@ public class ChatClientAgent_DeserializeThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions",
ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = _ =>
{
factoryCalled = true;
@@ -19,7 +19,7 @@ public class ChatClientAgent_GetNewThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions",
ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = _ =>
{
factoryCalled = true;
@@ -46,7 +46,7 @@ public class ChatClientAgent_GetNewThreadTests
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
Instructions = "Test instructions",
ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = _ =>
{
factoryCalled = true;
@@ -90,7 +90,7 @@ public sealed class ChatClientBuilderExtensionsTests
{
Name = "AgentWithOptions",
Description = "Desc",
Instructions = "Instr",
ChatOptions = new() { Instructions = "Instr" },
UseProvidedChatClientAsIs = true
};
@@ -115,7 +115,7 @@ public sealed class ChatClientBuilderExtensionsTests
var options = new ChatClientAgentOptions
{
Name = "ServiceAgent",
Instructions = "Service instructions"
ChatOptions = new() { Instructions = "Service instructions" }
};
// Act
@@ -148,7 +148,7 @@ public sealed class ChatClientBuilderExtensionsTests
ChatClientBuilder builder = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(options: new() { Instructions = "instructions" }));
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
}
[Fact]
@@ -166,7 +166,7 @@ public sealed class ChatClientBuilderExtensionsTests
var agent = builder.BuildAIAgent(
new ChatClientAgentOptions
{
Instructions = "Middleware test",
ChatOptions = new() { Instructions = "Middleware test" },
UseProvidedChatClientAsIs = true
}
);
@@ -57,7 +57,7 @@ public sealed class ChatClientExtensionsTests
{
Name = "AgentWithOptions",
Description = "Desc",
Instructions = "Instr",
ChatOptions = new() { Instructions = "Instr" },
UseProvidedChatClientAsIs = true
};
@@ -89,6 +89,6 @@ public sealed class ChatClientExtensionsTests
IChatClient chatClient = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => chatClient.CreateAIAgent(options: new() { Instructions = "instructions" }));
Assert.Throws<ArgumentNullException>(() => chatClient.CreateAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
}
}
@@ -6,7 +6,6 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Data;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Moq;
@@ -40,6 +40,12 @@ internal sealed class MockAgentProvider : Mock<WorkflowAgentProvider>
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(testMessages));
this.Setup(provider => provider.CreateMessageAsync(
It.IsAny<string>(),
It.IsAny<ChatMessage>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(testMessages.First()));
}
private string CreateConversationId()
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="AddConversationMessageExecutor"/>.
/// </summary>
public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Theory]
[InlineData(AgentMessageRole.User)]
[InlineData(AgentMessageRole.Agent)]
public async Task AddMessageSuccessfullyAsync(AgentMessageRole role)
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(AddMessageSuccessfullyAsync),
variableName: "TestMessage",
role: AgentMessageRoleWrapper.Get(role),
messageText: $"Hello from {role}");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
AgentMessageRoleWrapper role,
string messageText)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
AddConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
role,
messageText);
AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault();
Assert.NotNull(testMessage);
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private AddConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
AgentMessageRoleWrapper role,
string messageText)
{
AddConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
Role = role,
};
actionBuilder.Content.Add(new AddConversationMessageContent.Builder
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse(messageText)
});
return AssignParent<AddConversationMessage>(actionBuilder);
}
}
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
@@ -37,14 +38,24 @@ public class OpenAIAssistantClientExtensionsTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: AgentInstructions,
@@ -94,14 +105,24 @@ public class OpenAIAssistantClientExtensionsTests
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [codeInterpreterTool])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [codeInterpreterTool]
}
}),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [codeInterpreterTool])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [codeInterpreterTool]
}
}),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
@@ -153,20 +174,33 @@ public class OpenAIAssistantClientExtensionsTests
});
string vectorStoreId = vectorStoreCreate.Value.Id;
// Wait for vector store indexing to complete before using it
await WaitForVectorStoreReadyAsync(vectorStoreClient, vectorStoreId);
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreId)] };
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [fileSearchTool])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [fileSearchTool]
}
}),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [fileSearchTool])),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [fileSearchTool]
}
}),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
@@ -189,4 +223,43 @@ public class OpenAIAssistantClientExtensionsTests
File.Delete(searchFilePath);
}
}
/// <summary>
/// Waits for a vector store to complete indexing by polling its status.
/// </summary>
/// <param name="client">The vector store client.</param>
/// <param name="vectorStoreId">The ID of the vector store.</param>
/// <param name="maxWaitSeconds">Maximum time to wait in seconds (default: 30).</param>
/// <returns>A task that completes when the vector store is ready or throws on timeout/failure.</returns>
private static async Task WaitForVectorStoreReadyAsync(
VectorStoreClient client,
string vectorStoreId,
int maxWaitSeconds = 30)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < maxWaitSeconds)
{
VectorStore vectorStore = await client.GetVectorStoreAsync(vectorStoreId);
VectorStoreStatus status = vectorStore.Status;
if (status == VectorStoreStatus.Completed)
{
if (vectorStore.FileCounts.Failed > 0)
{
throw new InvalidOperationException("Vector store indexing failed for some files");
}
return;
}
if (status == VectorStoreStatus.Expired)
{
throw new InvalidOperationException("Vector store has expired");
}
await Task.Delay(1000);
}
throw new TimeoutException($"Vector store did not complete indexing within {maxWaitSeconds}s");
}
}
@@ -47,8 +47,7 @@ public class OpenAIChatCompletionFixture : IChatClientAgentFixture
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
Instructions = instructions,
ChatOptions = new() { Tools = aiTools }
ChatOptions = new() { Instructions = instructions, Tools = aiTools }
}));
}
@@ -73,9 +73,9 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
options: new()
{
Name = name,
Instructions = instructions,
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools = aiTools,
RawRepresentationFactory = new Func<IChatClient, object>(_ => new ResponseCreationOptions() { StoredOutputEnabled = store })
},