Remove AdditionalInstructions from AgentRunOptions since it is not well supported outside of ChatClientAgents (#87)

* Remove additional instructions from AgentRunOptions since it is not well supported outside of ChatClientAgents

* Fix typos and remove unused test.

* Make further namespace fixes and update AzureAIAgent with new tests.

* Expand tests to increase code coverage
This commit is contained in:
westey
2025-06-20 12:54:37 +01:00
committed by GitHub
Unverified
parent 2cd77596c3
commit 578b35723a
39 changed files with 398 additions and 180 deletions
@@ -27,17 +27,9 @@ public class AgentRunOptions
public AgentRunOptions(AgentRunOptions options)
{
Throw.IfNull(options);
this.AdditionalInstructions = options.AdditionalInstructions;
this.OnIntermediateMessages = options.OnIntermediateMessages;
}
/// <summary>
/// Gets or sets any instructions, in addition to those that were provided to the agent
/// initially, that need to be added to the prompt for this invocation only.
/// </summary>
public string? AdditionalInstructions { get; set; } = null;
/// <summary>
/// Gets or sets a function to be called when a complete new message is generated by the agent.
/// </summary>
@@ -363,11 +363,6 @@ public sealed class ChatClientAgent : Agent
private void UpdateThreadMessagesWithAgentInstructions(List<ChatMessage> threadMessages, AgentRunOptions? options)
{
if (!string.IsNullOrWhiteSpace(options?.AdditionalInstructions))
{
threadMessages.Insert(0, new(ChatRole.System, options?.AdditionalInstructions) { AuthorName = this.Name });
}
if (!string.IsNullOrWhiteSpace(this.Instructions))
{
threadMessages.Insert(0, new(ChatRole.System, this.Instructions) { AuthorName = this.Name });
@@ -17,7 +17,6 @@ internal sealed class ChatClientAgentRunOptions : AgentRunOptions
internal ChatClientAgentRunOptions(AgentRunOptions? source = null, ChatOptions? chatOptions = null)
{
this.OnIntermediateMessages = source?.OnIntermediateMessages;
this.AdditionalInstructions = source?.AdditionalInstructions;
this.ChatOptions = chatOptions;
}
@@ -1,25 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents;
using Microsoft.Extensions.AI;
namespace AgentConformanceTests;
/// <summary>
/// Base class for setting up and tearing down agents, to be used in tests.
/// Each agent type should have its own derived class.
/// </summary>
public abstract class AgentFixture : IAsyncLifetime
{
public abstract Agent Agent { get; }
public abstract Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread);
public abstract Task DeleteThreadAsync(AgentThread thread);
public abstract Task DisposeAsync();
public abstract Task InitializeAsync();
}
@@ -2,7 +2,6 @@
using System;
using System.Threading.Tasks;
using AgentConformanceTests;
namespace AgentConformance.IntegrationTests;
@@ -12,7 +11,7 @@ namespace AgentConformance.IntegrationTests;
/// <typeparam name="TAgentFixture">The type of the agent fixture used in these tests.</typeparam>
/// <param name="createAgentFixture">Used to create a new fixture for this test suite.</param>
public abstract class AgentTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : IAsyncLifetime
where TAgentFixture : AgentFixture
where TAgentFixture : IAgentFixture
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
protected TAgentFixture Fixture { get; private set; }
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Microsoft.Agents;
namespace AgentConformance.IntegrationTests;
/// <summary>
/// Conformance tests that are specific to the <see cref="ChatClientAgent"/> in addition to those in <see cref="RunStreamingTests{TAgentFixture}"/>.
/// </summary>
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
public abstract class ChatClientAgentRunStreamingTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
where TAgentFixture : IChatClientAgentFixture
{
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
{
// Arrange
var agent = await this.Fixture.CreateAgentWithInstructionsAsync("Always respond with 'Computer says no', even if there was no user input.");
var thread = agent.GetNewThread();
await using var agentCleanup = new AgentCleanup(agent, this.Fixture);
await using var threadCleanup = new ThreadCleanup(thread, this.Fixture);
// Act
var chatResponses = await agent.RunStreamingAsync(thread).ToListAsync();
// Assert
var chatResponseText = string.Join("", chatResponses.Select(x => x.Text));
Assert.Contains("Computer says no", chatResponseText, StringComparison.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Microsoft.Agents;
namespace AgentConformance.IntegrationTests;
/// <summary>
/// Conformance tests that are specific to the <see cref="ChatClientAgent"/> in addition to those in <see cref="RunTests{TAgentFixture}"/>.
/// </summary>
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
public abstract class ChatClientAgentRunTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
where TAgentFixture : IChatClientAgentFixture
{
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
{
// Arrange
var agent = await this.Fixture.CreateAgentWithInstructionsAsync("Always respond with 'Computer says no', even if there was no user input.");
var thread = agent.GetNewThread();
await using var agentCleanup = new AgentCleanup(agent, this.Fixture);
await using var threadCleanup = new ThreadCleanup(thread, this.Fixture);
// Act
var chatResponse = await agent.RunAsync(thread);
// Assert
Assert.NotNull(chatResponse);
Assert.Single(chatResponse.Messages);
Assert.Contains("Computer says no", chatResponse.Text, StringComparison.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents;
using Microsoft.Extensions.AI;
namespace AgentConformance.IntegrationTests;
/// <summary>
/// Interface for setting up and tearing down agents, to be used in tests.
/// Each agent type should have its own derived class.
/// </summary>
public interface IAgentFixture : IAsyncLifetime
{
Agent Agent { get; }
Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread);
Task DeleteThreadAsync(AgentThread thread);
}
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents;
using Microsoft.Extensions.AI;
namespace AgentConformance.IntegrationTests;
/// <summary>
/// Interface for setting up and tearing down <see cref="IChatClient"/> based agents, to be used in tests.
/// Each agent type should have its own derived class.
/// </summary>
public interface IChatClientAgentFixture : IAgentFixture
{
IChatClient ChatClient { get; }
Task<ChatClientAgent> CreateAgentWithInstructionsAsync(string instructions);
Task DeleteAgentAsync(ChatClientAgent agent);
}
@@ -4,7 +4,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using AgentConformanceTests;
using Microsoft.Extensions.AI;
namespace AgentConformance.IntegrationTests;
@@ -14,8 +13,8 @@ namespace AgentConformance.IntegrationTests;
/// </summary>
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
public abstract class RunStreamingAsyncTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
where TAgentFixture : AgentFixture
public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
where TAgentFixture : IAgentFixture
{
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task RunWithStringReturnsExpectedResultAsync()
@@ -70,22 +69,6 @@ public abstract class RunStreamingAsyncTests<TAgentFixture>(Func<TAgentFixture>
Assert.Contains("Paris", chatResponseText);
}
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task RunWithAdditionalInstructionsAndNoMessageReturnsExpectedResultAsync()
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
var chatResponses = await agent.RunStreamingAsync(thread, new() { AdditionalInstructions = "Always respond with `Computer says no`" }).ToListAsync();
// Assert
var chatResponseText = string.Join("", chatResponses.Select(x => x.Text));
Assert.Contains("Computer says no", chatResponseText);
}
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task ThreadMaintainsHistoryAsync()
{
@@ -4,7 +4,6 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using AgentConformanceTests;
using Microsoft.Extensions.AI;
namespace AgentConformance.IntegrationTests;
@@ -14,8 +13,8 @@ namespace AgentConformance.IntegrationTests;
/// </summary>
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
public abstract class RunAsyncTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
where TAgentFixture : AgentFixture
public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
where TAgentFixture : IAgentFixture
{
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task RunWithStringReturnsExpectedResultAsync()
@@ -73,23 +72,6 @@ public abstract class RunAsyncTests<TAgentFixture>(Func<TAgentFixture> createAge
Assert.Contains("Paris", chatResponse.Text);
}
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task RunWithAdditionalInstructionsAndNoMessageReturnsExpectedResultAsync()
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
var chatResponse = await agent.RunAsync(thread, new() { AdditionalInstructions = "Always respond with `Computer says no`, even when the user provided on input." });
// Assert
Assert.NotNull(chatResponse);
Assert.Single(chatResponse.Messages);
Assert.Contains("Computer says no", chatResponse.Text);
}
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
public virtual async Task ThreadMaintainsHistoryAsync()
{
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Agents;
namespace AgentConformance.IntegrationTests.Support;
/// <summary>
/// Helper class to delete agents after tests.
/// </summary>
/// <param name="agent">The agent to delete.</param>
/// <param name="fixture">The fixture that provides agent specific capabilities.</param>
internal sealed class AgentCleanup(ChatClientAgent agent, IChatClientAgentFixture fixture) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await fixture.DeleteAgentAsync(agent);
}
}
@@ -2,7 +2,6 @@
using System;
using System.Threading.Tasks;
using AgentConformanceTests;
using Microsoft.Agents;
namespace AgentConformance.IntegrationTests.Support;
@@ -12,7 +11,7 @@ namespace AgentConformance.IntegrationTests.Support;
/// </summary>
/// <param name="thread">The thread to delete.</param>
/// <param name="fixture">The fixture that provides agent specific capabilities.</param>
internal sealed class ThreadCleanup(AgentThread thread, AgentFixture fixture) : IAsyncDisposable
internal sealed class ThreadCleanup(AgentThread thread, IAgentFixture fixture) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<AzureAIAgentsPersistentFixture>(() => new())
{
}
@@ -4,6 +4,6 @@ using AgentConformance.IntegrationTests;
namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsPersistentInvokeStreamingTests() : RunStreamingAsyncTests<AzureAIAgentsPersistentFixture>(() => new())
public class AzureAIAgentsChatClientAgentRunTests() : ChatClientAgentRunTests<AzureAIAgentsPersistentFixture>(() => new())
{
}
@@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using AgentConformanceTests;
using Azure;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
@@ -15,17 +15,22 @@ using Shared.IntegrationTests;
namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsPersistentFixture : AgentFixture
public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture
{
private static readonly AzureAIConfiguration s_config = TestConfiguration.LoadSection<AzureAIConfiguration>();
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private Agent _agent;
private PersistentAgentsClient _persistentAgentsClient;
private IChatClient _chatClient;
private PersistentAgent _persistentAgent;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public override Agent Agent => this._agent;
public IChatClient ChatClient => this._chatClient;
public override async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
public Agent Agent => this._agent;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
{
if (thread is not ChatClientAgentThread chatClientThread)
{
@@ -57,7 +62,26 @@ public class AzureAIAgentsPersistentFixture : AgentFixture
return messages;
}
public override Task DeleteThreadAsync(AgentThread thread)
public async Task<ChatClientAgent> CreateAgentWithInstructionsAsync(string instructions)
{
var persistentAgentResponse = await this._persistentAgentsClient.Administration.CreateAgentAsync(
model: s_config.DeploymentName,
name: "HelpfulAssistant",
instructions: "You are a helpful assistant.");
var persistentAgent = persistentAgentResponse.Value;
var chatClient = this._persistentAgentsClient.AsIChatClient(persistentAgent.Id);
return new ChatClientAgent(chatClient, new() { Id = persistentAgent.Id });
}
public Task DeleteAgentAsync(ChatClientAgent agent)
{
return this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
public Task DeleteThreadAsync(AgentThread thread)
{
if (thread?.Id is not null)
{
@@ -67,7 +91,7 @@ public class AzureAIAgentsPersistentFixture : AgentFixture
return Task.CompletedTask;
}
public override Task DisposeAsync()
public Task DisposeAsync()
{
if (this._persistentAgentsClient is not null && this._persistentAgent is not null)
{
@@ -77,21 +101,19 @@ public class AzureAIAgentsPersistentFixture : AgentFixture
return Task.CompletedTask;
}
public override async Task InitializeAsync()
public async Task InitializeAsync()
{
var config = TestConfiguration.LoadSection<AzureAIConfiguration>();
this._persistentAgentsClient = new(config.Endpoint, new AzureCliCredential());
this._persistentAgentsClient = new(s_config.Endpoint, new AzureCliCredential());
var persistentAgentResponse = await this._persistentAgentsClient.Administration.CreateAgentAsync(
model: config.DeploymentName,
model: s_config.DeploymentName,
name: "HelpfulAssistant",
instructions: "You are a helpful assistant.");
this._persistentAgent = persistentAgentResponse.Value;
var chatClient = this._persistentAgentsClient.AsIChatClient(this._persistentAgent.Id);
this._chatClient = this._persistentAgentsClient.AsIChatClient(this._persistentAgent.Id);
this._agent = new ChatClientAgent(chatClient);
this._agent = new ChatClientAgent(this._chatClient);
}
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsPersistentRunStreamingTests() : RunStreamingTests<AzureAIAgentsPersistentFixture>(() => new())
{
}
@@ -4,6 +4,6 @@ using AgentConformance.IntegrationTests;
namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsPersistentInvokeTests() : RunAsyncTests<AzureAIAgentsPersistentFixture>(() => new())
public class AzureAIAgentsPersistentRunTests() : RunTests<AzureAIAgentsPersistentFixture>(() => new())
{
}
@@ -16,7 +16,6 @@ public class AgentRunOptionsTests
// Arrange
var options = new AgentRunOptions
{
AdditionalInstructions = "Test instructions",
OnIntermediateMessages = msg => Task.CompletedTask
};
@@ -24,7 +23,6 @@ public class AgentRunOptionsTests
var clone = new AgentRunOptions(options);
// Assert
Assert.Equal(options.AdditionalInstructions, clone.AdditionalInstructions);
Assert.Equal(options.OnIntermediateMessages, clone.OnIntermediateMessages);
}
@@ -95,10 +95,10 @@ public class ChatClientAgentExtensionsTests
}
/// <summary>
/// Verify that RunAsync extension method with messages passes AgentRunOptions correctly.
/// Verify that RunAsync extension method with messages passes Instructions correctly.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesPassesAgentRunOptionsCorrectlyAsync()
public async Task RunAsyncWithMessagesPassesInstructionsCorrectlyAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
@@ -118,14 +118,13 @@ public class ChatClientAgentExtensionsTests
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "base instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
var runOptions = new AgentRunOptions { AdditionalInstructions = "additional instructions" };
var runOptions = new AgentRunOptions();
// Act
await ChatClientAgentExtensions.RunAsync(agent, messages, agentRunOptions: runOptions);
// Assert
Assert.Contains(capturedMessages, m => m.Text == "base instructions" && m.Role == ChatRole.System);
Assert.Contains(capturedMessages, m => m.Text == "additional instructions" && m.Role == ChatRole.System);
Assert.Contains(capturedMessages, m => m.Text == "test" && m.Role == ChatRole.User);
Assert.All(capturedChatOptions, Assert.Null);
}
@@ -349,14 +348,13 @@ public class ChatClientAgentExtensionsTests
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "base instructions" });
const string TestPrompt = "test prompt";
var runOptions = new AgentRunOptions { AdditionalInstructions = "additional instructions" };
var runOptions = new AgentRunOptions();
// Act
await ChatClientAgentExtensions.RunAsync(agent, TestPrompt, agentRunOptions: runOptions);
// Assert
Assert.Contains(capturedMessages, m => m.Text == "base instructions" && m.Role == ChatRole.System);
Assert.Contains(capturedMessages, m => m.Text == "additional instructions" && m.Role == ChatRole.System);
Assert.Contains(capturedMessages, m => m.Text == "test prompt" && m.Role == ChatRole.User);
}
@@ -18,7 +18,6 @@ public class ChatClientAgentRunOptionsTests
// Assert
Assert.Null(runOptions.OnIntermediateMessages);
Assert.Null(runOptions.AdditionalInstructions);
Assert.Null(runOptions.ChatOptions);
}
@@ -36,7 +35,6 @@ public class ChatClientAgentRunOptionsTests
// Assert
Assert.Null(runOptions.OnIntermediateMessages);
Assert.Null(runOptions.AdditionalInstructions);
Assert.Same(chatOptions, runOptions.ChatOptions);
}
@@ -49,7 +47,6 @@ public class ChatClientAgentRunOptionsTests
// Arrange
var sourceRunOptions = new AgentRunOptions
{
AdditionalInstructions = "additional instructions",
OnIntermediateMessages = messages => Task.CompletedTask
};
var chatOptions = new ChatOptions { MaxOutputTokens = 200 };
@@ -59,7 +56,6 @@ public class ChatClientAgentRunOptionsTests
// Assert
Assert.Same(sourceRunOptions.OnIntermediateMessages, runOptions.OnIntermediateMessages);
Assert.Equal("additional instructions", runOptions.AdditionalInstructions);
Assert.Same(chatOptions, runOptions.ChatOptions);
}
@@ -72,14 +68,14 @@ public class ChatClientAgentRunOptionsTests
// Arrange
var sourceRunOptions = new AgentRunOptions
{
AdditionalInstructions = "test instructions"
OnIntermediateMessages = messages => Task.CompletedTask
};
// Act
var runOptions = new ChatClientAgentRunOptions(sourceRunOptions, null);
// Assert
Assert.Equal("test instructions", runOptions.AdditionalInstructions);
Assert.Same(sourceRunOptions.OnIntermediateMessages, runOptions.OnIntermediateMessages);
Assert.Null(runOptions.ChatOptions);
}
@@ -156,10 +156,10 @@ public class ChatClientAgentTests
}
/// <summary>
/// Verify that RunAsync includes additional instructions when provided in options.
/// Verify that RunAsync includes base instructions in messages.
/// </summary>
[Fact]
public async Task RunAsyncIncludesAdditionalInstructionsWhenProvidedInOptionsAsync()
public async Task RunAsyncIncludesBaseInstructionsAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
@@ -174,14 +174,13 @@ public class ChatClientAgentTests
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "base instructions" });
var runOptions = new AgentRunOptions { AdditionalInstructions = "additional instructions" };
var runOptions = new AgentRunOptions();
// Act
await agent.RunAsync([new(ChatRole.User, "test")], options: runOptions);
// Assert
Assert.Contains(capturedMessages, m => m.Text == "base instructions" && m.Role == ChatRole.System);
Assert.Contains(capturedMessages, m => m.Text == "additional instructions" && m.Role == ChatRole.System);
Assert.Contains(capturedMessages, m => m.Text == "test" && m.Role == ChatRole.User);
}
@@ -759,18 +758,21 @@ public class ChatClientAgentTests
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
ModelId = "agent-model"
ModelId = "agent-model",
AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "agent-value" }
};
var requestChatOptions = new ChatOptions
{
MaxOutputTokens = 200,
Temperature = 0.3f
Temperature = 0.3f,
AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" }
// TopP and ModelId not set, should use agent values
};
var expectedChatOptionsMerge = new ChatOptions
{
MaxOutputTokens = 200, // Request value takes priority
Temperature = 0.3f, // Request value takes priority
AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" }, // Request value takes priority
TopP = 0.9f, // Agent value used when request doesn't specify
ModelId = "agent-model" // Agent value used when request doesn't specify
};
@@ -801,6 +803,8 @@ public class ChatClientAgentTests
Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the same instance (modified in place)
Assert.Equal(200, capturedChatOptions.MaxOutputTokens); // Request value takes priority
Assert.Equal(0.3f, capturedChatOptions.Temperature); // Request value takes priority
Assert.NotNull(capturedChatOptions.AdditionalProperties);
Assert.Equal("request-value", capturedChatOptions.AdditionalProperties["key"]); // Request value takes priority
Assert.Equal(0.9f, capturedChatOptions.TopP); // Agent value used when request doesn't specify
Assert.Equal("agent-model", capturedChatOptions.ModelId); // Agent value used when request doesn't specify
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
public class OpenAIAssistantChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<OpenAIAssistantFixture>(() => new())
{
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
public class OpenAIAssistantChatClientAgentRunTests() : ChatClientAgentRunTests<OpenAIAssistantFixture>(() => new())
{
}
@@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using AgentConformanceTests;
using Microsoft.Agents;
using Microsoft.Extensions.AI;
using OpenAI;
@@ -15,8 +15,10 @@ namespace OpenAIAssistant.IntegrationTests;
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public class OpenAIAssistantFixture : AgentFixture
public class OpenAIAssistantFixture : IChatClientAgentFixture
{
private static readonly OpenAIConfiguration s_config = TestConfiguration.LoadSection<OpenAIConfiguration>();
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private AssistantClient? _assistantClient;
private Assistant? _assistant;
@@ -24,9 +26,11 @@ public class OpenAIAssistantFixture : AgentFixture
private Agent _agent;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public override Agent Agent => this._agent;
public Agent Agent => this._agent;
public override async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
public IChatClient ChatClient => this._chatClient;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
{
if (thread is not ChatClientAgentThread chatClientThread)
{
@@ -49,7 +53,26 @@ public class OpenAIAssistantFixture : AgentFixture
return messages;
}
public override Task DeleteThreadAsync(AgentThread thread)
public async Task<ChatClientAgent> CreateAgentWithInstructionsAsync(string instructions)
{
var assistant =
await this._assistantClient!.CreateAssistantAsync(
s_config.ChatModelId!,
new AssistantCreationOptions()
{
Name = "HelpfulAssistant",
Instructions = instructions
});
return new ChatClientAgent(this._assistantClient.AsIChatClient(assistant.Value.Id), new() { Id = assistant.Value.Id });
}
public Task DeleteAgentAsync(ChatClientAgent agent)
{
return this._assistantClient!.DeleteAssistantAsync(agent.Id);
}
public Task DeleteThreadAsync(AgentThread thread)
{
if (thread?.Id is not null)
{
@@ -59,16 +82,14 @@ public class OpenAIAssistantFixture : AgentFixture
return Task.CompletedTask;
}
public override async Task InitializeAsync()
public async Task InitializeAsync()
{
var config = TestConfiguration.LoadSection<OpenAIConfiguration>();
var client = new OpenAIClient(config.ApiKey);
var client = new OpenAIClient(s_config.ApiKey);
this._assistantClient = client.GetAssistantClient();
this._assistant =
await this._assistantClient.CreateAssistantAsync(
config.ChatModelId!,
s_config.ChatModelId!,
new AssistantCreationOptions()
{
Name = "HelpfulAssistant",
@@ -80,7 +101,7 @@ public class OpenAIAssistantFixture : AgentFixture
this._agent = new ChatClientAgent(this._chatClient);
}
public override Task DisposeAsync()
public Task DisposeAsync()
{
if (this._assistantClient is not null && this._assistant is not null)
{
@@ -4,6 +4,6 @@ using AgentConformance.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
public class OpenAIAssistantInvokeTests() : RunAsyncTests<OpenAIAssistantFixture>(() => new())
public class OpenAIAssistantIRunTests() : RunTests<OpenAIAssistantFixture>(() => new())
{
}
@@ -4,6 +4,6 @@ using AgentConformance.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
public class OpenAIAssistantInvokeStreamingTests() : RunStreamingAsyncTests<OpenAIAssistantFixture>(() => new())
public class OpenAIAssistantRunStreamingTests() : RunStreamingTests<OpenAIAssistantFixture>(() => new())
{
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIChatCompletion.IntegrationTests;
public class OpenAIChatCompletionChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<OpenAIChatCompletionFixture>(() => new())
{
}
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIChatCompletion.IntegrationTests;
public class OpenAIChatCompletionChatClientAgentRunTests() : ChatClientAgentRunTests<OpenAIChatCompletionFixture>(() => new())
{
}
@@ -4,8 +4,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using AgentConformanceTests;
using Microsoft.Agents;
using Microsoft.Extensions.AI;
using OpenAI;
@@ -13,16 +13,20 @@ using Shared.IntegrationTests;
namespace OpenAIChatCompletion.IntegrationTests;
public class OpenAIChatCompletionFixture : AgentFixture
public class OpenAIChatCompletionFixture : IChatClientAgentFixture
{
private static readonly OpenAIConfiguration s_config = TestConfiguration.LoadSection<OpenAIConfiguration>();
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private IChatClient _chatClient;
private Agent _agent;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public override Agent Agent => this._agent;
public Agent Agent => this._agent;
public override async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
public IChatClient ChatClient => this._chatClient;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
{
if (thread is not ChatClientAgentThread chatClientThread)
{
@@ -32,18 +36,35 @@ public class OpenAIChatCompletionFixture : AgentFixture
return await chatClientThread.GetMessagesAsync().ToListAsync();
}
public override Task DeleteThreadAsync(AgentThread thread)
public Task<ChatClientAgent> CreateAgentWithInstructionsAsync(string instructions)
{
this._chatClient = new OpenAIClient(s_config.ApiKey)
.GetChatClient(s_config.ChatModelId)
.AsIChatClient();
return Task.FromResult(new ChatClientAgent(this._chatClient, new()
{
Name = "HelpfulAssistant",
Instructions = instructions,
}));
}
public Task DeleteAgentAsync(ChatClientAgent agent)
{
// Chat Completion does not require/support deleting agents, so this is a no-op.
return Task.CompletedTask;
}
public Task DeleteThreadAsync(AgentThread thread)
{
// Chat Completion does not require/support deleting threads, so this is a no-op.
return Task.CompletedTask;
}
public override Task InitializeAsync()
public Task InitializeAsync()
{
var config = TestConfiguration.LoadSection<OpenAIConfiguration>();
this._chatClient = new OpenAIClient(config.ApiKey)
.GetChatClient(config.ChatModelId)
this._chatClient = new OpenAIClient(s_config.ApiKey)
.GetChatClient(s_config.ChatModelId)
.AsIChatClient();
this._agent =
@@ -56,7 +77,7 @@ public class OpenAIChatCompletionFixture : AgentFixture
return Task.CompletedTask;
}
public override Task DisposeAsync()
public Task DisposeAsync()
{
this._chatClient.Dispose();
return Task.CompletedTask;
@@ -4,6 +4,6 @@ using AgentConformance.IntegrationTests;
namespace OpenAIChatCompletion.IntegrationTests;
public class OpenAIChatCompletionInvokeStreamingTests() : RunStreamingAsyncTests<OpenAIChatCompletionFixture>(() => new())
public class OpenAIChatCompletionRunStreamingTests() : RunStreamingTests<OpenAIChatCompletionFixture>(() => new())
{
}
@@ -4,6 +4,6 @@ using AgentConformance.IntegrationTests;
namespace OpenAIChatCompletion.IntegrationTests;
public class OpenAIChatCompletionInvokeTests() : RunAsyncTests<OpenAIChatCompletionFixture>(() => new())
public class OpenAIChatCompletionRunTests() : RunTests<OpenAIChatCompletionFixture>(() => new())
{
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseStoreTrueChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<OpenAIResponseFixture>(() => new(store: true))
{
}
public class OpenAIResponseStoreFalseChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests<OpenAIResponseFixture>(() => new(store: false))
{
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseStoreTrueChatClientAgentRunTests() : ChatClientAgentRunTests<OpenAIResponseFixture>(() => new(store: true))
{
}
public class OpenAIResponseStoreFalseChatClientAgentRunTests() : ChatClientAgentRunTests<OpenAIResponseFixture>(() => new(store: false))
{
}
@@ -4,8 +4,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests;
using AgentConformance.IntegrationTests.Support;
using AgentConformanceTests;
using Microsoft.Agents;
using Microsoft.Extensions.AI;
using OpenAI;
@@ -14,17 +14,21 @@ using Shared.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseFixture(bool store) : AgentFixture
public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
{
private static readonly OpenAIConfiguration s_config = TestConfiguration.LoadSection<OpenAIConfiguration>();
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private OpenAIResponseClient _openAIResponseClient;
private IChatClient _chatClient;
private Agent _agent;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public override Agent Agent => this._agent;
public Agent Agent => this._agent;
public override async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
public IChatClient ChatClient => this._chatClient;
public async Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
{
if (thread is not ChatClientAgentThread chatClientThread)
{
@@ -68,18 +72,37 @@ public class OpenAIResponseFixture(bool store) : AgentFixture
throw new NotSupportedException("This test currently only supports text messages");
}
public override Task DeleteThreadAsync(AgentThread thread)
public Task<ChatClientAgent> CreateAgentWithInstructionsAsync(string instructions)
{
var options = new ChatClientAgentOptions
{
Name = "HelpfulAssistant",
Instructions = instructions,
ChatOptions = new ChatOptions
{
RawRepresentationFactory = new Func<IChatClient, object>((_) => new ResponseCreationOptions() { StoredOutputEnabled = store })
},
};
return Task.FromResult(new ChatClientAgent(this._chatClient, options));
}
public Task DeleteAgentAsync(ChatClientAgent agent)
{
// Chat Completion does not require/support deleting agents, so this is a no-op.
return Task.CompletedTask;
}
public Task DeleteThreadAsync(AgentThread thread)
{
// Chat Completion does not require/support deleting threads, so this is a no-op.
return Task.CompletedTask;
}
public override Task InitializeAsync()
public Task InitializeAsync()
{
var config = TestConfiguration.LoadSection<OpenAIConfiguration>();
this._openAIResponseClient = new OpenAIClient(config.ApiKey)
.GetOpenAIResponseClient(config.ChatModelId);
this._openAIResponseClient = new OpenAIClient(s_config.ApiKey)
.GetOpenAIResponseClient(s_config.ChatModelId);
this._chatClient = this._openAIResponseClient
.AsIChatClient();
@@ -99,7 +122,7 @@ public class OpenAIResponseFixture(bool store) : AgentFixture
return Task.CompletedTask;
}
public override Task DisposeAsync()
public Task DisposeAsync()
{
this._chatClient.Dispose();
return Task.CompletedTask;
@@ -1,13 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseStoreTrueInvokeStreamingTests() : RunStreamingAsyncTests<OpenAIResponseFixture>(() => new(store: true))
{
}
public class OpenAIResponseStoreFalseInvokeStreamingTests() : RunStreamingAsyncTests<OpenAIResponseFixture>(() => new(store: false))
{
}
@@ -1,13 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseStoreTrueInvokeTests() : RunAsyncTests<OpenAIResponseFixture>(() => new(store: true))
{
}
public class OpenAIResponseStoreFalseInvokeTests() : RunAsyncTests<OpenAIResponseFixture>(() => new(store: false))
{
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseStoreTrueRunStreamingTests() : RunStreamingTests<OpenAIResponseFixture>(() => new(store: true))
{
}
public class OpenAIResponseStoreFalseRunStreamingTests() : RunStreamingTests<OpenAIResponseFixture>(() => new(store: false))
{
}
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentConformance.IntegrationTests;
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseStoreTrueRunTests() : RunTests<OpenAIResponseFixture>(() => new(store: true))
{
}
public class OpenAIResponseStoreFalseRunTests() : RunTests<OpenAIResponseFixture>(() => new(store: false))
{
}