From 6c12b2c0f84e74551cf7d4aaa4c8c866ab6e478d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 20 Jul 2025 23:07:50 -0400 Subject: [PATCH] Round 3 of cleanup (#186) - Enable warnings as errors - Make the remaining src projects NativeAOT compatible - Use Throw helpers --- dotnet/Directory.Build.props | 3 +- .../NewOpenAIAssistantChatClient.cs | 2 +- .../GroupChatOrchestration_With_AIManager.cs | 8 +-- ...upChatOrchestration_With_HumanInTheLoop.cs | 2 +- .../IsExternalInit/IsExternalInit.cs | 4 +- .../AgentOrchestration.RequestActor.cs | 9 +-- .../AgentOrchestration.ResultActor.cs | 7 +- .../AgentOrchestration.cs | 20 +++--- .../ConcurrentOrchestration.String.cs | 2 +- .../DefaultTransforms.cs | 66 ++++++++----------- .../GroupChat/GroupChatManager.cs | 11 ++-- .../GroupChat/GroupChatOrchestration.cs | 4 +- .../GroupChat/RoundRobinGroupChatManager.cs | 6 +- .../Handoff/HandoffActor.cs | 32 ++++----- .../Handoff/HandoffOrchestration.cs | 11 ++-- .../Handoff/Handoffs.cs | 3 +- .../Microsoft.Agents.Orchestration.csproj | 1 - .../Transforms/StructuredOutputTransform.cs | 16 +++-- .../AIAgent.cs | 4 +- .../AgentAbstractionsJsonUtilities.cs | 62 +++++++++++++++++ .../AgentRunResponse.cs | 20 +++--- ...t.Extensions.AI.Agents.Abstractions.csproj | 1 + .../CopilotStudioAgent.cs | 14 ++-- .../CopilotStudioAgentThread.cs | 4 +- .../IdProxyActor.cs | 3 + .../InProcessRuntime.cs | 7 ++ .../MessageContext.cs | 15 ++--- .../RuntimeActor.cs | 2 + .../TopicId.cs | 8 +-- .../TypeSubscription.cs | 24 +++---- .../ChatCompletion/ChatClientAgent.cs | 9 +-- .../DefaultTransformsTests.cs | 3 +- .../AgentRunOptionsTests.cs | 4 +- .../AgentRunResponseUpdateExtensionsTests.cs | 8 +-- .../TestAgents.cs | 2 +- .../ChatClientAgentRunOptionsTests.cs | 8 +-- 36 files changed, 223 insertions(+), 182 deletions(-) create mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentAbstractionsJsonUtilities.cs diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 24ddabdb2b..695cbd32bd 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -9,7 +9,8 @@ 12 enable disable - $(NoWarn);IDE0290;IDE0079 + $(NoWarn);IDE0290;IDE0079;NU5128 + true net9.0;net8.0;netstandard2.0;net472 net9.0;net472 true diff --git a/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs b/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs index 9a747ea620..8c58871438 100644 --- a/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs +++ b/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs @@ -609,7 +609,7 @@ internal static class OpenAIClientExtensions2 // Roundtrip the schema through the ToolJson model type to remove extra properties // and force missing ones into existence, then return the serialized UTF8 bytes as BinaryData. - var tool = JsonSerializer.Deserialize(jsonSchema, OpenAIJsonContext.Default.ToolJson)!; + var tool = jsonSchema.Deserialize(OpenAIJsonContext.Default.ToolJson)!; var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson)); return functionParameters; diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs index 42f4844a92..185bccdc52 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs @@ -167,19 +167,19 @@ public class GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : O } /// - public override ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default) => + protected override ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default) => this.GetResponseAsync(history, Prompts.Filter(topic), cancellationToken); /// - public override ValueTask> SelectNextAgent(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default) => + protected override ValueTask> SelectNextAgent(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default) => this.GetResponseAsync(history, Prompts.Selection(topic, team.FormatList()), cancellationToken); /// - public override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) => + protected override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) => new(new GroupChatManagerResult(false) { Reason = "The AI group chat manager does not request user input." }); /// - public override async ValueTask> ShouldTerminate(IReadOnlyCollection history, CancellationToken cancellationToken = default) + protected override async ValueTask> ShouldTerminate(IReadOnlyCollection history, CancellationToken cancellationToken = default) { GroupChatManagerResult result = await base.ShouldTerminate(history, cancellationToken); if (!result.Value) diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs index 9e6e6286f5..7418cc54d8 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs @@ -84,7 +84,7 @@ public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output /// private sealed class CustomRoundRobinGroupChatManager : RoundRobinGroupChatManager { - public override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) + protected override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) { string? lastAgent = history.LastOrDefault()?.AuthorName; diff --git a/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs b/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs index e86a0cd0e7..94efaa8ebc 100644 --- a/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs +++ b/dotnet/src/LegacySupport/IsExternalInit/IsExternalInit.cs @@ -4,6 +4,4 @@ namespace System.Runtime.CompilerServices; -internal static class IsExternalInit -{ -} +internal static class IsExternalInit; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs index 7a03982f97..0cc1d13a74 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -17,7 +18,7 @@ public abstract partial class AgentOrchestration /// private sealed class RequestActor : OrchestrationActor { - private readonly Func>> _transform; + private readonly Func>> _transform; private readonly Func, ValueTask> _action; private readonly TaskCompletionSource _completionSource; @@ -35,7 +36,7 @@ public abstract partial class AgentOrchestration ActorId id, IAgentRuntime runtime, OrchestrationContext context, - Func>> transform, + Func>> transform, TaskCompletionSource completionSource, Func, ValueTask> action, ILogger? logger = null) @@ -60,8 +61,8 @@ public abstract partial class AgentOrchestration this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id); try { - IEnumerable input = await this._transform.Invoke(item, cancellationToken).ConfigureAwait(false); - Task task = this._action.Invoke(input).AsTask(); + IEnumerable input = await this._transform.Invoke(item, messageContext.SerializerOptions, cancellationToken).ConfigureAwait(false); + var task = this._action.Invoke(input); this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id); await task.ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs index a593c47a58..44f4d672cf 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -19,7 +20,7 @@ public abstract partial class AgentOrchestration { private readonly TaskCompletionSource _completionSource; private readonly Func> _transformResult; - private readonly Func, CancellationToken, ValueTask> _transform; + private readonly Func, JsonSerializerOptions?, CancellationToken, ValueTask> _transform; /// /// Initializes a new instance of the class. @@ -36,7 +37,7 @@ public abstract partial class AgentOrchestration IAgentRuntime runtime, OrchestrationContext context, Func> transformResult, - Func, CancellationToken, ValueTask> transformOutput, + Func, JsonSerializerOptions?, CancellationToken, ValueTask> transformOutput, TaskCompletionSource completionSource, ILogger>? logger = null) : base(id, runtime, context, $"{id.Type}_Actor", logger) @@ -66,7 +67,7 @@ public abstract partial class AgentOrchestration if (!this._completionSource.Task.IsCompleted) { IList result = this._transformResult.Invoke(item); - TOutput output = await this._transform.Invoke(result, cancellationToken).ConfigureAwait(false); + TOutput output = await this._transform.Invoke(result, messageContext.SerializerOptions, cancellationToken).ConfigureAwait(false); this._completionSource.TrySetResult(output); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs index c282207dd5..07d4c30594 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -16,11 +17,6 @@ using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; -/// -/// Called when human interaction is requested. -/// -public delegate ValueTask OrchestrationInteractiveCallback(); - /// /// Base class for multi-agent agent orchestration patterns. /// @@ -67,22 +63,22 @@ public abstract partial class AgentOrchestration /// /// Transforms the orchestration input into a source input suitable for processing. /// - public Func>> InputTransform { get; init; } = DefaultTransforms.FromInput; + public Func>>? InputTransform { get; set; } /// /// Transforms the processed result into the final output form. /// - public Func, CancellationToken, ValueTask> ResultTransform { get; init; } = DefaultTransforms.ToOutput; + public Func, JsonSerializerOptions?, CancellationToken, ValueTask>? ResultTransform { get; set; } /// /// Optional callback that is invoked for every agent response. /// - public Func, ValueTask>? ResponseCallback { get; init; } + public Func, ValueTask>? ResponseCallback { get; set; } /// /// Optional callback that is invoked for every agent update. /// - public Func? StreamingResponseCallback { get; init; } + public Func? StreamingResponseCallback { get; set; } /// /// Gets the list of member targets involved in the orchestration. @@ -183,7 +179,7 @@ public abstract partial class AgentOrchestration logger.LogOrchestrationRegistrationStart(context.Orchestration, context.Topic); // Register orchestration - RegistrationContext registrar = new(this.FormatAgentType(context.Topic, "Root"), runtime, context, completion, this.ResultTransform); + RegistrationContext registrar = new(this.FormatAgentType(context.Topic, "Root"), runtime, context, completion, this.ResultTransform ?? DefaultTransforms.ToOutput); ActorType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false); // Register actor for orchestration entry-point @@ -196,7 +192,7 @@ public abstract partial class AgentOrchestration new(agentId, runtime, context, - this.InputTransform, + this.InputTransform ?? DefaultTransforms.FromInput, completion, input => this.StartAsync(runtime, context.Topic, input, entryAgent), context.LoggerFactory.CreateLogger()); @@ -216,7 +212,7 @@ public abstract partial class AgentOrchestration IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource completion, - Func, CancellationToken, ValueTask> outputTransform) + Func, JsonSerializerOptions?, CancellationToken, ValueTask> outputTransform) { /// /// Register the final result type. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs index 114b3bd26b..81d49f6f3d 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -20,7 +20,7 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration + (response, _, cancellationToken) => { string[] result = [.. response.Select(r => r.Text)]; return new ValueTask(result); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs b/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs index 9b24dcb31b..054004bb6f 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/DefaultTransforms.cs @@ -6,68 +6,58 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; internal static class DefaultTransforms { - public static ValueTask> FromInput(TInput input, CancellationToken cancellationToken = default) => - new(input switch + public static ValueTask> FromInput(TInput input, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) + { + serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions; + return new(input switch { IEnumerable messages => messages, ChatMessage message => [message], string text => [new ChatMessage(ChatRole.User, text)], - _ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input))] + _ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input, serializerOptions.GetTypeInfo(typeof(TInput))))] }); + } - public static ValueTask ToOutput(IList result, CancellationToken cancellationToken = default) + public static ValueTask ToOutput(IList result, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) { + Throw.IfNull(result); + + serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions; bool isSingleResult = result.Count == 1; - TOutput output = - GetDefaultOutput() ?? - GetObjectOutput() ?? - throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}."); - - return new ValueTask(output); - - TOutput? GetObjectOutput() + if (result is TOutput) { - if (isSingleResult) - { - try - { - return JsonSerializer.Deserialize(result[0].Text); - } - catch (JsonException) - { - } - } - - return default; + return new((TOutput)(object)result); } - TOutput? GetDefaultOutput() + if (isSingleResult) { - if (typeof(TOutput).IsInstanceOfType(result)) + if (typeof(ChatMessage).IsAssignableFrom(typeof(TOutput))) { - return (TOutput)(object)result; + return new((TOutput)(object)result[0]); } - if (isSingleResult) + if (typeof(string) == typeof(TOutput)) { - if (typeof(ChatMessage).IsAssignableFrom(typeof(TOutput))) - { - return (TOutput)(object)result[0]; - } - - if (typeof(string) == typeof(TOutput)) - { - return (TOutput)(object)(result[0].Text ?? string.Empty); - } + return new((TOutput)(object)(result[0].Text ?? string.Empty)); } - return default; + try + { + return new((TOutput)JsonSerializer.Deserialize(result[0].Text, serializerOptions.GetTypeInfo(typeof(TOutput)))!); + } + catch (JsonException) + { + } } + + throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}."); } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs index 8063475e8c..01436ad5fe 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -50,7 +51,7 @@ public abstract class GroupChatManager /// /// Gets or sets the callback to be invoked for interactive input. /// - public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } + public Func>? InteractiveCallback { get; init; } /// /// Filters the results of the group chat based on the provided chat history. @@ -58,7 +59,7 @@ public abstract class GroupChatManager /// The chat history to filter. /// A cancellation token that can be used to cancel the operation. /// A containing the filtered result as a string. - public abstract ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default); + protected internal abstract ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default); /// /// Selects the next agent to participate in the group chat based on the provided chat history and team. @@ -67,7 +68,7 @@ public abstract class GroupChatManager /// The group of agents participating in the chat. /// A cancellation token that can be used to cancel the operation. /// A containing the identifier of the next agent as a string. - public abstract ValueTask> SelectNextAgent(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default); + protected internal abstract ValueTask> SelectNextAgent(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default); /// /// Determines whether user input should be requested based on the provided chat history. @@ -75,7 +76,7 @@ public abstract class GroupChatManager /// The chat history to consider. /// A cancellation token that can be used to cancel the operation. /// A indicating whether user input should be requested. - public abstract ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default); + protected internal abstract ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default); /// /// Determines whether the group chat should be terminated based on the provided chat history and invocation count. @@ -83,7 +84,7 @@ public abstract class GroupChatManager /// The chat history to consider. /// A cancellation token that can be used to cancel the operation. /// A indicating whether the chat should be terminated. - public virtual ValueTask> ShouldTerminate(IReadOnlyCollection history, CancellationToken cancellationToken = default) + protected internal virtual ValueTask> ShouldTerminate(IReadOnlyCollection history, CancellationToken cancellationToken = default) { Interlocked.Increment(ref this._invocationCount); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs index ee90e41c2e..0a10e8242b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -39,8 +38,9 @@ public class GroupChatOrchestration : { if (!entryAgent.HasValue) { - throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); + Throw.ArgumentException(nameof(entryAgent), "Entry agent is not defined."); } + return runtime.PublishMessageAsync(new GroupChatMessages.InputTask(input), entryAgent.Value); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs index 96170bf341..1e76ed4dd8 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -19,14 +19,14 @@ public class RoundRobinGroupChatManager : GroupChatManager private int _currentAgentIndex; /// - public override ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default) + protected internal override ValueTask> FilterResults(IReadOnlyCollection history, CancellationToken cancellationToken = default) { GroupChatManagerResult result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." }; return new ValueTask>(result); } /// - public override ValueTask> SelectNextAgent(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default) + protected internal override ValueTask> SelectNextAgent(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default) { string nextAgent = team.Skip(this._currentAgentIndex).First().Key; this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count; @@ -35,7 +35,7 @@ public class RoundRobinGroupChatManager : GroupChatManager } /// - public override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) + protected internal override ValueTask> ShouldRequestUserInput(IReadOnlyCollection history, CancellationToken cancellationToken = default) { GroupChatManagerResult result = new(false) { Reason = "The default round-robin group chat manager does not request user input." }; return new ValueTask>(result); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs index fef0536db3..f6c8c1a151 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; @@ -39,21 +40,19 @@ internal sealed partial class HandoffActor : AgentActor public HandoffActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, ActorType resultHandoff, ILogger? logger = null) : base(id, runtime, context, agent, logger) { + Throw.IfNull(handoffs); + Throw.IfNull(resultHandoff); + if (handoffs.ContainsKey(agent.Name ?? agent.Id)) { - throw new ArgumentException($"The agent {agent.Name ?? agent.Id} cannot have a handoff to itself.", nameof(handoffs)); + Throw.ArgumentException(nameof(handoffs), $"The agent {agent.Name ?? agent.Id} cannot have a handoff to itself."); } this._cache = []; this._chatAgent = agent; this._handoffs = handoffs; this._resultHandoff = resultHandoff; - this._options = - new ChatOptions - { - Tools = [.. this.CreateHandoffFunctions()], - ToolMode = ChatToolMode.Auto - }; + this._options = new() { Tools = this.CreateHandoffFunctions() }; this.RegisterMessageHandler(this.Handle); this.RegisterMessageHandler(this.HandleAsync); @@ -73,7 +72,7 @@ internal sealed partial class HandoffActor : AgentActor /// /// Gets or sets the callback to be invoked for interactive input. /// - public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } + public Func>? InteractiveCallback { get; init; } private void Handle(HandoffMessages.InputTask item, MessageContext messageContext) { @@ -144,23 +143,24 @@ internal sealed partial class HandoffActor : AgentActor } } - private IEnumerable CreateHandoffFunctions() + private List CreateHandoffFunctions() { - yield return AIFunctionFactory.Create( + List functions = []; + + functions.Add(AIFunctionFactory.Create( this.EndAsync, name: "end_task", - description: "Complete the task with a summary when no further requests are given."); + description: "Complete the task with a summary when no further requests are given.")); foreach (KeyValuePair handoff in this._handoffs) { - AIFunction handoffFunction = - AIFunctionFactory.Create( + functions.Add(AIFunctionFactory.Create( () => this.Handoff(handoff.Key), name: $"transfer_to_{InvalidNameCharsRegex().Replace(handoff.Key, "_")}", - description: handoff.Value.Description); - - yield return handoffFunction; + description: handoff.Value.Description)); } + + return functions; } private void Handoff(string agentName) diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs index 0bd7fb05b2..2d515b9332 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; @@ -41,7 +42,7 @@ public class HandoffOrchestration : AgentOrchestration 0) { - throw new ArgumentException($"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}", nameof(handoffs)); + Throw.ArgumentException(nameof(handoffs), $"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}"); } this._handoffs = handoffs; @@ -50,15 +51,13 @@ public class HandoffOrchestration : AgentOrchestration /// Gets or sets the callback to be invoked for interactive input. /// - public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } + public Func>? InteractiveCallback { get; init; } /// protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, ActorType? entryAgent) { - if (!entryAgent.HasValue) - { - throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); - } + Throw.IfNull(entryAgent); + await runtime.PublishMessageAsync(new HandoffMessages.InputTask([.. input]), topic).ConfigureAwait(false); await runtime.PublishMessageAsync(new HandoffMessages.Request(), entryAgent.Value).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs index 5fe8ec70f7..d1aac7efd2 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using Microsoft.Extensions.AI.Agents; using Microsoft.Extensions.AI.Agents.Runtime; @@ -82,7 +81,7 @@ public sealed class OrchestrationHandoffs : Dictionary { if (string.IsNullOrWhiteSpace(target.Description) && string.IsNullOrWhiteSpace(target.Name)) { - throw new InvalidOperationException($"The provided target agent with Id '{target.Id}' has no description or name, and no handoff description has been provided. At least one of these are required to register a handoff so that the appropriate target agent can be chosen."); + Throw.InvalidOperationException($"The provided target agent with Id '{target.Id}' has no description or name, and no handoff description has been provided. At least one of these are required to register a handoff so that the appropriate target agent can be chosen."); } this.Agents.Add(target); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj index 78878572cc..de550fc682 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj +++ b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj @@ -5,7 +5,6 @@ $(ProjectsDebugTargetFrameworks) Microsoft.Agents.Orchestration alpha - false diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs index 52c7fb08a0..3a1af8bd0c 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Transforms/StructuredOutputTransform.cs @@ -2,9 +2,11 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; @@ -42,17 +44,23 @@ public sealed class StructuredOutputTransform /// Transforms the provided into a strongly-typed structured output by invoking the chat completion service and deserializing the response. /// /// The chat messages to process. + /// The JSON serializer options to use when performing any JSON serialization. /// A cancellation token to observe while waiting for the task to complete. /// The structured output of type . /// Thrown if the response cannot be deserialized into . - public async ValueTask TransformAsync(IList messages, CancellationToken cancellationToken = default) + public async ValueTask TransformAsync(IList messages, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) { - IEnumerable input = + Throw.IfNull(messages); + + ChatResponse response = await this._client.GetResponseAsync( [ new ChatMessage(ChatRole.System, this.Instructions), .. messages, - ]; - ChatResponse response = await this._client.GetResponseAsync(input, this._options, useJsonSchemaResponseFormat: true, cancellationToken).ConfigureAwait(false); + ], + serializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions, + this._options, + cancellationToken: cancellationToken).ConfigureAwait(false); + return response.Result; } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs index a8fc41ed1a..334e5b5b30 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs @@ -207,7 +207,9 @@ public abstract class AIAgent Func constructThread) where TThreadType : AgentThread { - thread ??= constructThread is not null ? constructThread() : throw new ArgumentNullException(nameof(constructThread)); + Throw.IfNull(constructThread); + + thread ??= constructThread(); if (thread is not TThreadType concreteThreadType) { diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentAbstractionsJsonUtilities.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentAbstractionsJsonUtilities.cs new file mode 100644 index 0000000000..524b0b96a5 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentAbstractionsJsonUtilities.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI.Agents; + +/// Provides a collection of utility methods for working with JSON data in the context of agents. +public static partial class AgentAbstractionsJsonUtilities +{ + /// + /// Gets the singleton used as the default in JSON serialization operations. + /// + /// + /// + /// For Native AOT or applications disabling , this instance + /// includes source generated contracts for all common exchange types contained in this library. + /// + /// + /// It additionally turns on the following settings: + /// + /// Enables defaults. + /// Enables as the default ignore condition for properties. + /// Enables as the default number handling for number types. + /// + /// + /// + public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); + + /// + /// Creates default options to use for agents-related serialization. + /// + /// The configured options. + [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() + { + // Copy the configuration from the source generated context. + JsonSerializerOptions options = new(JsonContext.Default.Options); + + // Chain with all supported types from MEAI. + options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); + + options.MakeReadOnly(); + return options; + } + + // Keep in sync with CreateDefaultOptions above. + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] + + // Agent abstraction types + [JsonSerializable(typeof(AgentRunOptions))] + [JsonSerializable(typeof(AgentRunResponse))] + [JsonSerializable(typeof(AgentRunResponseUpdate))] + [JsonSerializable(typeof(AgentThread))] + + [ExcludeFromCodeCoverage] + internal sealed partial class JsonContext : JsonSerializerContext; +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs index 2dab486bf8..14ff73e884 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs @@ -1,10 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +#if NET9_0_OR_GREATER using System.Buffers; +#endif using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +#if NET9_0_OR_GREATER using System.Text; +#endif using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -22,13 +26,6 @@ namespace Microsoft.Extensions.AI.Agents; /// public class AgentRunResponse { - private static readonly JsonReaderOptions s_allowMultipleValuesJsonReaderOptions = new() - { -#if NET9_0_OR_GREATER - AllowMultipleValues = true -#endif - }; - /// The response messages. private IList? _messages; @@ -184,15 +181,16 @@ public class AgentRunResponse } #pragma warning disable CA1031 // Do not catch general exception types catch +#pragma warning restore CA1031 { structuredOutput = default; return false; } -#pragma warning restore CA1031 // Do not catch general exception types } private static T? DeserializeFirstTopLevelObject(string json, JsonTypeInfo typeInfo) { +#if NET9_0_OR_GREATER // We need to deserialize only the first top-level object as a workaround for a common LLM backend // issue. GPT 3.5 Turbo commonly returns multiple top-level objects after doing a function call. // See https://community.openai.com/t/2-json-objects-returned-when-using-function-calling-and-json-mode/574348 @@ -201,14 +199,16 @@ public class AgentRunResponse try { var utf8SpanLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0); - var utf8Span = new ReadOnlySpan(buffer, 0, utf8SpanLength); - var reader = new Utf8JsonReader(utf8Span, s_allowMultipleValuesJsonReaderOptions); + var reader = new Utf8JsonReader(new ReadOnlySpan(buffer, 0, utf8SpanLength), new() { AllowMultipleValues = true }); return JsonSerializer.Deserialize(ref reader, typeInfo); } finally { ArrayPool.Shared.Return(buffer); } +#else + return JsonSerializer.Deserialize(json, typeInfo); +#endif } private T? GetResultCore(JsonSerializerOptions serializerOptions, out FailureReason? failureReason) diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj index 4bc963ea9c..335022ffd9 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj @@ -9,6 +9,7 @@ true + true diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgent.cs index 8f4e18a866..ede152af99 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgent.cs @@ -53,12 +53,9 @@ public class CopilotStudioAgent : AIAgent Throw.IfNull(messages); // Ensure that we have a valid thread to work with. + // If the thread ID is null, we need to start a new conversation and set the thread ID accordingly. CopilotStudioAgentThread copilotStudioAgentThread = base.ValidateOrCreateThreadType(thread, () => new CopilotStudioAgentThread()); - if (copilotStudioAgentThread.Id is null) - { - // If the thread ID is null, we need to start a new conversation and set the thread ID accordingly. - copilotStudioAgentThread.Id = await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false); - } + copilotStudioAgentThread.Id ??= await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false); // Invoke the Copilot Studio agent with the provided messages. string question = string.Join("\n", messages.Select(m => m.Text)); @@ -89,12 +86,9 @@ public class CopilotStudioAgent : AIAgent Throw.IfNull(messages); // Ensure that we have a valid thread to work with. + // If the thread ID is null, we need to start a new conversation and set the thread ID accordingly. CopilotStudioAgentThread copilotStudioAgentThread = base.ValidateOrCreateThreadType(thread, () => new CopilotStudioAgentThread()); - if (copilotStudioAgentThread.Id is null) - { - // If the thread ID is null, we need to start a new conversation and set the thread ID accordingly. - copilotStudioAgentThread.Id = await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false); - } + copilotStudioAgentThread.Id ??= await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false); // Invoke the Copilot Studio agent with the provided messages. string question = string.Join("\n", messages.Select(m => m.Text)); diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgentThread.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgentThread.cs index b7493a1f7b..cff6d2c399 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgentThread.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.CopilotStudio/CopilotStudioAgentThread.cs @@ -5,6 +5,4 @@ namespace Microsoft.Extensions.AI.Agents.CopilotStudio; /// /// Represents a thread for interacting with a Copilot Studio agent. /// -public class CopilotStudioAgentThread : AgentThread -{ -} +public class CopilotStudioAgentThread : AgentThread; diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs index 250927bdae..6cafeacb9d 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IdProxyActor.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Agents.Runtime; @@ -21,6 +22,8 @@ public sealed class IdProxyActor : IRuntimeActor /// public IdProxyActor(IAgentRuntime runtime, ActorId actorId) { + Throw.IfNull(runtime); + this.Id = actorId; this._runtime = runtime; } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs index a947860759..6eb8b1de44 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/InProcessRuntime.cs @@ -73,6 +73,8 @@ public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable /// public ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { + Throw.IfNull(message); + MessageToProcess m = new(this, message, messageId, sender, topic, cancellationToken); this.IncrementRemainingWork(); @@ -84,6 +86,8 @@ public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable /// public ValueTask SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { + Throw.IfNull(message); + MessageToProcess m = new(this, message, messageId, sender, recipient, cancellationToken); this.IncrementRemainingWork(); @@ -140,6 +144,7 @@ public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable /// public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default) { + Throw.IfNull(subscription); ThrowIfInvalid(this._subscriptions.ContainsKey(subscription.Id), "Subscription with the specified ID already exists."); this._subscriptions.Add(subscription.Id, subscription); @@ -150,6 +155,7 @@ public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable /// public ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default) { + Throw.IfNull(subscriptionId); ThrowIfInvalid(!this._subscriptions.ContainsKey(subscriptionId), "Subscription with the specified ID does not exist."); this._subscriptions.Remove(subscriptionId); @@ -187,6 +193,7 @@ public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable /// public async ValueTask RegisterActorFactoryAsync(ActorType type, Func> factoryFunc, CancellationToken cancellationToken = default) { + Throw.IfNull(factoryFunc); ThrowIfInvalid(this._actorFactories.ContainsKey(type), "Actor type already registered."); this._actorFactories.Add(type, factoryFunc); diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs index 3e3d3482f4..ea92307f6d 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Text.Json; using System.Threading; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Agents.Runtime; @@ -21,15 +23,7 @@ public sealed class MessageContext public string MessageId { get => this._messageId ?? Interlocked.CompareExchange(ref this._messageId, Guid.NewGuid().ToString(), null) ?? this._messageId; - set - { - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentException("MessageId cannot be null or empty.", nameof(value)); - } - - this._messageId = value; - } + set => this._messageId = Throw.IfNullOrEmpty(value); } /// @@ -48,4 +42,7 @@ public sealed class MessageContext /// Gets or sets a value indicating whether this message is part of an RPC (Remote Procedure Call). /// public bool IsRpc { get; set; } + + /// Gets or sets the serializer options to be used when performing JSON serialization associated with this message. + public JsonSerializerOptions? SerializerOptions { get; set; } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs index 30a1312c51..7191cd1dd1 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/RuntimeActor.cs @@ -57,6 +57,8 @@ public abstract class RuntimeActor : IRuntimeActor string? description = null, ILogger? logger = null) { + Throw.IfNull(runtime); + this.Id = id; this._runtime = runtime; this.Logger = logger ?? NullLogger.Instance; diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs index ce16f39355..1de3456956 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Agents.Runtime; @@ -32,14 +33,11 @@ public readonly partial struct TopicId : IEquatable /// The source of the event. public TopicId(string type, string? source = null) { - if (type is null) - { - throw new ArgumentNullException(nameof(type)); - } + Throw.IfNull(type); if (!TypeRegex().IsMatch(type)) { - throw new ArgumentException("Invalid type format.", nameof(type)); + Throw.ArgumentException(nameof(type), "Invalid type format."); } // TODO: What validation should be performed on source? The cited cloudevents spec suggests it should be a URI reference. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs index 23f23ded9b..d1274ddb72 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI.Agents.Runtime; @@ -28,6 +29,8 @@ public sealed class TypeSubscription : ISubscriptionDefinition /// Unique identifier for the subscription. If not provided, a new UUID will be generated. public TypeSubscription(string topicType, ActorType actorType, string? id = null) { + Throw.IfNullOrEmpty(topicType); + this.TopicType = topicType; this.ActorType = actorType; this.Id = id ?? Guid.NewGuid().ToString(); @@ -53,10 +56,7 @@ public sealed class TypeSubscription : ISubscriptionDefinition /// /// The topic to check. /// true if the topic's type matches exactly, false otherwise. - public bool Matches(TopicId topic) - { - return topic.Type == this.TopicType; - } + public bool Matches(TopicId topic) => topic.Type == this.TopicType; /// /// Maps a to an . Should only be called if returns true. @@ -79,14 +79,9 @@ public sealed class TypeSubscription : ISubscriptionDefinition /// /// The object to compare with the current instance. /// true if the specified object is equal to this instance; otherwise, false. - public override bool Equals([NotNullWhen(true)] object? obj) - { - return - obj is TypeSubscription other && - (this.Id == other.Id || - (this.ActorType == other.ActorType && - this.TopicType == other.TopicType)); - } + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is TypeSubscription other && + (this.Id == other.Id || (this.ActorType == other.ActorType && this.TopicType == other.TopicType)); /// /// Determines whether the specified subscription is equal to the current subscription. @@ -99,8 +94,5 @@ public sealed class TypeSubscription : ISubscriptionDefinition /// Returns a hash code for this instance. /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures. - public override int GetHashCode() - { - return HashCode.Combine(this.Id, this.ActorType, this.TopicType); - } + public override int GetHashCode() => HashCode.Combine(this.Id, this.ActorType, this.TopicType); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs index b35f0e6ab5..9d0b64011f 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs @@ -360,12 +360,9 @@ public sealed class ChatClientAgent : AIAgent private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread chatClientThread, string? responseConversationId) { // Set the thread's storage location, the first time that we use it. - if (chatClientThread.StorageLocation is null) - { - chatClientThread.StorageLocation = string.IsNullOrWhiteSpace(responseConversationId) - ? ChatClientAgentThreadType.InMemoryMessages - : ChatClientAgentThreadType.ConversationId; - } + chatClientThread.StorageLocation ??= string.IsNullOrWhiteSpace(responseConversationId) + ? ChatClientAgentThreadType.InMemoryMessages + : ChatClientAgentThreadType.ConversationId; // If we got a conversation id back from the chat client, it means that the service supports server side thread storage // so we should capture the id and update the thread with the new id. diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs index 7a760b023d..7abeaf22a6 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/DefaultTransformsTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents; namespace Microsoft.Agents.Orchestration.UnitTest; @@ -72,7 +73,7 @@ public class DefaultTransformsTests ChatMessage message = result.First(); Assert.Equal(ChatRole.User, message.Role); - string expectedJson = JsonSerializer.Serialize(input); + string expectedJson = JsonSerializer.Serialize(input, AgentAbstractionsJsonUtilities.DefaultOptions); Assert.Equal(expectedJson, message.Text); } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs index c602af78f0..473501a554 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs @@ -13,9 +13,7 @@ public class AgentRunOptionsTests public void CloningConstructorCopiesProperties() { // Arrange - var options = new AgentRunOptions - { - }; + var options = new AgentRunOptions(); // Act var clone = new AgentRunOptions(options); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs index 7f91394417..041bc201b2 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs @@ -145,7 +145,7 @@ public class AgentRunResponseUpdateExtensionsTests public async Task ToAgentRunResponseCoalescesTextContentAndTextReasoningContentSeparatelyAsync(bool useAsync) { AgentRunResponseUpdate[] updates = - { + [ new(null, "A"), new(null, "B"), new(null, "C"), @@ -162,7 +162,7 @@ public class AgentRunResponseUpdateExtensionsTests new(null, "N"), new() { Contents = [new TextReasoningContent("O")] }, new() { Contents = [new TextReasoningContent("P")] }, - }; + ]; AgentRunResponse response = useAsync ? await YieldAsync(updates).ToAgentRunResponseAsync() : updates.ToAgentRunResponse(); ChatMessage message = Assert.Single(response.Messages); @@ -181,11 +181,11 @@ public class AgentRunResponseUpdateExtensionsTests public async Task ToAgentRunResponseUsesContentExtractedFromContentsAsync() { AgentRunResponseUpdate[] updates = - { + [ new(null, "Hello, "), new(null, "world!"), new() { Contents = [new UsageContent(new() { TotalTokenCount = 42 })] }, - }; + ]; AgentRunResponse response = await YieldAsync(updates).ToAgentRunResponseAsync(); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs index 3b35407d67..fabb13704c 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TestAgents.cs @@ -42,7 +42,7 @@ public sealed class MockAgent : TestAgent public override ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) { - this.ReceivedMessages = JsonSerializer.Deserialize>(state) ?? throw new InvalidOperationException("Failed to deserialize state"); + this.ReceivedMessages = state.Deserialize>() ?? throw new InvalidOperationException("Failed to deserialize state"); return default; } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs index 6639e05341..12cbe53779 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs @@ -40,9 +40,7 @@ public class ChatClientAgentRunOptionsTests public void ConstructorCopiesPropertiesFromSourceAgentRunOptions() { // Arrange - var sourceRunOptions = new AgentRunOptions - { - }; + var sourceRunOptions = new AgentRunOptions(); var chatOptions = new ChatOptions { MaxOutputTokens = 200 }; // Act @@ -59,9 +57,7 @@ public class ChatClientAgentRunOptionsTests public void ConstructorWorksWithSourceButNullChatOptions() { // Arrange - var sourceRunOptions = new AgentRunOptions - { - }; + var sourceRunOptions = new AgentRunOptions(); // Act var runOptions = new ChatClientAgentRunOptions(sourceRunOptions, null);