diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig index 56b926dd0c..75b647fee3 100644 --- a/dotnet/.editorconfig +++ b/dotnet/.editorconfig @@ -119,7 +119,6 @@ dotnet_diagnostic.RCS1232.severity = warning # Order elements in documentation c # Diagnostics elevated as warnings dotnet_diagnostic.CA1000.severity = warning # Do not declare static members on generic types -dotnet_diagnostic.CA1031.severity = warning # Do not catch general exception types dotnet_diagnostic.CA1050.severity = warning # Declare types in namespaces dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable correctly dotnet_diagnostic.CA1064.severity = warning # Exceptions should be public @@ -155,6 +154,7 @@ dotnet_diagnostic.IDE0161.severity = warning # Use file-scoped namespace # Suppressed diagnostics dotnet_diagnostic.CA1002.severity = none # Change 'List' in '...' to use 'Collection' ... +dotnet_diagnostic.CA1031.severity = suggestion # Do not catch general exception types dotnet_diagnostic.CA1032.severity = none # We're using RCS1194 which seems to cover more ctors dotnet_diagnostic.CA1034.severity = none # Do not nest type. Alternatively, change its accessibility so that it is not externally visible dotnet_diagnostic.CA1062.severity = none # Disable null check, C# already does it for us @@ -177,6 +177,7 @@ dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing th dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload +dotnet_diagnostic.CS1998.severity = suggestion # async method lacks 'await' operators and will run synchronously dotnet_diagnostic.VSTHRD003.severity = none # Waiting on thread from another context dotnet_diagnostic.VSTHRD103.severity = none # Use async equivalent; analyzer is currently noisy diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 547ed985e1..24ddabdb2b 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -10,8 +10,8 @@ enable disable $(NoWarn);IDE0290;IDE0079 - net472;net8.0;net9.0;netstandard2.0 - net9.0 + net9.0;net8.0;netstandard2.0;net472 + net9.0;net472 true diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 95442bacb1..892fbba214 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -17,19 +17,19 @@ + + + + - + - - - - diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 0ed2cbaa4e..5fb6bffbef 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -107,17 +107,29 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/Directory.Build.props b/dotnet/samples/Directory.Build.props index e5c47346d2..ae7ca1f99e 100644 --- a/dotnet/samples/Directory.Build.props +++ b/dotnet/samples/Directory.Build.props @@ -7,7 +7,6 @@ true false net472;net9.0 - net9.0 diff --git a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs index b50aa01414..1c8acfc101 100644 --- a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_Intro.cs @@ -3,7 +3,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.Concurrent; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs index 3ff1922f12..f62c9e2a9e 100644 --- a/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs +++ b/dotnet/samples/GettingStarted/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs @@ -5,7 +5,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.Concurrent; using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; using Microsoft.Shared.Samples; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs index 935e1e0848..920ebec115 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_Intro.cs @@ -3,7 +3,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs index 45dec54c6a..b26b37b721 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_AIManager.cs @@ -4,7 +4,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs index dc06280503..39e6a7c4da 100644 --- a/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs +++ b/dotnet/samples/GettingStarted/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs @@ -4,7 +4,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs index 80b6e34a14..641a6b5b72 100644 --- a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_Intro.cs @@ -4,7 +4,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs index ea57a8ea79..454a0fcb53 100644 --- a/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs +++ b/dotnet/samples/GettingStarted/Orchestration/HandoffOrchestration_With_StructuredInput.cs @@ -5,7 +5,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs index b220b21d20..44e0771af4 100644 --- a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs +++ b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_Intro.cs @@ -3,7 +3,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs index d67eaec781..a4394dac46 100644 --- a/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs +++ b/dotnet/samples/GettingStarted/Orchestration/SequentialOrchestration_With_Cancellation.cs @@ -3,7 +3,7 @@ using Microsoft.Agents.Orchestration; using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Orchestration; diff --git a/dotnet/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs b/dotnet/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs index a406732ed4..83c12adc10 100644 --- a/dotnet/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs +++ b/dotnet/src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; @@ -17,7 +17,7 @@ internal sealed class CallerArgumentExpressionAttribute : Attribute /// Function parameter to take the name from. public CallerArgumentExpressionAttribute(string parameterName) { - ParameterName = parameterName; + this.ParameterName = parameterName; } /// diff --git a/dotnet/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs b/dotnet/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs index 330b365f48..393f24d58b 100644 --- a/dotnet/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs +++ b/dotnet/src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. #pragma warning disable CA1019 @@ -42,7 +42,7 @@ internal sealed class MaybeNullWhenAttribute : Attribute /// /// The return value condition. If the method returns this value, the associated parameter may be . /// - public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + public MaybeNullWhenAttribute(bool returnValue) => this.ReturnValue = returnValue; /// Gets the return value condition. public bool ReturnValue { get; } @@ -57,7 +57,7 @@ internal sealed class NotNullWhenAttribute : Attribute /// /// The return value condition. If the method returns this value, the associated parameter will not be . /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + public NotNullWhenAttribute(bool returnValue) => this.ReturnValue = returnValue; /// Gets the return value condition. public bool ReturnValue { get; } @@ -72,7 +72,7 @@ internal sealed class NotNullIfNotNullAttribute : Attribute /// /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. /// - public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + public NotNullIfNotNullAttribute(string parameterName) => this.ParameterName = parameterName; /// Gets the associated parameter name. public string ParameterName { get; } @@ -95,7 +95,7 @@ internal sealed class DoesNotReturnIfAttribute : Attribute /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to /// the associated parameter matches this value. /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + public DoesNotReturnIfAttribute(bool parameterValue) => this.ParameterValue = parameterValue; /// Gets the condition parameter value. public bool ParameterValue { get; } @@ -111,13 +111,13 @@ internal sealed class MemberNotNullAttribute : Attribute /// /// The field or property member that is promised to be not-null. /// - public MemberNotNullAttribute(string member) => Members = new[] { member }; + public MemberNotNullAttribute(string member) => this.Members = new[] { member }; /// Initializes the attribute with the list of field and property members. /// /// The list of field and property members that are promised to be not-null. /// - public MemberNotNullAttribute(params string[] members) => Members = members; + public MemberNotNullAttribute(params string[] members) => this.Members = members; /// Gets field or property member names. public string[] Members { get; } @@ -137,8 +137,8 @@ internal sealed class MemberNotNullWhenAttribute : Attribute /// public MemberNotNullWhenAttribute(bool returnValue, string member) { - ReturnValue = returnValue; - Members = new[] { member }; + this.ReturnValue = returnValue; + this.Members = [member]; } /// Initializes the attribute with the specified return value condition and list of field and property members. @@ -150,8 +150,8 @@ internal sealed class MemberNotNullWhenAttribute : Attribute /// public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { - ReturnValue = returnValue; - Members = members; + this.ReturnValue = returnValue; + this.Members = members; } /// Gets the return value condition. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs index 12217c85a0..df8903840e 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentActor.cs @@ -7,8 +7,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs index c8cfe4f6e5..651ea3959b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.RequestActor.cs @@ -5,9 +5,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs index 3cb5cac6e9..edc2e79950 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.ResultActor.cs @@ -5,9 +5,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs index 7abdce82aa..8df9416205 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/AgentOrchestration.cs @@ -9,9 +9,9 @@ using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Agents.Orchestration.Transforms; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs index 6323cd18c3..5a783c6db4 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentActor.cs @@ -3,9 +3,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration.Concurrent; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs index bd9b797141..4f82f6309f 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -23,11 +23,7 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration { string[] result = [.. response.Select(r => r.Text)]; -#if !NETCOREAPP return new ValueTask(result); -#else - return ValueTask.FromResult(result); -#endif }; } } diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs index 4fab83f149..7f94189e14 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration.Concurrent; @@ -44,14 +44,10 @@ public class ConcurrentOrchestration AgentType resultType = this.FormatAgentType(context.Topic, "Results"); await runtime.RegisterOrchestrationAgentAsync( resultType, - (agentId, runtime) => + async (agentId, runtime) => { ConcurrentResultActor actor = new(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger()); -#if !NETCOREAPP - return new ValueTask(actor); -#else - return ValueTask.FromResult(actor); -#endif + return actor; }).ConfigureAwait(false); logger.LogRegisterActor(this.OrchestrationLabel, resultType, "RESULTS"); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs index 0742f4582c..c6b4dbd14d 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Concurrent/ConcurrentResultActor.cs @@ -3,9 +3,8 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration.Concurrent; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs b/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs index d919cf9838..eadf788292 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Extensions/RuntimeExtensions.cs @@ -3,8 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; +using Microsoft.Extensions.AI.Agents.Runtime; namespace Microsoft.Agents.Orchestration.Extensions; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs index c64b076093..a668430182 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatAgentActor.cs @@ -4,9 +4,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration.GroupChat; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs index 6ab1927a63..f7e51017d9 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManagerActor.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration.GroupChat; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs index 63fa0f34c1..a5440767a0 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration.GroupChat; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs index fb79c0eaec..7894477385 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffActor.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration.Handoff; @@ -177,7 +176,7 @@ internal sealed class HandoffActor : name: "end_task", description: "Complete the task with a summary when no further requests are given."); - foreach (KeyValuePair handoff in this._handoffs) + foreach (KeyValuePair handoff in this._handoffs) { AIFunction handoffFunction = AIFunctionFactory.Create( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs index 2de4af2e9d..a98a40e972 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs @@ -7,8 +7,8 @@ using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration.Handoff; @@ -29,8 +29,10 @@ public class HandoffOrchestration : AgentOrchestration agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal); - agentNames.Add(handoffs.FirstAgentName); + HashSet agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal) + { + handoffs.FirstAgentName + }; // Extract names from handoffs that don't align with a member agent. string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))]; // Fail fast if invalid names are present. diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs index 3e1b1983c0..9a562ee7c9 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Handoff/Handoffs.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Orchestration.Handoff; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs index a62764c38f..88efced26b 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/AgentOrchestrationLogMessages.cs @@ -2,8 +2,8 @@ using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -21,7 +21,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs the start of the registration phase for an orchestration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "REGISTER {Orchestration} Start: {Topic}")] public static partial void LogOrchestrationRegistrationStart( @@ -33,7 +32,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs pattern actor registration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "REGISTER ACTOR {Orchestration} {label}: {AgentType}")] public static partial void LogRegisterActor( @@ -46,7 +44,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs agent actor registration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "REGISTER ACTOR {Orchestration} {label} #{Count}: {AgentType}")] public static partial void LogRegisterActor( @@ -60,7 +57,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs the end of the registration phase for an orchestration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "REGISTER {Orchestration} Complete: {Topic}")] public static partial void LogOrchestrationRegistrationDone( @@ -72,7 +68,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs an orchestration invocation /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "INVOKE {Orchestration}: {Topic}")] public static partial void LogOrchestrationInvoke( @@ -85,7 +80,6 @@ internal static partial class AgentOrchestrationLogMessages /// yielded control back to the caller. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "YIELD {Orchestration}: {Topic}")] public static partial void LogOrchestrationYield( @@ -97,7 +91,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs the start an orchestration (top/outer). /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "START {Orchestration}: {AgentId}")] public static partial void LogOrchestrationStart( @@ -109,7 +102,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs that orchestration request actor is active /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "INIT {Orchestration}: {AgentId}")] public static partial void LogOrchestrationRequestInvoke( @@ -121,7 +113,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs that orchestration request actor experienced an unexpected failure. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Error, Message = "FAILURE {Orchestration}: {AgentId}")] public static partial void LogOrchestrationRequestFailure( @@ -134,7 +125,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs that orchestration result actor is active /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "EXIT {Orchestration}: {AgentId}")] public static partial void LogOrchestrationResultInvoke( @@ -146,7 +136,6 @@ internal static partial class AgentOrchestrationLogMessages /// Logs that orchestration result actor experienced an unexpected failure. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Error, Message = "FAILURE {Orchestration}: {AgentId}")] public static partial void LogOrchestrationResultFailure( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs index 0d36ddc9b3..b266310669 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs @@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.Orchestration.Concurrent; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -18,7 +18,6 @@ namespace Microsoft.Agents.Orchestration; internal static partial class ConcurrentOrchestrationLogMessages { [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "REQUEST Concurrent agent [{AgentId}]")] public static partial void LogConcurrentAgentInvoke( @@ -26,7 +25,6 @@ internal static partial class ConcurrentOrchestrationLogMessages AgentId agentId); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "RESULT Concurrent agent [{AgentId}]: {Message}")] public static partial void LogConcurrentAgentResult( @@ -38,7 +36,6 @@ internal static partial class ConcurrentOrchestrationLogMessages /// Logs result capture. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")] public static partial void LogConcurrentResultCapture( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs index 1dbabb7f8e..76a60c270a 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/GroupChatOrchestrationLogMessages.cs @@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.Orchestration.GroupChat; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -18,7 +18,6 @@ namespace Microsoft.Agents.Orchestration; internal static partial class GroupChatOrchestrationLogMessages { [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "CHAT AGENT invoked [{AgentId}]")] public static partial void LogChatAgentInvoke( @@ -26,7 +25,6 @@ internal static partial class GroupChatOrchestrationLogMessages AgentId agentId); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "CHAT AGENT result [{AgentId}]: {Message}")] public static partial void LogChatAgentResult( @@ -35,7 +33,6 @@ internal static partial class GroupChatOrchestrationLogMessages string? message); [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "CHAT MANAGER initialized [{AgentId}]")] public static partial void LogChatManagerInit( @@ -43,7 +40,6 @@ internal static partial class GroupChatOrchestrationLogMessages AgentId agentId); [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "CHAT MANAGER invoked [{AgentId}]")] public static partial void LogChatManagerInvoke( @@ -51,7 +47,6 @@ internal static partial class GroupChatOrchestrationLogMessages AgentId agentId); [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")] public static partial void LogChatManagerTerminate( @@ -61,7 +56,6 @@ internal static partial class GroupChatOrchestrationLogMessages string reason); [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")] public static partial void LogChatManagerSelect( @@ -70,7 +64,6 @@ internal static partial class GroupChatOrchestrationLogMessages AgentType nextAgent); [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")] public static partial void LogChatManagerResult( @@ -80,7 +73,6 @@ internal static partial class GroupChatOrchestrationLogMessages string reason); [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")] public static partial void LogChatManagerInput( @@ -90,7 +82,6 @@ internal static partial class GroupChatOrchestrationLogMessages string reason); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "CHAT AGENT user-input [{AgentId}]: {Message}")] public static partial void LogChatManagerUserInput( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs index b29938e0e7..132167e6a1 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/HandoffOrchestrationLogMessages.cs @@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.Orchestration.Handoff; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -18,7 +18,6 @@ namespace Microsoft.Agents.Orchestration; internal static partial class HandoffOrchestrationLogMessages { [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "REQUEST Handoff agent [{AgentId}]")] public static partial void LogHandoffAgentInvoke( @@ -26,7 +25,6 @@ internal static partial class HandoffOrchestrationLogMessages AgentId agentId); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "RESULT Handoff agent [{AgentId}]: {Message}")] public static partial void LogHandoffAgentResult( @@ -35,7 +33,6 @@ internal static partial class HandoffOrchestrationLogMessages string? message); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "TOOL Handoff [{AgentId}]: {Name}")] public static partial void LogHandoffFunctionCall( @@ -44,7 +41,6 @@ internal static partial class HandoffOrchestrationLogMessages string name); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "RESULT Handoff summary [{AgentId}]: {Summary}")] public static partial void LogHandoffSummary( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs index f3edf26b60..98760093b7 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/OrchestrationResultLogMessages.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -20,7 +20,6 @@ internal static partial class OrchestrationResultLogMessages /// Logs awaiting the orchestration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "AWAIT {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultAwait( @@ -32,7 +31,6 @@ internal static partial class OrchestrationResultLogMessages /// Logs timeout while awaiting the orchestration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Error, Message = "TIMEOUT {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultTimeout( @@ -44,7 +42,6 @@ internal static partial class OrchestrationResultLogMessages /// Logs cancelled the orchestration. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Error, Message = "CANCELLED {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultCancelled( @@ -56,7 +53,6 @@ internal static partial class OrchestrationResultLogMessages /// Logs the awaited the orchestration has completed. /// [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "COMPLETE {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultComplete( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs b/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs index a678fabcd1..39425ff792 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Logging/SequentialOrchestrationLogMessages.cs @@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.Orchestration.Sequential; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -18,7 +18,6 @@ namespace Microsoft.Agents.Orchestration; internal static partial class SequentialOrchestrationLogMessages { [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "REQUEST Sequential agent [{AgentId}]")] public static partial void LogSequentialAgentInvoke( @@ -26,7 +25,6 @@ internal static partial class SequentialOrchestrationLogMessages AgentId agentId); [LoggerMessage( - EventId = 0, Level = LogLevel.Trace, Message = "RESULT Sequential agent [{AgentId}]: {Message}")] public static partial void LogSequentialAgentResult( diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj index 21a2926857..3be18dcea3 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj +++ b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj @@ -22,12 +22,8 @@ - - - - - - + + diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs index 34a8937de3..5b1366c1a7 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationActor.cs @@ -2,9 +2,8 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs index 5daf6e8d68..3f6a439fd8 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationContext.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Threading; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs index cac361741a..2ecf0e2c75 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationResult.cs @@ -3,8 +3,8 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration; @@ -73,7 +73,7 @@ public sealed class OrchestrationResult : IDisposable if (timeout.HasValue) { - Task[] tasks = { this._completion.Task }; + Task[] tasks = [this._completion.Task]; if (!Task.WaitAll(tasks, timeout.Value)) { this._logger.LogOrchestrationResultTimeout(this.Orchestration, this.Topic); diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs index b07556eb11..59876e6cd0 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialActor.cs @@ -4,9 +4,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.Agents.Orchestration.Sequential; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs index 07bd5a086a..ba53494ffa 100644 --- a/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Microsoft.Agents.Orchestration/Sequential/SequentialOrchestration.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Extensions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration.Sequential; diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs new file mode 100644 index 0000000000..7b6b462568 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentId.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Agent ID uniquely identifies an agent instance within an agent runtime, including a distributed runtime. +/// It serves as the "address" of the agent instance for receiving messages. +/// \ +/// +/// See the Python equivalent: +/// AgentId in AutoGen (Python). +/// +[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")] +public struct AgentId : IEquatable +{ + /// + /// The default source value used when no source is explicitly provided. + /// + public const string DefaultKey = "default"; + + private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126 + + /// + /// An identifier that associates an agent with a specific factory function. + /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). + /// + public string Type { get; } + + /// + /// Agent instance identifier. + /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). + /// + public string Key { get; } + + internal static Regex KeyRegex1 => KeyRegex2; + + internal static Regex KeyRegex2 => KeyRegex; + + /// + /// Initializes a new instance of the struct. + /// + /// The agent type. + /// Agent instance identifier. + public AgentId(string type, string key) + { + AgentType.Validate(type); + + if (string.IsNullOrWhiteSpace(key) || !KeyRegex.IsMatch(key)) + { + throw new ArgumentException($"Invalid AgentId key: '{key}'. Must only contain ASCII characters 32-126."); + } + + this.Type = type; + this.Key = key; + } + + /// + /// Initializes a new instance of the struct from a tuple. + /// + /// A tuple containing the agent type and key. + public AgentId((string Type, string Key) kvPair) + : this(kvPair.Type, kvPair.Key) + { + } + + /// + /// Initializes a new instance of the struct from an . + /// + /// The agent type. + /// Agent instance identifier. + public AgentId(AgentType type, string key) + : this(type.Name, key) + { + } + + /// + /// Convert a string of the format "type/key" into an . + /// + /// The agent ID string. + /// An instance of . + public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key))); + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/key". + public override readonly string ToString() => $"{this.Type}/{this.Key}"; + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current ; otherwise, false. + public override readonly bool Equals([NotNullWhen(true)] object? obj) + { + return (obj is AgentId other && this.Equals(other)); + } + + /// + public readonly bool Equals(AgentId other) + { + return this.Type == other.Type && this.Key == other.Key; + } + + /// + /// Returns a hash code for this . + /// + /// A hash code for the current instance. + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Type, this.Key); + } + + /// + /// Explicitly converts a string to an . + /// + /// The string representation of an agent ID. + /// An instance of . + public static explicit operator AgentId(string id) => FromStr(id); + + /// + /// Equality operator for . + /// + public static bool operator ==(AgentId left, AgentId right) => left.Equals(right); + + /// + /// Inequality operator for . + /// + public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs new file mode 100644 index 0000000000..f90b95a048 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentMetadata.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents metadata associated with an agent, including its type, unique key, and description. +/// +public readonly struct AgentMetadata(string type, string key, string description) : IEquatable +{ + /// + /// An identifier that associates an agent with a specific factory function. + /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_). + /// + public string Type { get; } = type; + + /// + /// A unique key identifying the agent instance. + /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_). + /// + public string Key { get; } = key; + + /// + /// A brief description of the agent's purpose or functionality. + /// + public string Description { get; } = description; + + /// + public override readonly bool Equals(object? obj) + { + return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata); + } + + /// + public readonly bool Equals(AgentMetadata other) + { + return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal); + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Type, this.Key); + } + + /// + public static bool operator ==(AgentMetadata left, AgentMetadata right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(AgentMetadata left, AgentMetadata right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs new file mode 100644 index 0000000000..9ece29fda3 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentProxy.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// A proxy that allows you to use an in place of its associated . +/// +public class AgentProxy +{ + /// + /// The runtime instance used to interact with agents. + /// + private readonly IAgentRuntime _runtime; + private AgentMetadata? _metadata; + + /// + /// Initializes a new instance of the class. + /// + public AgentProxy(AgentId agentId, IAgentRuntime runtime) + { + this.Id = agentId; + this._runtime = runtime; + } + + /// + /// The target agent for this proxy. + /// + public AgentId Id { get; } + + /// + /// Gets the metadata of the agent. + /// + /// + /// An instance of containing details about the agent. + /// + public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap(); + + /// + /// Sends a message to the agent and processes the response. + /// + /// The message to send to the agent. + /// The agent that is sending the message. + /// + /// The message ID. If null, a new message ID will be generated. + /// This message ID must be unique and is recommended to be a UUID. + /// + /// + /// A token used to cancel an in-progress operation. Defaults to null. + /// + /// A task representing the asynchronous operation, returning the response from the agent. + public ValueTask SendMessageAsync(object message, AgentId sender, string? messageId = null, CancellationToken cancellationToken = default) + { + return this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken); + } + + /// + /// Loads the state of the agent from a previously saved state. + /// + /// A dictionary representing the state of the agent. Must be JSON serializable. + /// A task representing the asynchronous operation. + public ValueTask LoadStateAsync(JsonElement state) + { + return this._runtime.LoadAgentStateAsync(this.Id, state); + } + + /// + /// Saves the state of the agent. The result must be JSON serializable. + /// + /// A task representing the asynchronous operation, returning a dictionary containing the saved state. + public ValueTask SaveStateAsync() + { + return this._runtime.SaveAgentStateAsync(this.Id); + } + + private AgentMetadata QueryMetadataAndUnwrap() + { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs new file mode 100644 index 0000000000..12dcc0cfcc --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/AgentType.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents the type of an agent as a string. +/// This is a strongly-typed wrapper around a string, ensuring type safety when working with agent types. +/// +/// +/// This struct is immutable and provides implicit conversion to and from . +/// +public readonly partial struct AgentType : IEquatable +{ +#if NET + [GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]*$")] + private static partial Regex TypeRegex(); +#else + private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); +#endif + + internal static void Validate(string type) + { + if (string.IsNullOrWhiteSpace(type) || !TypeRegex().IsMatch(type)) + { + throw new ArgumentException($"Invalid AgentId type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces."); + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The agent type. + public AgentType(string type) + { + Validate(type); + this.Name = type; + } + + /// + /// The string representation of this agent type. + /// + public string Name { get; } + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/source". + public override readonly string ToString() => this.Name; + + /// + /// Explicitly converts a to an . + /// + /// The .NET to convert. + /// An instance with the name of the provided type. + public static explicit operator AgentType(Type type) => new(type.Name); + + /// + /// Implicitly converts a to an . + /// + /// The string representation of the agent type. + /// An instance with the given name. + public static implicit operator AgentType(string type) => new(type); + + /// + /// Implicitly converts an to a . + /// + /// The instance. + /// The string representation of the agent type. + public static implicit operator string(AgentType type) => type.ToString(); + + /// + public override bool Equals(object? obj) + { + return obj is AgentType other && this.Equals(other); + } + + /// + public bool Equals(AgentType other) + { + return this.Name.Equals(other.Name, StringComparison.Ordinal); + } + + /// + public override int GetHashCode() + { + return this.Name.GetHashCode(); + } + + /// + public static bool operator ==(AgentType left, AgentType right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(AgentType left, AgentType right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs new file mode 100644 index 0000000000..5c658cb743 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/BaseAgent.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents the base class for an agent in the AutoGen system. +/// +public abstract class BaseAgent : IHostableAgent +{ + private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement; + + /// + /// The activity source for tracing. + /// + public static readonly ActivitySource TraceSource = new($"{typeof(IAgent).Namespace}"); + + private readonly Dictionary _handlerInvokers; + private readonly IAgentRuntime _runtime; + + /// + /// Provides logging capabilities used for diagnostic and operational information. + /// + protected internal ILogger Logger { get; } + + /// + /// Gets the description of the agent. + /// + protected string Description { get; } + + /// + /// Gets the unique identifier of the agent. + /// + public AgentId Id { get; } + + /// + /// Gets the metadata of the agent. + /// + public AgentMetadata Metadata { get; } + + /// + /// Initializes a new instance of the BaseAgent class with the specified identifier, runtime, description, and optional logger. + /// + /// The unique identifier of the agent. + /// The runtime environment in which the agent operates. + /// A brief description of the agent's purpose. + /// An optional logger for recording diagnostic information. + protected BaseAgent( + AgentId id, + IAgentRuntime runtime, + string description, + ILogger? logger = null) + { + this.Logger = logger ?? NullLogger.Instance; + + this.Id = id; + this.Description = description; + this.Metadata = new AgentMetadata(this.Id.Type, this.Id.Key, this.Description); + + this._runtime = runtime; + this._handlerInvokers = HandlerInvoker.ReflectAgentHandlers(this); + } + + /// + /// Handles an incoming message by determining its type and invoking the corresponding handler method if available. + /// + /// The message object to be handled. + /// The context associated with the message. + /// A ValueTask that represents the asynchronous operation, containing the response object or null. + public async ValueTask OnMessageAsync(object message, MessageContext messageContext) + { + // Determine type of message, then get handler method and invoke it + Type messageType = message.GetType(); + if (this._handlerInvokers.TryGetValue(messageType, out HandlerInvoker? handlerInvoker)) + { + return await handlerInvoker.InvokeAsync(message, messageContext).ConfigureAwait(false); + } + + return null; + } + + /// + public virtual ValueTask SaveStateAsync() + { + return new ValueTask(s_emptyElement); + } + + /// + public virtual ValueTask LoadStateAsync(JsonElement state) => + default; + + /// + /// Closes this agent gracefully by releasing allocated resources and performing any necessary cleanup. + /// + public virtual ValueTask CloseAsync() => + default; + + /// + /// Sends a message to a specified recipient agent through the runtime. + /// + /// The requested agent's type. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, returning the response object or null. + protected async ValueTask GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default) + { + try + { + return await this._runtime.GetAgentAsync(agent, lazy: false).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + return null; + } + } + + /// + /// Sends a message to a specified recipient agent through the runtime. + /// + /// The message object to send. + /// The recipient agent's identifier. + /// An optional identifier for the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, returning the response object or null. + protected ValueTask SendMessageAsync(object message, AgentId recipient, string? messageId = null, CancellationToken cancellationToken = default) + { + return this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken); + } + + /// + /// Publishes a message to all agents subscribed to a specific topic through the runtime. + /// + /// The message object to publish. + /// The topic identifier to which the message is published. + /// An optional identifier for the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous publish operation. + protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) + { + return this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs new file mode 100644 index 0000000000..89f4348f38 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/CantHandleException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Exception thrown when a handler cannot process the given message. +/// +[ExcludeFromCodeCoverage] +public class CantHandleException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public CantHandleException() : base("The handler cannot process the given message.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public CantHandleException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public CantHandleException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs new file mode 100644 index 0000000000..72139afc1d --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/MessageDroppedException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Exception thrown when a message is dropped. +/// +[ExcludeFromCodeCoverage] +public class MessageDroppedException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public MessageDroppedException() : base("The message was dropped.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public MessageDroppedException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public MessageDroppedException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs new file mode 100644 index 0000000000..fe6d18c1d2 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/NotAccessibleException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Exception thrown when an attempt is made to access an unavailable value, such as a remote resource. +/// +[ExcludeFromCodeCoverage] +public class NotAccessibleException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotAccessibleException() : base("The requested value is not accessible.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public NotAccessibleException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public NotAccessibleException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs new file mode 100644 index 0000000000..3ecc046a60 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Exceptions/UndeliverableException.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Exception thrown when a message cannot be delivered. +/// +[ExcludeFromCodeCoverage] +public class UndeliverableException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public UndeliverableException() : base("The message cannot be delivered.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public UndeliverableException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public UndeliverableException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs new file mode 100644 index 0000000000..8a18243d80 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/HandlerInvoker.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Invokes handler methods asynchronously using reflection. +/// The target methods must return either a ValueTask or a ValueTask{T}. +/// This class wraps the reflection call and provides a unified asynchronous invocation interface. +/// +internal sealed class HandlerInvoker +{ + /// + /// Scans the provided agent for implemented handler interfaces (IHandle<> and IHandle<,>) via reflection, + /// creates a corresponding for each handler method, and returns a dictionary that maps + /// the message type (first generic argument of the interface) to its invoker. + /// + /// The agent instance whose handler interfaces will be reflected. + /// A dictionary mapping message types to their corresponding instances. + public static Dictionary ReflectAgentHandlers(BaseAgent agent) + { + Type realType = agent.GetType(); + + IEnumerable candidateInterfaces = + realType.GetInterfaces() + .Where(i => i.IsGenericType && + (i.GetGenericTypeDefinition() == typeof(IHandle<>) || + (i.GetGenericTypeDefinition() == typeof(IHandle<,>)))); + + Dictionary invokers = []; + foreach (Type interface_ in candidateInterfaces) + { + MethodInfo handleAsync = + interface_.GetMethod(nameof(IHandle.HandleAsync), BindingFlags.Instance | BindingFlags.Public) ?? + throw new InvalidOperationException($"No handler method found for interface {interface_.FullName}"); + + HandlerInvoker invoker = new(handleAsync, agent); + invokers.Add(interface_.GetGenericArguments()[0], invoker); + } + + return invokers; + } + + /// + /// Represents the asynchronous invocation function. + /// + private Func> Invocation { get; } + + /// + /// Initializes a new instance of the class with the specified method information and target object. + /// + /// The MethodInfo representing the handler method to be invoked. + /// The target instance of the agent. + /// Thrown if the target is missing for a non-static method or if the method's return type is not supported. + private HandlerInvoker(MethodInfo methodInfo, BaseAgent target) + { + object? invocation(object? message, MessageContext messageContext) => methodInfo.Invoke(target, [message, messageContext]); + + Func> getResultAsync; + // Check if the method returns a non-generic ValueTask + if (methodInfo.ReturnType.IsAssignableFrom(typeof(ValueTask))) + { + getResultAsync = async (message, messageContext) => + { + // Await the ValueTask and return null as there is no result value. + await ((ValueTask)invocation(message, messageContext)!).ConfigureAwait(false); + return null; + }; + } + // Check if the method returns a generic ValueTask + else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + // Obtain the generic type argument for ValueTask + MethodInfo typeEraseAwait = typeof(HandlerInvoker) + .GetMethod(nameof(TypeEraseAwaitAsync), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(methodInfo.ReturnType.GetGenericArguments()[0]); + + getResultAsync = async (message, messageContext) => + { + // Execute the invocation and then type-erase the ValueTask to ValueTask + object valueTask = invocation(message, messageContext)!; + object? typelessValueTask = typeEraseAwait.Invoke(null, [valueTask]); + + Debug.Assert(typelessValueTask is ValueTask, "Expected ValueTask after type erasure."); + + return await ((ValueTask)typelessValueTask).ConfigureAwait(false); + }; + } + else + { + throw new InvalidOperationException($"Method {methodInfo.Name} must return a ValueTask or ValueTask"); + } + + this.Invocation = getResultAsync; + } + + /// + /// Invokes the handler method asynchronously with the provided message and context. + /// + /// The message to be passed as the first argument to the handler. + /// The contextual information associated with the message. + /// A ValueTask representing the asynchronous operation, which yields the handler's result. + public async ValueTask InvokeAsync(object? obj, MessageContext messageContext) + { + try + { + return await this.Invocation.Invoke(obj, messageContext).ConfigureAwait(false); + } + catch (TargetInvocationException ex) + { + // Unwrap the exception to get the original exception thrown by the handler method. + Exception? innerException = ex.InnerException; + if (innerException != null) + { + throw innerException; + } + throw; + } + } + + /// + /// Awaits a generic ValueTask and returns its result as an object. + /// This method is used to convert a ValueTask{T} to ValueTask{object?}. + /// + /// The type of the result contained in the ValueTask. + /// The ValueTask to be awaited. + /// A ValueTask containing the result as an object. + private static async ValueTask TypeEraseAwaitAsync(ValueTask vt) + { + return await vt.ConfigureAwait(false); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgent.cs new file mode 100644 index 0000000000..bb9a2376ed --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents an agent within the runtime that can process messages, maintain state, and be closed when no longer needed. +/// +public interface IAgent : ISaveState +{ + /// + /// Gets the unique identifier of the agent. + /// + AgentId Id { get; } + + /// + /// Gets metadata associated with the agent. + /// + AgentMetadata Metadata { get; } + + /// + /// Handles an incoming message for the agent. + /// This should only be called by the runtime, not by other agents. + /// + /// The received message. The type should match one of the expected subscription types. + /// The context of the message, providing additional metadata. + /// + /// A task representing the asynchronous operation, returning a response to the message. + /// The response can be null if no reply is necessary. + /// + /// Thrown if the message was cancelled. + /// Thrown if the agent cannot handle the message. + ValueTask OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET? +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs new file mode 100644 index 0000000000..b99f882760 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IAgentRuntime.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Defines the runtime environment for agents, managing message sending, subscriptions, agent resolution, and state persistence. +/// +public interface IAgentRuntime : ISaveState +{ + /// + /// Sends a message to an agent and gets a response. + /// This method should be used to communicate directly with an agent. + /// + /// The message to send. + /// The agent to send the message to. + /// The agent sending the message. Should be null if sent from an external source. + /// A unique identifier for the message. If null, a new ID will be generated. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning the response from the agent. + /// Thrown if the recipient cannot handle the message. + /// Thrown if the message cannot be delivered. + ValueTask SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + + /// + /// Publishes a message to all agents subscribed to the given topic. + /// No responses are expected from publishing. + /// + /// The message to publish. + /// The topic to publish the message to. + /// The agent sending the message. Defaults to null. + /// A unique message ID. If null, a new one will be generated. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation. + /// Thrown if the message cannot be delivered. + ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + + /// + /// Retrieves an agent by its unique identifier. + /// + /// The unique identifier of the agent. + /// If true, the agent is fetched lazily. + /// A task representing the asynchronous operation, returning the agent's ID. + ValueTask GetAgentAsync(AgentId agentId, bool lazy = true/*, CancellationToken? = default*/); + + /// + /// Retrieves an agent by its type. + /// + /// The type of the agent. + /// An optional key to specify variations of the agent. Defaults to "default". + /// If true, the agent is fetched lazily. + /// A task representing the asynchronous operation, returning the agent's ID. + ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + + /// + /// Retrieves an agent by its string representation. + /// + /// The string representation of the agent. + /// An optional key to specify variations of the agent. Defaults to "default". + /// If true, the agent is fetched lazily. + /// A task representing the asynchronous operation, returning the agent's ID. + ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + + /// + /// Saves the state of an agent. + /// The result must be JSON serializable. + /// + /// The ID of the agent whose state is being saved. + /// A task representing the asynchronous operation, returning a dictionary of the saved state. + ValueTask SaveAgentStateAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/); + + /// + /// Loads the saved state into an agent. + /// + /// The ID of the agent whose state is being restored. + /// The state dictionary to restore. + /// A task representing the asynchronous operation. + ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state/*, CancellationToken? cancellationToken = default*/); + + /// + /// Retrieves metadata for an agent. + /// + /// The ID of the agent. + /// A task representing the asynchronous operation, returning the agent's metadata. + ValueTask GetAgentMetadataAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/); + + /// + /// Adds a new subscription for the runtime to handle when processing published messages. + /// + /// The subscription to add. + /// A task representing the asynchronous operation. + ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription/*, CancellationToken? cancellationToken = default*/); + + /// + /// Removes a subscription from the runtime. + /// + /// The unique identifier of the subscription to remove. + /// A task representing the asynchronous operation. + /// Thrown if the subscription does not exist. + ValueTask RemoveSubscriptionAsync(string subscriptionId/*, CancellationToken? cancellationToken = default*/); + + /// + /// Registers an agent factory with the runtime, associating it with a specific agent type. + /// The type must be unique. + /// + /// The agent type to associate with the factory. + /// A function that asynchronously creates the agent instance. + /// A task representing the asynchronous operation, returning the registered . + ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc); + + /// + /// Attempts to retrieve an for the specified agent. + /// + /// The ID of the agent. + /// A task representing the asynchronous operation, returning an if successful. + ValueTask TryGetAgentProxyAsync(AgentId agentId); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs new file mode 100644 index 0000000000..bdfb926a85 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHandle.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Defines a handler interface for processing items of type . +/// +/// The type of item to be handled. +public interface IHandle +{ + /// + /// Handles the specified item asynchronously. + /// + /// The item to be handled. + /// The context of the message being handled. + /// A task that represents the asynchronous operation. + ValueTask HandleAsync(T item, MessageContext messageContext); +} + +/// +/// Defines a handler interface for processing items of type and . +/// +/// The input type +/// The output type +public interface IHandle +{ + /// + /// Handles the specified item asynchronously. + /// + /// The item to be handled. + /// The context of the message being handled. + /// A task that represents the asynchronous operation. + ValueTask HandleAsync(TIn item, MessageContext messageContext); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs new file mode 100644 index 0000000000..cac3727b07 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/IHostableAgent.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents an agent that can be explicitly hosted and closed when the runtime shuts down. +/// +public interface IHostableAgent : IAgent +{ + /// + /// Called when the runtime is closing. + /// + /// A task representing the asynchronous operation. + ValueTask CloseAsync(); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs new file mode 100644 index 0000000000..ca75467831 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISaveState.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Defines a contract for saving and loading the state of an object. +/// The state must be JSON serializable. +/// +public interface ISaveState +{ + /// + /// Saves the current state of the object. + /// + /// + /// A task representing the asynchronous operation, returning a dictionary + /// containing the saved state. The structure of the state is implementation-defined + /// but must be JSON serializable. + /// + ValueTask SaveStateAsync(); + + /// + /// Loads a previously saved state into the object. + /// + /// + /// A dictionary representing the saved state. The structure of the state + /// is implementation-defined but must be JSON serializable. + /// + /// A task representing the asynchronous operation. + ValueTask LoadStateAsync(JsonElement state); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs new file mode 100644 index 0000000000..5fa4245e97 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/ISubscriptionDefinition.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Defines a subscription that matches topics and maps them to agents. +/// +public interface ISubscriptionDefinition +{ + /// + /// Gets the unique identifier of the subscription. + /// + string Id { get; } + + /// + /// Determines whether the specified object is equal to the current subscription. + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to this instance; otherwise, false. + bool Equals([NotNullWhen(true)] object? obj); + + /// + /// Determines whether the specified subscription is equal to the current subscription. + /// + /// The subscription to compare. + /// true if the subscriptions are equal; otherwise, false. + bool Equals(ISubscriptionDefinition? other); + + /// + /// Returns a hash code for this subscription. + /// + /// A hash code for the subscription. + int GetHashCode(); + + /// + /// Checks if a given matches the subscription. + /// + /// The topic to check. + /// true if the topic matches the subscription; otherwise, false. + bool Matches(TopicId topic); + + /// + /// Maps a to an . + /// Should only be called if returns true. + /// + /// The topic to map. + /// The that should handle the topic. + AgentId MapToAgent(TopicId topic); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs new file mode 100644 index 0000000000..68ee6ad95e --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/KeyValueParserExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.RegularExpressions; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Provides helper methods for parsing key-value string representations. +/// +internal static class KeyValueParserExtensions +{ + /// + /// The regular expression pattern used to match key-value pairs in the format "key/value". + /// + private const string KVPairPattern = @"^(?\w+)/(?\w+)$"; + + /// + /// The compiled regex used for extracting key-value pairs from a string. + /// + private static readonly Regex KVPairRegex = new(KVPairPattern, RegexOptions.Compiled); + + /// + /// Parses a string in the format "key/value" into a tuple containing the key and value. + /// + /// The input string containing a key-value pair. + /// The expected name of the key component. + /// The expected name of the value component. + /// A tuple containing the extracted key and value. + /// + /// Thrown if the input string does not match the expected "key/value" format. + /// + /// + /// Example usage: + /// + /// string input = "agent1/12345"; + /// var result = input.ToKVPair("Type", "Key"); + /// Console.WriteLine(result.Item1); // Outputs: agent1 + /// Console.WriteLine(result.Item2); // Outputs: 12345 + /// + /// + public static (string, string) ToKeyValuePair(this string inputPair, string keyName, string valueName) + { + Match match = KVPairRegex.Match(inputPair); + if (match.Success) + { + return (match.Groups["key"].Value, match.Groups["value"].Value); + } + + throw new FormatException($"Invalid key-value pair format: {inputPair}; expecting \"{{{keyName}}}/{{{valueName}}}\""); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs new file mode 100644 index 0000000000..c3d7b9c956 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/MessageContext.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents the context of a message being sent within the agent runtime. +/// This includes metadata such as the sender, topic, RPC status, and cancellation handling. +/// +public class MessageContext(string messageId, CancellationToken cancellationToken) +{ + /// + /// Initializes a new instance of the class. + /// + public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation) + { } + + /// + /// Gets or sets the unique identifier for this message. + /// + public string MessageId { get; } = messageId; + + /// + /// Gets or sets the cancellation token associated with this message. + /// This can be used to cancel the operation if necessary. + /// + public CancellationToken CancellationToken { get; } = cancellationToken; + + /// + /// Gets or sets the sender of the message. + /// If null, the sender is unspecified. + /// + public AgentId? Sender { get; set; } + + /// + /// Gets or sets the topic associated with the message. + /// If null, the message is not tied to a specific topic. + /// + public TopicId? Topic { get; set; } + + /// + /// Gets or sets a value indicating whether this message is part of an RPC (Remote Procedure Call). + /// + public bool IsRpc { get; set; } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj new file mode 100644 index 0000000000..f28086b0da --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj @@ -0,0 +1,28 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + $(NoWarn);IDE1006;IDE0130 + alpha + false + + + + true + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs new file mode 100644 index 0000000000..781fe76874 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TopicId.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// Represents a topic identifier that defines the scope of a broadcast message. +/// The agent runtime implements a publish-subscribe model through its broadcast API, +/// where messages must be published with a specific topic. +/// +/// See the Python equivalent: +/// CloudEvents Type Specification. +/// +public struct TopicId : IEquatable +{ + /// + /// The default source value used when no source is explicitly provided. + /// + public const string DefaultSource = "default"; + + /// + /// The separator character for the string representation of the topic. + /// + public const string Separator = "/"; + + /// + /// Gets the type of the event that this represents. + /// This adheres to the CloudEvents specification. + /// + /// Must match the pattern: ^[\w\-\.\:\=]+$. + /// + /// Learn more here: + /// CloudEvents Type. + /// + public string Type { get; } + + /// + /// Gets the source that identifies the context in which an event happened. + /// This adheres to the CloudEvents specification. + /// + /// Learn more here: + /// CloudEvents Source. + /// + public string Source { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The type of the topic. + /// The source of the event. Defaults to if not specified. + public TopicId(string type, string source = DefaultSource) + { + this.Type = type; + this.Source = source; + } + + /// + /// Initializes a new instance of the struct from a tuple. + /// + /// A tuple containing the topic type and source. + public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.Source) + { + } + + /// + /// Converts a string in the format "type/source" into a . + /// + /// The topic ID string. + /// An instance of . + /// Thrown when the string is not in the valid "type/source" format. + public static TopicId FromStr(string maybeTopicId) => new(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source))); + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/source". + public override readonly string ToString() => $"{this.Type}{Separator}{this.Source}"; + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current ; otherwise, false. + public override readonly bool Equals([NotNullWhen(true)] object? obj) + { + if (obj is TopicId other) + { + return this.Type == other.Type && this.Source == other.Source; + } + + return false; + } + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current ; otherwise, false. + public readonly bool Equals([NotNullWhen(true)] TopicId other) + { + return this.Type == other.Type && this.Source == other.Source; + } + + /// + /// Returns a hash code for this . + /// + /// A hash code for the current instance. + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Type, this.Source); + } + + /// + /// Explicitly converts a string to a . + /// + /// The string representation of a topic ID. + /// An instance of . + public static explicit operator TopicId(string id) => FromStr(id); + + // TODO: Implement < for wildcard matching (type, *) + // == => < + // Type == other.Type => < + /// + /// Determines whether the given matches another topic. + /// + /// The topic ID to compare against. + /// + /// true if the topic types are equal; otherwise, false. + /// + public readonly bool IsWildcardMatch(TopicId other) + { + return this.Type == other.Type; + } + + /// + public static bool operator ==(TopicId left, TopicId right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(TopicId left, TopicId right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs new file mode 100644 index 0000000000..75f03a864a --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/TypeSubscription.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime; + +/// +/// This subscription matches on topics based on the exact type and maps to agents using the source of the topic as the agent key. +/// This subscription causes each source to have its own agent instance. +/// +/// +/// Example: +/// +/// var subscription = new TypeSubscription("t1", "a1"); +/// +/// In this case: +/// - A with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`. +/// - A with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`. +/// +public class TypeSubscription : ISubscriptionDefinition +{ + /// + /// Initializes a new instance of the class. + /// + /// The exact topic type to match against. + /// Agent type to handle this subscription. + /// Unique identifier for the subscription. If not provided, a new UUID will be generated. + public TypeSubscription(string topicType, AgentType agentType, string? id = null) + { + this.TopicType = topicType; + this.AgentType = agentType; + this.Id = id ?? Guid.NewGuid().ToString(); + } + + /// + /// Gets the unique identifier of the subscription. + /// + public string Id { get; } + + /// + /// Gets the exact topic type used for matching. + /// + public string TopicType { get; } + + /// + /// Gets the agent type that handles this subscription. + /// + public AgentType AgentType { get; } + + /// + /// Checks if a given matches the subscription based on an exact type match. + /// + /// The topic to check. + /// true if the topic's type matches exactly, false otherwise. + public bool Matches(TopicId topic) + { + return topic.Type == this.TopicType; + } + + /// + /// Maps a to an . Should only be called if returns true. + /// + /// The topic to map. + /// An representing the agent that should handle the topic. + /// Thrown if the topic does not match the subscription. + public AgentId MapToAgent(TopicId topic) + { + if (!this.Matches(topic)) + { + throw new InvalidOperationException("TopicId does not match the subscription."); + } + + return new AgentId(this.AgentType, topic.Source); + } + + /// + /// Determines whether the specified object is equal to the current subscription. + /// + /// 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.AgentType == other.AgentType && + this.TopicType == other.TopicType)); + } + + /// + /// Determines whether the specified subscription is equal to the current subscription. + /// + /// The subscription to compare. + /// true if the subscriptions are equal; otherwise, false. + public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; + + /// + /// 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.AgentType, this.TopicType); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs new file mode 100644 index 0000000000..172d7f50ba --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/InProcessRuntime.cs @@ -0,0 +1,444 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; + +/// +/// Provides an in-process/in-memory implementation of the agent runtime. +/// +public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable +{ + private readonly Dictionary>> _agentFactories = []; + private readonly Dictionary _subscriptions = []; + private readonly ConcurrentQueue _messageDeliveryQueue = new(); + + private CancellationTokenSource? _shutdownSource; + private CancellationTokenSource? _finishSource; + private Task _messageDeliveryTask = Task.CompletedTask; + private Func _shouldContinue = () => true; + + // Exposed for testing purposes. + internal int messageQueueCount; + internal readonly Dictionary agentInstances = []; + + /// + /// Gets or sets a value indicating whether agents should receive messages they send themselves. + /// + public bool DeliverToSelf { get; set; } //= false; + + /// + public async ValueTask DisposeAsync() + { + await this.RunUntilIdleAsync().ConfigureAwait(false); + this._shutdownSource?.Dispose(); + this._finishSource?.Dispose(); + } + + /// + /// Starts the runtime service. + /// + /// Token to monitor for shutdown requests. + /// A task representing the asynchronous operation. + /// Thrown if the runtime is already started. + public Task StartAsync(CancellationToken cancellationToken = default) + { + if (this._shutdownSource != null) + { + throw new InvalidOperationException("Runtime is already running."); + } + + this._shutdownSource = new CancellationTokenSource(); + this._messageDeliveryTask = Task.Run(() => this.RunAsync(this._shutdownSource.Token), cancellationToken); + + return Task.CompletedTask; + } + + /// + /// Stops the runtime service. + /// + /// Token to propagate when stopping the runtime. + /// A task representing the asynchronous operation. + /// Thrown if the runtime is in the process of stopping. + public Task StopAsync(CancellationToken cancellationToken = default) + { + if (this._shutdownSource != null) + { + if (this._finishSource != null) + { + throw new InvalidOperationException("Runtime is already stopping."); + } + + this._finishSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + this._shutdownSource.Cancel(); + } + + return Task.CompletedTask; + } + + /// + /// This will run until the message queue is empty and then stop the runtime. + /// + public async Task RunUntilIdleAsync() + { + Func oldShouldContinue = this._shouldContinue; + this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty; + + // TODO: Do we want detach semantics? + await this._messageDeliveryTask.ConfigureAwait(false); + + this._shouldContinue = oldShouldContinue; + } + + /// + public ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + return this.ExecuteTracedAsync(async () => + { + MessageDelivery delivery = + new MessageEnvelope(message, messageId, cancellationToken) + .WithSender(sender) + .ForPublish(topic, this.PublishMessageServicerAsync); + + this._messageDeliveryQueue.Enqueue(delivery); + Interlocked.Increment(ref this.messageQueueCount); + + await delivery.ResultSink.Future.ConfigureAwait(false); + }); + } + + /// + public async ValueTask SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + return await this.ExecuteTracedAsync(async () => + { + MessageDelivery delivery = + new MessageEnvelope(message, messageId, cancellationToken) + .WithSender(sender) + .ForSend(recipient, this.SendMessageServicerAsync); + + this._messageDeliveryQueue.Enqueue(delivery); + Interlocked.Increment(ref this.messageQueueCount); + + try + { + return await delivery.ResultSink.Future.ConfigureAwait(false); + } + catch (TargetInvocationException ex) when (ex.InnerException is OperationCanceledException innerOCEx) + { + throw new OperationCanceledException($"Delivery of message {messageId} was cancelled.", innerOCEx); + } + }).ConfigureAwait(false); + } + + /// + public async ValueTask GetAgentAsync(AgentId agentId, bool lazy = true) + { + if (!lazy) + { + await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + } + + return agentId; + } + + /// + public ValueTask GetAgentAsync(AgentType agentType, string key = AgentId.DefaultKey, bool lazy = true) + => this.GetAgentAsync(new AgentId(agentType, key), lazy); + + /// + public ValueTask GetAgentAsync(string agent, string key = AgentId.DefaultKey, bool lazy = true) + => this.GetAgentAsync(new AgentId(agent, key), lazy); + + /// + public async ValueTask GetAgentMetadataAsync(AgentId agentId) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + return agent.Metadata; + } + + /// + public async ValueTask TryGetUnderlyingAgentInstanceAsync(AgentId agentId) where TAgent : IHostableAgent + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + + if (agent is not TAgent concreteAgent) + { + throw new InvalidOperationException($"Agent with name {agentId.Type} is not of type {typeof(TAgent).Name}."); + } + + return concreteAgent; + } + + /// + public async ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + await agent.LoadStateAsync(state).ConfigureAwait(false); + } + + /// + public async ValueTask SaveAgentStateAsync(AgentId agentId) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + return await agent.SaveStateAsync().ConfigureAwait(false); + } + + /// + public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) + { + if (this._subscriptions.ContainsKey(subscription.Id)) + { + throw new InvalidOperationException($"Subscription with id {subscription.Id} already exists."); + } + + this._subscriptions.Add(subscription.Id, subscription); + + return default; + } + + /// + public ValueTask RemoveSubscriptionAsync(string subscriptionId) + { + if (!this._subscriptions.ContainsKey(subscriptionId)) + { + throw new InvalidOperationException($"Subscription with id {subscriptionId} does not exist."); + } + + this._subscriptions.Remove(subscriptionId); + + return default; + } + + /// + public async ValueTask LoadStateAsync(JsonElement state) + { + foreach (JsonProperty agentIdStr in state.EnumerateObject()) + { + AgentId agentId = AgentId.FromStr(agentIdStr.Name); + + if (this._agentFactories.ContainsKey(agentId.Type)) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + await agent.LoadStateAsync(agentIdStr.Value).ConfigureAwait(false); + } + } + } + + /// + public async ValueTask SaveStateAsync() + { + Dictionary state = []; + foreach (AgentId agentId in this.agentInstances.Keys) + { + JsonElement agentState = await this.agentInstances[agentId].SaveStateAsync().ConfigureAwait(false); + state[agentId.ToString()] = agentState; + } + return JsonSerializer.SerializeToElement(state); + } + + /// + /// Registers an agent factory with the runtime, associating it with a specific agent type. + /// + /// The type of agent created by the factory. + /// The agent type to associate with the factory. + /// A function that asynchronously creates the agent instance. + /// A task representing the asynchronous operation, returning the registered agent type. + public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) where TAgent : IHostableAgent + // Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask' + // and recurse into the same call, causing a stack overflow. + => this.RegisterAgentFactoryAsync(type, async ValueTask (agentId, runtime) => await factoryFunc(agentId, runtime).ConfigureAwait(false)); + + /// + public async ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + { + if (this._agentFactories.ContainsKey(type)) + { + throw new InvalidOperationException($"Agent with type {type} already exists."); + } + + this._agentFactories.Add(type, factoryFunc); + + return type; + } + + /// + public async ValueTask TryGetAgentProxyAsync(AgentId agentId) + { + AgentProxy proxy = new(agentId, this); + + return proxy; + } + + private async ValueTask ProcessNextMessageAsync(CancellationToken cancellation = default) + { + if (this._messageDeliveryQueue.TryDequeue(out MessageDelivery? delivery)) + { + Interlocked.Decrement(ref this.messageQueueCount); + Debug.WriteLine($"Processing message {delivery.Message.MessageId}..."); + await delivery.InvokeAsync(cancellation).ConfigureAwait(false); + } + } + + private async Task RunAsync(CancellationToken cancellation) + { + ConcurrentDictionary pendingTasks = []; + while (!cancellation.IsCancellationRequested && this._shouldContinue()) + { + // Get a unique task id + Guid taskId; + do + { + taskId = Guid.NewGuid(); + } while (pendingTasks.ContainsKey(taskId)); + + // There is potentially a race condition here, but even if we leak a Task, we will + // still catch it on the Finish() pass. + ValueTask processTask = this.ProcessNextMessageAsync(cancellation); + await Task.Yield(); + + // Check if the task is already completed + if (processTask.IsCompleted) + { + continue; + } + + Task actualTask = processTask.AsTask(); + pendingTasks.TryAdd(taskId, actualTask.ContinueWith(t => pendingTasks.TryRemove(taskId, out _), TaskScheduler.Current)); + } + + // The pending task dictionary may contain null values when a race condition is experienced during + // the prior "ContinueWith" call. This could be solved with a ConcurrentDictionary, but locking + // is entirely undesirable in this context. + await Task.WhenAll(pendingTasks.Values.Where(task => task is not null)).ConfigureAwait(false); + await this.FinishAsync(this._finishSource?.Token ?? CancellationToken.None).ConfigureAwait(false); + } + + private async ValueTask PublishMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken) + { + if (!envelope.Topic.HasValue) + { + throw new InvalidOperationException("Message must have a topic to be published."); + } + + List exceptions = []; + TopicId topic = envelope.Topic.Value; + foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic))) + { + try + { + deliveryToken.ThrowIfCancellationRequested(); + + AgentId? sender = envelope.Sender; + + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); + MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) + { + Sender = sender, + Topic = topic, + IsRpc = false + }; + + AgentId agentId = subscription.MapToAgent(topic); + if (!this.DeliverToSelf && sender.HasValue && sender == agentId) + { + continue; + } + + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + + // TODO: Cancellation propagation! + await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + + if (exceptions.Count > 0) + { + // TODO: Unwrap TargetInvocationException? + throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions); + } + } + + private async ValueTask SendMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken) + { + if (!envelope.Receiver.HasValue) + { + throw new InvalidOperationException("Message must have a receiver to be sent."); + } + + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); + MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) + { + Sender = envelope.Sender, + IsRpc = false + }; + + AgentId receiver = envelope.Receiver.Value; + IHostableAgent agent = await this.EnsureAgentAsync(receiver).ConfigureAwait(false); + + return await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false); + } + + private async ValueTask EnsureAgentAsync(AgentId agentId) + { + if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent)) + { + if (!this._agentFactories.TryGetValue(agentId.Type, out Func>? factoryFunc)) + { + throw new InvalidOperationException($"Agent with name {agentId.Type} not found."); + } + + agent = await factoryFunc(agentId, this).ConfigureAwait(false); + this.agentInstances.Add(agentId, agent); + } + + return this.agentInstances[agentId]; + } + + private async Task FinishAsync(CancellationToken token) + { + foreach (IHostableAgent agent in this.agentInstances.Values) + { + if (!token.IsCancellationRequested) + { + await agent.CloseAsync().ConfigureAwait(false); + } + } + + this._shutdownSource?.Dispose(); + this._finishSource?.Dispose(); + this._finishSource = null; + this._shutdownSource = null; + } + +#pragma warning disable CA1822 // Mark members as static + private ValueTask ExecuteTracedAsync(Func> func) +#pragma warning restore CA1822 // Mark members as static + { + // TODO: Bind tracing + return func(); + } + +#pragma warning disable CA1822 // Mark members as static + private ValueTask ExecuteTracedAsync(Func func) +#pragma warning restore CA1822 // Mark members as static + { + // TODO: Bind tracing + return func(); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs new file mode 100644 index 0000000000..0080eada0c --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageDelivery.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; + +internal sealed class MessageDelivery(MessageEnvelope message, Func servicer, IResultSink resultSink) +{ + public MessageEnvelope Message { get; } = message; + public Func Servicer { get; } = servicer; + public IResultSink ResultSink { get; } = resultSink; + + public ValueTask InvokeAsync(CancellationToken cancellation) + { + return this.Servicer(this.Message, cancellation); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs new file mode 100644 index 0000000000..d4fa500d92 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/MessageEnvelope.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; + +internal sealed class MessageEnvelope +{ + public object Message { get; } + public string MessageId { get; } + public TopicId? Topic { get; private set; } + public AgentId? Sender { get; private set; } + public AgentId? Receiver { get; private set; } + public CancellationToken Cancellation { get; } + + public MessageEnvelope(object message, string? messageId = null, CancellationToken cancellation = default) + { + this.Message = message; + this.MessageId = messageId ?? Guid.NewGuid().ToString(); + this.Cancellation = cancellation; + } + + public MessageEnvelope WithSender(AgentId? sender) + { + this.Sender = sender; + return this; + } + + public MessageDelivery ForSend(AgentId receiver, Func> servicer) + { + this.Receiver = receiver; + + ResultSink resultSink = new(); + + return new MessageDelivery(this, BoundServicer, resultSink); + + async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation) + { + try + { + object? result = await servicer(envelope, cancellation).ConfigureAwait(false); + resultSink.SetResult(result); + } + catch (OperationCanceledException exception) + { + resultSink.SetCancelled(exception); + } + catch (Exception exception) + { + resultSink.SetException(exception); + } + } + } + + public MessageDelivery ForPublish(TopicId topic, Func servicer) + { + this.Topic = topic; + + ResultSink waitForPublish = new(); + + async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation) + { + try + { + await servicer(envelope, cancellation).ConfigureAwait(false); + waitForPublish.SetResult(null); + } + catch (Exception ex) + { + waitForPublish.SetException(ex); + } + } + + return new MessageDelivery(this, BoundServicer, waitForPublish); + } +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj new file mode 100644 index 0000000000..40cb8f1339 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj @@ -0,0 +1,28 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + alpha + false + + + + true + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs new file mode 100644 index 0000000000..b9c44f2171 --- /dev/null +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/ResultSink.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess; + +internal interface IResultSink : IValueTaskSource +{ + void SetResult(TResult result); + void SetException(Exception exception); + void SetCancelled(OperationCanceledException? exception = null); + + ValueTask Future { get; } +} + +internal sealed class ResultSink : IResultSink +{ + private ManualResetValueTaskSourceCore _core; + + public bool IsCancelled { get; private set; } + + public TResult GetResult(short token) + { + return this._core.GetResult(token); + } + + public ValueTaskSourceStatus GetStatus(short token) + { + return this._core.GetStatus(token); + } + + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + this._core.OnCompleted(continuation, state, token, flags); + } + + public void SetCancelled(OperationCanceledException? exception = null) + { + this.IsCancelled = true; + this._core.SetException(exception ?? new OperationCanceledException()); + } + + public void SetException(Exception exception) + { + this._core.SetException(exception); + } + + public void SetResult(TResult result) + { + this._core.SetResult(result); + } + + public ValueTask Future => new(this, this._core.Version); +} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentLogMessages.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentLogMessages.cs index 18bef1d687..ff94bd14d2 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentLogMessages.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentLogMessages.cs @@ -21,7 +21,6 @@ internal static partial class ChatClientAgentLogMessages /// Logs invoking agent (started). /// [LoggerMessage( - EventId = 0, Level = LogLevel.Debug, Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoking client {ClientType}.")] public static partial void LogAgentChatClientInvokingAgent( @@ -35,7 +34,6 @@ internal static partial class ChatClientAgentLogMessages /// Logs invoked agent (complete). /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked client {ClientType} with message count: {MessageCount}.")] public static partial void LogAgentChatClientInvokedAgent( @@ -50,7 +48,6 @@ internal static partial class ChatClientAgentLogMessages /// Logs invoked streaming agent (complete). /// [LoggerMessage( - EventId = 0, Level = LogLevel.Information, Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked client {ClientType}.")] public static partial void LogAgentChatClientInvokedStreamingAgent( diff --git a/dotnet/tests/.editorconfig b/dotnet/tests/.editorconfig index 25000c5400..2554dbdc7c 100644 --- a/dotnet/tests/.editorconfig +++ b/dotnet/tests/.editorconfig @@ -1,5 +1,6 @@ # Suppressing errors for Test projects under dotnet/tests folder [*.cs] +dotnet_diagnostic.CA1707.severity = none # Underscores in method names dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations diff --git a/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs index c71e17fea1..52d12a1109 100644 --- a/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AgentConformance.IntegrationTests/ChatClientAgentRunStreamingTests.cs @@ -30,7 +30,7 @@ public abstract class ChatClientAgentRunStreamingTests(Func x.Text)); + var chatResponseText = string.Concat(chatResponses.Select(x => x.Text)); Assert.Contains("Computer says no", chatResponseText, StringComparison.OrdinalIgnoreCase); } @@ -63,7 +63,7 @@ public abstract class ChatClientAgentRunStreamingTests(Func x.Text)); + var chatResponseText = string.Concat(chatResponses.Select(x => x.Text)); Assert.Contains(questionAndAnswer.ExpectedAnswer, chatResponseText, StringComparison.OrdinalIgnoreCase); } } diff --git a/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs index 633d205366..7f99594a53 100644 --- a/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs +++ b/dotnet/tests/AgentConformance.IntegrationTests/RunStreamingTests.cs @@ -28,7 +28,7 @@ public abstract class RunStreamingTests(Func creat var chatResponses = await agent.RunStreamingAsync("What is the capital of France.", thread).ToListAsync(); // Assert - var chatResponseText = string.Join("", chatResponses.Select(x => x.Text)); + var chatResponseText = string.Concat(chatResponses.Select(x => x.Text)); Assert.Contains("Paris", chatResponseText); } @@ -44,7 +44,7 @@ public abstract class RunStreamingTests(Func creat var chatResponses = await agent.RunStreamingAsync(new ChatMessage(ChatRole.User, "What is the capital of France."), thread).ToListAsync(); // Assert - var chatResponseText = string.Join("", chatResponses.Select(x => x.Text)); + var chatResponseText = string.Concat(chatResponses.Select(x => x.Text)); Assert.Contains("Paris", chatResponseText); } @@ -65,7 +65,7 @@ public abstract class RunStreamingTests(Func creat thread).ToListAsync(); // Assert - var chatResponseText = string.Join("", chatResponses.Select(x => x.Text)); + var chatResponseText = string.Concat(chatResponses.Select(x => x.Text)); Assert.Contains("Paris", chatResponseText); } @@ -84,8 +84,8 @@ public abstract class RunStreamingTests(Func creat var chatResponses2 = await agent.RunStreamingAsync(q2, thread).ToListAsync(); // Assert - var chatResponse1Text = string.Join("", chatResponses1.Select(x => x.Text)); - var chatResponse2Text = string.Join("", chatResponses2.Select(x => x.Text)); + var chatResponse1Text = string.Concat(chatResponses1.Select(x => x.Text)); + var chatResponse2Text = string.Concat(chatResponses2.Select(x => x.Text)); Assert.Contains("Paris", chatResponse1Text); Assert.Contains("Vienna", chatResponse2Text); diff --git a/dotnet/tests/Directory.Build.props b/dotnet/tests/Directory.Build.props index e327ae25e0..3a8ad53b48 100644 --- a/dotnet/tests/Directory.Build.props +++ b/dotnet/tests/Directory.Build.props @@ -7,7 +7,6 @@ true false net472;net9.0 - net9.0 b7762d10-e29b-4bb1-8b74-b6d69a667dd4 diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs index 54e4f72ef9..8516a466e9 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Concurrent; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs index f336970dda..f5bbef1711 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.GroupChat; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs index 6a5f5c3359..3549e86b66 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Handoff; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; using OpenAI; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs index 875ecd5bbb..ae9187a372 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs @@ -3,8 +3,8 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI.Agents.Runtime; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs index a366f132db..9d0a332e0f 100644 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Agents.Orchestration.Sequential; using Microsoft.Extensions.AI.Agents; -using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.Extensions.AI.Agents.Runtime.InProcess; namespace Microsoft.Agents.Orchestration.UnitTest; diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs index 3c9bf35247..c676ebca56 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs @@ -18,7 +18,7 @@ public class AgentTests private readonly Mock _agentMock; private readonly Mock _agentThreadMock; private readonly ChatResponse _invokeResponse = new(); - private readonly List _invokeStreamingResponses = new(); + private readonly List _invokeStreamingResponses = []; /// /// Initializes a new instance of the class. @@ -255,9 +255,7 @@ public class AgentTests /// /// Typed mock thread. /// - public abstract class TestAgentThread : AgentThread - { - } + public abstract class TestAgentThread : AgentThread; /// /// Mock class to test the method. diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs new file mode 100644 index 0000000000..a22ae1b32f --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentIdTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; + +public class AgentIdTests() +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("invalid\u007Fkey")] // DEL character (127) is outside ASCII 32-126 range + [InlineData("invalid\u0000key")] // NULL character is outside ASCII 32-126 range + [InlineData("invalid\u0010key")] // Control character is outside ASCII 32-126 range + [InlineData("InvalidKey💀")] // Control character is outside ASCII 32-126 range + public void AgentIdShouldThrowArgumentExceptionWithInvalidKey(string? invalidKey) + { + // Act & Assert + ArgumentException exception = Assert.Throws(() => new AgentId("validType", invalidKey!)); + Assert.Contains("Invalid AgentId key", exception.Message); + } + + [Fact] + public void AgentIdShouldInitializeCorrectlyTest() + { + AgentId agentId = new("TestType", "TestKey"); + + Assert.Equal("TestType", agentId.Type); + Assert.Equal("TestKey", agentId.Key); + } + + [Fact] + public void AgentIdShouldConvertFromTupleTest() + { + (string, string) agentTuple = ("TupleType", "TupleKey"); + AgentId agentId = new(agentTuple); + + Assert.Equal("TupleType", agentId.Type); + Assert.Equal("TupleKey", agentId.Key); + } + + [Fact] + public void AgentIdShouldConvertFromAgentType() + { + AgentType agentType = "TestType"; + AgentId agentId = new(agentType, "TestKey"); + + Assert.Equal("TestType", agentId.Type); + Assert.Equal("TestKey", agentId.Key); + } + + [Fact] + public void AgentIdShouldParseFromStringTest() + { + AgentId agentId = AgentId.FromStr("ParsedType/ParsedKey"); + + Assert.Equal("ParsedType", agentId.Type); + Assert.Equal("ParsedKey", agentId.Key); + } + + [Fact] + public void AgentIdShouldCompareEqualityCorrectlyTest() + { + AgentId agentId1 = new("SameType", "SameKey"); + AgentId agentId2 = new("SameType", "SameKey"); + AgentId agentId3 = new("DifferentType", "DifferentKey"); + + Assert.Equal(agentId2, agentId1); + Assert.NotEqual(agentId3, agentId1); + Assert.True(agentId1 == agentId2); + Assert.True(agentId1 != agentId3); + } + + [Fact] + public void AgentIdShouldGenerateCorrectHashCodeTest() + { + AgentId agentId1 = new("HashType", "HashKey"); + AgentId agentId2 = new("HashType", "HashKey"); + AgentId agentId3 = new("DifferentType", "DifferentKey"); + + Assert.Equal(agentId2.GetHashCode(), agentId1.GetHashCode()); + Assert.NotEqual(agentId3.GetHashCode(), agentId1.GetHashCode()); + } + + [Fact] + public void AgentIdShouldConvertExplicitlyFromStringTest() + { + AgentId agentId = (AgentId)"ConvertedType/ConvertedKey"; + + Assert.Equal("ConvertedType", agentId.Type); + Assert.Equal("ConvertedKey", agentId.Key); + } + + [Fact] + public void AgentIdShouldReturnCorrectToStringTest() + { + AgentId agentId = new("ToStringType", "ToStringKey"); + + Assert.Equal("ToStringType/ToStringKey", agentId.ToString()); + } + + [Fact] + public void AgentIdShouldCompareInequalityForWrongTypeTest() + { + AgentId agentId1 = new("Type1", "Key1"); + + Assert.False(agentId1.Equals(Guid.NewGuid())); + } + + [Fact] + public void AgentIdShouldCompareInequalityCorrectlyTest() + { + AgentId agentId1 = new("Type1", "Key1"); + AgentId agentId2 = new("Type2", "Key2"); + + Assert.True(agentId1 != agentId2); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs new file mode 100644 index 0000000000..7bec3a33b1 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentMetaDataTests.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; + +public class AgentMetadataTests() +{ + [Fact] + public void AgentMetadataShouldInitializeCorrectlyTest() + { + // Arrange & Act + AgentMetadata metadata = new("TestType", "TestKey", "TestDescription"); + + // Assert + Assert.Equal("TestType", metadata.Type); + Assert.Equal("TestKey", metadata.Key); + Assert.Equal("TestDescription", metadata.Description); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs new file mode 100644 index 0000000000..084c8981d8 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentProxyTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Moq; + +namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; + +public class AgentProxyTests +{ + private readonly Mock _mockRuntime; + private readonly AgentId _agentId; + private readonly AgentProxy _agentProxy; + + public AgentProxyTests() + { + this._mockRuntime = new Mock(); + this._agentId = new AgentId("testType", "testKey"); + this._agentProxy = new AgentProxy(this._agentId, this._mockRuntime.Object); + } + + [Fact] + public void IdMatchesAgentIdTest() + { + // Assert + Assert.Equal(this._agentId, this._agentProxy.Id); + } + + [Fact] + public void MetadataShouldMatchAgentTest() + { + AgentMetadata expectedMetadata = new("testType", "testKey", "testDescription"); + this._mockRuntime.Setup(r => r.GetAgentMetadataAsync(this._agentId)) + .ReturnsAsync(expectedMetadata); + + Assert.Equal(expectedMetadata, this._agentProxy.Metadata); + } + + [Fact] + public async Task SendMessageResponseTestAsync() + { + // Arrange + object message = new { Content = "Hello" }; + AgentId sender = new("senderType", "senderKey"); + object response = new { Content = "Response" }; + + this._mockRuntime.Setup(r => r.SendMessageAsync(message, this._agentId, sender, null, It.IsAny())) + .ReturnsAsync(response); + + // Act + object? result = await this._agentProxy.SendMessageAsync(message, sender); + + // Assert + Assert.Equal(response, result); + } + + [Fact] + public async Task LoadStateTestAsync() + { + // Arrange + JsonElement state = JsonDocument.Parse("{\"key\":\"value\"}").RootElement; + + this._mockRuntime.Setup(r => r.LoadAgentStateAsync(this._agentId, state)) + .Returns(default(ValueTask)); + + // Act + await this._agentProxy.LoadStateAsync(state); + + // Assert + this._mockRuntime.Verify(r => r.LoadAgentStateAsync(this._agentId, state), Times.Once); + } + + [Fact] + public async Task SaveStateTestAsync() + { + // Arrange + JsonElement expectedState = JsonDocument.Parse("{\"key\":\"value\"}").RootElement; + + this._mockRuntime.Setup(r => r.SaveAgentStateAsync(this._agentId)) + .ReturnsAsync(expectedState); + + // Act + JsonElement result = await this._agentProxy.SaveStateAsync(); + + // Assert + Assert.Equal(expectedState, result); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs new file mode 100644 index 0000000000..32bce85a7d --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/AgentTypeTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; + +public class AgentTypeTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("invalid type")] // Agent type must only contain alphanumeric letters or underscores + [InlineData("123invalidType")] // Agent type cannot start with a number + [InlineData("invalid@type")] // Agent type must only contain alphanumeric letters or underscores + [InlineData("invalid-type")] // Agent type cannot alphanumeric underscores. + public void AgentIdShouldThrowArgumentExceptionWithInvalidType(string? invalidType) + { + // Act & Assert + ArgumentException exception = Assert.Throws(() => new AgentType(invalidType!)); + Assert.Contains("Invalid AgentId type", exception.Message); + } + + [Fact] + public void ImplicitConversionFromStringTest() + { + // Arrange + string agentTypeName = "TestAgent"; + + // Act + AgentType agentType = agentTypeName; + + // Assert + Assert.Equal(agentTypeName, agentType.Name); + } + + [Fact] + public void ImplicitConversionToStringTest() + { + // Arrange + AgentType agentType = "TestAgent"; + + // Act + string agentTypeName = agentType; + + // Assert + Assert.Equal("TestAgent", agentTypeName); + } + + [Fact] + public void ExplicitConversionFromTypeTest() + { + // Arrange + Type type = typeof(string); + + // Act + AgentType agentType = (AgentType)type; + + // Assert + Assert.Equal(type.Name, agentType.Name); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs new file mode 100644 index 0000000000..5a1f1074f0 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/MessageContextTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; + +namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; + +public class MessageContextTests +{ + [Fact] + public void ConstructWithMessageIdAndCancellationTokenTest() + { + // Arrange + string messageId = Guid.NewGuid().ToString(); + CancellationToken cancellationToken = new(); + + // Act + MessageContext messageContext = new(messageId, cancellationToken); + + // Assert + Assert.Equal(messageId, messageContext.MessageId); + Assert.Equal(cancellationToken, messageContext.CancellationToken); + } + + [Fact] + public void ConstructWithCancellationTokenTest() + { + // Arrange + CancellationToken cancellationToken = new(); + + // Act + MessageContext messageContext = new(cancellationToken); + + // Assert + Assert.NotNull(messageContext.MessageId); + Assert.Equal(cancellationToken, messageContext.CancellationToken); + } + + [Fact] + public void AssignSenderTest() + { + // Arrange + MessageContext messageContext = new(new CancellationToken()); + AgentId sender = new("type", "key"); + + // Act + messageContext.Sender = sender; + + // Assert + Assert.Equal(sender, messageContext.Sender); + } + + [Fact] + public void AssignTopicTest() + { + // Arrange + MessageContext messageContext = new(new CancellationToken()); + TopicId topic = new("type", "source"); + + // Act + messageContext.Topic = topic; + + // Assert + Assert.Equal(topic, messageContext.Topic); + } + + [Fact] + public void AssignIsRpcPropertyTest() + { + // Arrange + MessageContext messageContext = new(new CancellationToken()) + { + // Act + IsRpc = true + }; + + // Assert + Assert.True(messageContext.IsRpc); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests.csproj b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests.csproj new file mode 100644 index 0000000000..776d9c8dde --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests.csproj @@ -0,0 +1,12 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + + + + + + + diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs new file mode 100644 index 0000000000..51ac6f7981 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/TopicIdTests.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Extensions.AI.Agents.Runtime.Abstractions.Tests; + +public class TopicIdTests +{ + [Fact] + public void ConstrWithTypeOnlyTest() + { + // Arrange & Act + TopicId topicId = new("testtype"); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal(TopicId.DefaultSource, topicId.Source); + } + + [Fact] + public void ConstructWithTypeAndSourceTest() + { + // Arrange & Act + TopicId topicId = new("testtype", "customsource"); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Fact] + public void ConstructWithTupleTest() + { + // Arrange + (string, string) tuple = ("testtype", "customsource"); + + // Act + TopicId topicId = new(tuple); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Fact] + public void ConvertFromStringTest() + { + // Arrange + const string TopicIdStr = "testtype/customsource"; + + // Act + TopicId topicId = TopicId.FromStr(TopicIdStr); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Theory] + [InlineData("invalid-format")] + [InlineData("too/many/parts")] + [InlineData("")] + public void InvalidFormatFromStringThrowsTest(string invalidInput) + { + // Act & Assert + Assert.Throws(() => TopicId.FromStr(invalidInput)); + } + + [Fact] + public void ToStringTest() + { + // Arrange + TopicId topicId = new("testtype", "customsource"); + + // Act + string result = topicId.ToString(); + + // Assert + Assert.Equal("testtype/customsource", result); + } + + [Fact] + public void EqualityTest() + { + // Arrange + TopicId topicId1 = new("testtype", "customsource"); + TopicId topicId2 = new("testtype", "customsource"); + + // Act & Assert + Assert.True(topicId1.Equals(topicId2)); + Assert.True(topicId1.Equals((object)topicId2)); + } + + [Fact] + public void InequalityTest() + { + // Arrange + TopicId topicId1 = new("testtype1", "source1"); + TopicId topicId2 = new("testtype2", "source2"); + TopicId topicId3 = new("testtype1", "source2"); + TopicId topicId4 = new("testtype2", "source1"); + + // Act & Assert + Assert.False(topicId1.Equals(topicId2)); + Assert.False(topicId1.Equals(topicId3)); + Assert.False(topicId1.Equals(topicId4)); + } + + [Fact] + public void NullEqualityTest() + { + // Arrange + TopicId topicId = new("testtype", "customsource"); + + // Act & Assert + Assert.False(topicId.Equals(null)); + } + + [Fact] + public void DifferentTypeEqualityTest() + { + // Arrange + TopicId topicId = new("testtype", "customsource"); + const string DifferentType = "not-a-topic-id"; + + // Act & Assert + Assert.False(topicId.Equals(DifferentType)); + } + + [Fact] + public void GetHashCodeTest() + { + // Arrange + TopicId topicId1 = new("testtype", "customsource"); + TopicId topicId2 = new("testtype", "customsource"); + + // Act + int hash1 = topicId1.GetHashCode(); + int hash2 = topicId2.GetHashCode(); + + // Assert + Assert.Equal(hash1, hash2); + } + + [Fact] + public void ExplicitConversionTest() + { + // Arrange + string topicIdStr = "testtype/customsource"; + + // Act + TopicId topicId = (TopicId)topicIdStr; + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Fact] + public void IsWildcardMatchTest() + { + // Arrange + TopicId topicId1 = new("testtype", "source1"); + TopicId topicId2 = new("testtype", "source2"); + + // Act & Assert + Assert.True(topicId1.IsWildcardMatch(topicId2)); + Assert.True(topicId2.IsWildcardMatch(topicId1)); + } + + [Fact] + public void IsWildcardMismatchTest() + { + // Arrange + TopicId topicId1 = new("testtype1", "source"); + TopicId topicId2 = new("testtype2", "source"); + + // Act & Assert + Assert.False(topicId1.IsWildcardMatch(topicId2)); + Assert.False(topicId2.IsWildcardMatch(topicId1)); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs new file mode 100644 index 0000000000..edfb51a171 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/InProcessRuntimeTests.cs @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public class InProcessRuntimeTests() +{ + [Fact] + public async Task RuntimeStatusLifecycleTestAsync() + { + // Arrange & Act + await using InProcessRuntime runtime = new(); + + // Assert + Assert.False(runtime.DeliverToSelf); + Assert.Equal(0, runtime.messageQueueCount); + + // Act + await runtime.StopAsync(); // Already stopped + await runtime.RunUntilIdleAsync(); // Never throws + + await runtime.StartAsync(); + + // Assert + // Invalid to start runtime that is already started + await Assert.ThrowsAsync(() => runtime.StartAsync()); + Assert.Equal(0, runtime.messageQueueCount); + + // Act + await runtime.StopAsync(); + + // Assert + Assert.Equal(0, runtime.messageQueueCount); + } + + [Fact] + public async Task SubscriptionRegistrationLifecycleTestAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + TestSubscription subscription = new("TestTopic", "MyAgent"); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.RemoveSubscriptionAsync(subscription.Id)); + + // Arrange + await runtime.AddSubscriptionAsync(subscription); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.AddSubscriptionAsync(subscription)); + + // Act + await runtime.RemoveSubscriptionAsync(subscription.Id); + } + + [Fact] + public async Task AgentRegistrationLifecycleTestAsync() + { + // Arrange + const string AgentType = "MyAgent"; + const string AgentDescription = "A test agent"; + List agents = []; + await using InProcessRuntime runtime = new(); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.GetAgentAsync(AgentType, lazy: false)); + + // Arrange + await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc)); + + // Act: Lookup by type + AgentId agentId = await runtime.GetAgentAsync(AgentType, lazy: false); + + // Assert + Assert.Single(agents); + Assert.Single(runtime.agentInstances); + + // Act + MockAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(agentId); + + // Assert + Assert.Equal(agentId, agent.Id); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.TryGetUnderlyingAgentInstanceAsync(agentId)); + + // Act: Lookup by ID + AgentId sameId = await runtime.GetAgentAsync(agentId, lazy: false); + + // Assert + Assert.Equal(agentId, sameId); + + // Act: Lookup by Type + sameId = await runtime.GetAgentAsync((AgentType)agent.Id.Type, lazy: false); + + // Assert + Assert.Equal(agentId, sameId); + + // Act: Lookup metadata + AgentMetadata metadata = await runtime.GetAgentMetadataAsync(agentId); + + // Assert + Assert.Equal(agentId.Type, metadata.Type); + Assert.Equal(AgentDescription, metadata.Description); + Assert.Equal(agentId.Key, metadata.Key); + + // Act: Access proxy + AgentProxy proxy = await runtime.TryGetAgentProxyAsync(agentId); + + // Assert + Assert.Equal(agentId, proxy.Id); + Assert.Equal(metadata.Type, proxy.Metadata.Type); + Assert.Equal(metadata.Description, proxy.Metadata.Description); + Assert.Equal(metadata.Key, proxy.Metadata.Key); + + async ValueTask factoryFunc(AgentId id, IAgentRuntime runtime) + { + MockAgent agent = new(id, runtime, AgentDescription); + agents.Add(agent); + return agent; + } + } + + [Fact] + public async Task AgentStateLifecycleTestAsync() + { + // Arrange + const string AgentType = "MyAgent"; + const string TestMessage = "test message"; + + await using InProcessRuntime firstRuntime = new(); + await firstRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc); + + // Act + AgentId agentId = await firstRuntime.GetAgentAsync(AgentType, lazy: false); + + // Assert + Assert.Single(firstRuntime.agentInstances); + + // Arrange + MockAgent agent = (MockAgent)firstRuntime.agentInstances[agentId]; + agent.ReceivedMessages.Add(TestMessage); + + // Act + JsonElement agentState = await firstRuntime.SaveAgentStateAsync(agentId); + + // Arrange + await using InProcessRuntime secondRuntime = new(); + await secondRuntime.RegisterAgentFactoryAsync(AgentType, factoryFunc); + + // Act + await secondRuntime.LoadAgentStateAsync(agentId, agentState); + + // Assert + Assert.Single(secondRuntime.agentInstances); + MockAgent copy = (MockAgent)secondRuntime.agentInstances[agentId]; + Assert.Single(copy.ReceivedMessages); + Assert.Equal(TestMessage, copy.ReceivedMessages.Single().ToString()); + + static async ValueTask factoryFunc(AgentId id, IAgentRuntime runtime) + { + MockAgent agent = new(id, runtime, "A test agent"); + return agent; + } + } + + [Fact] + public async Task RuntimeSendMessageTestAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + MockAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + { + agent = new MockAgent(id, runtime, "A test agent"); + return agent; + }); + + // Act: Ensure the agent is actually created + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + + // Assert + Assert.NotNull(agent); + Assert.Empty(agent.ReceivedMessages); + + // Act: Send message + await runtime.StartAsync(); + await runtime.SendMessageAsync("TestMessage", agent.Id); + await runtime.RunUntilIdleAsync(); + + // Assert + Assert.Equal(0, runtime.messageQueueCount); + Assert.Single(agent.ReceivedMessages); + } + + // Agent will not deliver to self will success when runtime.DeliverToSelf is false (default) + [Theory] + [InlineData(false, 0)] + [InlineData(true, 1)] + public async Task RuntimeAgentPublishToSelfTestAsync(bool selfPublish, int receiveCount) + { + // Arrange + await using InProcessRuntime runtime = new() + { + DeliverToSelf = selfPublish + }; + + MockAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + { + agent = new MockAgent(id, runtime, "A test agent"); + return agent; + }); + + // Assert + Assert.Empty(runtime.agentInstances); + + // Act: Ensure the agent is actually created + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + + // Assert + Assert.NotNull(agent); + Assert.Single(runtime.agentInstances); + + const string TopicType = "TestTopic"; + + // Arrange + await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); + + // Act + await runtime.StartAsync(); + await runtime.PublishMessageAsync("SelfMessage", new TopicId(TopicType), sender: agentId); + await runtime.RunUntilIdleAsync(); + + // Assert + Assert.Equal(receiveCount, agent.ReceivedMessages.Count); + } + + [Fact] + public async Task RuntimeShouldSaveLoadStateCorrectlyTestAsync() + { + // Arrange: Create a runtime and register an agent + await using InProcessRuntime runtime = new(); + MockAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + { + agent = new MockAgent(id, runtime, "test agent"); + return agent; + }); + + // Get agent ID and instantiate agent by publishing + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + const string TopicType = "TestTopic"; + await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); + + await runtime.StartAsync(); + await runtime.PublishMessageAsync("test", new TopicId(TopicType)); + await runtime.RunUntilIdleAsync(); + + // Act: Save the state + JsonElement savedState = await runtime.SaveStateAsync(); + + // Assert: Ensure the agent's state is stored as a valid JSON type + Assert.NotNull(agent); + Assert.True(savedState.TryGetProperty(agentId.ToString(), out JsonElement agentState)); + Assert.Equal(JsonValueKind.Array, agentState.ValueKind); + Assert.Single(agent.ReceivedMessages); + + // Arrange: Serialize and Deserialize the state to simulate persistence + string json = JsonSerializer.Serialize(savedState); + Assert.NotNull(json); + Assert.NotEmpty(json); + IDictionary deserializedState = JsonSerializer.Deserialize>(json) + ?? throw new InvalidOperationException("Deserialized state is unexpectedly null"); + Assert.True(deserializedState.ContainsKey(agentId.ToString())); + + // Act: Start new runtime and restore the state + agent = null; + await using InProcessRuntime newRuntime = new(); + await newRuntime.StartAsync(); + await newRuntime.RegisterAgentFactoryAsync("MyAgent", async (id, runtime) => + { + agent = new MockAgent(id, runtime, "another agent"); + return agent; + }); + + // Assert: Show that no agent instances exist in the new runtime + Assert.Empty(newRuntime.agentInstances); + + // Act: Load the state into the new runtime and show that agent is now instantiated + await newRuntime.LoadStateAsync(savedState); + + // Assert + Assert.NotNull(agent); + Assert.Single(newRuntime.agentInstances); + Assert.True(newRuntime.agentInstances.ContainsKey(agentId)); + Assert.Single(agent.ReceivedMessages); + } + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed class WrongAgent : IHostableAgent +#pragma warning restore CA1812 + { + public AgentId Id => throw new NotImplementedException(); + + public AgentMetadata Metadata => throw new NotImplementedException(); + + public ValueTask CloseAsync() => default; + + public ValueTask LoadStateAsync(JsonElement state) + { + throw new NotImplementedException(); + } + + public ValueTask OnMessageAsync(object message, MessageContext messageContext) + { + throw new NotImplementedException(); + } + + public ValueTask SaveStateAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs new file mode 100644 index 0000000000..55c0c2d632 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessageEnvelopeTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public class MessageEnvelopeTests +{ + [Fact] + public void ConstructAllParametersTest() + { + // Arrange + object message = new { Content = "Test message" }; + const string MessageId = "testid"; + CancellationToken cancellation = new(); + + // Act + MessageEnvelope envelope = new(message, MessageId, cancellation); + + // Assert + Assert.Same(message, envelope.Message); + Assert.Equal(MessageId, envelope.MessageId); + Assert.Equal(cancellation, envelope.Cancellation); + Assert.Null(envelope.Sender); + Assert.Null(envelope.Receiver); + Assert.Null(envelope.Topic); + } + + [Fact] + public void ConstructOnlyRequiredParametersTest() + { + // Arrange & Act + MessageEnvelope envelope = new("test"); + + // Assert + Assert.NotNull(envelope.MessageId); + Assert.NotEmpty(envelope.MessageId); + // Verify it's a valid GUID + Assert.True(Guid.TryParse(envelope.MessageId, out _)); + } + + [Fact] + public void WithSenderTest() + { + // Arrange + MessageEnvelope envelope = new("test"); + AgentId sender = new("testtype", "testkey"); + + // Act + MessageEnvelope result = envelope.WithSender(sender); + + // Assert + Assert.Same(envelope, result); + Assert.Equal(sender, envelope.Sender); + } + + [Fact] + public async Task ForSendTestAsync() + { + // Arrange + MessageEnvelope envelope = new("test"); + AgentId receiver = new("receivertype", "receiverkey"); + object expectedResult = new { Response = "Success" }; + + ValueTask servicer(MessageEnvelope env, CancellationToken ct) => new(expectedResult); + + // Act + MessageDelivery delivery = envelope.ForSend(receiver, servicer); + + // Assert + Assert.NotNull(delivery); + Assert.Same(envelope, delivery.Message); + Assert.Equal(receiver, envelope.Receiver); + + // Invoke the servicer to verify result sink works + await delivery.InvokeAsync(CancellationToken.None); + Assert.True(delivery.ResultSink.Future.IsCompleted); + object? result = await delivery.ResultSink.Future; + Assert.Same(expectedResult, result); + } + + [Fact] + public void ForPublishTest() + { + // Arrange + MessageEnvelope envelope = new("test"); + TopicId topic = new("testtopic"); + + static ValueTask servicer(MessageEnvelope env, CancellationToken ct) => default; + + // Act + MessageDelivery delivery = envelope.ForPublish(topic, servicer); + + // Assert + Assert.NotNull(delivery); + Assert.Same(envelope, delivery.Message); + Assert.Equal(topic, envelope.Topic); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs new file mode 100644 index 0000000000..95466f08a1 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/MessagingTestFixture.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public sealed class BasicMessage +{ + public string Content { get; set; } = string.Empty; +} + +#pragma warning disable RCS1194 // Implement exception constructors +public sealed class TestException : Exception; +#pragma warning restore RCS1194 // Implement exception constructors + +public sealed class PublisherAgent : TestAgent, IHandle +{ + private readonly IList _targetTopics; + + public PublisherAgent(AgentId id, IAgentRuntime runtime, string description, IList targetTopics) + : base(id, runtime, description) + { + this._targetTopics = targetTopics; + } + + public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item); + foreach (TopicId targetTopic in this._targetTopics) + { + await this.PublishMessageAsync( + new BasicMessage { Content = $"@{targetTopic}: {item.Content}" }, + targetTopic); + } + } +} + +public sealed class SendOnAgent : TestAgent, IHandle +{ + private readonly IList _targetKeys; + + public SendOnAgent(AgentId id, IAgentRuntime runtime, string description, IList targetKeys) + : base(id, runtime, description) + { + this._targetKeys = targetKeys; + } + + public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + foreach (Guid targetKey in this._targetKeys) + { + AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); + BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" }; + await this.SendMessageAsync(response, targetId); + } + } +} + +public sealed class ReceiverAgent : TestAgent, IHandle +{ + public List Messages { get; } = []; + + public ReceiverAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + this.Messages.Add(item); + return default; + } +} + +public sealed class ProcessorAgent : TestAgent, IHandle +{ + private Func ProcessFunc { get; } + + public ProcessorAgent(AgentId id, IAgentRuntime runtime, Func processFunc, string description) + : base(id, runtime, description) + { + this.ProcessFunc = processFunc; + } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + BasicMessage result = new() { Content = this.ProcessFunc.Invoke(((BasicMessage)item).Content) }; + + return new(result); + } +} + +public sealed class CancelAgent : TestAgent, IHandle +{ + public CancelAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + CancellationToken cancelledToken = new(canceled: true); + cancelledToken.ThrowIfCancellationRequested(); + + return default; + } +} + +public sealed class ErrorAgent : TestAgent, IHandle +{ + public ErrorAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } + + public bool DidThrow { get; private set; } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + this.DidThrow = true; + + throw new TestException(); + } +} + +public sealed class MessagingTestFixture +{ + private Dictionary AgentsTypeMap { get; } = []; + public InProcessRuntime Runtime { get; } = new(); + + public ValueTask RegisterFactoryMapInstances(AgentType type, Func> factory) + where TAgent : IHostableAgent + { + async ValueTask WrappedFactory(AgentId id, IAgentRuntime runtime) + { + TAgent agent = await factory(id, runtime); + this.GetAgentInstances()[id] = agent; + return agent; + } + + return this.Runtime.RegisterAgentFactoryAsync(type, WrappedFactory); + } + + public Dictionary GetAgentInstances() where TAgent : IHostableAgent + { + if (!this.AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) || + maybeAgentMap is not Dictionary result) + { + this.AgentsTypeMap[typeof(TAgent)] = result = []; + } + + return result; + } + public async ValueTask RegisterReceiverAgentAsync(string? agentNameSuffix = null, params string[] topicTypes) + { + await this.RegisterFactoryMapInstances( + $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}", + (id, runtime) => new ValueTask(new ReceiverAgent(id, runtime, string.Empty))); + + foreach (string topicType in topicTypes) + { + await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}")); + } + } + + public async ValueTask RegisterErrorAgentAsync(string? agentNameSuffix = null, params string[] topicTypes) + { + await this.RegisterFactoryMapInstances( + $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}", + (id, runtime) => new ValueTask(new ErrorAgent(id, runtime, string.Empty))); + + foreach (string topicType in topicTypes) + { + await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}")); + } + } + + public async ValueTask RunPublishTestAsync(TopicId sendTarget, object message, string? messageId = null) + { + messageId ??= Guid.NewGuid().ToString(); + + await this.Runtime.StartAsync(); + await this.Runtime.PublishMessageAsync(message, sendTarget, messageId: messageId); + await this.Runtime.RunUntilIdleAsync(); + } + + public async ValueTask RunSendTestAsync(AgentId sendTarget, object message, string? messageId = null) + { + messageId ??= Guid.NewGuid().ToString(); + + await this.Runtime.StartAsync(); + + object? result = await this.Runtime.SendMessageAsync(message, sendTarget, messageId: messageId); + + await this.Runtime.RunUntilIdleAsync(); + + return result; + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj new file mode 100644 index 0000000000..39d45f9cd2 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj @@ -0,0 +1,12 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + + + + + + + diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs new file mode 100644 index 0000000000..87e2d25fb7 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/PublishMessageTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public class PublishMessageTests +{ + [Fact] + public async Task Test_PublishMessage_SuccessAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic"); + await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic"); + + await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }); + + var values = fixture.GetAgentInstances().Values; + Assert.Equal(2, values.Count); + Assert.All(values, receiverAgent => + { + Assert.NotNull(receiverAgent.Messages); + Assert.Single(receiverAgent.Messages); + Assert.Contains(receiverAgent.Messages, m => m.Content == "1"); + }); + } + + [Fact] + public async Task Test_PublishMessage_SingleFailureAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterErrorAgentAsync(topicTypes: "TestTopic"); + + // Test that we wrap single errors appropriately + var e = await Assert.ThrowsAsync(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" })); + Assert.IsType(Assert.Single(e.InnerExceptions)); + + var values = fixture.GetAgentInstances().Values; + } + + [Fact] + public async Task Test_PublishMessage_MultipleFailuresAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterErrorAgentAsync(topicTypes: "TestTopic"); + await fixture.RegisterErrorAgentAsync("2", topicTypes: "TestTopic"); + + // What we are really testing here is that a single exception does not prevent sending to the remaining agents + var e = await Assert.ThrowsAsync(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" })); + Assert.Equal(2, e.InnerExceptions.Count); + Assert.All(e.InnerExceptions, innerException => Assert.IsType(innerException)); + + var values = fixture.GetAgentInstances().Values; + Assert.Equal(2, values.Count); + } + + [Fact] + public async Task Test_PublishMessage_MixedSuccessFailureAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic"); + await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic"); + + await fixture.RegisterErrorAgentAsync(topicTypes: "TestTopic"); + await fixture.RegisterErrorAgentAsync("2", topicTypes: "TestTopic"); + + // What we are really testing here is that raising exceptions does not prevent sending to the remaining agents + var e = await Assert.ThrowsAsync(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" })); + Assert.Equal(2, e.InnerExceptions.Count); + Assert.All(e.InnerExceptions, innerException => Assert.IsType(innerException)); + + var agents = fixture.GetAgentInstances().Values; + Assert.Equal(2, agents.Count); + Assert.All(agents, receiverAgent => + { + Assert.NotNull(receiverAgent.Messages); + Assert.Single(receiverAgent.Messages); + Assert.Contains(receiverAgent.Messages, m => m.Content == "1"); + }); + + var errors = fixture.GetAgentInstances().Values; + Assert.Equal(2, errors.Count); + } + + [Fact] + public async Task Test_PublishMessage_RecurrentPublishSucceedsAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances( + nameof(PublisherAgent), + (id, runtime) => new ValueTask(new PublisherAgent(id, runtime, string.Empty, [new TopicId("TestTopic")]))); + + await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", nameof(PublisherAgent))); + + await fixture.RegisterReceiverAgentAsync(topicTypes: "TestTopic"); + await fixture.RegisterReceiverAgentAsync("2", topicTypes: "TestTopic"); + + await fixture.RunPublishTestAsync(new TopicId("RunTest"), new BasicMessage { Content = "1" }); + + TopicId testTopicId = new("TestTopic"); + var values = fixture.GetAgentInstances().Values; + Assert.Equal(2, values.Count); + Assert.All(values, receiver => + { + Assert.NotNull(receiver.Messages); + Assert.Single(receiver.Messages); + Assert.Contains(receiver.Messages, m => m.Content == $"@{testTopicId}: 1"); + }); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs new file mode 100644 index 0000000000..604e359a90 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/ResultSinkTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public class ResultSinkTests +{ + [Fact] + public void GetResultTest() + { + // Arrange + ResultSink sink = new(); + const int ExpectedResult = 42; + + // Act + sink.SetResult(ExpectedResult); + int result = sink.GetResult(0); + + // Assert + Assert.Equal(ExpectedResult, result); + Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); + } + + [Fact] + public async Task FutureResultTestAsync() + { + // Arrange + ResultSink sink = new(); + const string ExpectedResult = "test"; + + // Act + sink.SetResult(ExpectedResult); + string result = await sink.Future; + + // Assert + Assert.Equal(ExpectedResult, result); + Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); + } + + [Fact] + public async Task SetExceptionTestAsync() + { + // Arrange + ResultSink sink = new(); + InvalidOperationException expectedException = new("Test exception"); + + // Act + sink.SetException(expectedException); + + // Assert + Exception exception = await Assert.ThrowsAsync(async () => await sink.Future); + Assert.Equal(expectedException.Message, exception.Message); + exception = Assert.Throws(() => sink.GetResult(0)); + Assert.Equal(expectedException.Message, exception.Message); + Assert.Equal(ValueTaskSourceStatus.Faulted, sink.GetStatus(0)); + } + + [Fact] + public async Task SetCancelledTestAsync() + { + // Arrange + ResultSink sink = new(); + + // Act + sink.SetCancelled(); + + // Assert + Assert.True(sink.IsCancelled); + Assert.Throws(() => sink.GetResult(0)); + await Assert.ThrowsAsync(async () => await sink.Future); + Assert.Equal(ValueTaskSourceStatus.Canceled, sink.GetStatus(0)); + } + + [Fact] + public void OnCompletedTest() + { + // Arrange + ResultSink sink = new(); + bool continuationCalled = false; + const int ExpectedResult = 42; + + // Register the continuation + sink.OnCompleted( + state => continuationCalled = true, + state: null, + token: 0, + ValueTaskSourceOnCompletedFlags.None); + + // Assert + Assert.False(continuationCalled, "Continuation should have been called"); + + // Act + sink.SetResult(ExpectedResult); + + // Assert + Assert.Equal(ExpectedResult, sink.GetResult(0)); + Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); + Assert.True(continuationCalled, "Continuation should have been called"); + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs new file mode 100644 index 0000000000..d2e1223e85 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/SendMessageTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public class SendMessageTests +{ + [Fact] + public async Task Test_SendMessage_ReturnsValueAsync() + { + static string ProcessFunc(string s) => $"Processed({s})"; + + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances(nameof(ProcessorAgent), + (id, runtime) => new ValueTask(new ProcessorAgent(id, runtime, ProcessFunc, string.Empty))); + + AgentId targetAgent = new(nameof(ProcessorAgent), Guid.NewGuid().ToString()); + object? maybeResult = await fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }); + + Assert.Equal("Processed(1)", Assert.IsType(maybeResult).Content); + } + + [Fact] + public async Task Test_SendMessage_CancellationAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances(nameof(CancelAgent), + (id, runtime) => new ValueTask(new CancelAgent(id, runtime, string.Empty))); + + AgentId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString()); + + await Assert.ThrowsAsync(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask()); + } + + [Fact] + public async Task Test_SendMessage_ErrorAsync() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances(nameof(ErrorAgent), + (id, runtime) => new ValueTask(new ErrorAgent(id, runtime, string.Empty))); + + AgentId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString()); + + await Assert.ThrowsAsync(() => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask()); + } + + [Fact] + public async Task Test_SendMessage_FromSendMessageHandlerAsync() + { + Guid[] targetGuids = [Guid.NewGuid(), Guid.NewGuid()]; + + MessagingTestFixture fixture = new(); + + Dictionary sendAgents = fixture.GetAgentInstances(); + Dictionary receiverAgents = fixture.GetAgentInstances(); + + await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent), + (id, runtime) => new ValueTask(new SendOnAgent(id, runtime, string.Empty, targetGuids))); + + await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent), + (id, runtime) => new ValueTask(new ReceiverAgent(id, runtime, string.Empty))); + + AgentId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString()); + BasicMessage input = new() { Content = "Hello" }; + Task testTask = fixture.RunSendTestAsync(targetAgent, input).AsTask(); + + // We do not actually expect to wait the timeout here, but it is still better than waiting the 10 min + // timeout that the tests default to. A failure will fail regardless of what timeout value we set. + TimeSpan timeout = Debugger.IsAttached ? TimeSpan.FromSeconds(120) : TimeSpan.FromSeconds(10); + Task timeoutTask = Task.Delay(timeout); + + Task completedTask = await Task.WhenAny([testTask, timeoutTask]); + Assert.Same(testTask, completedTask); + + // Check that each of the target agents received the message + foreach (Guid targetKey in targetGuids) + { + AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); + Assert.Single(receiverAgents[targetId].Messages); + Assert.Contains(receiverAgents[targetId].Messages, m => m.Content == $"@{targetKey}: {input.Content}"); + } + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs new file mode 100644 index 0000000000..577c9479d9 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestAgents.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public abstract class TestAgent : BaseAgent +{ + internal List ReceivedMessages = []; + + protected TestAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } +} + +/// +/// A test agent that captures the messages it receives and +/// is able to save and load its state. +/// +public sealed class MockAgent : TestAgent, IHandle +{ + public MockAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) { } + + public ValueTask HandleAsync(string item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item); + return default; + } + + public override async ValueTask SaveStateAsync() + { + JsonElement json = JsonSerializer.SerializeToElement(this.ReceivedMessages); + return json; + } + + public override ValueTask LoadStateAsync(JsonElement state) + { + this.ReceivedMessages = JsonSerializer.Deserialize>(state) ?? throw new InvalidOperationException("Failed to deserialize state"); + return default; + } +} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs new file mode 100644 index 0000000000..9c926b9db9 --- /dev/null +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/TestSubscription.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess.Tests; + +public class TestSubscription(string topicType, string agentType, string? id = null) : ISubscriptionDefinition +{ + public string Id { get; } = id ?? Guid.NewGuid().ToString(); + + public string TopicType { get; } = topicType; + + public AgentId MapToAgent(TopicId topic) + { + if (!this.Matches(topic)) + { + throw new InvalidOperationException("TopicId does not match the subscription."); + } + + return new AgentId(agentType, topic.Source); + } + + public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; + + public override bool Equals([NotNullWhen(true)] object? obj) => obj is TestSubscription other && other.Equals(this); + + public override int GetHashCode() => this.Id.GetHashCode(); + + public bool Matches(TopicId topic) + { + return topic.Type == this.TopicType; + } +} diff --git a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs index dfd7180372..250da1ce63 100644 --- a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs +++ b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs @@ -35,16 +35,16 @@ public class OpenAIAssistantFixture : IChatClientAgentFixture throw new InvalidOperationException("The thread must be of type ChatClientAgentThread to retrieve chat history."); } - List messages = new(); + List messages = []; await foreach (var agentMessage in this._assistantClient!.GetMessagesAsync(chatClientThread.Id, new() { Order = MessageCollectionOrder.Ascending })) { messages.Add(new() { Role = agentMessage.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, - Contents = new List() - { + Contents = + [ new TextContent(agentMessage.Content[0].Text ?? string.Empty) - }, + ], }); }