diff --git a/dotnet/samples/GettingStarted/Custom/Custom_OpenAIChatClientAgent.cs b/dotnet/samples/GettingStarted/Custom/Custom_OpenAIChatClientAgent.cs new file mode 100644 index 000000000..2363b06cb --- /dev/null +++ b/dotnet/samples/GettingStarted/Custom/Custom_OpenAIChatClientAgent.cs @@ -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; + +/// +/// End-to-end sample showing how to use a custom . +/// +public sealed class Custom_OpenAIChatClientAgent(ITestOutputHelper output) : AgentSample(output) +{ + /// + /// This will create an instance of and run it. + /// + [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) + { + } +} diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj index c3175cc5e..0707da1ad 100644 --- a/dotnet/samples/GettingStarted/GettingStarted.csproj +++ b/dotnet/samples/GettingStarted/GettingStarted.csproj @@ -4,7 +4,7 @@ GettingStarted Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - $(NoWarn);CA1707;CA1716;IDE0009;IDE1006;OPENAI001; + $(NoWarn);CA1707;CA1716;IDE0009;IDE1006; OPENAI001; enable true true diff --git a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Foundry_Agents.cs b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Foundry_Agents.cs new file mode 100644 index 000000000..a9ff1ee20 --- /dev/null +++ b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Foundry_Agents.cs @@ -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; + +/// +/// Demonstrates how to use the for +/// executing multiple Foundry agents in sequence. +/// +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); + } +} diff --git a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Multi_Agent.cs b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Multi_Agent.cs new file mode 100644 index 000000000..f79b6ad61 --- /dev/null +++ b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Multi_Agent.cs @@ -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; + +/// +/// Demonstrates how to use the for +/// executing multiple heterogeneous agents in sequence. +/// +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) + } +} diff --git a/dotnet/samples/GettingStarted/Providers/AIAgent_With_AzureAIAgentsPersistent.cs b/dotnet/samples/GettingStarted/Providers/AIAgent_With_AzureAIAgentsPersistent.cs new file mode 100644 index 000000000..89f4d704d --- /dev/null +++ b/dotnet/samples/GettingStarted/Providers/AIAgent_With_AzureAIAgentsPersistent.cs @@ -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; + +/// +/// Shows how to use with Azure AI Persistent Agents. +/// +/// +/// Running "az login" command in terminal is required for authentication with Azure AI service. +/// +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); + } +} diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureOpenAIChatCompletion.cs b/dotnet/samples/GettingStarted/Providers/AIAgent_With_AzureOpenAIChatCompletion.cs similarity index 74% rename from dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureOpenAIChatCompletion.cs rename to dotnet/samples/GettingStarted/Providers/AIAgent_With_AzureOpenAIChatCompletion.cs index e52c7a5d3..86b33c35f 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureOpenAIChatCompletion.cs +++ b/dotnet/samples/GettingStarted/Providers/AIAgent_With_AzureOpenAIChatCompletion.cs @@ -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; /// /// End-to-end sample showing how to use with Azure OpenAI Chat Completion. /// -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(); diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs b/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIAssistant.cs similarity index 62% rename from dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs rename to dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIAssistant.cs index f3c12a3ff..df9772922 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIAssistant.cs @@ -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 @@ namespace Providers; /// -/// End-to-end sample showing how to use with OpenAI Assistants. +/// End-to-end sample showing how to use with OpenAI Assistants. /// -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 @@ async Task RunAgentAsync(string input) } // Cleanup + var assistantClient = openAIClient.GetAssistantClient(); await assistantClient.DeleteThreadAsync(thread.Id); - await assistantClient.DeleteAssistantAsync(assistantId); + await assistantClient.DeleteAssistantAsync(agent.Id); } } diff --git a/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIClient.cs b/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIClient.cs new file mode 100644 index 000000000..2c85b93bf --- /dev/null +++ b/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIClient.cs @@ -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; + +/// +/// End-to-end sample showing how to use with OpenAI Chat Completion and Responses. +/// +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); + } + } +} diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs b/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIResponseClient.cs similarity index 77% rename from dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs rename to dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIResponseClient.cs index 3e3489da4..314a49c76 100644 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs +++ b/dotnet/samples/GettingStarted/Providers/AIAgent_With_OpenAIResponseClient.cs @@ -11,7 +11,7 @@ namespace Providers; /// /// End-to-end sample showing how to use with OpenAI Chat Completion. /// -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. /// [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 @@ async Task RunAgentAsync(string input) /// This will use in-memory messages to store the thread state. /// [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, diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs deleted file mode 100644 index 4bbb3df0d..000000000 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_AzureAIAgentsPersistent.cs +++ /dev/null @@ -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; - -/// -/// Shows how to use with Azure AI Persistent Agents. -/// -/// -/// Running "az login" command in terminal is required for authentication with Azure AI service. -/// -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); - } -} diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs deleted file mode 100644 index e21cf5e3a..000000000 --- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIChatCompletion.cs +++ /dev/null @@ -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; - -/// -/// End-to-end sample showing how to use with OpenAI Chat Completion. -/// -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); - } - } -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj index 58008d7b3..a9e91d1e7 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj @@ -1,11 +1,11 @@ - + $(ProjectsTargetFrameworks) $(ProjectsDebugTargetFrameworks) alpha - $(NoWarn);IDE0009; enable + $(NoWarn);IDE0009; diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/ResponseExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs similarity index 85% rename from dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/ResponseExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs index 517350da1..d63cfb915 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/ResponseExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentResponseExtensions.cs @@ -17,14 +17,14 @@ internal static class PersistentAgentResponseExtensions /// The client used to interact with persistent agents. Cannot be . /// The default to use when interacting with the agent. /// A instance that can be used to perform operations on the persistent agent. - public static ChatClientAgent AsRunnableAgent(this Response persistentAgentResponse, PersistentAgentsClient persistentAgentsClient, ChatOptions? chatOptions = null) + public static ChatClientAgent AsAIAgent(this Response 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); } /// @@ -34,7 +34,7 @@ public static ChatClientAgent AsRunnableAgent(this Response per /// The client used to interact with persistent agents. Cannot be . /// The default to use when interacting with the agent. /// A instance that can be used to perform operations on the persistent agent. - 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) { diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs index 1f9e217c5..acfa0a11e 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs @@ -19,7 +19,7 @@ public static class PersistentAgentsClientExtensions /// Options that should apply to all runs of the agent. /// The to monitor for cancellation requests. The default is . /// A instance that can be used to perform operations on the persistent agent. - public static async Task GetRunnableAgentAsync( + public static async Task GetAIAgentAsync( this PersistentAgentsClient persistentAgentsClient, string agentId, ChatOptions? chatOptions = null, @@ -36,6 +36,57 @@ public static async Task GetRunnableAgentAsync( } var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false); - return persistentAgentResponse.AsRunnableAgent(persistentAgentsClient, chatOptions); + return persistentAgentResponse.AsAIAgent(persistentAgentsClient, chatOptions); + } + + /// + /// Creates a new server side agent using the provided . + /// + /// The to create the agent with. + /// The model to be used by the agent. + /// The name of the agent. + /// The description of the agent. + /// The instructions for the agent. + /// The tools to be used by the agent. + /// The resources for the tools. + /// The temperature setting for the agent. + /// The top-p setting for the agent. + /// The response format for the agent. + /// The metadata for the agent. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations on the newly created agent. + public static async Task CreateAIAgentAsync( + this PersistentAgentsClient persistentAgentsClient, + string model, + string? name = null, + string? description = null, + string? instructions = null, + IEnumerable? tools = null, + ToolResources? toolResources = null, + float? temperature = null, + float? topP = null, + BinaryData? responseFormat = null, + IReadOnlyDictionary? 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); } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AIAgentWithOpenAIExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AIAgentWithOpenAIExtensions.cs new file mode 100644 index 000000000..dc23ad1df --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AIAgentWithOpenAIExtensions.cs @@ -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; + +/// +/// Provides extension methods for to simplify interaction with OpenAI chat messages +/// and return native OpenAI responses. +/// +/// +/// 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 objects directly from the agent's . +/// +public static class AIAgentWithOpenAIExtensions +{ + /// + /// Runs the AI agent with a single OpenAI chat message and returns the response as a native OpenAI . + /// + /// The AI agent to run. + /// The OpenAI chat message to send to the agent. + /// 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. + /// Optional parameters for agent invocation. + /// The to monitor for cancellation requests. The default is . + /// A representing the asynchronous operation that returns a native OpenAI response. + /// Thrown when or is . + /// Thrown when the agent's response cannot be converted to a , typically when the underlying representation is not an OpenAI response. + /// Thrown when the type is not supported by the message conversion method. + /// + /// 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 from the response using . + /// + public static async Task 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; + } + + /// + /// Runs the AI agent with a collection of OpenAI chat messages and returns the response as a native OpenAI . + /// + /// The AI agent to run. + /// The collection of OpenAI chat messages to send to the agent. + /// 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. + /// Optional parameters for agent invocation. + /// The to monitor for cancellation requests. The default is . + /// A representing the asynchronous operation that returns a native OpenAI response. + /// Thrown when or is . + /// Thrown when the agent's response cannot be converted to a , typically when the underlying representation is not an OpenAI response. + /// Thrown when any message in has a type that is not supported by the message conversion method. + /// + /// This method converts each OpenAI chat message to the Microsoft Extensions AI format using , + /// runs the agent with the converted message collection, and then extracts the native OpenAI from the response using . + /// + public static async Task RunAsync(this AIAgent agent, IEnumerable 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; + } + + /// + /// Creates a sequence of instances from the specified OpenAI input messages. + /// + /// The OpenAI input messages to convert. + /// A sequence of Microsoft Extensions AI chat messages converted from the OpenAI messages. + /// Thrown when is . + /// Thrown when a message type is encountered that cannot be converted. + /// + /// This method supports conversion of the following OpenAI message types: + /// + /// + /// + /// (obsolete) + /// + /// + /// + /// + /// + internal static IEnumerable AsChatMessages(this IEnumerable 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; + } + } + } + + /// + /// Converts an OpenAI chat message to a Microsoft Extensions AI . + /// + /// The OpenAI chat message to convert. + /// A equivalent of the input OpenAI message. + /// Thrown when is . + /// Thrown when the type is not supported for conversion. + /// + /// 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. + /// + 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.") + }; + } + + /// + /// Converts OpenAI chat message content to Microsoft Extensions AI content items. + /// + /// The OpenAI chat message content to convert. + /// A sequence of items converted from the OpenAI content. + /// + /// This method supports conversion of the following OpenAI content part types: + /// + /// Text content (converted to ) + /// Refusal content (converted to ) + /// Image content (converted to or ) + /// Input audio content (converted to ) + /// File content (converted to ) + /// + /// + private static IEnumerable 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."); + } + } + } + + /// + /// Converts OpenAI chat message content to text. + /// + /// The OpenAI chat message content to convert. + /// A string created from the text and refusal parts of the OpenAI content. + /// + /// Using when converting OpenAI For tool messages, the contents can only be of type text. + /// + 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(); + } + + /// + /// Converts an OpenAI to a Microsoft Extensions AI . + /// + /// The OpenAI assistant message to convert. + /// A Microsoft Extensions AI chat message with assistant role. + /// + /// This method converts the assistant message content using and preserves + /// the participant name as the author name in the resulting message. + /// + 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 + }; + } + + /// + /// Converts an OpenAI to a Microsoft Extensions AI . + /// + /// The OpenAI developer message to convert. + /// A Microsoft Extensions AI chat message with system role. + /// + /// Developer messages are treated as system messages in the Microsoft Extensions AI framework. + /// The participant name is preserved as the author name. + /// + 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 + }; + } + + /// + /// Converts an OpenAI to a Microsoft Extensions AI . + /// + /// The OpenAI system message to convert. + /// A Microsoft Extensions AI chat message with system role. + /// + /// This method converts the system message content using and preserves + /// the participant name as the author name in the resulting message. + /// + 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 + }; + } + + /// + /// Converts an OpenAI to a Microsoft Extensions AI . + /// + /// The OpenAI tool message to convert. + /// A Microsoft Extensions AI chat message with tool role. + /// + /// This method converts tool message content using and includes the tool call ID + /// in the resulting message's additional properties for traceability. + /// + 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 } + }; + } + + /// + /// Converts an OpenAI to a Microsoft Extensions AI . + /// + /// The OpenAI user message to convert. + /// A Microsoft Extensions AI chat message with user role. + /// + /// This method converts the user message content using and preserves + /// the participant name as the author name in the resulting message. + /// + 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 + }; + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AgentRunResponseExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AgentRunResponseExtensions.cs new file mode 100644 index 000000000..b3e5c5c93 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AgentRunResponseExtensions.cs @@ -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; + +/// +/// Provides extension methods for to extract native OpenAI response objects +/// from the Microsoft Extensions AI Agent framework responses. +/// +/// +/// 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 property, providing a bridge between +/// the Microsoft Extensions AI framework and the native OpenAI SDK types. +/// +public static class AgentRunResponseExtensions +{ + /// + /// Extracts a native OpenAI object from an . + /// + /// The agent response containing the raw OpenAI representation. + /// The native OpenAI object. + /// Thrown when is . + /// + /// Thrown when the is not a 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. + /// + /// + /// + /// This method provides access to the native OpenAI 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. + /// + /// + 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"); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj index 826eead02..eb4ad0143 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Microsoft.Extensions.AI.Agents.OpenAI.csproj @@ -4,7 +4,7 @@ $(ProjectsTargetFrameworks) $(ProjectsDebugTargetFrameworks) alpha - $(NoWarn);IDE0009; + $(NoWarn);IDE0009;OPENAI001; enable true diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs index 7cf1a7d0f..f6a4ef951 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/NewOpenAIAssistantChatClient.cs @@ -49,7 +49,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient private IReadOnlyList? _assistantTools; /// Initializes a new instance of the class for the specified . - 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); diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs new file mode 100644 index 000000000..71277db74 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs @@ -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; + +/// +/// Provides extension methods for OpenAI +/// to simplify the creation of AI agents that work with OpenAI services. +/// +/// +/// 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 instances and then wrap them +/// in objects that implement the interface. +/// +public static class OpenAIAssistantClientExtensions +{ + /// + /// Creates an AI agent from an using the OpenAI Assistant API. + /// + /// The OpenAI to use for the agent. + /// The model identifier to use (e.g., "gpt-4"). + /// Optional system instructions that define the agent's behavior and personality. + /// Optional name for the agent for identification purposes. + /// Optional description of the agent's capabilities and purpose. + /// Optional collection of AI tools that the agent can use during conversations. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Assistant service. + /// Thrown when or is . + /// Thrown when is empty or whitespace. + public static AIAgent CreateAIAgent(this AssistantClient client, string model, string? instructions = null, string? name = null, string? description = null, IList? 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); + } + + /// + /// Creates an AI agent from an using the OpenAI Assistant API. + /// + /// The OpenAI to use for the agent. + /// The model identifier to use (e.g., "gpt-4"). + /// Full set of options to configure the agent. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Assistant service. + /// Thrown when or or is . + /// Thrown when is empty or whitespace. + 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); + } + + /// + /// Creates an AI agent from an using the OpenAI Assistant API. + /// + /// The OpenAI to use for the agent. + /// The model identifier to use (e.g., "gpt-4"). + /// Optional system instructions that define the agent's behavior and personality. + /// Optional name for the agent for identification purposes. + /// Optional description of the agent's capabilities and purpose. + /// Optional collection of AI tools that the agent can use during conversations. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Assistant service. + /// Thrown when or is . + /// Thrown when is empty or whitespace. + public static async Task CreateAIAgentAsync(this AssistantClient client, string model, string? instructions = null, string? name = null, string? description = null, IList? 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); + } + + /// + /// Creates an AI agent from an using the OpenAI Assistant API. + /// + /// The OpenAI to use for the agent. + /// The model identifier to use (e.g., "gpt-4"). + /// Full set of options to configure the agent. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Assistant service. + /// Thrown when or is . + /// Thrown when is empty or whitespace. + public static async Task 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); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs new file mode 100644 index 000000000..61c3b12fa --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientAgent.cs @@ -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; + +/// +/// OpenAI chat completion based implementation of . +/// +public class OpenAIChatClientAgent : AIAgent +{ + private readonly ChatClientAgent _chatClientAgent; + + /// + /// Initialize an instance of + /// + /// Instance of + /// Optional instructions for the agent. + /// Optional name for the agent. + /// Optional description for the agent. + /// Optional instance of + 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); + } + + /// + /// Initialize an instance of + /// + /// Instance of + /// Options to create the agent. + /// Optional instance of + public OpenAIChatClientAgent(ChatClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) + { + Throw.IfNull(client); + + var chatClient = client.AsIChatClient(); + this._chatClientAgent = new(chatClient, options, loggerFactory); + } + + /// + /// Run the agent with the provided message and arguments. + /// + /// The messages to pass to the agent. + /// 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. + /// Optional parameters for agent invocation. + /// The to monitor for cancellation requests. The default is . + /// A containing the list of items. + public virtual async Task RunAsync(IEnumerable 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; + } + + /// + public sealed override AgentThread GetNewThread() + { + return this._chatClientAgent.GetNewThread(); + } + + /// + public sealed override Task RunAsync(IReadOnlyCollection messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return this._chatClientAgent.RunAsync(messages, thread, options, cancellationToken); + } + + /// + public sealed override IAsyncEnumerable RunStreamingAsync(IReadOnlyCollection messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return this._chatClientAgent.RunStreamingAsync(messages, thread, options, cancellationToken); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientExtensions.cs new file mode 100644 index 000000000..90e279eab --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientExtensions.cs @@ -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; + +/// +/// Provides extension methods for +/// to simplify the creation of AI agents that work with OpenAI services. +/// +/// +/// 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 instances and then wrap them +/// in objects that implement the interface. +/// +public static class OpenAIChatClientExtensions +{ + /// + /// Creates an AI agent from an using the OpenAI Chat Completion API. + /// + /// The OpenAI to use for the agent. + /// Optional system instructions that define the agent's behavior and personality. + /// Optional name for the agent for identification purposes. + /// Optional description of the agent's capabilities and purpose. + /// Optional collection of AI tools that the agent can use during conversations. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Chat Completion service. + /// Thrown when is . + public static AIAgent CreateAIAgent(this ChatClient client, string? instructions = null, string? name = null, string? description = null, IList? 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); + } + + /// + /// Creates an AI agent from an using the OpenAI Chat Completion API. + /// + /// The OpenAI to use for the agent. + /// Full set of options to configure the agent. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Chat Completion service. + /// Thrown when or is . + 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; + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIResponseClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIResponseClientExtensions.cs new file mode 100644 index 000000000..37f27f304 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIResponseClientExtensions.cs @@ -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; + +/// +/// Provides extension methods for +/// to simplify the creation of AI agents that work with OpenAI services. +/// +/// +/// 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 instances and then wrap them +/// in objects that implement the interface. +/// +public static class OpenAIResponseClientExtensions +{ + /// + /// Creates an AI agent from an using the OpenAI Response API. + /// + /// The to use for the agent. + /// Optional system instructions that define the agent's behavior and personality. + /// Optional name for the agent for identification purposes. + /// Optional description of the agent's capabilities and purpose. + /// Optional collection of AI tools that the agent can use during conversations. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Response service. + /// Thrown when is . + public static AIAgent CreateAIAgent(this OpenAIResponseClient client, string? instructions = null, string? name = null, string? description = null, IList? 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); + } + + /// + /// Creates an AI agent from an using the OpenAI Response API. + /// + /// The to use for the agent. + /// Full set of options to configure the agent. + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the OpenAI Response service. + /// Thrown when or is . + 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; + } +}