mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Enable access to hosted AIAgents via OpenAI Responses (#947)
* init * wip * wip wip wip * wip wip * open up API * enable for multiple agents * more wip * make frontend respond. * wip * not sure if proper setup * define type * cleanup * frontend streaming wip * use System.Net.ServerSentEvents * usings * reformat via ichatclient * merge main renaming + refactor * fix main merge + fix sample (a2a change) * fix sample * some rebase (not working yet) * make it at least build somehow * make non-stream work without internal types * Input without custom models * implement streaming * test frontend * enable alerts and fix * build fixes & rereview * Update dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI.Responses/Microsoft.Agents.AI.Hosting.OpenAI.Responses.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI.Responses/Utils/ResponseItemJsonConverter.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix agent discovery * rename * rename project into Microsoft.Agents.AI.Hosting.OpenAI (no responses in name) * PR address comments x1 * address PR comments x2 * correctly instantiate OpenAIResponse * address PR comments x3 * reconfigure JSON serialization & handle AOT warnings * fix build * proper ref * check update differently * correct check * exclude dotnet format diagnostics for IL2026 and IL3050 * space :) * re-review * add comments * remove unnecessary using * always take last openai response item * set responseItem Id explicitly * add agent.name validation for uri * cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
3fd5768b34
commit
d10b44d6bb
@@ -86,10 +86,11 @@ jobs:
|
||||
run: docker pull mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }}
|
||||
|
||||
# This step will run dotnet format on each of the unique csproj files and fail if any changes are made
|
||||
# exclude-diagnostics should be removed after fixes for IL2026 and IL3050 are out: https://github.com/dotnet/sdk/issues/51136
|
||||
- name: Run dotnet format
|
||||
if: steps.find-csproj.outputs.csproj_files != ''
|
||||
run: |
|
||||
for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
|
||||
echo "Running dotnet format on $csproj"
|
||||
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic"
|
||||
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic --exclude-diagnostics IL2026 IL3050"
|
||||
done
|
||||
|
||||
@@ -290,6 +290,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\AgentWebChat.ServiceDefaults\AgentWebChat.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A.AspNetCore;
|
||||
using AgentWebChat.AgentHost;
|
||||
using AgentWebChat.AgentHost.Utilities;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.AspNetCore;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
@@ -84,6 +86,9 @@ app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", age
|
||||
// Url = "http://localhost:5390/a2a/knights-and-knaves"
|
||||
});
|
||||
|
||||
app.MapOpenAIResponses("pirate");
|
||||
app.MapOpenAIResponses("knights-and-knaves");
|
||||
|
||||
// Map the agents HTTP endpoints
|
||||
app.MapAgentDiscovery("/agents");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
@@ -20,7 +21,10 @@ public class AgentDiscoveryClient(HttpClient httpClient, ILogger<AgentDiscoveryC
|
||||
|
||||
public class AgentDiscoveryCard
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.OpenAI\Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
<ProjectReference Include="..\AgentWebChat.ServiceDefaults\AgentWebChat.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ILogger<Home> Logger
|
||||
@inject A2AAgentClient A2AActorClient
|
||||
@inject OpenAIResponsesAgentClient OpenAIResponsesAgentClient
|
||||
@rendermode InteractiveServer
|
||||
@using System.Text
|
||||
@using System.Text.Json
|
||||
@@ -51,18 +52,18 @@
|
||||
<label for="protocol-select" class="protocol-select-label">Choose communication protocol:</label>
|
||||
<div class="protocol-select-wrapper">
|
||||
<select id="protocol-select" class="protocol-select" @bind="selectedProtocol" disabled="@(isStreaming)">
|
||||
<option value="AgenticFramework">Agentic Framework</option>
|
||||
<option value="A2A">A2A (Agent2Agent)</option>
|
||||
<option value="OpenAIResponses">OpenAI Responses</option>
|
||||
<option value="A2A">A2A (Agent-to-Agent)</option>
|
||||
</select>
|
||||
<div class="protocol-info">
|
||||
@switch (selectedProtocol)
|
||||
{
|
||||
case Protocol.A2A:
|
||||
<span class="protocol-description">🔗 A2A protocol supports long-running agentic processes</span>
|
||||
case Protocol.OpenAIResponses:
|
||||
<span class="protocol-description">֎ OpenAI Responses</span>
|
||||
break;
|
||||
case Protocol.AgenticFramework:
|
||||
default:
|
||||
<span class="protocol-description">⚡ Direct agentic framework communication</span>
|
||||
case Protocol.A2A:
|
||||
default:
|
||||
<span class="protocol-description">🔗 A2A protocol supports long-running agentic processes</span>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
@@ -881,208 +882,212 @@
|
||||
|
||||
@code {
|
||||
|
||||
private string currentMessage = "";
|
||||
private bool isStreaming = false;
|
||||
private bool isLoadingAgents = true;
|
||||
private string currentStreamedMessage = "";
|
||||
private string selectedAgentName = "";
|
||||
private List<AgentDiscoveryClient.AgentDiscoveryCard> availableAgents = new();
|
||||
private List<Conversation> conversations = new();
|
||||
private Conversation? currentConversation;
|
||||
private string currentMessage = "";
|
||||
private bool isStreaming = false;
|
||||
private bool isLoadingAgents = true;
|
||||
private string currentStreamedMessage = "";
|
||||
private string selectedAgentName = "";
|
||||
private List<AgentDiscoveryClient.AgentDiscoveryCard> availableAgents = new();
|
||||
private List<Conversation> conversations = new();
|
||||
private Conversation? currentConversation;
|
||||
|
||||
// protocol
|
||||
private Protocol selectedProtocol;
|
||||
// protocol
|
||||
private Protocol selectedProtocol;
|
||||
|
||||
// a2a agent card
|
||||
private bool isA2AExpanded = false;
|
||||
private bool isDiscoveringCard = false;
|
||||
private string? discoveredAgentCardJson = null;
|
||||
private string? discoveryError = null;
|
||||
// a2a agent card
|
||||
private bool isA2AExpanded = false;
|
||||
private bool isDiscoveringCard = false;
|
||||
private string? discoveredAgentCardJson = null;
|
||||
private string? discoveryError = null;
|
||||
|
||||
private enum Protocol
|
||||
{
|
||||
AgenticFramework,
|
||||
A2A // Agent2Agent protocol
|
||||
}
|
||||
private enum Protocol
|
||||
{
|
||||
A2A, // Agent-to-Agent protocol
|
||||
OpenAIResponses
|
||||
}
|
||||
|
||||
private sealed class Conversation
|
||||
{
|
||||
public string SessionId { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string AgentName { get; set; } = "";
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
}
|
||||
private sealed class Conversation
|
||||
{
|
||||
public string SessionId { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public string AgentName { get; set; } = "";
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Logger.LogDebug("Initializing Agent Chat component");
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Logger.LogDebug("Initializing Agent Chat component");
|
||||
|
||||
// Load agents
|
||||
try
|
||||
{
|
||||
availableAgents = await AgentClient.GetAgentsAsync();
|
||||
Logger.LogInformation("Loaded {AgentCount} agents", availableAgents.Count);
|
||||
Logger.LogInformation("Loaded Agents info: {AgentData}", JsonSerializer.Serialize(availableAgents, new JsonSerializerOptions() { WriteIndented = true }));
|
||||
// Load agents
|
||||
try
|
||||
{
|
||||
availableAgents = await AgentClient.GetAgentsAsync();
|
||||
Logger.LogInformation("Loaded {AgentCount} agents", availableAgents.Count);
|
||||
Logger.LogInformation("Loaded Agents info: {AgentData}", JsonSerializer.Serialize(availableAgents, new JsonSerializerOptions() { WriteIndented = true }));
|
||||
|
||||
// Default to first agent and start a conversation
|
||||
if (availableAgents.Any())
|
||||
{
|
||||
selectedAgentName = availableAgents.First().Name!;
|
||||
StartNewConversation();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load agents");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingAgents = false;
|
||||
}
|
||||
// Default to first agent and start a conversation
|
||||
if (availableAgents.Any())
|
||||
{
|
||||
selectedAgentName = availableAgents.First().Name!;
|
||||
StartNewConversation();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load agents");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingAgents = false;
|
||||
}
|
||||
|
||||
// Conversations start fresh on page load
|
||||
}
|
||||
// Conversations start fresh on page load
|
||||
}
|
||||
|
||||
private string GetAgentIcon(string agentName) => agentName?.ToLower() switch
|
||||
{
|
||||
"pirate" => "🏴☠️",
|
||||
"knights-and-knaves" => "⚔️",
|
||||
_ => "🤖"
|
||||
};
|
||||
private string GetAgentIcon(string agentName) => agentName?.ToLower() switch
|
||||
{
|
||||
"pirate" => "🏴☠️",
|
||||
"knights-and-knaves" => "⚔️",
|
||||
_ => "🤖"
|
||||
};
|
||||
|
||||
private string GetAgentDisplayName(string agentName) => agentName?.ToLower() switch
|
||||
{
|
||||
"pirate" => "Pirate",
|
||||
"knights-and-knaves" => "Knights & Knaves",
|
||||
_ => agentName ?? "Agent"
|
||||
};
|
||||
private string GetAgentDisplayName(string agentName) => agentName?.ToLower() switch
|
||||
{
|
||||
"pirate" => "Pirate",
|
||||
"knights-and-knaves" => "Knights & Knaves",
|
||||
_ => agentName ?? "Agent"
|
||||
};
|
||||
|
||||
private void ToggleA2AExpanded() => isA2AExpanded = !isA2AExpanded;
|
||||
private void ToggleA2AExpanded() => isA2AExpanded = !isA2AExpanded;
|
||||
|
||||
private async Task DiscoverAgentCard()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedAgentName) || isDiscoveringCard)
|
||||
return;
|
||||
private async Task DiscoverAgentCard()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedAgentName) || isDiscoveringCard)
|
||||
return;
|
||||
|
||||
isDiscoveringCard = true;
|
||||
discoveryError = null;
|
||||
discoveredAgentCardJson = null;
|
||||
StateHasChanged();
|
||||
isDiscoveringCard = true;
|
||||
discoveryError = null;
|
||||
discoveredAgentCardJson = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Discovering agent card for agent: {AgentName}", selectedAgentName);
|
||||
var agentCard = await A2AActorClient.GetAgentCardAsync(selectedAgentName);
|
||||
if (agentCard is not null)
|
||||
{
|
||||
discoveredAgentCardJson = JsonSerializer.Serialize(agentCard, new JsonSerializerOptions() { WriteIndented = true });
|
||||
Logger.LogInformation("Successfully discovered agent card for {AgentName}: {CardData}", selectedAgentName, discoveredAgentCardJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
discoveryError = "No agent card found for this agent.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to discover agent card for {AgentName}", selectedAgentName);
|
||||
discoveryError = $"Failed to discover agent card: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isDiscoveringCard = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Discovering agent card for agent: {AgentName}", selectedAgentName);
|
||||
var agentCard = await A2AActorClient.GetAgentCardAsync(selectedAgentName);
|
||||
if (agentCard is not null)
|
||||
{
|
||||
discoveredAgentCardJson = JsonSerializer.Serialize(agentCard, new JsonSerializerOptions() { WriteIndented = true });
|
||||
Logger.LogInformation("Successfully discovered agent card for {AgentName}: {CardData}", selectedAgentName, discoveredAgentCardJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
discoveryError = "No agent card found for this agent.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to discover agent card for {AgentName}", selectedAgentName);
|
||||
discoveryError = $"Failed to discover agent card: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isDiscoveringCard = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartNewConversation()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedAgentName))
|
||||
return;
|
||||
private void StartNewConversation()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedAgentName))
|
||||
return;
|
||||
|
||||
var newConversation = new Conversation
|
||||
var newConversation = new Conversation
|
||||
{
|
||||
AgentName = selectedAgentName
|
||||
};
|
||||
|
||||
conversations.Add(newConversation);
|
||||
currentConversation = newConversation;
|
||||
conversations.Add(newConversation);
|
||||
currentConversation = newConversation;
|
||||
|
||||
Logger.LogInformation("Started new conversation with agent: {AgentName}, session: {SessionId}",
|
||||
newConversation.AgentName, newConversation.SessionId);
|
||||
Logger.LogInformation("Started new conversation with agent: {AgentName}, session: {SessionId}",
|
||||
newConversation.AgentName, newConversation.SessionId);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void SelectConversation(string sessionId)
|
||||
{
|
||||
currentConversation = conversations.FirstOrDefault(c => c.SessionId == sessionId);
|
||||
if (currentConversation is not null)
|
||||
{
|
||||
selectedAgentName = currentConversation.AgentName;
|
||||
Logger.LogDebug("Selected conversation with session: {SessionId}", sessionId);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
private void SelectConversation(string sessionId)
|
||||
{
|
||||
currentConversation = conversations.FirstOrDefault(c => c.SessionId == sessionId);
|
||||
if (currentConversation is not null)
|
||||
{
|
||||
selectedAgentName = currentConversation.AgentName;
|
||||
Logger.LogDebug("Selected conversation with session: {SessionId}", sessionId);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void CloseConversation(string sessionId)
|
||||
{
|
||||
var conversationToRemove = conversations.FirstOrDefault(c => c.SessionId == sessionId);
|
||||
if (conversationToRemove is not null)
|
||||
{
|
||||
conversations.Remove(conversationToRemove);
|
||||
private void CloseConversation(string sessionId)
|
||||
{
|
||||
var conversationToRemove = conversations.FirstOrDefault(c => c.SessionId == sessionId);
|
||||
if (conversationToRemove is not null)
|
||||
{
|
||||
conversations.Remove(conversationToRemove);
|
||||
|
||||
if (currentConversation?.SessionId == sessionId)
|
||||
{
|
||||
currentConversation = conversations.FirstOrDefault();
|
||||
if (currentConversation is not null)
|
||||
{
|
||||
selectedAgentName = currentConversation.AgentName;
|
||||
}
|
||||
}
|
||||
if (currentConversation?.SessionId == sessionId)
|
||||
{
|
||||
currentConversation = conversations.FirstOrDefault();
|
||||
if (currentConversation is not null)
|
||||
{
|
||||
selectedAgentName = currentConversation.AgentName;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInformation("Closed conversation with session: {SessionId}", sessionId);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
Logger.LogInformation("Closed conversation with session: {SessionId}", sessionId);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming || currentConversation is null)
|
||||
return;
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentMessage) || isStreaming || currentConversation is null)
|
||||
return;
|
||||
|
||||
var userMessage = currentMessage.Trim();
|
||||
currentMessage = "";
|
||||
var userMessage = currentMessage.Trim();
|
||||
currentMessage = "";
|
||||
|
||||
Logger.LogInformation("User sending message: '{UserMessage}' to agent {AgentName} in session {SessionId}",
|
||||
userMessage, currentConversation.AgentName, currentConversation.SessionId);
|
||||
Logger.LogInformation("User sending message: '{UserMessage}' to agent {AgentName} in session {SessionId}",
|
||||
userMessage, currentConversation.AgentName, currentConversation.SessionId);
|
||||
|
||||
// Add user message to chat
|
||||
currentConversation.Messages.Add(new ChatMessage(ChatRole.User, userMessage));
|
||||
StateHasChanged();
|
||||
await ScrollToBottom();
|
||||
// Add user message to chat
|
||||
currentConversation.Messages.Add(new ChatMessage(ChatRole.User, userMessage));
|
||||
StateHasChanged();
|
||||
await ScrollToBottom();
|
||||
|
||||
// Start streaming response
|
||||
isStreaming = true;
|
||||
currentStreamedMessage = "";
|
||||
StateHasChanged();
|
||||
// Start streaming response
|
||||
isStreaming = true;
|
||||
currentStreamedMessage = "";
|
||||
StateHasChanged();
|
||||
|
||||
StringBuilder responseContent = new();
|
||||
var hasReceivedContent = false;
|
||||
StringBuilder responseContent = new();
|
||||
var hasReceivedContent = false;
|
||||
|
||||
using var timeoutCts = new CancellationTokenSource(
|
||||
using var timeoutCts = new CancellationTokenSource(
|
||||
#if DEBUG
|
||||
TimeSpan.FromSeconds(120)
|
||||
#else
|
||||
TimeSpan.FromSeconds(20)
|
||||
TimeSpan.FromSeconds(20)
|
||||
#endif
|
||||
);
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
// Select the appropriate client based on protocol
|
||||
// Select the appropriate client based on protocol
|
||||
IAgentClient agentClient = selectedProtocol switch
|
||||
{
|
||||
Protocol.OpenAIResponses => OpenAIResponsesAgentClient,
|
||||
Protocol.A2A or _ => A2AActorClient
|
||||
};
|
||||
|
||||
var agentClient = A2AActorClient;
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, userMessage) };
|
||||
|
||||
await foreach (var update in agentClient.RunStreamingAsync(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace AgentWebChat.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Is a simple frontend client which exercises the ability of exposed agent to communicate via OpenAI Responses protocol.
|
||||
/// </summary>
|
||||
internal sealed class OpenAIResponsesAgentClient : IAgentClient
|
||||
{
|
||||
private readonly Uri _baseUri;
|
||||
|
||||
public OpenAIResponsesAgentClient(string baseUri)
|
||||
{
|
||||
this._baseUri = new Uri(baseUri.TrimEnd('/'));
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
string agentName,
|
||||
IList<ChatMessage> messages,
|
||||
string? threadId = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
OpenAIClientOptions options = new()
|
||||
{
|
||||
Endpoint = new Uri(this._baseUri, $"/{agentName}/v1/")
|
||||
};
|
||||
|
||||
var openAiClient = new OpenAIResponseClient(model: "myModel!", credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
|
||||
var chatOptions = new ChatOptions()
|
||||
{
|
||||
ConversationId = threadId
|
||||
};
|
||||
|
||||
await foreach (var update in openAiClient.GetStreamingResponseAsync(messages, chatOptions, cancellationToken: cancellationToken))
|
||||
{
|
||||
yield return new AgentRunResponseUpdate(update);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<AgentCard?>(null!);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ Uri a2aAddress = new("http://localhost:5390/a2a");
|
||||
|
||||
builder.Services.AddHttpClient<AgentDiscoveryClient>(client => client.BaseAddress = baseAddress);
|
||||
builder.Services.AddSingleton(sp => new A2AAgentClient(sp.GetRequiredService<ILogger<A2AAgentClient>>(), a2aAddress));
|
||||
builder.Services.AddSingleton(sp => new OpenAIResponsesAgentClient("http://localhost:5390"));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for mapping OpenAI Responses capabilities to an <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
public static class EndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps OpenAI Responses API endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the OpenAI Responses endpoints to.</param>
|
||||
/// <param name="agentName">The name of the AI agent service registered in the dependency injection container. This name is used to resolve the <see cref="AIAgent"/> instance from the keyed services.</param>
|
||||
/// <param name="responsesPath">Custom route path for the responses endpoint.</param>
|
||||
/// <param name="conversationsPath">Custom route path for the conversations endpoint.</param>
|
||||
public static void MapOpenAIResponses(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
string agentName,
|
||||
[StringSyntax("Route")] string? responsesPath = null,
|
||||
[StringSyntax("Route")] string? conversationsPath = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(agentName);
|
||||
if (responsesPath is null || conversationsPath is null)
|
||||
{
|
||||
ValidateAgentName(agentName);
|
||||
}
|
||||
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
|
||||
responsesPath ??= $"/{agentName}/v1/responses";
|
||||
var responsesRouteGroup = endpoints.MapGroup(responsesPath);
|
||||
MapResponses(responsesRouteGroup, agent);
|
||||
|
||||
// Will be included once we obtain the API to operate with thread (conversation).
|
||||
|
||||
// conversationsPath ??= $"/{agentName}/v1/conversations";
|
||||
// var conversationsRouteGroup = endpoints.MapGroup(conversationsPath);
|
||||
// MapConversations(conversationsRouteGroup, agent, loggerFactory);
|
||||
}
|
||||
|
||||
private static void MapResponses(IEndpointRouteBuilder routeGroup, AIAgent agent)
|
||||
{
|
||||
var endpointAgentName = agent.DisplayName;
|
||||
var responsesProcessor = new AIAgentResponsesProcessor(agent);
|
||||
|
||||
routeGroup.MapPost("/", async (HttpContext requestContext, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var requestBinary = await BinaryData.FromStreamAsync(requestContext.Request.Body, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var responseOptions = new ResponseCreationOptions();
|
||||
var responseOptionsJsonModel = responseOptions as IJsonModel<ResponseCreationOptions>;
|
||||
Debug.Assert(responseOptionsJsonModel is not null);
|
||||
|
||||
responseOptions = responseOptionsJsonModel.Create(requestBinary, ModelReaderWriterOptions.Json);
|
||||
if (responseOptions is null)
|
||||
{
|
||||
return Results.BadRequest("Invalid request payload.");
|
||||
}
|
||||
|
||||
return await responsesProcessor.CreateModelResponseAsync(responseOptions, cancellationToken).ConfigureAwait(false);
|
||||
}).WithName(endpointAgentName + "/CreateResponse");
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0051 // Remove unused private members
|
||||
private static void MapConversations(IEndpointRouteBuilder routeGroup, AIAgent agent)
|
||||
#pragma warning restore IDE0051 // Remove unused private members
|
||||
{
|
||||
var endpointAgentName = agent.DisplayName;
|
||||
var conversationsProcessor = new AIAgentConversationsProcessor(agent);
|
||||
|
||||
routeGroup.MapGet("/{conversation_id}", (string conversationId, CancellationToken cancellationToken)
|
||||
=> conversationsProcessor.GetConversationAsync(conversationId, cancellationToken)
|
||||
).WithName(endpointAgentName + "/RetrieveConversation");
|
||||
}
|
||||
|
||||
private static void ValidateAgentName([NotNull] string agentName)
|
||||
{
|
||||
var escaped = Uri.EscapeDataString(agentName);
|
||||
if (!string.Equals(escaped, agentName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException($"Agent name '{agentName}' contains characters invalid for URL routes.", nameof(agentName));
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsCoreTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006;IDE0130;NU1504;OPENAI001</NoWarn>
|
||||
<RootNamespace>Microsoft.Agents.AI.Hosting.OpenAI</RootNamespace>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsNamespaces>
|
||||
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.1.25451.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
||||
|
||||
internal sealed class AIAgentConversationsProcessor
|
||||
{
|
||||
#pragma warning disable IDE0052 // Remove unread private members
|
||||
private readonly AIAgent _aiAgent;
|
||||
#pragma warning restore IDE0052 // Remove unread private members
|
||||
|
||||
public AIAgentConversationsProcessor(AIAgent aiAgent)
|
||||
{
|
||||
this._aiAgent = aiAgent ?? throw new ArgumentNullException(nameof(aiAgent));
|
||||
}
|
||||
|
||||
public async Task<IResult> GetConversationAsync(string conversationId, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO come back to it later
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Model;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI Responses processor associated with a specific <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
internal sealed class AIAgentResponsesProcessor
|
||||
{
|
||||
private readonly AIAgent _agent;
|
||||
|
||||
public AIAgentResponsesProcessor(AIAgent agent)
|
||||
{
|
||||
this._agent = agent ?? throw new ArgumentNullException(nameof(agent));
|
||||
}
|
||||
|
||||
public async Task<IResult> CreateModelResponseAsync(ResponseCreationOptions responseCreationOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = new OpenAIResponsesRunOptions();
|
||||
AgentThread? agentThread = null; // not supported to resolve from conversationId
|
||||
|
||||
var inputItems = responseCreationOptions.GetInput();
|
||||
var chatMessages = inputItems.AsChatMessages();
|
||||
|
||||
if (responseCreationOptions.GetStream())
|
||||
{
|
||||
return new OpenAIStreamingResponsesResult(this._agent, chatMessages);
|
||||
}
|
||||
|
||||
var agentResponse = await this._agent.RunAsync(chatMessages, agentThread, options, cancellationToken).ConfigureAwait(false);
|
||||
return new OpenAIResponseResult(agentResponse);
|
||||
}
|
||||
|
||||
private sealed class OpenAIResponseResult(AgentRunResponse agentResponse) : IResult
|
||||
{
|
||||
public async Task ExecuteAsync(HttpContext httpContext)
|
||||
{
|
||||
// note: OpenAI SDK types provide their own serialization implementation
|
||||
// so we cant simply return IResult wrap for the typed-object.
|
||||
// instead writing to the response body can be done.
|
||||
|
||||
var cancellationToken = httpContext.RequestAborted;
|
||||
var response = httpContext.Response;
|
||||
|
||||
var chatResponse = agentResponse.AsChatResponse();
|
||||
var openAIResponse = chatResponse.AsOpenAIResponse();
|
||||
var openAIResponseJsonModel = openAIResponse as IJsonModel<OpenAIResponse>;
|
||||
Debug.Assert(openAIResponseJsonModel is not null);
|
||||
|
||||
var writer = new Utf8JsonWriter(response.BodyWriter, new JsonWriterOptions { SkipValidation = false });
|
||||
openAIResponseJsonModel.Write(writer, ModelReaderWriterOptions.Json);
|
||||
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OpenAIStreamingResponsesResult(AIAgent agent, IEnumerable<ChatMessage> chatMessages) : IResult
|
||||
{
|
||||
public Task ExecuteAsync(HttpContext httpContext)
|
||||
{
|
||||
var cancellationToken = httpContext.RequestAborted;
|
||||
var response = httpContext.Response;
|
||||
|
||||
// Set SSE headers
|
||||
response.Headers.ContentType = "text/event-stream";
|
||||
response.Headers.CacheControl = "no-cache,no-store";
|
||||
response.Headers.Connection = "keep-alive";
|
||||
response.Headers.ContentEncoding = "identity";
|
||||
httpContext.Features.GetRequiredFeature<IHttpResponseBodyFeature>().DisableBuffering();
|
||||
|
||||
return SseFormatter.WriteAsync(
|
||||
source: this.GetStreamingResponsesAsync(cancellationToken),
|
||||
destination: response.Body,
|
||||
itemFormatter: (sseItem, bufferWriter) =>
|
||||
{
|
||||
var jsonTypeInfo = OpenAIResponsesJsonUtilities.DefaultOptions.GetTypeInfo(sseItem.Data.GetType());
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(sseItem.Data, jsonTypeInfo);
|
||||
bufferWriter.Write(json);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SseItem<StreamingResponseEventBase>> GetStreamingResponsesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sequenceNumber = 1;
|
||||
var outputIndex = 1;
|
||||
AgentThread? agentThread = null;
|
||||
|
||||
ResponseItem? lastResponseItem = null;
|
||||
OpenAIResponse? lastOpenAIResponse = null;
|
||||
|
||||
await foreach (var update in agent.RunStreamingAsync(chatMessages, thread: agentThread, cancellationToken: cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (string.IsNullOrEmpty(update.ResponseId)
|
||||
&& string.IsNullOrEmpty(update.MessageId)
|
||||
&& update.Contents is not { Count: > 0 })
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sequenceNumber == 1)
|
||||
{
|
||||
lastOpenAIResponse = update.AsChatResponse().AsOpenAIResponse();
|
||||
|
||||
var responseCreated = new StreamingCreatedResponse(sequenceNumber++)
|
||||
{
|
||||
Response = lastOpenAIResponse
|
||||
};
|
||||
yield return new(responseCreated, responseCreated.Type);
|
||||
}
|
||||
|
||||
if (update.Contents is not { Count: > 0 })
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// to help convert the AIContent into OpenAI ResponseItem we pack it into the known "chatMessage"
|
||||
// and use existing convertion extension method
|
||||
var chatMessage = new ChatMessage(ChatRole.Assistant, update.Contents)
|
||||
{
|
||||
MessageId = update.MessageId,
|
||||
CreatedAt = update.CreatedAt,
|
||||
RawRepresentation = update.RawRepresentation
|
||||
};
|
||||
|
||||
foreach (var openAIResponseItem in MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseItems([chatMessage]))
|
||||
{
|
||||
if (chatMessage.MessageId is not null)
|
||||
{
|
||||
openAIResponseItem.SetId(chatMessage.MessageId);
|
||||
}
|
||||
|
||||
lastResponseItem = openAIResponseItem;
|
||||
|
||||
var responseOutputItemAdded = new StreamingOutputItemAddedResponse(sequenceNumber++)
|
||||
{
|
||||
OutputIndex = outputIndex++,
|
||||
Item = openAIResponseItem
|
||||
};
|
||||
yield return new(responseOutputItemAdded, responseOutputItemAdded.Type);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastResponseItem is not null)
|
||||
{
|
||||
// we were streaming "response.output_item.added" before
|
||||
// so we should complete it now via "response.output_item.done"
|
||||
var responseOutputDoneAdded = new StreamingOutputItemDoneResponse(sequenceNumber++)
|
||||
{
|
||||
OutputIndex = outputIndex++,
|
||||
Item = lastResponseItem
|
||||
};
|
||||
yield return new(responseOutputDoneAdded, responseOutputDoneAdded.Type);
|
||||
}
|
||||
|
||||
if (lastOpenAIResponse is not null)
|
||||
{
|
||||
// complete the whole streaming with the full response model
|
||||
var responseCompleted = new StreamingCompletedResponse(sequenceNumber++)
|
||||
{
|
||||
Response = lastOpenAIResponse
|
||||
};
|
||||
yield return new(responseCompleted, responseCompleted.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for all streaming response events in the OpenAI Responses API.
|
||||
/// Provides common properties shared across all streaming event types.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
|
||||
[JsonDerivedType(typeof(StreamingOutputItemAddedResponse), StreamingOutputItemAddedResponse.EventType)]
|
||||
[JsonDerivedType(typeof(StreamingOutputItemDoneResponse), StreamingOutputItemDoneResponse.EventType)]
|
||||
[JsonDerivedType(typeof(StreamingCreatedResponse), StreamingCreatedResponse.EventType)]
|
||||
[JsonDerivedType(typeof(StreamingCompletedResponse), StreamingCompletedResponse.EventType)]
|
||||
internal abstract class StreamingResponseEventBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type identifier for the streaming response event.
|
||||
/// This property is used to discriminate between different event types during serialization.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sequence number of this event in the streaming response.
|
||||
/// Events are numbered sequentially starting from 1 to maintain ordering.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sequence_number")]
|
||||
public int SequenceNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamingResponseEventBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type identifier for this streaming response event.</param>
|
||||
/// <param name="sequenceNumber">The sequence number of this event in the streaming response.</param>
|
||||
[JsonConstructor]
|
||||
public StreamingResponseEventBase(string type, int sequenceNumber)
|
||||
{
|
||||
this.Type = type;
|
||||
this.SequenceNumber = sequenceNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a streaming response event indicating that a new output item has been added to the response.
|
||||
/// This event is sent when the AI agent produces a new piece of content during streaming.
|
||||
/// </summary>
|
||||
internal sealed class StreamingOutputItemAddedResponse : StreamingResponseEventBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The constant event type identifier for output item added events.
|
||||
/// </summary>
|
||||
public const string EventType = "response.output_item.added";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamingOutputItemAddedResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sequenceNumber">The sequence number of this event in the streaming response.</param>
|
||||
public StreamingOutputItemAddedResponse(int sequenceNumber) : base(EventType, sequenceNumber)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the output in the response where this item was added.
|
||||
/// Multiple outputs can exist in a single response, and this identifies which one.
|
||||
/// </summary>
|
||||
[JsonPropertyName("output_index")]
|
||||
public int OutputIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response item that was added to the output.
|
||||
/// This contains the actual content or data produced by the AI agent.
|
||||
/// </summary>
|
||||
[JsonPropertyName("item")]
|
||||
public ResponseItem? Item { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a streaming response event indicating that an output item has been completed.
|
||||
/// This event is sent when the AI agent finishes producing a particular piece of content.
|
||||
/// </summary>
|
||||
internal sealed class StreamingOutputItemDoneResponse : StreamingResponseEventBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The constant event type identifier for output item done events.
|
||||
/// </summary>
|
||||
public const string EventType = "response.output_item.done";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamingOutputItemDoneResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sequenceNumber">The sequence number of this event in the streaming response.</param>
|
||||
public StreamingOutputItemDoneResponse(int sequenceNumber) : base(EventType, sequenceNumber)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the output in the response where this item was completed.
|
||||
/// This corresponds to the same output index from the associated <see cref="StreamingOutputItemAddedResponse"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("output_index")]
|
||||
public int OutputIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the completed response item.
|
||||
/// This contains the final version of the content produced by the AI agent.
|
||||
/// </summary>
|
||||
[JsonPropertyName("item")]
|
||||
public ResponseItem? Item { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a streaming response event indicating that a new response has been created and streaming has begun.
|
||||
/// This is typically the first event sent in a streaming response sequence.
|
||||
/// </summary>
|
||||
internal sealed class StreamingCreatedResponse : StreamingResponseEventBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The constant event type identifier for response created events.
|
||||
/// </summary>
|
||||
public const string EventType = "response.created";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamingCreatedResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sequenceNumber">The sequence number of this event in the streaming response.</param>
|
||||
public StreamingCreatedResponse(int sequenceNumber) : base(EventType, sequenceNumber)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the OpenAI response object that was created.
|
||||
/// This contains metadata about the response including ID, creation timestamp, and other properties.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response")]
|
||||
public required OpenAIResponse Response { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a streaming response event indicating that the response has been completed.
|
||||
/// This is typically the last event sent in a streaming response sequence.
|
||||
/// </summary>
|
||||
internal sealed class StreamingCompletedResponse : StreamingResponseEventBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The constant event type identifier for response completed events.
|
||||
/// </summary>
|
||||
public const string EventType = "response.completed";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamingCompletedResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sequenceNumber">The sequence number of this event in the streaming response.</param>
|
||||
public StreamingCompletedResponse(int sequenceNumber) : base(EventType, sequenceNumber)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the completed OpenAI response object.
|
||||
/// This contains the final state of the response including all generated content and metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response")]
|
||||
public required OpenAIResponse Response { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
||||
|
||||
internal sealed class OpenAIResponsesRunOptions : AgentRunOptions
|
||||
{
|
||||
public bool Background { get; init; }
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
|
||||
internal static class AgentRunResponseUpdateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="AgentRunResponseUpdate"/> instance to a <see cref="ChatResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see cref="AgentRunResponse"/> to convert. Cannot be null.</param>
|
||||
/// <param name="role">The role of agent run response contents. By default is <see cref="ChatRole.Assistant"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> populated with values from <paramref name="response"/>.</returns>
|
||||
public static ChatResponse AsChatResponse(this AgentRunResponseUpdate response, ChatRole? role = null) => new()
|
||||
{
|
||||
CreatedAt = response.CreatedAt,
|
||||
ResponseId = response.ResponseId,
|
||||
RawRepresentation = response.RawRepresentation,
|
||||
AdditionalProperties = response.AdditionalProperties,
|
||||
Messages = [new ChatMessage(response.Role ?? role ?? ChatRole.Assistant, response.Contents)]
|
||||
};
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
|
||||
internal sealed class OpenAIResponseJsonConverter : JsonConverter<OpenAIResponse>
|
||||
{
|
||||
public override OpenAIResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var item = OpenAIResponsesModelFactory.OpenAIResponse();
|
||||
var jsonModel = item as IJsonModel<OpenAIResponse>;
|
||||
Debug.Assert(jsonModel is not null, "OpenAIResponse should implement IJsonModel<OpenAIResponse>");
|
||||
|
||||
return jsonModel.Create(ref reader, ModelReaderWriterOptions.Json);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, OpenAIResponse value, JsonSerializerOptions options)
|
||||
{
|
||||
var jsonModel = value as IJsonModel<OpenAIResponse>;
|
||||
Debug.Assert(jsonModel is not null, "OpenAIResponse should implement IJsonModel<OpenAIResponse>");
|
||||
|
||||
jsonModel.Write(writer, ModelReaderWriterOptions.Json);
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Model;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
|
||||
internal static partial class OpenAIResponsesJsonUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
|
||||
/// includes source generated contracts for all common exchange types contained in this library.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It additionally turns on the following settings:
|
||||
/// <list type="number">
|
||||
/// <item>Enables <see cref="JsonSerializerDefaults.Web"/> defaults.</item>
|
||||
/// <item>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</item>
|
||||
/// <item>Enables <see cref="JsonNumberHandling.AllowReadingFromString"/> as the default number handling for number types.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Creates default options to use for agents-related serialization.
|
||||
/// </summary>
|
||||
/// <returns>The configured options.</returns>
|
||||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
JsonSerializerOptions options = new(JsonContext.Default.Options);
|
||||
|
||||
options.Converters.Add(new ResponseItemJsonConverter());
|
||||
options.Converters.Add(new OpenAIResponseJsonConverter());
|
||||
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(StreamingResponseEventBase))]
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed partial class JsonContext : JsonSerializerContext;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Specifically for accessing hidden members")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Specifically for accessing hidden members")]
|
||||
internal static class ResponseCreationOptionsExtensions
|
||||
{
|
||||
private static readonly Func<ResponseCreationOptions, bool?> _getStreamNullable;
|
||||
private static readonly Func<ResponseCreationOptions, IList<ResponseItem>> _getInput;
|
||||
|
||||
static ResponseCreationOptionsExtensions()
|
||||
{
|
||||
// OpenAI SDK does not have a simple way to get the input as a c# object.
|
||||
// However, it does parse most of the interesting fields into internal properties of `ResponseCreationOptions` object.
|
||||
|
||||
// --- Stream (internal bool? Stream { get; set; }) ---
|
||||
const string streamPropName = "Stream";
|
||||
var streamProp = typeof(ResponseCreationOptions).GetProperty(streamPropName, BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new MissingMemberException(typeof(ResponseCreationOptions).FullName!, streamPropName);
|
||||
var streamGetter = streamProp.GetGetMethod(nonPublic: true) ?? throw new MissingMethodException($"{streamPropName} getter not found.");
|
||||
|
||||
_getStreamNullable = streamGetter.CreateDelegate<Func<ResponseCreationOptions, bool?>>();
|
||||
|
||||
// --- Input (internal IList<ResponseItem> Input { get; set; }) ---
|
||||
const string inputPropName = "Input";
|
||||
var inputProp = typeof(ResponseCreationOptions).GetProperty(inputPropName, BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new MissingMemberException(typeof(ResponseCreationOptions).FullName!, inputPropName);
|
||||
var inputGetter = inputProp.GetGetMethod(nonPublic: true)
|
||||
?? throw new MissingMethodException($"{inputPropName} getter not found.");
|
||||
|
||||
_getInput = inputGetter.CreateDelegate<Func<ResponseCreationOptions, IList<ResponseItem>>>();
|
||||
}
|
||||
|
||||
public static bool GetStream(this ResponseCreationOptions options)
|
||||
{
|
||||
Throw.IfNull(options);
|
||||
return _getStreamNullable(options) ?? false;
|
||||
}
|
||||
|
||||
public static IList<ResponseItem> GetInput(this ResponseCreationOptions options)
|
||||
{
|
||||
Throw.IfNull(options);
|
||||
return _getInput(options);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Specifically for accessing hidden members")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Specifically for accessing hidden members")]
|
||||
internal static class ResponseItemExtensions
|
||||
{
|
||||
private static readonly Action<ResponseItem, string> _setId;
|
||||
|
||||
static ResponseItemExtensions()
|
||||
{
|
||||
// OpenAI SDK ResponseItem has an internal setter for Id property.
|
||||
// We need to access it via reflection to set the Id when creating response items.
|
||||
|
||||
// --- Id (public string Id { get; internal set; }) ---
|
||||
const string idPropName = "Id";
|
||||
var idProp = typeof(ResponseItem).GetProperty(idPropName, BindingFlags.Instance | BindingFlags.Public)
|
||||
?? throw new MissingMemberException(typeof(ResponseItem).FullName!, idPropName);
|
||||
var idSetter = idProp.GetSetMethod(nonPublic: true) ?? throw new MissingMethodException($"{idPropName} setter not found.");
|
||||
|
||||
_setId = idSetter.CreateDelegate<Action<ResponseItem, string>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Id property on a ResponseItem using reflection to access the internal setter.
|
||||
/// </summary>
|
||||
/// <param name="responseItem">The ResponseItem to set the Id on.</param>
|
||||
/// <param name="id">The Id value to set.</param>
|
||||
public static void SetId(this ResponseItem responseItem, string id)
|
||||
{
|
||||
Throw.IfNull(responseItem);
|
||||
_setId(responseItem, id);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
|
||||
|
||||
internal sealed class ResponseItemJsonConverter : JsonConverter<ResponseItem?>
|
||||
{
|
||||
public override ResponseItem? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var item = ResponseItem.CreateUserMessageItem(""); // no other way to instantiate it.
|
||||
var jsonModel = item as IJsonModel<ResponseItem>;
|
||||
Debug.Assert(jsonModel is not null, "ResponseItem should implement IJsonModel<ResponseItem>");
|
||||
return jsonModel.Create(ref reader, ModelReaderWriterOptions.Json);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ResponseItem? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonmodel = value as IJsonModel<ResponseItem>;
|
||||
Debug.Assert(jsonmodel is not null, "ResponseItem should implement IJsonModel<ResponseItem>");
|
||||
jsonmodel.Write(writer, ModelReaderWriterOptions.Json);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Sent to an <see cref="AIAgent"/>-based executor to request
|
||||
/// a response to accumulated <see cref="Extensions.AI.ChatMessage"/>.
|
||||
/// a response to accumulated <see cref="ChatMessage"/>.
|
||||
/// </summary>
|
||||
/// <param name="emitEvents">Whether to raise AgentRunEvents for this executor.</param>
|
||||
public class TurnToken(bool? emitEvents = null)
|
||||
|
||||
@@ -601,7 +601,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"""
|
||||
The {nameof(chatOptions.ConversationId)} provided via {nameof(Extensions.AI.ChatOptions)} is different to the id of the provided {nameof(AgentThread)}.
|
||||
The {nameof(chatOptions.ConversationId)} provided via {nameof(this.ChatOptions)} is different to the id of the provided {nameof(AgentThread)}.
|
||||
Only one id can be used for a run.
|
||||
""");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user