From d54edf20c901872f508dacf4ae27c3c4e63979f8 Mon Sep 17 00:00:00 2001
From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 3 Sep 2025 17:42:08 +0100
Subject: [PATCH] .NET: Getting started samples which use OpenAI exchange types
(#598)
* Getting started samples which use OpenAI exchange types
* Update dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseUpdateExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseUpdateExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/StreamingUpdatePipelineResponse.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update dotnet/samples/GettingStarted/AgentWithOpenAI/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Fix pipeline response
* Update dotnet/samples/GettingStarted/AgentWithOpenAI/README.md
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
* Update comment to reflect OpenAI backend usage
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
---
dotnet/agent-framework-dotnet.slnx | 3 +
.../Agent_OpenAI_Step01_Running.csproj | 22 +++++
.../Agent_OpenAI_Step01_Running/Program.cs | 36 ++++++++
.../GettingStarted/AgentWithOpenAI/README.md | 14 +++
.../AsyncStreamingUpdateCollectionResult.cs | 35 ++++++++
.../StreamingUpdatePipelineResponse.cs | 86 +++++++++++++++++++
.../AIAgentWithOpenAIExtensions.cs | 54 ++++++++++++
.../AgentRunResponseExtensions.cs | 0
.../AgentRunResponseUpdateExtensions.cs | 51 +++++++++++
.../{ => Extensions}/ChatOptionsExtensions.cs | 0
.../OpenAIAssistantClientExtensions.cs | 0
.../OpenAIChatClientExtensions.cs | 0
.../OpenAIResponseClientExtensions.cs | 0
13 files changed, 301 insertions(+)
create mode 100644 dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj
create mode 100644 dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Program.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithOpenAI/README.md
create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/AsyncStreamingUpdateCollectionResult.cs
create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/ChatCompletion/StreamingUpdatePipelineResponse.cs
rename dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/{ => Extensions}/AIAgentWithOpenAIExtensions.cs (79%)
rename dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/{ => Extensions}/AgentRunResponseExtensions.cs (100%)
create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/Extensions/AgentRunResponseUpdateExtensions.cs
rename dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/{ => Extensions}/ChatOptionsExtensions.cs (100%)
rename dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/{ => Extensions}/OpenAIAssistantClientExtensions.cs (100%)
rename dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/{ => Extensions}/OpenAIChatClientExtensions.cs (100%)
rename dotnet/src/Microsoft.Extensions.AI.Agents.OpenAI/{ => Extensions}/OpenAIResponseClientExtensions.cs (100%)
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