.NET: Updates to Foundry Agents Package (#2125)

* Remove the conversation creation always

* Update unit tests + address IL + refactor

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Internalize unused methods

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Roger Barreto
2025-11-12 13:32:42 +00:00
committed by GitHub
Unverified
parent ec86cb56b5
commit 9687e6e6a5
12 changed files with 533 additions and 182 deletions
@@ -564,7 +564,7 @@ public static class AgentClientExtensions
/// </summary>
private static AgentVersion CreateAgentVersionWithProtocol(AgentClient agentClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
{
using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions));
using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions, ModelReaderWriterOptions.Json, AzureAIAgentsContext.Default));
ClientResult protocolResponse = agentClient.CreateAgentVersion(agentName, protocolRequest, cancellationToken.ToRequestOptions(false));
return ClientResult.FromValue((AgentVersion)protocolResponse, protocolResponse.GetRawResponse()).Value;
}
@@ -574,7 +574,7 @@ public static class AgentClientExtensions
/// </summary>
private static async Task<AgentVersion> CreateAgentVersionWithProtocolAsync(AgentClient agentClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
{
using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions));
using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions, ModelReaderWriterOptions.Json, AzureAIAgentsContext.Default));
ClientResult protocolResponse = await agentClient.CreateAgentVersionAsync(agentName, protocolRequest, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
return ClientResult.FromValue((AgentVersion)protocolResponse, protocolResponse.GetRawResponse()).Value;
}
@@ -72,42 +72,41 @@ internal sealed class AzureAIAgentChatClient : DelegatingChatClient
/// <inheritdoc/>
public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
var conversationId = await this.GetOrCreateConversationAsync(options, cancellationToken).ConfigureAwait(false);
var conversationChatOptions = this.GetConversationEnabledChatOptions(options, conversationId);
var agentOptions = this.GetAgentEnabledChatOptions(options);
return await base.GetResponseAsync(messages, conversationChatOptions, cancellationToken).ConfigureAwait(false);
return await base.GetResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async override IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var conversation = await this.GetOrCreateConversationAsync(options, cancellationToken).ConfigureAwait(false);
var conversationOptions = this.GetConversationEnabledChatOptions(options, conversation);
var agentOptions = this.GetAgentEnabledChatOptions(options);
await foreach (var chunk in base.GetStreamingResponseAsync(messages, conversationOptions, cancellationToken).ConfigureAwait(false))
await foreach (var chunk in base.GetStreamingResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false))
{
yield return chunk;
}
}
private async Task<string> GetOrCreateConversationAsync(ChatOptions? options, CancellationToken cancellationToken)
=> string.IsNullOrWhiteSpace(options?.ConversationId)
? (await this._agentClient.GetConversationClient().CreateConversationAsync(cancellationToken: cancellationToken).ConfigureAwait(false)).Value.Id
: options.ConversationId;
private ChatOptions GetConversationEnabledChatOptions(ChatOptions? chatOptions, string conversationId)
private ChatOptions GetAgentEnabledChatOptions(ChatOptions? options)
{
// Start with a clone of the base chat options defined for the agent, if any.
ChatOptions conversationChatOptions = this._chatOptions?.Clone() ?? new();
ChatOptions agentEnabledChatOptions = this._chatOptions?.Clone() ?? new();
// Ignore per-request all options that can't be overridden.
conversationChatOptions.Instructions = null;
conversationChatOptions.Tools = null;
agentEnabledChatOptions.Instructions = null;
agentEnabledChatOptions.Tools = null;
agentEnabledChatOptions.Temperature = null;
agentEnabledChatOptions.TopP = null;
agentEnabledChatOptions.PresencePenalty = null;
// Use the conversation from the request, or the one defined at the client level.
agentEnabledChatOptions.ConversationId = options?.ConversationId ?? this._chatOptions?.ConversationId;
// Preserve the original RawRepresentationFactory
var originalFactory = chatOptions?.RawRepresentationFactory;
var originalFactory = options?.RawRepresentationFactory;
conversationChatOptions.RawRepresentationFactory = (client) =>
agentEnabledChatOptions.RawRepresentationFactory = (client) =>
{
if (originalFactory?.Invoke(this) is not ResponseCreationOptions responseCreationOptions)
{
@@ -115,12 +114,11 @@ internal sealed class AzureAIAgentChatClient : DelegatingChatClient
}
SetAgentReference(responseCreationOptions, this._agentVersion);
SetConversationReference(responseCreationOptions, conversationId);
return responseCreationOptions;
};
return conversationChatOptions;
return agentEnabledChatOptions;
}
// Since the SetAdditionalProperty/SetAgentReference/SetConversationReference extensions in Azure.AI.Agents does not yet support the recent updates in OpenAI 2.6.0
@@ -139,10 +137,5 @@ internal sealed class AzureAIAgentChatClient : DelegatingChatClient
SetAdditionalProperty(responseCreationOptions, "agent", ModelReaderWriter.Write(agentReference, new ModelReaderWriterOptions("W"), AzureAIAgentsContext.Default));
responseCreationOptions.Patch.Remove([.. "$."u8, .. Encoding.UTF8.GetBytes("model")]);
}
private static void SetConversationReference(ResponseCreationOptions responseCreationOptions, string conversationId)
{
SetAdditionalProperty(responseCreationOptions, "conversation", BinaryData.FromString($"\"{conversationId}\""));
}
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}
@@ -110,7 +110,7 @@ public abstract class StatefulExecutor<TState> : Executor
{
if (!skipCache && !context.ConcurrentRunsEnabled)
{
TState newState = await invocation(this._stateCache ?? (this._initialStateFactory()),
TState newState = await invocation(this._stateCache ?? this._initialStateFactory(),
context,
cancellationToken).ConfigureAwait(false)
?? this._initialStateFactory();
@@ -992,7 +992,7 @@ public sealed class AgentClientExtensionsTests
Assert.Contains("required_tool", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentVersionTestJsonObject, Encoding.UTF8, "application/json") };
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
@@ -1034,7 +1034,7 @@ public sealed class AgentClientExtensionsTests
Assert.Contains("required_tool", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentVersionTestJsonObject, Encoding.UTF8, "application/json") };
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
@@ -1788,7 +1788,7 @@ public sealed class AgentClientExtensionsTests
mockAgentClient
.Setup(x => x.GetAgent(It.IsAny<string>(), It.IsAny<RequestOptions>()))
.Callback<string, RequestOptions>((name, options) => capturedRequestOptions = options)
.Returns(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentTestJsonObject))));
.Returns(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(TestDataUtil.GetAgentResponseJson()))));
mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny<OpenAIClientOptions?>()))
.Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
@@ -1813,7 +1813,7 @@ public sealed class AgentClientExtensionsTests
mockAgentClient
.Setup(x => x.GetAgentAsync(It.IsAny<string>(), It.IsAny<RequestOptions>()))
.Callback<string, RequestOptions>((name, options) => capturedRequestOptions = options)
.Returns(Task.FromResult(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentTestJsonObject)))));
.Returns(Task.FromResult(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(TestDataUtil.GetAgentResponseJson())))));
mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny<OpenAIClientOptions?>()))
.Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
@@ -1838,7 +1838,7 @@ public sealed class AgentClientExtensionsTests
mockAgentClient
.Setup(x => x.CreateAgentVersion(It.IsAny<string>(), It.IsAny<BinaryContent>(), It.IsAny<RequestOptions>()))
.Callback<string, BinaryContent, RequestOptions>((name, content, options) => capturedRequestOptions = options)
.Returns(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentVersionTestJsonObject))));
.Returns(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))));
mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny<OpenAIClientOptions?>()))
.Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
@@ -1865,7 +1865,7 @@ public sealed class AgentClientExtensionsTests
mockAgentClient
.Setup(x => x.CreateAgentVersionAsync(It.IsAny<string>(), It.IsAny<BinaryContent>(), It.IsAny<RequestOptions>()))
.Callback<string, BinaryContent, RequestOptions>((name, content, options) => capturedRequestOptions = options)
.Returns(Task.FromResult(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentVersionTestJsonObject)))));
.Returns(Task.FromResult(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson())))));
mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny<OpenAIClientOptions?>()))
.Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
@@ -1891,7 +1891,7 @@ public sealed class AgentClientExtensionsTests
Assert.Equal("POST", request.Method.Method);
Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentTestJsonObject, Encoding.UTF8, "application/json") };
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
@@ -1923,7 +1923,7 @@ public sealed class AgentClientExtensionsTests
Assert.Equal("GET", request.Method.Method);
Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentTestJsonObject, Encoding.UTF8, "application/json") };
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
@@ -1959,50 +1959,9 @@ public sealed class AgentClientExtensionsTests
/// </summary>
private AgentRecord CreateTestAgentRecord()
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(AgentTestJsonObject))!;
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJson()))!;
}
private const string AgentDefinitionPlaceholder = """
{
"kind": "prompt",
"model": "gpt-5-mini",
"instructions": "You are a storytelling agent. You craft engaging one-line stories based on user prompts and context.",
"tools": []
}
""";
private const string AgentTestJsonObject = $$"""
{
"object": "agent",
"id": "agent_abc123",
"name": "agent_abc123",
"versions": {
"latest": {
"metadata": {},
"object": "agent.version",
"id": "agent_abc123:1",
"name": "agent_abc123",
"version": "1",
"description": "",
"created_at": 1761771936,
"definition": {{AgentDefinitionPlaceholder}}
}
}
}
""";
private const string AgentVersionTestJsonObject = $$"""
{
"object": "agent.version",
"id": "agent_abc123:1",
"name": "agent_abc123",
"version": "1",
"description": "",
"created_at": 1761771936,
"definition": {{AgentDefinitionPlaceholder}}
}
""";
private const string OpenAPISpec = """
{
"openapi": "3.0.3",
@@ -2038,7 +1997,7 @@ public sealed class AgentClientExtensionsTests
/// </summary>
private AgentVersion CreateTestAgentVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(AgentVersionTestJsonObject))!;
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
}
/// <summary>
@@ -2066,88 +2025,50 @@ public sealed class AgentClientExtensionsTests
public override ClientResult GetAgent(string agentName, RequestOptions options)
{
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject))));
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentRecord> GetAgent(string agentName, CancellationToken cancellationToken = default)
{
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200));
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> GetAgentAsync(string agentName, RequestOptions options)
{
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))));
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentRecord>> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
{
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200)));
var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null)
{
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject))));
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200));
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null)
{
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))));
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200)));
}
private static string TryApplyAgentDefinition(string json, AgentDefinition? definition)
{
if (definition is not null)
{
json = json.Replace(AgentDefinitionPlaceholder, ModelReaderWriter.Write(definition).ToString());
}
return json;
}
private static string TryApplyAgentName(string json, string? agentName)
{
if (!string.IsNullOrEmpty(agentName))
{
return json.Replace("\"agent_abc123\"", $"\"{agentName}\"");
}
return json;
}
private static string TryApplyInstructions(string json, string? instructions)
{
if (!string.IsNullOrEmpty(instructions))
{
return json.Replace("You are a storytelling agent. You craft engaging one-line stories based on user prompts and context.", instructions);
}
return json;
}
private static string TryApplyDescription(string json, string? description)
{
if (!string.IsNullOrEmpty(description))
{
return json.Replace("\"description\": \"\"", $"\"description\": \"{description}\"");
}
return json;
}
private string ApplyResponseChanges(string json)
{
var modifiedJson = TryApplyAgentName(json, this._agentName);
modifiedJson = TryApplyAgentDefinition(modifiedJson, this._agentDefinition);
modifiedJson = TryApplyInstructions(modifiedJson, this._instructions);
modifiedJson = TryApplyDescription(modifiedJson, this._description);
return modifiedJson;
var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description);
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
}
@@ -2247,58 +2168,8 @@ public sealed class AgentClientExtensionsTests
}
}
private sealed class FakeAuthenticationTokenProvider : AuthenticationTokenProvider
{
public override GetTokenOptions? CreateTokenOptions(IReadOnlyDictionary<string, object> properties)
{
return new GetTokenOptions(new Dictionary<string, object>());
}
public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken)
{
return new AuthenticationToken("token-value", "token-type", DateTimeOffset.UtcNow.AddHours(1));
}
public override ValueTask<AuthenticationToken> GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken)
{
return new ValueTask<AuthenticationToken>(this.GetToken(options, cancellationToken));
}
}
#endregion
private sealed class HttpHandlerAssert : HttpClientHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage>? _assertion;
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>>? _assertionAsync;
public HttpHandlerAssert(Func<HttpRequestMessage, HttpResponseMessage> assertion)
{
this._assertion = assertion;
}
public HttpHandlerAssert(Func<HttpRequestMessage, Task<HttpResponseMessage>> assertionAsync)
{
this._assertionAsync = assertionAsync;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (this._assertionAsync is not null)
{
return await this._assertionAsync.Invoke(request);
}
return this._assertion!.Invoke(request);
}
#if NET
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
return this._assertion!(request);
}
#endif
}
/// <summary>
/// Helper method to access internal ChatOptions property via reflection.
/// </summary>
@@ -0,0 +1,212 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel.Primitives;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Azure.AI.Agents;
namespace Microsoft.Agents.AI.AzureAI.UnitTests;
public class AzureAIChatClientTests
{
/// <summary>
/// Verify that when the ChatOptions has a "conv_" prefixed conversation ID, the chat client uses conversation in the http requests via the chat client
/// </summary>
[Fact]
public async Task ChatClient_UsesDefaultConversationIdAsync()
{
// Arrange
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
// Assert
if (request.Content is not null)
{
var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("conv_12345", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") };
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
var agent = await client.GetAIAgentAsync(
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
ChatOptions = new() { ConversationId = "conv_12345" }
}, openAIClientOptions: new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act
var thread = agent.GetNewThread();
await agent.RunAsync("Hello", thread);
Assert.True(requestTriggered);
var chatClientThread = Assert.IsType<ChatClientAgentThread>(thread);
Assert.Equal("conv_12345", chatClientThread.ConversationId);
}
/// <summary>
/// Verify that when the chat client doesn't have a default "conv_" conversation id, the chat client still uses the conversation ID in HTTP requests.
/// </summary>
[Fact]
public async Task ChatClient_UsesPerRequestConversationId_WhenNoDefaultConversationIdIsProvidedAsync()
{
// Arrange
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
// Assert
if (request.Content is not null)
{
var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("conv_12345", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") };
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
var agent = await client.GetAIAgentAsync(
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
}, openAIClientOptions: new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act
var thread = agent.GetNewThread();
await agent.RunAsync("Hello", thread, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } });
Assert.True(requestTriggered);
var chatClientThread = Assert.IsType<ChatClientAgentThread>(thread);
Assert.Equal("conv_12345", chatClientThread.ConversationId);
}
/// <summary>
/// Verify that even when the chat client has a default conversation id, the chat client will prioritize the per-request conversation id provided in HTTP requests.
/// </summary>
[Fact]
public async Task ChatClient_UsesPerRequestConversationId_EvenWhenDefaultConversationIdIsProvidedAsync()
{
// Arrange
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
// Assert
if (request.Content is not null)
{
var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("conv_12345", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") };
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
var agent = await client.GetAIAgentAsync(
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
ChatOptions = new() { ConversationId = "conv_should_not_use_default" }
}, openAIClientOptions: new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act
var thread = agent.GetNewThread();
await agent.RunAsync("Hello", thread, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } });
Assert.True(requestTriggered);
var chatClientThread = Assert.IsType<ChatClientAgentThread>(thread);
Assert.Equal("conv_12345", chatClientThread.ConversationId);
}
/// <summary>
/// Verify that when the chat client is provided without a "conv_" prefixed conversation ID, the chat client uses the previous conversation ID in HTTP requests.
/// </summary>
[Fact]
public async Task ChatClient_UsesPreviousResponseId_WhenConversationIsNotPrefixedAsConvAsync()
{
// Arrange
var requestTriggered = false;
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.RequestUri!.PathAndQuery.Contains("openai/responses"))
{
requestTriggered = true;
// Assert
if (request.Content is not null)
{
var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("resp_0888a", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") };
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
var agent = await client.GetAIAgentAsync(
new ChatClientAgentOptions
{
Name = "test-agent",
Instructions = "Test instructions",
}, openAIClientOptions: new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act
var thread = agent.GetNewThread();
await agent.RunAsync("Hello", thread, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "resp_0888a" } });
Assert.True(requestTriggered);
var chatClientThread = Assert.IsType<ChatClientAgentThread>(thread);
Assert.Equal("resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7", chatClientThread.ConversationId);
}
}
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.AzureAI.UnitTests;
internal sealed class FakeAuthenticationTokenProvider : AuthenticationTokenProvider
{
public override GetTokenOptions? CreateTokenOptions(IReadOnlyDictionary<string, object> properties)
{
return new GetTokenOptions(new Dictionary<string, object>());
}
public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken)
{
return new AuthenticationToken("token-value", "token-type", DateTimeOffset.UtcNow.AddHours(1));
}
public override ValueTask<AuthenticationToken> GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken)
{
return new ValueTask<AuthenticationToken>(this.GetToken(options, cancellationToken));
}
}
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.AzureAI.UnitTests;
internal sealed class HttpHandlerAssert : HttpClientHandler
{
private readonly Func<HttpRequestMessage, HttpResponseMessage>? _assertion;
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>>? _assertionAsync;
public HttpHandlerAssert(Func<HttpRequestMessage, HttpResponseMessage> assertion)
{
this._assertion = assertion;
}
public HttpHandlerAssert(Func<HttpRequestMessage, Task<HttpResponseMessage>> assertionAsync)
{
this._assertionAsync = assertionAsync;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (this._assertionAsync is not null)
{
return await this._assertionAsync.Invoke(request);
}
return this._assertion!.Invoke(request);
}
#if NET
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
return this._assertion!(request);
}
#endif
}
@@ -8,4 +8,16 @@
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="TestData\AgentResponse.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\AgentVersionResponse.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\OpenAIDefaultResponse.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,17 @@
{
"object": "agent",
"id": "agent_abc123",
"name": "agent_abc123",
"versions": {
"latest": {
"metadata": {},
"object": "agent.version",
"id": "agent_abc123:1",
"name": "agent_abc123",
"version": "1",
"description": "",
"created_at": 1761771936,
"definition": "agent-definition-placeholder"
}
}
}
@@ -0,0 +1,9 @@
{
"object": "agent.version",
"id": "agent_abc123:1",
"name": "agent_abc123",
"version": "1",
"description": "",
"created_at": 1761771936,
"definition": "agent-definition-placeholder"
}
@@ -0,0 +1,68 @@
{
"id": "resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7",
"object": "response",
"created_at": 1762941294,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0888a46cbf2b1ff3006914596f814481958e8cf500a6dabbec",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "Hello! How can I assist you today?"
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 9,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 10,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 19
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel.Primitives;
using System.IO;
using Azure.AI.Agents;
namespace Microsoft.Agents.AI.AzureAI.UnitTests;
/// <summary>
/// Utility class for loading and processing test data files.
/// </summary>
internal static class TestDataUtil
{
private static readonly string s_agentResponseJson = File.ReadAllText("TestData/AgentResponse.json");
private static readonly string s_agentVersionResponseJson = File.ReadAllText("TestData/AgentVersionResponse.json");
private static readonly string s_openAIDefaultResponseJson = File.ReadAllText("TestData/OpenAIDefaultResponse.json");
private const string AgentDefinitionPlaceholder = "\"agent-definition-placeholder\"";
private const string DefaultAgentDefinition = """
{
"kind": "prompt",
"model": "gpt-5-mini",
"instructions": "You are a storytelling agent. You craft engaging one-line stories based on user prompts and context.",
"tools": []
}
""";
/// <summary>
/// Gets the agent response JSON with optional placeholder replacements applied.
/// </summary>
public static string GetAgentResponseJson(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
return json;
}
/// <summary>
/// Gets the agent version response JSON with optional placeholder replacements applied.
/// </summary>
public static string GetAgentVersionResponseJson(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_agentVersionResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
return json;
}
/// <summary>
/// Gets the OpenAI default response JSON with optional placeholder replacements applied.
/// </summary>
public static string GetOpenAIDefaultResponseJson(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null)
{
var json = s_openAIDefaultResponseJson;
json = ApplyAgentName(json, agentName);
json = ApplyAgentDefinition(json, agentDefinition);
json = ApplyInstructions(json, instructions);
json = ApplyDescription(json, description);
return json;
}
private static string ApplyAgentName(string json, string? agentName)
{
if (!string.IsNullOrEmpty(agentName))
{
return json.Replace("\"agent_abc123\"", $"\"{agentName}\"");
}
return json;
}
private static string ApplyAgentDefinition(string json, AgentDefinition? definition)
{
return (definition is not null)
? json.Replace(AgentDefinitionPlaceholder, ModelReaderWriter.Write(definition).ToString())
: json.Replace(AgentDefinitionPlaceholder, DefaultAgentDefinition);
}
private static string ApplyInstructions(string json, string? instructions)
{
if (!string.IsNullOrEmpty(instructions))
{
return json.Replace("You are a storytelling agent. You craft engaging one-line stories based on user prompts and context.", instructions);
}
return json;
}
private static string ApplyDescription(string json, string? description)
{
if (!string.IsNullOrEmpty(description))
{
return json.Replace("\"description\": \"\"", $"\"description\": \"{description}\"");
}
return json;
}
}