Add some OpenAI and Foundry extension methods (#225)

* Add some OpenAI specific extensions

* Update samples and extension methods

* Apply suggestion from @Copilot

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

* Apply suggestion from @Copilot

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

* Add extension methods for creating agents using the Assistant API

* Add orchestration sample

* Add orchestration sample

* Sample for the Foundry alignment document

* Address code review feedback

* Rename provider samples

* Sample showing how to get an AI agent for Foundry SDK

* Add OpenAI chat completion based implementation of AIAgent

* Split OpenAI client extension methods by client type

* Remove OpenAIClient extension methods

* Rename AsRunnableAgent

* Fix XML comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Mark Wallace
2025-07-31 19:08:17 +01:00
committed by GitHub
Unverified
parent 42c7a59640
commit 624709e5d1
22 changed files with 1330 additions and 160 deletions
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Samples;
using OpenAI;
using OpenAI.Chat;
namespace Custom;
/// <summary>
/// End-to-end sample showing how to use a custom <see cref="OpenAIChatClientAgent"/>.
/// </summary>
public sealed class Custom_OpenAIChatClientAgent(ITestOutputHelper output) : AgentSample(output)
{
/// <summary>
/// This will create an instance of <see cref="MyOpenAIChatClientAgent"/> and run it.
/// </summary>
[Fact]
public async Task RunCustomChatClientAgent()
{
var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey).GetChatClient(TestConfiguration.OpenAI.ChatModelId);
var agent = new MyOpenAIChatClientAgent(chatClient);
var chatMessage = new UserChatMessage("Tell me a joke about a pirate.");
var chatCompletion = await agent.RunAsync(chatMessage);
Console.WriteLine(chatCompletion.Content.Last().Text);
}
}
public class MyOpenAIChatClientAgent : OpenAIChatClientAgent
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
public MyOpenAIChatClientAgent(ChatClient client, ILoggerFactory? loggerFactory = null) :
base(client, instructions: JokerInstructions, name: JokerName, loggerFactory: loggerFactory)
{
}
}
@@ -4,7 +4,7 @@
<RootNamespace>GettingStarted</RootNamespace>
<OutputType>Library</OutputType>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CA1707;CA1716;IDE0009;IDE1006;OPENAI001;</NoWarn>
<NoWarn>$(NoWarn);CA1707;CA1716;IDE0009;IDE1006; OPENAI001;</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedSamples>true</InjectSharedSamples>
<InjectSharedThrow>true</InjectSharedThrow>
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.Orchestration;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="SequentialOrchestration"/> for
/// executing multiple Foundry agents in sequence.
/// </summary>
public class SequentialOrchestration_Foundry_Agents(ITestOutputHelper output) : OrchestrationSample(output)
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task RunOrchestrationAsync(bool streamedResponse)
{
// Get a client to create server side agents with.
var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
var model = TestConfiguration.OpenAI.ChatModelId;
// Define the agents
AIAgent analystAgent =
await persistentAgentsClient.CreateAIAgentAsync(
model,
name: "Analyst",
instructions:
"""
You are a marketing analyst. Given a product description, identify:
- Key features
- Target audience
- Unique selling points
""",
description: "A agent that extracts key concepts from a product description.");
AIAgent writerAgent =
await persistentAgentsClient.CreateAIAgentAsync(
model,
name: "copywriter",
instructions:
"""
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
compose a compelling marketing copy (like a newsletter section) that highlights these points.
Output should be short (around 150 words), output just the copy as a single text block.
""",
description: "An agent that writes a marketing copy based on the extracted concepts.");
AIAgent editorAgent =
await persistentAgentsClient.CreateAIAgentAsync(
model,
name: "editor",
instructions:
"""
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
give format and make it polished. Output the final improved copy as a single text block.
""",
description: "An agent that formats and proofreads the marketing copy.");
// Create a monitor to capturing agent responses (via ResponseCallback)
// to display at the end of this sample. (optional)
// NOTE: Create your own callback to capture responses in your application or service.
OrchestrationMonitor monitor = new();
// Define the orchestration
SequentialOrchestration orchestration =
new(analystAgent, writerAgent, editorAgent)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
};
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
// Cleanup
await persistentAgentsClient.Administration.DeleteAgentAsync(editorAgent.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(writerAgent.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(analystAgent.Id);
}
}
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAI;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="SequentialOrchestration"/> for
/// executing multiple heterogeneous agents in sequence.
/// </summary>
public class SequentialOrchestration_Multi_Agent(ITestOutputHelper output) : OrchestrationSample(output)
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task RunOrchestrationAsync(bool streamedResponse)
{
var openAIClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey);
var model = TestConfiguration.OpenAI.ChatModelId;
// Define the agents
AIAgent analystAgent =
openAIClient.GetChatClient(model).CreateAIAgent(
name: "Analyst",
instructions:
"""
You are a marketing analyst. Given a product description, identify:
- Key features
- Target audience
- Unique selling points
""",
description: "A agent that extracts key concepts from a product description.");
AIAgent writerAgent =
openAIClient.GetOpenAIResponseClient(model).CreateAIAgent(
name: "copywriter",
instructions:
"""
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
compose a compelling marketing copy (like a newsletter section) that highlights these points.
Output should be short (around 150 words), output just the copy as a single text block.
""",
description: "An agent that writes a marketing copy based on the extracted concepts.");
AIAgent editorAgent =
openAIClient.GetAssistantClient().CreateAIAgent(
model,
name: "editor",
instructions:
"""
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
give format and make it polished. Output the final improved copy as a single text block.
""",
description: "An agent that formats and proofreads the marketing copy.");
// Create a monitor to capturing agent responses (via ResponseCallback)
// to display at the end of this sample. (optional)
// NOTE: Create your own callback to capture responses in your application or service.
OrchestrationMonitor monitor = new();
// Define the orchestration
SequentialOrchestration orchestration =
new(analystAgent, writerAgent, editorAgent)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
};
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
AgentRunResponse result = await orchestration.RunAsync(input);
Console.WriteLine($"\n# RESULT: {result}");
this.DisplayHistory(monitor.History);
// Cleanup
var assistantClient = openAIClient.GetAssistantClient();
await assistantClient.DeleteAssistantAsync(editorAgent.Id);
// Need to know how to get the assistant thread ID to delete the thread (issue #260)
}
}
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
namespace Providers;
/// <summary>
/// Shows how to use <see cref="AIAgent"/> with Azure AI Persistent Agents.
/// </summary>
/// <remarks>
/// Running "az login" command in terminal is required for authentication with Azure AI service.
/// </remarks>
public sealed class AIAgent_With_AzureAIAgentsPersistent(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Fact]
public async Task GetWithAzureAIAgentsPersistent()
{
// Get a client to create server side agents with.
var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
// Create a service side persistent agent.
var persistentAgent = await persistentAgentsClient.Administration.CreateAgentAsync(
model: TestConfiguration.AzureAI.DeploymentName!,
name: JokerName,
instructions: JokerInstructions);
// Get a server side agent.
AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(persistentAgent.Value.Id);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to run agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
Console.WriteLine(input);
var response = await agent.RunAsync(input, thread);
Console.WriteLine(response);
}
// Cleanup
await persistentAgentsClient.Threads.DeleteThreadAsync(thread.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
[Fact]
public async Task CreateWithAzureAIAgentsPersistent()
{
// Get a client to create server side agents with.
var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
// Create a server side persistent agent.
AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
model: TestConfiguration.AzureAI.DeploymentName!,
name: JokerName,
instructions: JokerInstructions);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to run agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
Console.WriteLine(input);
var response = await agent.RunAsync(input, thread);
Console.WriteLine(response);
}
// Cleanup
await persistentAgentsClient.Threads.DeleteThreadAsync(thread.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
}
@@ -3,16 +3,16 @@
using System.ClientModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAI;
namespace Providers;
/// <summary>
/// End-to-end sample showing how to use <see cref="ChatClientAgent"/> with Azure OpenAI Chat Completion.
/// </summary>
public sealed class ChatClientAgent_With_AzureOpenAIChatCompletion(ITestOutputHelper output) : AgentSample(output)
public sealed class AIAgent_With_AzureOpenAIChatCompletion(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
@@ -20,16 +20,14 @@ public sealed class ChatClientAgent_With_AzureOpenAIChatCompletion(ITestOutputHe
[Fact]
public async Task RunWithChatCompletion()
{
// Get the chat client to use for the agent.
using var chatClient = ((TestConfiguration.AzureOpenAI.ApiKey is null)
// Get the OpenAI client to use for the agent.
var openAIClient = (TestConfiguration.AzureOpenAI.ApiKey is null)
// Use Azure CLI credentials if API key is not provided.
? new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new AzureCliCredential())
: new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new ApiKeyCredential(TestConfiguration.AzureOpenAI.ApiKey)))
.GetChatClient(TestConfiguration.AzureOpenAI.DeploymentName)
.AsIChatClient();
: new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new ApiKeyCredential(TestConfiguration.AzureOpenAI.ApiKey));
// Define the agent
ChatClientAgent agent = new(chatClient, JokerInstructions, JokerName);
// Create the agent
AIAgent agent = openAIClient.GetChatClient(TestConfiguration.AzureOpenAI.DeploymentName).CreateAIAgent(JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAI;
@@ -10,36 +9,29 @@ using OpenAI;
namespace Providers;
/// <summary>
/// End-to-end sample showing how to use <see cref="ChatClientAgent"/> with OpenAI Assistants.
/// End-to-end sample showing how to use <see cref="AIAgent"/> with OpenAI Assistants.
/// </summary>
public sealed class ChatClientAgent_With_OpenAIAssistant(ITestOutputHelper output) : AgentSample(output)
public sealed class AIAgent_With_OpenAIAssistant(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Fact]
public async Task RunWithOpenAIAssistant()
public async Task RunWithAssistant()
{
// Get a client to create server side agents with.
var openAIClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey);
var assistantClient = openAIClient.GetAssistantClient();
// Create a server side agent to work with.
var assistantCreateResult = await assistantClient.CreateAssistantAsync(
TestConfiguration.OpenAI.ChatModelId,
new()
{
Name = JokerName,
Instructions = JokerInstructions
});
var assistantId = assistantCreateResult.Value.Id;
// Get the chat client to use for the agent.
using var chatClient = assistantClient.AsIChatClient(assistantId);
// Define the agent.
ChatClientAgent agent = new(chatClient);
// Get the agent directly from OpenAIClient.
AIAgent agent = openAIClient
.GetAssistantClient()
.CreateAIAgent(
TestConfiguration.OpenAI.ChatModelId,
options: new()
{
Name = JokerName,
Instructions = JokerInstructions,
});
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
@@ -59,7 +51,8 @@ public sealed class ChatClientAgent_With_OpenAIAssistant(ITestOutputHelper outpu
}
// Cleanup
var assistantClient = openAIClient.GetAssistantClient();
await assistantClient.DeleteThreadAsync(thread.Id);
await assistantClient.DeleteAssistantAsync(assistantId);
await assistantClient.DeleteAssistantAsync(agent.Id);
}
}
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAI;
using OpenAI.Chat;
namespace Providers;
/// <summary>
/// End-to-end sample showing how to use <see cref="AIAgent"/> with OpenAI Chat Completion and Responses.
/// </summary>
public sealed class AIAgent_With_OpenAIClient(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Fact]
public async Task RunWithChatCompletion()
{
// Get the agent directly from OpenAIClient.
AIAgent agent = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
.CreateAIAgent(JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input.
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
Console.WriteLine(input);
var response = await agent.RunAsync(input, thread);
Console.WriteLine(response.Messages.Last().Text);
}
}
[Fact]
public async Task RunWithChatCompletionReturnChatCompletion()
{
// Get the agent directly from OpenAIClient.
var agent = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
.CreateAIAgent(JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input.
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
Console.WriteLine(input);
var response = await agent.RunAsync(input, thread);
var chatCompletion = response.AsChatCompletion();
Console.WriteLine(chatCompletion.Content.Last().Text);
}
}
[Fact]
public async Task RunWithChatCompletionWithOpenAIChatMessage()
{
// Get the agent directly from OpenAIClient.
var agent = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
.CreateAIAgent(JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input.
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
Console.WriteLine(input);
// Use the OpenAI.Chat message types directly
var chatMessage = new UserChatMessage(input);
var chatCompletion = await agent.RunAsync(chatMessage, thread);
Console.WriteLine(chatCompletion.Content.Last().Text);
}
}
}
@@ -11,7 +11,7 @@ namespace Providers;
/// <summary>
/// End-to-end sample showing how to use <see cref="ChatClientAgent"/> with OpenAI Chat Completion.
/// </summary>
public sealed class ChatClientAgent_With_OpenAIResponsesChatCompletion(ITestOutputHelper output) : AgentSample(output)
public sealed class AIAgent_With_OpenAIResponseClient(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
@@ -20,15 +20,12 @@ public sealed class ChatClientAgent_With_OpenAIResponsesChatCompletion(ITestOutp
/// This will use the conversation id to reference the thread state on the server side.
/// </summary>
[Fact]
public async Task RunWithChatCompletionServiceManagedThread()
public async Task RunWithResponses()
{
// Get the chat client to use for the agent.
using var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
// Get the agent directly from OpenAIClient.
AIAgent agent = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient();
// Define the agent
ChatClientAgent agent = new(chatClient, JokerInstructions, JokerName);
.CreateAIAgent(JokerInstructions, JokerName);
// Start a new thread for the agent conversation based on the type.
AgentThread thread = agent.GetNewThread();
@@ -52,16 +49,12 @@ public sealed class ChatClientAgent_With_OpenAIResponsesChatCompletion(ITestOutp
/// This will use in-memory messages to store the thread state.
/// </summary>
[Fact]
public async Task RunWithChatCompletionInMemoryThread()
public async Task RunWithResponsesAndStoreOutputDisabled()
{
// Get the chat client to use for the agent.
using var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
// Get the agent directly from OpenAIClient.
AIAgent agent = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient();
// Define the agent
ChatClientAgent agent =
new(chatClient, options: new()
.CreateAIAgent(options: new()
{
Name = JokerName,
Instructions = JokerInstructions,
@@ -1,57 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
namespace Providers;
/// <summary>
/// Shows how to use <see cref="ChatClientAgent"/> with Azure AI Persistent Agents.
/// </summary>
/// <remarks>
/// Running "az login" command in terminal is required for authentication with Azure AI service.
/// </remarks>
public sealed class ChatClientAgent_With_AzureAIAgentsPersistent(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Fact]
public async Task RunWithAzureAIAgentsPersistent()
{
// Get a client to create server side agents with.
var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
// Create a server side persistent agent.
var createPersistentAgentResponse = await persistentAgentsClient.Administration.CreateAgentAsync(
model: TestConfiguration.AzureAI.DeploymentName,
name: JokerName,
instructions: JokerInstructions);
// Get a local proxy for the agent to work with.
AIAgent agent = await persistentAgentsClient.GetRunnableAgentAsync(createPersistentAgentResponse.Value.Id);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to run agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
this.WriteResponseOutput(response);
}
// Cleanup
await persistentAgentsClient.Threads.DeleteThreadAsync(thread.Id);
await persistentAgentsClient.Administration.DeleteAgentAsync(createPersistentAgentResponse.Value.Id);
}
}
@@ -1,46 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAI;
namespace Providers;
/// <summary>
/// End-to-end sample showing how to use <see cref="ChatClientAgent"/> with OpenAI Chat Completion.
/// </summary>
public sealed class ChatClientAgent_With_OpenAIChatCompletion(ITestOutputHelper output) : AgentSample(output)
{
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Fact]
public async Task RunWithChatCompletion()
{
// Get the chat client to use for the agent.
using var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient();
// Define the agent
ChatClientAgent agent = new(chatClient, JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
// Respond to user input.
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
this.WriteResponseOutput(response);
}
}
}
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<VersionSuffix>alpha</VersionSuffix>
<NoWarn>$(NoWarn);IDE0009;</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);IDE0009;</NoWarn>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
@@ -17,14 +17,14 @@ internal static class PersistentAgentResponseExtensions
/// <param name="persistentAgentsClient">The client used to interact with persistent agents. Cannot be <see langword="null"/>.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static ChatClientAgent AsRunnableAgent(this Response<PersistentAgent> persistentAgentResponse, PersistentAgentsClient persistentAgentsClient, ChatOptions? chatOptions = null)
public static ChatClientAgent AsAIAgent(this Response<PersistentAgent> persistentAgentResponse, PersistentAgentsClient persistentAgentsClient, ChatOptions? chatOptions = null)
{
if (persistentAgentResponse is null)
{
throw new ArgumentNullException(nameof(persistentAgentResponse));
}
return AsRunnableAgent(persistentAgentResponse.Value, persistentAgentsClient, chatOptions);
return AsAIAgent(persistentAgentResponse.Value, persistentAgentsClient, chatOptions);
}
/// <summary>
@@ -34,7 +34,7 @@ internal static class PersistentAgentResponseExtensions
/// <param name="persistentAgentsClient">The client used to interact with persistent agents. Cannot be <see langword="null"/>.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static ChatClientAgent AsRunnableAgent(this PersistentAgent persistentAgentMetadata, PersistentAgentsClient persistentAgentsClient, ChatOptions? chatOptions = null)
public static ChatClientAgent AsAIAgent(this PersistentAgent persistentAgentMetadata, PersistentAgentsClient persistentAgentsClient, ChatOptions? chatOptions = null)
{
if (persistentAgentMetadata is null)
{
@@ -19,7 +19,7 @@ public static class PersistentAgentsClientExtensions
/// <param name="chatOptions">Options that should apply to all runs of the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static async Task<ChatClientAgent> GetRunnableAgentAsync(
public static async Task<ChatClientAgent> GetAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
ChatOptions? chatOptions = null,
@@ -36,6 +36,57 @@ public static class PersistentAgentsClientExtensions
}
var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false);
return persistentAgentResponse.AsRunnableAgent(persistentAgentsClient, chatOptions);
return persistentAgentResponse.AsAIAgent(persistentAgentsClient, chatOptions);
}
/// <summary>
/// Creates a new server side agent using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="tools">The tools to be used by the agent.</param>
/// <param name="toolResources">The resources for the tools.</param>
/// <param name="temperature">The temperature setting for the agent.</param>
/// <param name="topP">The top-p setting for the agent.</param>
/// <param name="responseFormat">The response format for the agent.</param>
/// <param name="metadata">The metadata for the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string model,
string? name = null,
string? description = null,
string? instructions = null,
IEnumerable<ToolDefinition>? tools = null,
ToolResources? toolResources = null,
float? temperature = null,
float? topP = null,
BinaryData? responseFormat = null,
IReadOnlyDictionary<string, string>? metadata = null,
CancellationToken cancellationToken = default)
{
if (persistentAgentsClient is null)
{
throw new ArgumentNullException(nameof(persistentAgentsClient));
}
var createPersistentAgentResponse = await persistentAgentsClient.Administration.CreateAgentAsync(
model,
name,
instructions,
tools: tools,
toolResources: toolResources,
temperature: temperature,
topP: topP,
responseFormat: responseFormat,
metadata: metadata,
cancellationToken: cancellationToken).ConfigureAwait(false);
// Get a local proxy for the agent to work with.
return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,356 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Diagnostics;
using OpenAI.Chat;
namespace OpenAI;
/// <summary>
/// Provides extension methods for <see cref="AIAgent"/> to simplify interaction with OpenAI chat messages
/// and return native OpenAI <see cref="ChatCompletion"/> responses.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between the Microsoft Extensions AI framework and the OpenAI SDK,
/// allowing developers to work with native OpenAI types while leveraging the AI Agent framework.
/// The methods handle the conversion between OpenAI chat message types and Microsoft Extensions AI types,
/// and return OpenAI <see cref="ChatCompletion"/> objects directly from the agent's <see cref="AgentRunResponse"/>.
/// </remarks>
public static class AIAgentWithOpenAIExtensions
{
/// <summary>
/// Runs the AI agent with a single OpenAI chat message and returns the response as a native OpenAI <see cref="ChatCompletion"/>.
/// </summary>
/// <param name="agent">The AI agent to run.</param>
/// <param name="message">The OpenAI chat message to send to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided message and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="Task{ChatCompletion}"/> representing the asynchronous operation that returns a native OpenAI <see cref="ChatCompletion"/> response.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> or <paramref name="message"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to a <see cref="ChatCompletion"/>, typically when the underlying representation is not an OpenAI response.</exception>
/// <exception cref="NotSupportedException">Thrown when the <paramref name="message"/> type is not supported by the message conversion method.</exception>
/// <remarks>
/// This method converts the OpenAI chat message to the Microsoft Extensions AI format using the appropriate conversion method,
/// runs the agent, and then extracts the native OpenAI <see cref="ChatCompletion"/> from the response using <see cref="AgentRunResponseExtensions.AsChatCompletion"/>.
/// </remarks>
public static async Task<ChatCompletion> RunAsync(this AIAgent agent, OpenAI.Chat.ChatMessage message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(agent);
Throw.IfNull(message);
var response = await agent.RunAsync(message.AsChatMessage(), thread, options, cancellationToken).ConfigureAwait(false);
var chatCompletion = response.AsChatCompletion();
return chatCompletion;
}
/// <summary>
/// Runs the AI agent with a collection of OpenAI chat messages and returns the response as a native OpenAI <see cref="ChatCompletion"/>.
/// </summary>
/// <param name="agent">The AI agent to run.</param>
/// <param name="messages">The collection of OpenAI chat messages to send to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="Task{ChatCompletion}"/> representing the asynchronous operation that returns a native OpenAI <see cref="ChatCompletion"/> response.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> or <paramref name="messages"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to a <see cref="ChatCompletion"/>, typically when the underlying representation is not an OpenAI response.</exception>
/// <exception cref="NotSupportedException">Thrown when any message in <paramref name="messages"/> has a type that is not supported by the message conversion method.</exception>
/// <remarks>
/// This method converts each OpenAI chat message to the Microsoft Extensions AI format using <see cref="AsChatMessages"/>,
/// runs the agent with the converted message collection, and then extracts the native OpenAI <see cref="ChatCompletion"/> from the response using <see cref="AgentRunResponseExtensions.AsChatCompletion"/>.
/// </remarks>
public static async Task<ChatCompletion> RunAsync(this AIAgent agent, IEnumerable<OpenAI.Chat.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(agent);
Throw.IfNull(messages);
var response = await agent.RunAsync([.. messages.AsChatMessages()], thread, options, cancellationToken).ConfigureAwait(false);
var chatCompletion = response.AsChatCompletion();
return chatCompletion;
}
/// <summary>
/// Creates a sequence of <see cref="Microsoft.Extensions.AI.ChatMessage"/> instances from the specified OpenAI input messages.
/// </summary>
/// <param name="messages">The OpenAI input messages to convert.</param>
/// <returns>A sequence of Microsoft Extensions AI chat messages converted from the OpenAI messages.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="messages"/> is <see langword="null"/>.</exception>
/// <exception cref="NotSupportedException">Thrown when a message type is encountered that cannot be converted.</exception>
/// <remarks>
/// This method supports conversion of the following OpenAI message types:
/// <list type="bullet">
/// <item><description><see cref="AssistantChatMessage"/></description></item>
/// <item><description><see cref="DeveloperChatMessage"/></description></item>
/// <item><description><see cref="FunctionChatMessage"/> (obsolete)</description></item>
/// <item><description><see cref="SystemChatMessage"/></description></item>
/// <item><description><see cref="ToolChatMessage"/></description></item>
/// <item><description><see cref="UserChatMessage"/></description></item>
/// </list>
/// </remarks>
internal static IEnumerable<Microsoft.Extensions.AI.ChatMessage> AsChatMessages(this IEnumerable<OpenAI.Chat.ChatMessage> messages)
{
Throw.IfNull(messages);
foreach (OpenAI.Chat.ChatMessage message in messages)
{
switch (message)
{
case OpenAI.Chat.AssistantChatMessage assistantMessage:
yield return assistantMessage.AsChatMessage();
break;
case OpenAI.Chat.DeveloperChatMessage developerMessage:
yield return developerMessage.AsChatMessage();
break;
#pragma warning disable CS0618 // Type or member is obsolete
case OpenAI.Chat.FunctionChatMessage functionMessage:
yield return functionMessage.AsChatMessage();
break;
#pragma warning restore CS0618 // Type or member is obsolete
case OpenAI.Chat.SystemChatMessage systemMessage:
yield return systemMessage.AsChatMessage();
break;
case OpenAI.Chat.ToolChatMessage toolMessage:
yield return toolMessage.AsChatMessage();
break;
case OpenAI.Chat.UserChatMessage userMessage:
yield return userMessage.AsChatMessage();
break;
}
}
}
/// <summary>
/// Converts an OpenAI chat message to a Microsoft Extensions AI <see cref="Microsoft.Extensions.AI.ChatMessage"/>.
/// </summary>
/// <param name="chatMessage">The OpenAI chat message to convert.</param>
/// <returns>A <see cref="Microsoft.Extensions.AI.ChatMessage"/> equivalent of the input OpenAI message.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="chatMessage"/> is <see langword="null"/>.</exception>
/// <exception cref="NotSupportedException">Thrown when the <paramref name="chatMessage"/> type is not supported for conversion.</exception>
/// <remarks>
/// This method provides a bridge between OpenAI SDK message types and Microsoft Extensions AI message types.
/// It handles the conversion by switching on the concrete type of the OpenAI message and calling the appropriate
/// specialized conversion method.
/// </remarks>
internal static Microsoft.Extensions.AI.ChatMessage AsChatMessage(this OpenAI.Chat.ChatMessage chatMessage)
{
Throw.IfNull(chatMessage);
return chatMessage switch
{
AssistantChatMessage assistantMessage => assistantMessage.AsChatMessage(),
DeveloperChatMessage developerMessage => developerMessage.AsChatMessage(),
SystemChatMessage systemMessage => systemMessage.AsChatMessage(),
ToolChatMessage toolMessage => toolMessage.AsChatMessage(),
UserChatMessage userMessage => userMessage.AsChatMessage(),
_ => throw new NotSupportedException($"Message type {chatMessage.GetType().Name} is not supported for conversion.")
};
}
/// <summary>
/// Converts OpenAI chat message content to Microsoft Extensions AI content items.
/// </summary>
/// <param name="content">The OpenAI chat message content to convert.</param>
/// <returns>A sequence of <see cref="AIContent"/> items converted from the OpenAI content.</returns>
/// <remarks>
/// This method supports conversion of the following OpenAI content part types:
/// <list type="bullet">
/// <item><description>Text content (converted to <see cref="TextContent"/>)</description></item>
/// <item><description>Refusal content (converted to <see cref="TextContent"/>)</description></item>
/// <item><description>Image content (converted to <see cref="DataContent"/> or <see cref="UriContent"/>)</description></item>
/// <item><description>Input audio content (converted to <see cref="DataContent"/>)</description></item>
/// <item><description>File content (converted to <see cref="DataContent"/>)</description></item>
/// </list>
/// </remarks>
private static IEnumerable<AIContent> AsAIContent(this OpenAI.Chat.ChatMessageContent content)
{
Throw.IfNull(content);
foreach (OpenAI.Chat.ChatMessageContentPart part in content)
{
switch (part.Kind)
{
case OpenAI.Chat.ChatMessageContentPartKind.Text:
yield return new TextContent(part.Text)
{
RawRepresentation = content
};
break;
case OpenAI.Chat.ChatMessageContentPartKind.Refusal:
yield return new TextContent(part.Refusal)
{
RawRepresentation = content
};
break;
case OpenAI.Chat.ChatMessageContentPartKind.Image:
if (part.ImageBytes is not null)
{
yield return new DataContent(part.ImageBytes, part.ImageBytesMediaType)
{
RawRepresentation = content
};
}
else
{
yield return new UriContent(part.ImageUri, "image/*")
{
RawRepresentation = content
};
}
break;
case OpenAI.Chat.ChatMessageContentPartKind.InputAudio:
yield return new DataContent(part.InputAudioBytes, "audio/*")
{
RawRepresentation = content
};
break;
case OpenAI.Chat.ChatMessageContentPartKind.File:
yield return new DataContent(part.FileBytes, part.FileBytesMediaType)
{
RawRepresentation = content
};
break;
default:
throw new NotSupportedException($"Content part kind '{part.Kind}' is not supported for conversion to AIContent.");
}
}
}
/// <summary>
/// Converts OpenAI chat message content to text.
/// </summary>
/// <param name="content">The OpenAI chat message content to convert.</param>
/// <returns>A string created from the text and refusal parts of the OpenAI content.</returns>
/// <remarks>
/// Using when converting OpenAI For <c>tool</c> messages, the contents can only be of type <c>text</c>.
/// </remarks>
private static string AsText(this OpenAI.Chat.ChatMessageContent content)
{
Throw.IfNull(content);
StringBuilder text = new();
foreach (OpenAI.Chat.ChatMessageContentPart part in content)
{
switch (part.Kind)
{
case OpenAI.Chat.ChatMessageContentPartKind.Text:
text.Append(part.Text);
break;
case OpenAI.Chat.ChatMessageContentPartKind.Refusal:
text.Append(part.Refusal);
break;
default:
throw new NotSupportedException($"Content part kind '{part.Kind}' is not supported for conversion to text.");
}
}
return text.ToString();
}
/// <summary>
/// Converts an OpenAI <see cref="AssistantChatMessage"/> to a Microsoft Extensions AI <see cref="Microsoft.Extensions.AI.ChatMessage"/>.
/// </summary>
/// <param name="assistantMessage">The OpenAI assistant message to convert.</param>
/// <returns>A Microsoft Extensions AI chat message with assistant role.</returns>
/// <remarks>
/// This method converts the assistant message content using <see cref="AsAIContent"/> and preserves
/// the participant name as the author name in the resulting message.
/// </remarks>
private static Microsoft.Extensions.AI.ChatMessage AsChatMessage(this AssistantChatMessage assistantMessage)
{
Throw.IfNull(assistantMessage);
return new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.Assistant, [.. assistantMessage.Content.AsAIContent()])
{
AuthorName = assistantMessage.ParticipantName,
RawRepresentation = assistantMessage
};
}
/// <summary>
/// Converts an OpenAI <see cref="DeveloperChatMessage"/> to a Microsoft Extensions AI <see cref="Microsoft.Extensions.AI.ChatMessage"/>.
/// </summary>
/// <param name="developerMessage">The OpenAI developer message to convert.</param>
/// <returns>A Microsoft Extensions AI chat message with system role.</returns>
/// <remarks>
/// Developer messages are treated as system messages in the Microsoft Extensions AI framework.
/// The participant name is preserved as the author name.
/// </remarks>
private static Microsoft.Extensions.AI.ChatMessage AsChatMessage(this DeveloperChatMessage developerMessage)
{
Throw.IfNull(developerMessage);
return new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.System, [.. developerMessage.Content.AsAIContent()])
{
AuthorName = developerMessage.ParticipantName,
RawRepresentation = developerMessage
};
}
/// <summary>
/// Converts an OpenAI <see cref="SystemChatMessage"/> to a Microsoft Extensions AI <see cref="Microsoft.Extensions.AI.ChatMessage"/>.
/// </summary>
/// <param name="systemMessage">The OpenAI system message to convert.</param>
/// <returns>A Microsoft Extensions AI chat message with system role.</returns>
/// <remarks>
/// This method converts the system message content using <see cref="AsAIContent"/> and preserves
/// the participant name as the author name in the resulting message.
/// </remarks>
private static Microsoft.Extensions.AI.ChatMessage AsChatMessage(this SystemChatMessage systemMessage)
{
Throw.IfNull(systemMessage);
return new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.System, [.. systemMessage.Content.AsAIContent()])
{
AuthorName = systemMessage.ParticipantName,
RawRepresentation = systemMessage
};
}
/// <summary>
/// Converts an OpenAI <see cref="ToolChatMessage"/> to a Microsoft Extensions AI <see cref="Microsoft.Extensions.AI.ChatMessage"/>.
/// </summary>
/// <param name="toolMessage">The OpenAI tool message to convert.</param>
/// <returns>A Microsoft Extensions AI chat message with tool role.</returns>
/// <remarks>
/// This method converts tool message content using <see cref="AsAIContent"/> and includes the tool call ID
/// in the resulting message's additional properties for traceability.
/// </remarks>
private static Microsoft.Extensions.AI.ChatMessage AsChatMessage(this ToolChatMessage toolMessage)
{
Throw.IfNull(toolMessage);
var content = new FunctionResultContent(toolMessage.ToolCallId, toolMessage.Content.AsText())
{
RawRepresentation = toolMessage
};
return new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.Tool, [content])
{
RawRepresentation = toolMessage,
AdditionalProperties = new() { ["tool_call_id"] = toolMessage.ToolCallId }
};
}
/// <summary>
/// Converts an OpenAI <see cref="UserChatMessage"/> to a Microsoft Extensions AI <see cref="Microsoft.Extensions.AI.ChatMessage"/>.
/// </summary>
/// <param name="userMessage">The OpenAI user message to convert.</param>
/// <returns>A Microsoft Extensions AI chat message with user role.</returns>
/// <remarks>
/// This method converts the user message content using <see cref="AsAIContent"/> and preserves
/// the participant name as the author name in the resulting message.
/// </remarks>
private static Microsoft.Extensions.AI.ChatMessage AsChatMessage(this UserChatMessage userMessage)
{
Throw.IfNull(userMessage);
return new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.User, [.. userMessage.Content.AsAIContent()])
{
AuthorName = userMessage.ParticipantName,
RawRepresentation = userMessage
};
}
}
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Diagnostics;
using OpenAI.Chat;
namespace OpenAI;
/// <summary>
/// Provides extension methods for <see cref="AgentRunResponse"/> to extract native OpenAI response objects
/// from the Microsoft Extensions AI Agent framework responses.
/// </summary>
/// <remarks>
/// These extensions enable developers to access the underlying OpenAI SDK objects when working with
/// AI agents that are backed by OpenAI services. The methods extract strongly-typed OpenAI responses
/// from the <see cref="AgentRunResponse.RawRepresentation"/> property, providing a bridge between
/// the Microsoft Extensions AI framework and the native OpenAI SDK types.
/// </remarks>
public static class AgentRunResponseExtensions
{
/// <summary>
/// Extracts a native OpenAI <see cref="ChatCompletion"/> object from an <see cref="AgentRunResponse"/>.
/// </summary>
/// <param name="agentResponse">The agent response containing the raw OpenAI representation.</param>
/// <returns>The native OpenAI <see cref="ChatCompletion"/> object.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentResponse"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// Thrown when the <see cref="AgentRunResponse.RawRepresentation"/> is not a <see cref="ChatCompletion"/> object.
/// This typically occurs when the agent response was not generated by an OpenAI chat completion service
/// or when the underlying representation has been modified or corrupted.
/// </exception>
/// <remarks>
/// <para>
/// This method provides access to the native OpenAI <see cref="ChatCompletion"/> object that was used
/// to generate the agent response. This is useful when you need to access OpenAI-specific properties
/// or metadata that are not exposed through the Microsoft Extensions AI abstractions.
/// </para>
/// </remarks>
public static ChatCompletion AsChatCompletion(this AgentRunResponse agentResponse)
{
Throw.IfNull(agentResponse);
if (agentResponse.RawRepresentation is ChatResponse chatResponse)
{
return chatResponse.RawRepresentation is ChatCompletion chatCompletion
? chatCompletion
: throw new ArgumentException("ChatResponse.RawRepresentation must be a ChatCompletion");
}
throw new ArgumentException("AgentRunResponse.RawRepresentation must be a ChatResponse");
}
}
@@ -4,7 +4,7 @@
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<VersionSuffix>alpha</VersionSuffix>
<NoWarn>$(NoWarn);IDE0009;</NoWarn>
<NoWarn>$(NoWarn);IDE0009;OPENAI001;</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
@@ -49,7 +49,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
private IReadOnlyList<ToolDefinition>? _assistantTools;
/// <summary>Initializes a new instance of the <see cref="OpenAIAssistantChatClient"/> class for the specified <see cref="AssistantClient"/>.</summary>
public NewOpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId)
public NewOpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId = null)
{
_client = Throw.IfNull(assistantClient);
_assistantId = Throw.IfNullOrWhitespace(assistantId);
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI.Assistants;
namespace OpenAI;
/// <summary>
/// Provides extension methods for OpenAI <see cref="AssistantClient"/>
/// to simplify the creation of AI agents that work with OpenAI services.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between OpenAI SDK client objects and the Microsoft Extensions AI Agent framework,
/// allowing developers to easily create AI agents that leverage OpenAI's chat completion and response services.
/// The methods handle the conversion from OpenAI clients to <see cref="IChatClient"/> instances and then wrap them
/// in <see cref="ChatClientAgent"/> objects that implement the <see cref="AIAgent"/> interface.
/// </remarks>
public static class OpenAIAssistantClientExtensions
{
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
/// <param name="client">The OpenAI <see cref="AssistantClient" /> to use for the agent.</param>
/// <param name="model">The model identifier to use (e.g., "gpt-4").</param>
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
/// <param name="name">Optional name for the agent for identification purposes.</param>
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
/// <param name="tools">Optional collection of AI tools that the agent can use during conversations.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Assistant service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="model"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
public static AIAgent CreateAIAgent(this AssistantClient client, string model, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
{
return client.CreateAIAgent(
model,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
}
},
loggerFactory);
}
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
/// <param name="client">The OpenAI <see cref="AssistantClient" /> to use for the agent.</param>
/// <param name="model">The model identifier to use (e.g., "gpt-4").</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Assistant service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="model"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
public static AIAgent CreateAIAgent(this AssistantClient client, string model, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(client);
Throw.IfNullOrEmpty(model);
Throw.IfNull(options);
var assistantOptions = new AssistantCreationOptions()
{
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
};
if (options.ChatOptions?.Tools is not null)
{
foreach (AITool tool in options.ChatOptions.Tools)
{
switch (tool)
{
case AIFunction aiFunction:
assistantOptions.Tools.Add(NewOpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
break;
case HostedCodeInterpreterTool:
var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
assistantOptions.Tools.Add(codeInterpreterToolDefinition);
break;
}
}
}
var assistantCreateResult = client.CreateAssistant(model, assistantOptions);
var assistantId = assistantCreateResult.Value.Id;
var agentOptions = new ChatClientAgentOptions()
{
Id = assistantId,
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
ChatOptions = options.ChatOptions?.Tools is null ? null : new ChatOptions()
{
Tools = options.ChatOptions.Tools,
}
};
#pragma warning disable CA2000 // Dispose objects before losing scope
var chatClient = new NewOpenAIAssistantChatClient(client, assistantId);
#pragma warning restore CA2000 // Dispose objects before losing scope
return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
}
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
/// <param name="client">The OpenAI <see cref="AssistantClient" /> to use for the agent.</param>
/// <param name="model">The model identifier to use (e.g., "gpt-4").</param>
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
/// <param name="name">Optional name for the agent for identification purposes.</param>
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
/// <param name="tools">Optional collection of AI tools that the agent can use during conversations.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Assistant service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="model"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
public static async Task<AIAgent> CreateAIAgentAsync(this AssistantClient client, string model, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
{
return await client.CreateAIAgentAsync(
model,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
}
},
loggerFactory).ConfigureAwait(false);
}
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
/// <param name="client">The OpenAI <see cref="AssistantClient" /> to use for the agent.</param>
/// <param name="model">The model identifier to use (e.g., "gpt-4").</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Assistant service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="model"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
public static async Task<AIAgent> CreateAIAgentAsync(this AssistantClient client, string model, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(client);
Throw.IfNull(model);
Throw.IfNull(options);
var assistantOptions = new AssistantCreationOptions()
{
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
};
if (options.ChatOptions?.Tools is not null)
{
foreach (AITool tool in options.ChatOptions.Tools)
{
switch (tool)
{
case AIFunction aiFunction:
assistantOptions.Tools.Add(NewOpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
break;
case HostedCodeInterpreterTool:
var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
assistantOptions.Tools.Add(codeInterpreterToolDefinition);
break;
}
}
}
var assistantCreateResult = await client.CreateAssistantAsync(model, assistantOptions).ConfigureAwait(false);
var assistantId = assistantCreateResult.Value.Id;
var agentOptions = new ChatClientAgentOptions()
{
Id = assistantId,
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
ChatOptions = options.ChatOptions?.Tools is null ? null : new ChatOptions()
{
Tools = options.ChatOptions.Tools,
}
};
#pragma warning disable CA2000 // Dispose objects before losing scope
var chatClient = new NewOpenAIAssistantChatClient(client, assistantId);
#pragma warning restore CA2000 // Dispose objects before losing scope
return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
}
}
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI.Chat;
using ChatMessage = OpenAI.Chat.ChatMessage;
namespace OpenAI;
/// <summary>
/// OpenAI chat completion based implementation of <see cref="AIAgent"/>.
/// </summary>
public class OpenAIChatClientAgent : AIAgent
{
private readonly ChatClientAgent _chatClientAgent;
/// <summary>
/// Initialize an instance of <see cref="OpenAIChatClientAgent"/>
/// </summary>
/// <param name="client">Instance of <see cref="ChatClient"/></param>
/// <param name="instructions">Optional instructions for the agent.</param>
/// <param name="name">Optional name for the agent.</param>
/// <param name="description">Optional description for the agent.</param>
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
public OpenAIChatClientAgent(ChatClient client, string? instructions = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(client);
var chatClient = client.AsIChatClient();
this._chatClientAgent = new(
chatClient,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
Instructions = instructions,
},
loggerFactory);
}
/// <summary>
/// Initialize an instance of <see cref="OpenAIChatClientAgent"/>
/// </summary>
/// <param name="client">Instance of <see cref="ChatClient"/></param>
/// <param name="options">Options to create the agent.</param>
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
public OpenAIChatClientAgent(ChatClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(client);
var chatClient = client.AsIChatClient();
this._chatClientAgent = new(chatClient, options, loggerFactory);
}
/// <summary>
/// Run the agent with the provided message and arguments.
/// </summary>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatCompletion"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async Task<ChatCompletion> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
var response = await this.RunAsync([.. messages.AsChatMessages()], thread, options, cancellationToken).ConfigureAwait(false);
var chatCompletion = response.AsChatCompletion();
return chatCompletion;
}
/// <inheritdoc/>
public sealed override AgentThread GetNewThread()
{
return this._chatClientAgent.GetNewThread();
}
/// <inheritdoc/>
public sealed override Task<AgentRunResponse> RunAsync(IReadOnlyCollection<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this._chatClientAgent.RunAsync(messages, thread, options, cancellationToken);
}
/// <inheritdoc/>
public sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IReadOnlyCollection<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this._chatClientAgent.RunStreamingAsync(messages, thread, options, cancellationToken);
}
}
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI.Chat;
namespace OpenAI;
/// <summary>
/// Provides extension methods for <see cref="ChatClient"/>
/// to simplify the creation of AI agents that work with OpenAI services.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between OpenAI SDK client objects and the Microsoft Extensions AI Agent framework,
/// allowing developers to easily create AI agents that leverage OpenAI's chat completion and response services.
/// The methods handle the conversion from OpenAI clients to <see cref="IChatClient"/> instances and then wrap them
/// in <see cref="ChatClientAgent"/> objects that implement the <see cref="AIAgent"/> interface.
/// </remarks>
public static class OpenAIChatClientExtensions
{
/// <summary>
/// Creates an AI agent from an <see cref="ChatClient"/> using the OpenAI Chat Completion API.
/// </summary>
/// <param name="client">The OpenAI <see cref="ChatClient"/> to use for the agent.</param>
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
/// <param name="name">Optional name for the agent for identification purposes.</param>
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
/// <param name="tools">Optional collection of AI tools that the agent can use during conversations.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Chat Completion service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> is <see langword="null"/>.</exception>
public static AIAgent CreateAIAgent(this ChatClient client, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
{
return client.CreateAIAgent(
new ChatClientAgentOptions()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
}
},
loggerFactory);
}
/// <summary>
/// Creates an AI agent from an <see cref="ChatClient"/> using the OpenAI Chat Completion API.
/// </summary>
/// <param name="client">The OpenAI <see cref="ChatClient"/> to use for the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Chat Completion service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static AIAgent CreateAIAgent(this ChatClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(client);
Throw.IfNull(options);
var chatClient = client.AsIChatClient();
ChatClientAgent agent = new(chatClient, options, loggerFactory);
return agent;
}
}
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI.Responses;
namespace OpenAI;
/// <summary>
/// Provides extension methods for <see cref="OpenAIResponseClient"/>
/// to simplify the creation of AI agents that work with OpenAI services.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between OpenAI SDK client objects and the Microsoft Extensions AI Agent framework,
/// allowing developers to easily create AI agents that leverage OpenAI's chat completion and response services.
/// The methods handle the conversion from OpenAI clients to <see cref="IChatClient"/> instances and then wrap them
/// in <see cref="ChatClientAgent"/> objects that implement the <see cref="AIAgent"/> interface.
/// </remarks>
public static class OpenAIResponseClientExtensions
{
/// <summary>
/// Creates an AI agent from an <see cref="OpenAIResponseClient"/> using the OpenAI Response API.
/// </summary>
/// <param name="client">The <see cref="OpenAIResponseClient" /> to use for the agent.</param>
/// <param name="instructions">Optional system instructions that define the agent's behavior and personality.</param>
/// <param name="name">Optional name for the agent for identification purposes.</param>
/// <param name="description">Optional description of the agent's capabilities and purpose.</param>
/// <param name="tools">Optional collection of AI tools that the agent can use during conversations.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Response service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> is <see langword="null"/>.</exception>
public static AIAgent CreateAIAgent(this OpenAIResponseClient client, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
{
return client.CreateAIAgent(
new ChatClientAgentOptions()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
}
},
loggerFactory);
}
/// <summary>
/// Creates an AI agent from an <see cref="OpenAIResponseClient"/> using the OpenAI Response API.
/// </summary>
/// <param name="client">The <see cref="OpenAIResponseClient" /> to use for the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the OpenAI Response service.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static AIAgent CreateAIAgent(this OpenAIResponseClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(client);
Throw.IfNull(options);
var chatClient = client.AsIChatClient();
ChatClientAgent agent = new(chatClient, options, loggerFactory);
return agent;
}
}