diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 390bdd52bb..6e5ba32c90 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -41,6 +41,9 @@ + + + diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj new file mode 100644 index 0000000000..718aa6650b --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + 12 + + enable + disable + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Program.cs b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Program.cs new file mode 100644 index 0000000000..6625bdad2c --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with OpenAI as the backend. + +using System; +using System.ClientModel; +using System.Linq; +using Microsoft.Extensions.AI.Agents; +using OpenAI; +using OpenAI.Chat; + +var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set."); +var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini"; + +const string JokerName = "Joker"; +const string JokerInstructions = "You are good at telling jokes."; + +AIAgent agent = new OpenAIClient(apiKey) + .GetChatClient(model) + .CreateAIAgent(JokerInstructions, JokerName); + +UserChatMessage chatMessage = new("Tell me a joke about a pirate."); + +// Invoke the agent and output the text result. +ChatCompletion chatCompletion = await agent.RunAsync(chatMessage); +Console.WriteLine(chatCompletion.Content.Last().Text); + +// Invoke the agent with streaming support. +AsyncCollectionResult completionUpdates = agent.RunStreamingAsync(chatMessage); +await foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates) +{ + if (completionUpdate.ContentUpdate.Count > 0) + { + Console.WriteLine(completionUpdate.ContentUpdate[0].Text); + } +} diff --git a/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md b/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md new file mode 100644 index 0000000000..4ed609ae81 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithOpenAI/README.md @@ -0,0 +1,14 @@ +# Agent Framework with OpenAI + +These samples show how to use the Agent Framework with the OpenAI exchange types. + +By default, the .Net version of Agent Framework uses the [Microsoft.Extensions.AI.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/) exchange types. + +For developers who are using the [OpenAI SDK](https://www.nuget.org/packages/OpenAI) this can be problematic because there are conflicting exchange types which can cause confusion. + +Agent Framework provides additional support to allow OpenAI developers to use the OpenAI exchange types. + +|Sample|Description| +|---|---| +|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent instructions with native OpenAI SDK types.| + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/AsyncStreamingUpdateCollectionResult.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/AsyncStreamingUpdateCollectionResult.cs new file mode 100644 index 0000000000..e398a7d481 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/AsyncStreamingUpdateCollectionResult.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using OpenAI.Chat; + +namespace Microsoft.Extensions.AI.Agents.OpenAI.ChatCompletion; + +internal sealed class AsyncStreamingUpdateCollectionResult : AsyncCollectionResult +{ + private readonly IAsyncEnumerable _updates; + + internal AsyncStreamingUpdateCollectionResult(IAsyncEnumerable updates) + { + this._updates = updates; + } + + public override ContinuationToken? GetContinuationToken(ClientResult page) => null; + + public override IAsyncEnumerable GetRawPagesAsync() + { +#pragma warning disable CA2000 // Dispose objects before losing scope + return AsyncEnumerable.Repeat(ClientResult.FromValue(this._updates, new StreamingUpdatePipelineResponse(this._updates)), 1); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + protected async override IAsyncEnumerable GetValuesFromPageAsync(ClientResult page) + { + var updates = ((ClientResult>)page).Value; + + await foreach (var update in updates.ConfigureAwait(false)) + { + yield return update.AsStreamingChatCompletionUpdate(); + } + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/StreamingUpdatePipelineResponse.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/StreamingUpdatePipelineResponse.cs new file mode 100644 index 0000000000..70e3967445 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/StreamingUpdatePipelineResponse.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; + +namespace Microsoft.Extensions.AI.Agents.OpenAI.ChatCompletion; + +internal sealed class StreamingUpdatePipelineResponse : PipelineResponse +{ + /// + /// Gets the HTTP status code. For streaming responses, this is typically 200. + /// + public override int Status => 200; + + /// + /// Gets the reason phrase. For streaming responses, this is typically "OK". + /// + public override string ReasonPhrase => "OK"; + + /// + /// Streaming responses do not support direct content stream access. + /// + public override Stream? ContentStream + { + get => null; + set { /* no-op */ } + } + + /// + /// Streaming responses do not support direct content access. + /// + public override BinaryData Content => BinaryData.FromString(string.Empty); + + /// + /// Streaming responses do not have headers. + /// + protected override PipelineResponseHeaders HeadersCore => new EmptyPipelineResponseHeaders(); + + /// + /// Buffering content is not supported for streaming responses. + /// + public override BinaryData BufferContent(CancellationToken cancellationToken = default) + { + throw new NotSupportedException("Buffering content is not supported for streaming responses."); + } + + /// + /// Buffering content asynchronously is not supported for streaming responses. + /// + public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) + { + throw new NotSupportedException("Buffering content asynchronously is not supported for streaming responses."); + } + + /// + /// Disposes resources. No resources to dispose for streaming response. + /// + public override void Dispose() + { + // No resources to dispose. + } + + internal StreamingUpdatePipelineResponse(IAsyncEnumerable updates) + { + this._updates = updates; + } + + private readonly IAsyncEnumerable _updates; + + private sealed class EmptyPipelineResponseHeaders : PipelineResponseHeaders + { + public override bool TryGetValue(string name, out string? value) + { + value = null; + return false; + } + public override bool TryGetValues(string name, out IEnumerable? values) + { + values = null; + return false; + } + public override IEnumerator> GetEnumerator() + { + yield break; + } + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AIAgentWithOpenAIExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AIAgentWithOpenAIExtensions.cs similarity index 79% rename from dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AIAgentWithOpenAIExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AIAgentWithOpenAIExtensions.cs index dc23ad1df3..c1a8373cfd 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AIAgentWithOpenAIExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AIAgentWithOpenAIExtensions.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel; using System.Text; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.OpenAI.ChatCompletion; using Microsoft.Shared.Diagnostics; using OpenAI.Chat; @@ -74,6 +76,58 @@ public static class AIAgentWithOpenAIExtensions return chatCompletion; } + /// + /// Runs the AI agent with a single OpenAI chat message and returns the response as collection of 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 AsyncCollectionResult RunStreamingAsync(this AIAgent agent, OpenAI.Chat.ChatMessage message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + Throw.IfNull(agent); + Throw.IfNull(message); + + IAsyncEnumerable response = agent.RunStreamingAsync(message.AsChatMessage(), thread, options, cancellationToken); + + return new AsyncStreamingUpdateCollectionResult(response); + } + + /// + /// Runs the AI agent with a single OpenAI chat message and returns the response as collection of 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 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 AsyncCollectionResult RunStreamingAsync(this AIAgent agent, IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + Throw.IfNull(agent); + Throw.IfNull(messages); + + IAsyncEnumerable response = agent.RunStreamingAsync([.. messages.AsChatMessages()], thread, options, cancellationToken); + + return new AsyncStreamingUpdateCollectionResult(response); + } + /// /// Creates a sequence of instances from the specified OpenAI input messages. /// diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AgentRunResponseExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseExtensions.cs similarity index 100% rename from dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/AgentRunResponseExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseExtensions.cs diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseUpdateExtensions.cs new file mode 100644 index 0000000000..9120688cde --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseUpdateExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Shared.Diagnostics; +using OpenAI.Chat; + +namespace Microsoft.Extensions.AI.Agents.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 AgentRunResponseUpdateExtensions +{ + /// + /// 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, + /// or when the nested is not a object. + /// This typically occurs when the agent response was not generated by an OpenAI streaming 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 StreamingChatCompletionUpdate AsStreamingChatCompletionUpdate(this AgentRunResponseUpdate agentResponseUpdate) + { + Throw.IfNull(agentResponseUpdate); + + if (agentResponseUpdate.RawRepresentation is ChatResponseUpdate chatResponseUpdate) + { + return chatResponseUpdate.RawRepresentation is StreamingChatCompletionUpdate streamingChatCompletionUpdate + ? streamingChatCompletionUpdate + : throw new ArgumentException("ChatResponseUpdate.RawRepresentation must be a StreamingChatCompletionUpdate"); + } + throw new ArgumentException("AgentRunResponseUpdate.RawRepresentation must be a ChatResponseUpdate"); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatOptionsExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/ChatOptionsExtensions.cs similarity index 100% rename from dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatOptionsExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/ChatOptionsExtensions.cs diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs similarity index 100% rename from dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIAssistantClientExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/OpenAIChatClientExtensions.cs similarity index 100% rename from dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIChatClientExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/OpenAIChatClientExtensions.cs diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIResponseClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/OpenAIResponseClientExtensions.cs similarity index 100% rename from dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/OpenAIResponseClientExtensions.cs rename to dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/OpenAIResponseClientExtensions.cs