.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>
This commit is contained in:
Mark Wallace
2025-09-03 17:42:08 +01:00
committed by GitHub
Unverified
parent 24ad03af6f
commit d54edf20c9
13 changed files with 301 additions and 0 deletions
+3
View File
@@ -41,6 +41,9 @@
<Project Path="samples/GettingStarted/Agents/Agent_Step08_Telemetry/Agent_Step08_Telemetry.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step09_DependencyInjection/Agent_Step09_DependencyInjection.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Telemetry/">
<Project Path="samples/GettingStarted/AgentOpenTelemetry/AgentOpenTelemetry.csproj" />
</Folder>
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
</Project>
@@ -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<StreamingChatCompletionUpdate> completionUpdates = agent.RunStreamingAsync(chatMessage);
await foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
{
if (completionUpdate.ContentUpdate.Count > 0)
{
Console.WriteLine(completionUpdate.ContentUpdate[0].Text);
}
}
@@ -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.|
@@ -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<StreamingChatCompletionUpdate>
{
private readonly IAsyncEnumerable<AgentRunResponseUpdate> _updates;
internal AsyncStreamingUpdateCollectionResult(IAsyncEnumerable<AgentRunResponseUpdate> updates)
{
this._updates = updates;
}
public override ContinuationToken? GetContinuationToken(ClientResult page) => null;
public override IAsyncEnumerable<ClientResult> 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<StreamingChatCompletionUpdate> GetValuesFromPageAsync(ClientResult page)
{
var updates = ((ClientResult<IAsyncEnumerable<AgentRunResponseUpdate>>)page).Value;
await foreach (var update in updates.ConfigureAwait(false))
{
yield return update.AsStreamingChatCompletionUpdate();
}
}
}
@@ -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
{
/// <summary>
/// Gets the HTTP status code. For streaming responses, this is typically 200.
/// </summary>
public override int Status => 200;
/// <summary>
/// Gets the reason phrase. For streaming responses, this is typically "OK".
/// </summary>
public override string ReasonPhrase => "OK";
/// <summary>
/// Streaming responses do not support direct content stream access.
/// </summary>
public override Stream? ContentStream
{
get => null;
set { /* no-op */ }
}
/// <summary>
/// Streaming responses do not support direct content access.
/// </summary>
public override BinaryData Content => BinaryData.FromString(string.Empty);
/// <summary>
/// Streaming responses do not have headers.
/// </summary>
protected override PipelineResponseHeaders HeadersCore => new EmptyPipelineResponseHeaders();
/// <summary>
/// Buffering content is not supported for streaming responses.
/// </summary>
public override BinaryData BufferContent(CancellationToken cancellationToken = default)
{
throw new NotSupportedException("Buffering content is not supported for streaming responses.");
}
/// <summary>
/// Buffering content asynchronously is not supported for streaming responses.
/// </summary>
public override ValueTask<BinaryData> BufferContentAsync(CancellationToken cancellationToken = default)
{
throw new NotSupportedException("Buffering content asynchronously is not supported for streaming responses.");
}
/// <summary>
/// Disposes resources. No resources to dispose for streaming response.
/// </summary>
public override void Dispose()
{
// No resources to dispose.
}
internal StreamingUpdatePipelineResponse(IAsyncEnumerable<AgentRunResponseUpdate> updates)
{
this._updates = updates;
}
private readonly IAsyncEnumerable<AgentRunResponseUpdate> _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<string>? values)
{
values = null;
return false;
}
public override IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
yield break;
}
}
}
@@ -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;
}
/// <summary>
/// Runs the AI agent with a single OpenAI chat message and returns the response as collection of native OpenAI <see cref="StreamingChatCompletionUpdate"/>.
/// </summary>
/// <param name="agent">The AI agent to run.</param>
/// <param name="message">The OpenAI chat message to send to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided message and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="Task{ChatCompletion}"/> representing the asynchronous operation that returns a native OpenAI <see cref="ChatCompletion"/> response.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> or <paramref name="message"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to a <see cref="ChatCompletion"/>, typically when the underlying representation is not an OpenAI response.</exception>
/// <exception cref="NotSupportedException">Thrown when the <paramref name="message"/> type is not supported by the message conversion method.</exception>
/// <remarks>
/// This method converts the OpenAI chat message to the Microsoft Extensions AI format using the appropriate conversion method,
/// runs the agent, and then extracts the native OpenAI <see cref="ChatCompletion"/> from the response using <see cref="AgentRunResponseExtensions.AsChatCompletion"/>.
/// </remarks>
public static AsyncCollectionResult<StreamingChatCompletionUpdate> RunStreamingAsync(this AIAgent agent, OpenAI.Chat.ChatMessage message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(agent);
Throw.IfNull(message);
IAsyncEnumerable<AgentRunResponseUpdate> response = agent.RunStreamingAsync(message.AsChatMessage(), thread, options, cancellationToken);
return new AsyncStreamingUpdateCollectionResult(response);
}
/// <summary>
/// Runs the AI agent with a single OpenAI chat message and returns the response as collection of native OpenAI <see cref="StreamingChatCompletionUpdate"/>.
/// </summary>
/// <param name="agent">The AI agent to run.</param>
/// <param name="messages">The collection of OpenAI chat messages to send to the agent.</param>
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided message and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="Task{ChatCompletion}"/> representing the asynchronous operation that returns a native OpenAI <see cref="ChatCompletion"/> response.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> or <paramref name="messages"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when the agent's response cannot be converted to a <see cref="ChatCompletion"/>, typically when the underlying representation is not an OpenAI response.</exception>
/// <exception cref="NotSupportedException">Thrown when the <paramref name="messages"/> type is not supported by the message conversion method.</exception>
/// <remarks>
/// This method converts the OpenAI chat message to the Microsoft Extensions AI format using the appropriate conversion method,
/// runs the agent, and then extracts the native OpenAI <see cref="ChatCompletion"/> from the response using <see cref="AgentRunResponseExtensions.AsChatCompletion"/>.
/// </remarks>
public static AsyncCollectionResult<StreamingChatCompletionUpdate> RunStreamingAsync(this AIAgent agent, IEnumerable<OpenAI.Chat.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(agent);
Throw.IfNull(messages);
IAsyncEnumerable<AgentRunResponseUpdate> response = agent.RunStreamingAsync([.. messages.AsChatMessages()], thread, options, cancellationToken);
return new AsyncStreamingUpdateCollectionResult(response);
}
/// <summary>
/// Creates a sequence of <see cref="Microsoft.Extensions.AI.ChatMessage"/> instances from the specified OpenAI input messages.
/// </summary>
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Shared.Diagnostics;
using OpenAI.Chat;
namespace Microsoft.Extensions.AI.Agents.OpenAI;
/// <summary>
/// Provides extension methods for <see cref="AgentRunResponseUpdate"/> to extract native OpenAI response objects
/// from the Microsoft Extensions AI Agent framework responses.
/// </summary>
/// <remarks>
/// These extensions enable developers to access the underlying OpenAI SDK objects when working with
/// AI agents that are backed by OpenAI services. The methods extract strongly-typed OpenAI responses
/// from the <see cref="AgentRunResponseUpdate.RawRepresentation"/> property, providing a bridge between
/// the Microsoft Extensions AI framework and the native OpenAI SDK types.
/// </remarks>
public static class AgentRunResponseUpdateExtensions
{
/// <summary>
/// Extracts a native OpenAI <see cref="StreamingChatCompletionUpdate"/> object from an <see cref="AgentRunResponseUpdate"/>.
/// </summary>
/// <param name="agentResponseUpdate">The agent response containing the raw OpenAI representation.</param>
/// <returns>The native OpenAI <see cref="StreamingChatCompletionUpdate"/> object.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentResponseUpdate"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// Thrown when the <see cref="AgentRunResponseUpdate.RawRepresentation"/> is not a <see cref="ChatResponseUpdate"/> object,
/// or when the nested <see cref="ChatResponseUpdate.RawRepresentation"/> is not a <see cref="StreamingChatCompletionUpdate"/> 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.
/// </exception>
/// <remarks>
/// <para>
/// This method provides access to the native OpenAI <see cref="StreamingChatCompletionUpdate"/> object that was used
/// to generate the agent response. This is useful when you need to access OpenAI-specific properties
/// or metadata that are not exposed through the Microsoft Extensions AI abstractions.
/// </para>
/// </remarks>
public static 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");
}
}