mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* Initial plan * Update code for Microsoft.Extensions.AI.Abstractions 10.4.0 breaking changes - Rename FunctionApprovalRequestContent → ToolApprovalRequestContent - Rename FunctionApprovalResponseContent → ToolApprovalResponseContent - Rename UserInputRequestContent → ToolApprovalRequestContent - Rename UserInputResponseContent → ToolApprovalResponseContent - Update .FunctionCall property → .ToolCall with FunctionCallContent casts where needed - Update .Id property → .RequestId on the renamed types - Rename FunctionApprovalRequestEventGenerator → ToolApprovalRequestEventGenerator - Rename FunctionApprovalResponseEventGenerator → ToolApprovalResponseEventGenerator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update OpenAI 2.9.1, ME.AI 10.4.0, fix breaking API changes Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix remaining ME.AI 10.4.0 breaking changes: MCP approval types, .Output→.Outputs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Use pattern matching with `when` for ToolApprovalRequestContent/FunctionCallContent Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Update Azure.AI.OpenAI to 2.9.0-beta.1 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix remaining GetResponsesClient(model) build failures for Azure.AI.OpenAI 2.9.0-beta.1 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Address review feedback: remove redundant type checks in TestRequestAgent.cs and fix error message in AIAgentHostExecutor.cs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Update Azure.AI.Projects to 2.0.0-beta.2 with namespace migration - Azure.AI.Projects 2.0.0-beta.1 → 2.0.0-beta.2 - Azure.AI.Projects.OpenAI → Azure.AI.Extensions.OpenAI (transitive) - Agent types moved to Azure.AI.Projects.Agents namespace - AgentRecord.Versions.Latest → AgentRecord.GetLatestVersion() - OpenAPIFunctionDefinition → OpenApiFunctionDefinition - BingCustomSearchToolParameters → BingCustomSearchToolOptions - MemorySearchPreviewTool.UpdateDelay → UpdateDelayInSecs - Azure.Identity 1.17.1 → 1.19.0 - Microsoft.Identity.Client.Extensions.Msal 4.78.0 → 4.83.1 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix remaining type renames for Azure.AI.Projects 2.0.0-beta.2 - BrowserAutomationToolParameters → BrowserAutomationToolOptions - MemoryUpdateOptions.UpdateDelay stays as UpdateDelay (not renamed) - WaitForMemoriesUpdateAsync parameter order: pollingInterval before options - AIProjectAgentsOperations → AgentsClient Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix format errors and OpenTelemetry test for ME.AI 10.4.0 - Remove unused 'using Azure.AI.Extensions.OpenAI' and fix import ordering in Agent_With_AzureAIProject/Program.cs - Update OpenTelemetryAgentTests: gen_ai.tool.definitions is now always emitted regardless of EnableSensitiveData per ME.AI 10.4.0 change (dotnet/extensions#7346). Tool definitions are not considered sensitive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix GetRepoFolder() to work in git worktrees Use 'workflow-samples' directory as repo root marker instead of '.git', which fails in worktrees (.git is a file) and also matches too early when a '.github' folder exists in subdirectories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix formatting: remove unused usings and fix import ordering dotnet format applied across 59 impacted projects. Primarily removes unnecessary 'using Azure.AI.Projects' where Azure.AI.Projects.Agents provides all needed types, and fixes import ordering per editorconfig. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Disable AzureAIAgentsPersistent integration tests (#4769) Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent which was removed in ME.AI 10.4.0 (renamed to ToolApprovalResponseContent), causing TypeLoadException at runtime. Mark all 6 test classes with IntegrationDisabled trait until Persistent ships a version targeting ME.AI 10.4.0+. Upstream fix: https://github.com/Azure/azure-sdk-for-net/pull/56929 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add README with compatibility note for AzureAI.Persistent (#4769) Documents that Azure.AI.Agents.Persistent 1.2.0-beta.9 is only compatible with ME.AI ≤10.3.0 and OpenAI ≤2.8.0 due to type renames in ME.AI 10.4.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix file encoding: restore UTF-8 BOM on Persistent test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Mark AzureAI.Persistent as IsPackable=false (#4769) Prevent shipping until Azure.AI.Agents.Persistent targets ME.AI 10.4.0+. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Moving IsPackable after import --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
340 lines
15 KiB
C#
340 lines
15 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
|
|
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
|
|
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
|
|
using Microsoft.Agents.AI.Workflows;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
|
|
|
|
/// <summary>
|
|
/// Extension methods for <see cref="AgentResponseUpdate"/>.
|
|
/// </summary>
|
|
internal static class AgentResponseUpdateExtensions
|
|
{
|
|
/// <summary>
|
|
/// Converts a stream of <see cref="AgentResponseUpdate"/> to stream of <see cref="StreamingResponseEvent"/>.
|
|
/// </summary>
|
|
/// <param name="updates">The agent run response updates.</param>
|
|
/// <param name="request">The create response request.</param>
|
|
/// <param name="context">The agent invocation context.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>A stream of response events.</returns>
|
|
public static async IAsyncEnumerable<StreamingResponseEvent> ToStreamingResponseAsync(
|
|
this IAsyncEnumerable<AgentResponseUpdate> updates,
|
|
CreateResponse request,
|
|
AgentInvocationContext context,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
var seq = new SequenceNumber();
|
|
var createdAt = DateTimeOffset.UtcNow;
|
|
var latestUsage = ResponseUsage.Zero;
|
|
yield return new StreamingResponseCreated { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.InProgress) };
|
|
yield return new StreamingResponseInProgress { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.InProgress) };
|
|
|
|
var outputIndex = 0;
|
|
List<ItemResource> items = [];
|
|
var updateEnumerator = updates.GetAsyncEnumerator(cancellationToken);
|
|
await using var _ = updateEnumerator.ConfigureAwait(false);
|
|
|
|
// Track active item IDs by executor ID to pair invoked/completed/failed events
|
|
Dictionary<string, string> executorItemIds = [];
|
|
|
|
AgentResponseUpdate? previousUpdate = null;
|
|
StreamingEventGenerator? generator = null;
|
|
while (await updateEnumerator.MoveNextAsync().ConfigureAwait(false))
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var update = updateEnumerator.Current;
|
|
|
|
// Special-case for agent framework workflow events.
|
|
if (update.RawRepresentation is WorkflowEvent workflowEvent)
|
|
{
|
|
// Convert executor events to standard OpenAI output_item events
|
|
if (workflowEvent is ExecutorInvokedEvent invokedEvent)
|
|
{
|
|
var itemId = IdGenerator.NewId(prefix: "item");
|
|
// Store the item ID for this executor so we can reuse it for completion/failure
|
|
executorItemIds[invokedEvent.ExecutorId] = itemId;
|
|
|
|
var item = new ExecutorActionItemResource
|
|
{
|
|
Id = itemId,
|
|
ExecutorId = invokedEvent.ExecutorId,
|
|
Status = "in_progress",
|
|
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
|
};
|
|
|
|
yield return new StreamingOutputItemAdded
|
|
{
|
|
SequenceNumber = seq.Increment(),
|
|
OutputIndex = outputIndex,
|
|
Item = item
|
|
};
|
|
}
|
|
else if (workflowEvent is ExecutorCompletedEvent completedEvent)
|
|
{
|
|
// Reuse the item ID from the invoked event, or generate a new one if not found
|
|
var itemId = executorItemIds.TryGetValue(completedEvent.ExecutorId, out var existingId)
|
|
? existingId
|
|
: IdGenerator.NewId(prefix: "item");
|
|
|
|
// Remove from tracking as this executor run is now complete
|
|
executorItemIds.Remove(completedEvent.ExecutorId);
|
|
JsonElement? resultData = null;
|
|
if (completedEvent.Data != null && JsonSerializer.IsReflectionEnabledByDefault)
|
|
{
|
|
resultData = JsonSerializer.SerializeToElement(
|
|
completedEvent.Data,
|
|
OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
|
}
|
|
|
|
var item = new ExecutorActionItemResource
|
|
{
|
|
Id = itemId,
|
|
ExecutorId = completedEvent.ExecutorId,
|
|
Status = "completed",
|
|
Result = resultData,
|
|
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
|
};
|
|
|
|
yield return new StreamingOutputItemDone
|
|
{
|
|
SequenceNumber = seq.Increment(),
|
|
OutputIndex = outputIndex,
|
|
Item = item
|
|
};
|
|
}
|
|
else if (workflowEvent is ExecutorFailedEvent failedEvent)
|
|
{
|
|
// Reuse the item ID from the invoked event, or generate a new one if not found
|
|
var itemId = executorItemIds.TryGetValue(failedEvent.ExecutorId, out var existingId)
|
|
? existingId
|
|
: IdGenerator.NewId(prefix: "item");
|
|
|
|
// Remove from tracking as this executor run has now failed
|
|
executorItemIds.Remove(failedEvent.ExecutorId);
|
|
|
|
var item = new ExecutorActionItemResource
|
|
{
|
|
Id = itemId,
|
|
ExecutorId = failedEvent.ExecutorId,
|
|
Status = "failed",
|
|
Error = failedEvent.Data?.ToString(),
|
|
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
|
};
|
|
|
|
yield return new StreamingOutputItemDone
|
|
{
|
|
SequenceNumber = seq.Increment(),
|
|
OutputIndex = outputIndex,
|
|
Item = item
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// For other workflow events (not executor-specific), keep the old format as fallback
|
|
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!IsSameMessage(update, previousUpdate))
|
|
{
|
|
// Finalize the current generator when moving to a new message.
|
|
foreach (var evt in generator?.Complete() ?? [])
|
|
{
|
|
OnEvent(evt);
|
|
yield return evt;
|
|
}
|
|
|
|
generator = null;
|
|
outputIndex++;
|
|
previousUpdate = update;
|
|
}
|
|
|
|
using var contentEnumerator = update.Contents.GetEnumerator();
|
|
while (contentEnumerator.MoveNext())
|
|
{
|
|
var content = contentEnumerator.Current;
|
|
|
|
// Usage content is handled separately.
|
|
if (content is UsageContent usageContent && usageContent.Details != null)
|
|
{
|
|
latestUsage += usageContent.Details.ToResponseUsage();
|
|
continue;
|
|
}
|
|
|
|
// Create a new generator if there is no existing one or the existing one does not support the content.
|
|
if (generator?.IsSupported(content) != true)
|
|
{
|
|
// Finalize the current generator, if there is one.
|
|
foreach (var evt in generator?.Complete() ?? [])
|
|
{
|
|
OnEvent(evt);
|
|
yield return evt;
|
|
}
|
|
|
|
// Increment output index when switching generators
|
|
if (generator is not null)
|
|
{
|
|
outputIndex++;
|
|
}
|
|
|
|
// Create a new generator based on the content type.
|
|
generator = content switch
|
|
{
|
|
TextContent => new AssistantMessageEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
TextReasoningContent => new TextReasoningContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
FunctionCallContent => new FunctionCallEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
|
|
FunctionResultContent => new FunctionResultEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
ToolApprovalRequestContent => new ToolApprovalRequestEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
|
|
ToolApprovalResponseContent => new ToolApprovalResponseEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
ErrorContent => new ErrorContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
UriContent uriContent when uriContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
HostedFileContent => new HostedFileContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
DataContent => new FileContentEventGenerator(context.IdGenerator, seq, outputIndex),
|
|
_ => null
|
|
};
|
|
|
|
// If no generator could be created, skip this content.
|
|
if (generator is null)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach (var evt in generator.ProcessContent(content))
|
|
{
|
|
OnEvent(evt);
|
|
yield return evt;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finalize the active generator.
|
|
foreach (var evt in generator?.Complete() ?? [])
|
|
{
|
|
OnEvent(evt);
|
|
yield return evt;
|
|
}
|
|
|
|
yield return new StreamingResponseCompleted { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.Completed, outputs: items) };
|
|
|
|
void OnEvent(StreamingResponseEvent evt)
|
|
{
|
|
if (evt is StreamingOutputItemDone itemDone)
|
|
{
|
|
items.Add(itemDone.Item);
|
|
}
|
|
}
|
|
|
|
Response CreateResponse(ResponseStatus status = ResponseStatus.Completed, IEnumerable<ItemResource>? outputs = null)
|
|
{
|
|
return new Response
|
|
{
|
|
Agent = request.Agent?.ToAgentId(),
|
|
Background = request.Background,
|
|
Conversation = request.Conversation ?? new ConversationReference { Id = context.ConversationId },
|
|
CreatedAt = createdAt.ToUnixTimeSeconds(),
|
|
Error = null,
|
|
Id = context.ResponseId,
|
|
Instructions = request.Instructions,
|
|
MaxOutputTokens = request.MaxOutputTokens,
|
|
MaxToolCalls = request.MaxToolCalls,
|
|
Metadata = request.Metadata != null ? new Dictionary<string, string>(request.Metadata) : [],
|
|
Model = request.Model,
|
|
Output = outputs?.ToList() ?? [],
|
|
ParallelToolCalls = request.ParallelToolCalls ?? true,
|
|
PreviousResponseId = request.PreviousResponseId,
|
|
Prompt = request.Prompt,
|
|
PromptCacheKey = request.PromptCacheKey,
|
|
Reasoning = request.Reasoning,
|
|
SafetyIdentifier = request.SafetyIdentifier,
|
|
ServiceTier = request.ServiceTier,
|
|
Status = status,
|
|
Store = request.Store ?? true,
|
|
Temperature = request.Temperature ?? 1.0,
|
|
Text = request.Text,
|
|
ToolChoice = request.ToolChoice,
|
|
Tools = [.. request.Tools ?? []],
|
|
TopLogprobs = request.TopLogprobs,
|
|
TopP = request.TopP ?? 1.0,
|
|
Truncation = request.Truncation,
|
|
Usage = latestUsage,
|
|
#pragma warning disable CS0618 // Type or member is obsolete
|
|
User = request.User,
|
|
#pragma warning restore CS0618 // Type or member is obsolete
|
|
};
|
|
}
|
|
}
|
|
|
|
private static bool IsSameMessage(AgentResponseUpdate? first, AgentResponseUpdate? second)
|
|
{
|
|
return IsSameValue(first?.MessageId, second?.MessageId)
|
|
&& IsSameValue(first?.AuthorName, second?.AuthorName)
|
|
&& IsSameRole(first?.Role, second?.Role);
|
|
|
|
static bool IsSameValue(string? str1, string? str2) =>
|
|
str1 is not { Length: > 0 } || str2 is not { Length: > 0 } || str1 == str2;
|
|
|
|
static bool IsSameRole(ChatRole? value1, ChatRole? value2) =>
|
|
!value1.HasValue || !value2.HasValue || value1.Value == value2.Value;
|
|
}
|
|
|
|
private static StreamingWorkflowEventComplete CreateWorkflowEventResponse(WorkflowEvent workflowEvent, int sequenceNumber, int outputIndex)
|
|
{
|
|
// Extract executor_id if this is an ExecutorEvent
|
|
string? executorId = null;
|
|
if (workflowEvent is ExecutorEvent execEvent)
|
|
{
|
|
executorId = execEvent.ExecutorId;
|
|
}
|
|
JsonElement eventData;
|
|
if (JsonSerializer.IsReflectionEnabledByDefault)
|
|
{
|
|
JsonElement? dataElement = null;
|
|
if (workflowEvent.Data is not null)
|
|
{
|
|
dataElement = JsonSerializer.SerializeToElement(workflowEvent.Data, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
|
}
|
|
|
|
var eventDataObj = new WorkflowEventData
|
|
{
|
|
EventType = workflowEvent.GetType().Name,
|
|
Data = dataElement,
|
|
ExecutorId = executorId,
|
|
Timestamp = DateTime.UtcNow.ToString("O")
|
|
};
|
|
|
|
eventData = JsonSerializer.SerializeToElement(eventDataObj, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(WorkflowEventData)));
|
|
}
|
|
else
|
|
{
|
|
eventData = JsonSerializer.SerializeToElement(
|
|
"Unsupported. Workflow event serialization is currently only supported when JsonSerializer.IsReflectionEnabledByDefault is true.",
|
|
OpenAIHostingJsonContext.Default.String);
|
|
}
|
|
|
|
// Create the properly typed streaming workflow event
|
|
return new StreamingWorkflowEventComplete
|
|
{
|
|
SequenceNumber = sequenceNumber,
|
|
OutputIndex = outputIndex,
|
|
Data = eventData,
|
|
ExecutorId = executorId,
|
|
ItemId = IdGenerator.NewId(prefix: "wf", stringLength: 8, delimiter: "")
|
|
};
|
|
}
|
|
}
|