mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Port Agent Runtime abstractions / inprocess runtime (#149)
This commit is contained in:
@@ -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<string>' in '...' to use 'Collection<T>' ...
|
||||
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
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);IDE0290;IDE0079</NoWarn>
|
||||
<ProjectsTargetFrameworks>net472;net8.0;net9.0;netstandard2.0</ProjectsTargetFrameworks>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<ProjectsTargetFrameworks>net9.0;net8.0;netstandard2.0;net472</ProjectsTargetFrameworks>
|
||||
<ProjectsDebugTargetFrameworks>net9.0;net472</ProjectsDebugTargetFrameworks>
|
||||
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.6.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.6.0-preview.1.25310.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<!-- Microsoft.SemanticKernel.Agents.Runtime.* -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Runtime.Abstractions" Version="1.58.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Runtime.Core" Version="1.58.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" Version="1.58.0-preview" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
|
||||
@@ -107,17 +107,29 @@
|
||||
<File Path="src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs" />
|
||||
<File Path="src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs" />
|
||||
</Folder>
|
||||
<Project Path="src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.UnitTests/Microsoft.Extensions.AI.Agents.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/UnitTests/">
|
||||
<Project Path="tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests/Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="tests/Microsoft.Extensions.AI.Agents.UnitTests/Microsoft.Extensions.AI.Agents.UnitTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Debug" />
|
||||
</Project>
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<ProjectsTargetFrameworks>net472;net9.0</ProjectsTargetFrameworks>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <param name="parameterName">Function parameter to take the name from.</param>
|
||||
public CallerArgumentExpressionAttribute(string parameterName)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
this.ParameterName = parameterName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter may be <see langword="null" />.
|
||||
/// </param>
|
||||
public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
public MaybeNullWhenAttribute(bool returnValue) => this.ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
@@ -57,7 +57,7 @@ internal sealed class NotNullWhenAttribute : Attribute
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be <see langword="null" />.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
public NotNullWhenAttribute(bool returnValue) => this.ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
@@ -72,7 +72,7 @@ internal sealed class NotNullIfNotNullAttribute : Attribute
|
||||
/// <param name="parameterName">
|
||||
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
|
||||
/// </param>
|
||||
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
|
||||
public NotNullIfNotNullAttribute(string parameterName) => this.ParameterName = parameterName;
|
||||
|
||||
/// <summary>Gets the associated parameter name.</summary>
|
||||
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.
|
||||
/// </param>
|
||||
public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
|
||||
public DoesNotReturnIfAttribute(bool parameterValue) => this.ParameterValue = parameterValue;
|
||||
|
||||
/// <summary>Gets the condition parameter value.</summary>
|
||||
public bool ParameterValue { get; }
|
||||
@@ -111,13 +111,13 @@ internal sealed class MemberNotNullAttribute : Attribute
|
||||
/// <param name="member">
|
||||
/// The field or property member that is promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(string member) => Members = new[] { member };
|
||||
public MemberNotNullAttribute(string member) => this.Members = new[] { member };
|
||||
|
||||
/// <summary>Initializes the attribute with the list of field and property members.</summary>
|
||||
/// <param name="members">
|
||||
/// The list of field and property members that are promised to be not-null.
|
||||
/// </param>
|
||||
public MemberNotNullAttribute(params string[] members) => Members = members;
|
||||
public MemberNotNullAttribute(params string[] members) => this.Members = members;
|
||||
|
||||
/// <summary>Gets field or property member names.</summary>
|
||||
public string[] Members { get; }
|
||||
@@ -137,8 +137,8 @@ internal sealed class MemberNotNullWhenAttribute : Attribute
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, string member)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = new[] { member };
|
||||
this.ReturnValue = returnValue;
|
||||
this.Members = [member];
|
||||
}
|
||||
|
||||
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
|
||||
@@ -150,8 +150,8 @@ internal sealed class MemberNotNullWhenAttribute : Attribute
|
||||
/// </param>
|
||||
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
Members = members;
|
||||
this.ReturnValue = returnValue;
|
||||
this.Members = members;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration<string, st
|
||||
(response, cancellationToken) =>
|
||||
{
|
||||
string[] result = [.. response.Select(r => r.Text)];
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<string[]>(result);
|
||||
#else
|
||||
return ValueTask.FromResult(result);
|
||||
#endif
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TInput, TOutput>
|
||||
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<ConcurrentResultActor>());
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<IHostableAgent>(actor);
|
||||
#else
|
||||
return ValueTask.FromResult<IHostableAgent>(actor);
|
||||
#endif
|
||||
return actor;
|
||||
}).ConfigureAwait(false);
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, resultType, "RESULTS");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, (AgentType _, string Description)> handoff in this._handoffs)
|
||||
foreach (KeyValuePair<string, (AgentType AgentType, string Description)> handoff in this._handoffs)
|
||||
{
|
||||
AIFunction handoffFunction =
|
||||
AIFunctionFactory.Create(
|
||||
|
||||
@@ -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<TInput, TOutput> : AgentOrchestration<TInput,
|
||||
: base(agents)
|
||||
{
|
||||
// Create list of distinct agent names
|
||||
HashSet<string> agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal);
|
||||
agentNames.Add(handoffs.FirstAgentName);
|
||||
HashSet<string> 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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).
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
EventId = 0,
|
||||
Level = LogLevel.Error,
|
||||
Message = "FAILURE {Orchestration}: {AgentId}")]
|
||||
public static partial void LogOrchestrationResultFailure(
|
||||
|
||||
+1
-4
@@ -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.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
EventId = 0,
|
||||
Level = LogLevel.Information,
|
||||
Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")]
|
||||
public static partial void LogConcurrentResultCapture(
|
||||
|
||||
+1
-10
@@ -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(
|
||||
|
||||
+1
-5
@@ -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(
|
||||
|
||||
@@ -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 <see cref="OrchestrationResult{TValue}"/> awaiting the orchestration.
|
||||
/// </summary>
|
||||
[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 <see cref="OrchestrationResult{TValue}"/> timeout while awaiting the orchestration.
|
||||
/// </summary>
|
||||
[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 <see cref="OrchestrationResult{TValue}"/> cancelled the orchestration.
|
||||
/// </summary>
|
||||
[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 <see cref="OrchestrationResult{TValue}"/> the awaited the orchestration has completed.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
EventId = 0,
|
||||
Level = LogLevel.Trace,
|
||||
Message = "COMPLETE {Orchestration}: {Topic}")]
|
||||
public static partial void LogOrchestrationResultComplete(
|
||||
|
||||
+1
-3
@@ -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(
|
||||
|
||||
@@ -22,12 +22,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.Abstractions" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.Core" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" />
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.Runtime.Abstractions\Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.Runtime.InProcess\Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<TValue> : 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>\
|
||||
/// <remarks>
|
||||
/// See the Python equivalent:
|
||||
/// <see href="https://github.com/microsoft/agent-runtime/blob/main/python/agent_runtime/core/agent_id.py">AgentId in AutoGen (Python)</see>.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")]
|
||||
public struct AgentId : IEquatable<AgentId>
|
||||
{
|
||||
/// <summary>
|
||||
/// The default source value used when no source is explicitly provided.
|
||||
/// </summary>
|
||||
public const string DefaultKey = "default";
|
||||
|
||||
private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126
|
||||
|
||||
/// <summary>
|
||||
/// 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 (_).
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Agent instance identifier.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
internal static Regex KeyRegex1 => KeyRegex2;
|
||||
|
||||
internal static Regex KeyRegex2 => KeyRegex;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type.</param>
|
||||
/// <param name="key">Agent instance identifier.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct from a tuple.
|
||||
/// </summary>
|
||||
/// <param name="kvPair">A tuple containing the agent type and key.</param>
|
||||
public AgentId((string Type, string Key) kvPair)
|
||||
: this(kvPair.Type, kvPair.Key)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct from an <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type.</param>
|
||||
/// <param name="key">Agent instance identifier.</param>
|
||||
public AgentId(AgentType type, string key)
|
||||
: this(type.Name, key)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string of the format "type/key" into an <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="maybeAgentId">The agent ID string.</param>
|
||||
/// <returns>An instance of <see cref="AgentId"/>.</returns>
|
||||
public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key)));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/key".</returns>
|
||||
public override readonly string ToString() => $"{this.Type}/{this.Key}";
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current <see cref="AgentId"/>; otherwise, <c>false</c>.</returns>
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return (obj is AgentId other && this.Equals(other));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(AgentId other)
|
||||
{
|
||||
return this.Type == other.Type && this.Key == other.Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the current instance.</returns>
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Type, this.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts a string to an <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The string representation of an agent ID.</param>
|
||||
/// <returns>An instance of <see cref="AgentId"/>.</returns>
|
||||
public static explicit operator AgentId(string id) => FromStr(id);
|
||||
|
||||
/// <summary>
|
||||
/// Equality operator for <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
public static bool operator ==(AgentId left, AgentId right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Inequality operator for <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata associated with an agent, including its type, unique key, and description.
|
||||
/// </summary>
|
||||
public readonly struct AgentMetadata(string type, string key, string description) : IEquatable<AgentMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 (_).
|
||||
/// </summary>
|
||||
public string Type { get; } = type;
|
||||
|
||||
/// <summary>
|
||||
/// A unique key identifying the agent instance.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Key { get; } = key;
|
||||
|
||||
/// <summary>
|
||||
/// A brief description of the agent's purpose or functionality.
|
||||
/// </summary>
|
||||
public string Description { get; } = description;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(AgentMetadata other)
|
||||
{
|
||||
return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Type, this.Key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(AgentMetadata left, AgentMetadata right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(AgentMetadata left, AgentMetadata right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A proxy that allows you to use an <see cref="AgentId"/> in place of its associated <see cref="IAgent"/>.
|
||||
/// </summary>
|
||||
public class AgentProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// The runtime instance used to interact with agents.
|
||||
/// </summary>
|
||||
private readonly IAgentRuntime _runtime;
|
||||
private AgentMetadata? _metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentProxy"/> class.
|
||||
/// </summary>
|
||||
public AgentProxy(AgentId agentId, IAgentRuntime runtime)
|
||||
{
|
||||
this.Id = agentId;
|
||||
this._runtime = runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The target agent for this proxy.
|
||||
/// </summary>
|
||||
public AgentId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata of the agent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An instance of <see cref="AgentMetadata"/> containing details about the agent.
|
||||
/// </value>
|
||||
public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap();
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the agent and processes the response.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send to the agent.</param>
|
||||
/// <param name="sender">The agent that is sending the message.</param>
|
||||
/// <param name="messageId">
|
||||
/// The message ID. If <c>null</c>, a new message ID will be generated.
|
||||
/// This message ID must be unique and is recommended to be a UUID.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">
|
||||
/// A token used to cancel an in-progress operation. Defaults to <c>null</c>.
|
||||
/// </param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the response from the agent.</returns>
|
||||
public ValueTask<object?> SendMessageAsync(object message, AgentId sender, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the state of the agent from a previously saved state.
|
||||
/// </summary>
|
||||
/// <param name="state">A dictionary representing the state of the agent. Must be JSON serializable.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public ValueTask LoadStateAsync(JsonElement state)
|
||||
{
|
||||
return this._runtime.LoadAgentStateAsync(this.Id, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the state of the agent. The result must be JSON serializable.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation, returning a dictionary containing the saved state.</returns>
|
||||
public ValueTask<JsonElement> 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is immutable and provides implicit conversion to and from <see cref="string"/>.
|
||||
/// </remarks>
|
||||
public readonly partial struct AgentType : IEquatable<AgentType>
|
||||
{
|
||||
#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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type.</param>
|
||||
public AgentType(string type)
|
||||
{
|
||||
Validate(type);
|
||||
this.Name = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The string representation of this agent type.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/source".</returns>
|
||||
public override readonly string ToString() => this.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts a <see cref="Type"/> to an <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The .NET <see cref="Type"/> to convert.</param>
|
||||
/// <returns>An <see cref="AgentType"/> instance with the name of the provided type.</returns>
|
||||
public static explicit operator AgentType(Type type) => new(type.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a <see cref="string"/> to an <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The string representation of the agent type.</param>
|
||||
/// <returns>An <see cref="AgentType"/> instance with the given name.</returns>
|
||||
public static implicit operator AgentType(string type) => new(type);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts an <see cref="AgentType"/> to a <see cref="string"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The <see cref="AgentType"/> instance.</param>
|
||||
/// <returns>The string representation of the agent type.</returns>
|
||||
public static implicit operator string(AgentType type) => type.ToString();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is AgentType other && this.Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(AgentType other)
|
||||
{
|
||||
return this.Name.Equals(other.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.Name.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(AgentType left, AgentType right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(AgentType left, AgentType right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the base class for an agent in the AutoGen system.
|
||||
/// </summary>
|
||||
public abstract class BaseAgent : IHostableAgent
|
||||
{
|
||||
private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement;
|
||||
|
||||
/// <summary>
|
||||
/// The activity source for tracing.
|
||||
/// </summary>
|
||||
public static readonly ActivitySource TraceSource = new($"{typeof(IAgent).Namespace}");
|
||||
|
||||
private readonly Dictionary<Type, HandlerInvoker> _handlerInvokers;
|
||||
private readonly IAgentRuntime _runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides logging capabilities used for diagnostic and operational information.
|
||||
/// </summary>
|
||||
protected internal ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the agent.
|
||||
/// </summary>
|
||||
protected string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the agent.
|
||||
/// </summary>
|
||||
public AgentId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata of the agent.
|
||||
/// </summary>
|
||||
public AgentMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the BaseAgent class with the specified identifier, runtime, description, and optional logger.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime environment in which the agent operates.</param>
|
||||
/// <param name="description">A brief description of the agent's purpose.</param>
|
||||
/// <param name="logger">An optional logger for recording diagnostic information.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message by determining its type and invoking the corresponding handler method if available.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to be handled.</param>
|
||||
/// <param name="messageContext">The context associated with the message.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, containing the response object or null.</returns>
|
||||
public async ValueTask<object?> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask<JsonElement> SaveStateAsync()
|
||||
{
|
||||
return new ValueTask<JsonElement>(s_emptyElement);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask LoadStateAsync(JsonElement state) =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Closes this agent gracefully by releasing allocated resources and performing any necessary cleanup.
|
||||
/// </summary>
|
||||
public virtual ValueTask CloseAsync() =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient agent through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="agent">The requested agent's type.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected async ValueTask<AgentId?> GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._runtime.GetAgentAsync(agent, lazy: false).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient agent through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to send.</param>
|
||||
/// <param name="recipient">The recipient agent's identifier.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected ValueTask<object?> SendMessageAsync(object message, AgentId recipient, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all agents subscribed to a specific topic through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to publish.</param>
|
||||
/// <param name="topic">The topic identifier to which the message is published.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous publish operation.</returns>
|
||||
protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a handler cannot process the given message.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class CantHandleException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CantHandleException"/> class.
|
||||
/// </summary>
|
||||
public CantHandleException() : base("The handler cannot process the given message.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CantHandleException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public CantHandleException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CantHandleException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public CantHandleException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a message is dropped.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class MessageDroppedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class.
|
||||
/// </summary>
|
||||
public MessageDroppedException() : base("The message was dropped.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public MessageDroppedException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public MessageDroppedException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an attempt is made to access an unavailable value, such as a remote resource.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NotAccessibleException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class.
|
||||
/// </summary>
|
||||
public NotAccessibleException() : base("The requested value is not accessible.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public NotAccessibleException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public NotAccessibleException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a message cannot be delivered.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class UndeliverableException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndeliverableException"/> class.
|
||||
/// </summary>
|
||||
public UndeliverableException() : base("The message cannot be delivered.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndeliverableException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public UndeliverableException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndeliverableException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public UndeliverableException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class HandlerInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans the provided agent for implemented handler interfaces (IHandle<> and IHandle<,>) via reflection,
|
||||
/// creates a corresponding <see cref="HandlerInvoker"/> for each handler method, and returns a dictionary that maps
|
||||
/// the message type (first generic argument of the interface) to its invoker.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent instance whose handler interfaces will be reflected.</param>
|
||||
/// <returns>A dictionary mapping message types to their corresponding <see cref="HandlerInvoker"/> instances.</returns>
|
||||
public static Dictionary<Type, HandlerInvoker> ReflectAgentHandlers(BaseAgent agent)
|
||||
{
|
||||
Type realType = agent.GetType();
|
||||
|
||||
IEnumerable<Type> candidateInterfaces =
|
||||
realType.GetInterfaces()
|
||||
.Where(i => i.IsGenericType &&
|
||||
(i.GetGenericTypeDefinition() == typeof(IHandle<>) ||
|
||||
(i.GetGenericTypeDefinition() == typeof(IHandle<,>))));
|
||||
|
||||
Dictionary<Type, HandlerInvoker> invokers = [];
|
||||
foreach (Type interface_ in candidateInterfaces)
|
||||
{
|
||||
MethodInfo handleAsync =
|
||||
interface_.GetMethod(nameof(IHandle<object>.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the asynchronous invocation function.
|
||||
/// </summary>
|
||||
private Func<object?, MessageContext, ValueTask<object?>> Invocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandlerInvoker"/> class with the specified method information and target object.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The MethodInfo representing the handler method to be invoked.</param>
|
||||
/// <param name="target">The target instance of the agent.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the target is missing for a non-static method or if the method's return type is not supported.</exception>
|
||||
private HandlerInvoker(MethodInfo methodInfo, BaseAgent target)
|
||||
{
|
||||
object? invocation(object? message, MessageContext messageContext) => methodInfo.Invoke(target, [message, messageContext]);
|
||||
|
||||
Func<object?, MessageContext, ValueTask<object?>> 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<T>
|
||||
else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
|
||||
{
|
||||
// Obtain the generic type argument for ValueTask<T>
|
||||
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<T> to ValueTask<object?>
|
||||
object valueTask = invocation(message, messageContext)!;
|
||||
object? typelessValueTask = typeEraseAwait.Invoke(null, [valueTask]);
|
||||
|
||||
Debug.Assert(typelessValueTask is ValueTask<object?>, "Expected ValueTask<object?> after type erasure.");
|
||||
|
||||
return await ((ValueTask<object?>)typelessValueTask).ConfigureAwait(false);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Method {methodInfo.Name} must return a ValueTask or ValueTask<T>");
|
||||
}
|
||||
|
||||
this.Invocation = getResultAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the handler method asynchronously with the provided message and context.
|
||||
/// </summary>
|
||||
/// <param name="obj">The message to be passed as the first argument to the handler.</param>
|
||||
/// <param name="messageContext">The contextual information associated with the message.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation, which yields the handler's result.</returns>
|
||||
public async ValueTask<object?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits a generic ValueTask and returns its result as an object.
|
||||
/// This method is used to convert a ValueTask{T} to ValueTask{object?}.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result contained in the ValueTask.</typeparam>
|
||||
/// <param name="vt">The ValueTask to be awaited.</param>
|
||||
/// <returns>A ValueTask containing the result as an object.</returns>
|
||||
private static async ValueTask<object?> TypeEraseAwaitAsync<T>(ValueTask<T> vt)
|
||||
{
|
||||
return await vt.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an agent within the runtime that can process messages, maintain state, and be closed when no longer needed.
|
||||
/// </summary>
|
||||
public interface IAgent : ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the agent.
|
||||
/// </summary>
|
||||
AgentId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets metadata associated with the agent.
|
||||
/// </summary>
|
||||
AgentMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message for the agent.
|
||||
/// This should only be called by the runtime, not by other agents.
|
||||
/// </summary>
|
||||
/// <param name="message">The received message. The type should match one of the expected subscription types.</param>
|
||||
/// <param name="messageContext">The context of the message, providing additional metadata.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation, returning a response to the message.
|
||||
/// The response can be <c>null</c> if no reply is necessary.
|
||||
/// </returns>
|
||||
/// <exception cref="OperationCanceledException">Thrown if the message was cancelled.</exception>
|
||||
/// <exception cref="CantHandleException">Thrown if the agent cannot handle the message.</exception>
|
||||
ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET?
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the runtime environment for agents, managing message sending, subscriptions, agent resolution, and state persistence.
|
||||
/// </summary>
|
||||
public interface IAgentRuntime : ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a message to an agent and gets a response.
|
||||
/// This method should be used to communicate directly with an agent.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="recipient">The agent to send the message to.</param>
|
||||
/// <param name="sender">The agent sending the message. Should be <c>null</c> if sent from an external source.</param>
|
||||
/// <param name="messageId">A unique identifier for the message. If <c>null</c>, a new ID will be generated.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the response from the agent.</returns>
|
||||
/// <exception cref="CantHandleException">Thrown if the recipient cannot handle the message.</exception>
|
||||
/// <exception cref="UndeliverableException">Thrown if the message cannot be delivered.</exception>
|
||||
ValueTask<object?> SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all agents subscribed to the given topic.
|
||||
/// No responses are expected from publishing.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to publish.</param>
|
||||
/// <param name="topic">The topic to publish the message to.</param>
|
||||
/// <param name="sender">The agent sending the message. Defaults to <c>null</c>.</param>
|
||||
/// <param name="messageId">A unique message ID. If <c>null</c>, a new one will be generated.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="UndeliverableException">Thrown if the message cannot be delivered.</exception>
|
||||
ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the agent.</param>
|
||||
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
|
||||
ValueTask<AgentId> GetAgentAsync(AgentId agentId, bool lazy = true/*, CancellationToken? = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent by its type.
|
||||
/// </summary>
|
||||
/// <param name="agentType">The type of the agent.</param>
|
||||
/// <param name="key">An optional key to specify variations of the agent. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
|
||||
ValueTask<AgentId> GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent by its string representation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The string representation of the agent.</param>
|
||||
/// <param name="key">An optional key to specify variations of the agent. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
|
||||
ValueTask<AgentId> GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the state of an agent.
|
||||
/// The result must be JSON serializable.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent whose state is being saved.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning a dictionary of the saved state.</returns>
|
||||
ValueTask<JsonElement> SaveAgentStateAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the saved state into an agent.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent whose state is being restored.</param>
|
||||
/// <param name="state">The state dictionary to restore.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state/*, CancellationToken? cancellationToken = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves metadata for an agent.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's metadata.</returns>
|
||||
ValueTask<AgentMetadata> GetAgentMetadataAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new subscription for the runtime to handle when processing published messages.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to add.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription/*, CancellationToken? cancellationToken = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a subscription from the runtime.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The unique identifier of the subscription to remove.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if the subscription does not exist.</exception>
|
||||
ValueTask RemoveSubscriptionAsync(string subscriptionId/*, CancellationToken? cancellationToken = default*/);
|
||||
|
||||
/// <summary>
|
||||
/// Registers an agent factory with the runtime, associating it with a specific agent type.
|
||||
/// The type must be unique.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the agent instance.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered <see cref="AgentType"/>.</returns>
|
||||
ValueTask<AgentType> RegisterAgentFactoryAsync(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve an <see cref="AgentProxy"/> for the specified agent.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning an <see cref="AgentProxy"/> if successful.</returns>
|
||||
ValueTask<AgentProxy> TryGetAgentProxyAsync(AgentId agentId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler interface for processing items of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item to be handled.</typeparam>
|
||||
public interface IHandle<in T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the specified item asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be handled.</param>
|
||||
/// <param name="messageContext">The context of the message being handled.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
ValueTask HandleAsync(T item, MessageContext messageContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler interface for processing items of type <typeparamref name="TIn"/> and <typeparamref name="TOut"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TIn">The input type</typeparam>
|
||||
/// <typeparam name="TOut">The output type</typeparam>
|
||||
public interface IHandle<in TIn, TOut>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the specified item asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be handled.</param>
|
||||
/// <param name="messageContext">The context of the message being handled.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
ValueTask<TOut> HandleAsync(TIn item, MessageContext messageContext);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an agent that can be explicitly hosted and closed when the runtime shuts down.
|
||||
/// </summary>
|
||||
public interface IHostableAgent : IAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the runtime is closing.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask CloseAsync();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for saving and loading the state of an object.
|
||||
/// The state must be JSON serializable.
|
||||
/// </summary>
|
||||
public interface ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the current state of the object.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// 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.
|
||||
/// </returns>
|
||||
ValueTask<JsonElement> SaveStateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Loads a previously saved state into the object.
|
||||
/// </summary>
|
||||
/// <param name="state">
|
||||
/// A dictionary representing the saved state. The structure of the state
|
||||
/// is implementation-defined but must be JSON serializable.
|
||||
/// </param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask LoadStateAsync(JsonElement state);
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a subscription that matches topics and maps them to agents.
|
||||
/// </summary>
|
||||
public interface ISubscriptionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the subscription.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
bool Equals([NotNullWhen(true)] object? obj);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified subscription is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="other">The subscription to compare.</param>
|
||||
/// <returns><c>true</c> if the subscriptions are equal; otherwise, <c>false</c>.</returns>
|
||||
bool Equals(ISubscriptionDefinition? other);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this subscription.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the subscription.</returns>
|
||||
int GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given <see cref="TopicId"/> matches the subscription.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to check.</param>
|
||||
/// <returns><c>true</c> if the topic matches the subscription; otherwise, <c>false</c>.</returns>
|
||||
bool Matches(TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="AgentId"/>.
|
||||
/// Should only be called if <see cref="Matches"/> returns <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to map.</param>
|
||||
/// <returns>The <see cref="AgentId"/> that should handle the topic.</returns>
|
||||
AgentId MapToAgent(TopicId topic);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for parsing key-value string representations.
|
||||
/// </summary>
|
||||
internal static class KeyValueParserExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The regular expression pattern used to match key-value pairs in the format "key/value".
|
||||
/// </summary>
|
||||
private const string KVPairPattern = @"^(?<key>\w+)/(?<value>\w+)$";
|
||||
|
||||
/// <summary>
|
||||
/// The compiled regex used for extracting key-value pairs from a string.
|
||||
/// </summary>
|
||||
private static readonly Regex KVPairRegex = new(KVPairPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string in the format "key/value" into a tuple containing the key and value.
|
||||
/// </summary>
|
||||
/// <param name="inputPair">The input string containing a key-value pair.</param>
|
||||
/// <param name="keyName">The expected name of the key component.</param>
|
||||
/// <param name="valueName">The expected name of the value component.</param>
|
||||
/// <returns>A tuple containing the extracted key and value.</returns>
|
||||
/// <exception cref="FormatException">
|
||||
/// Thrown if the input string does not match the expected "key/value" format.
|
||||
/// </exception>
|
||||
/// <example>
|
||||
/// Example usage:
|
||||
/// <code>
|
||||
/// string input = "agent1/12345";
|
||||
/// var result = input.ToKVPair("Type", "Key");
|
||||
/// Console.WriteLine(result.Item1); // Outputs: agent1
|
||||
/// Console.WriteLine(result.Item2); // Outputs: 12345
|
||||
/// </code>
|
||||
/// </example>
|
||||
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}}}\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class MessageContext(string messageId, CancellationToken cancellationToken)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageContext"/> class.
|
||||
/// </summary>
|
||||
public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this message.
|
||||
/// </summary>
|
||||
public string MessageId { get; } = messageId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cancellation token associated with this message.
|
||||
/// This can be used to cancel the operation if necessary.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; } = cancellationToken;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender of the message.
|
||||
/// If <c>null</c>, the sender is unspecified.
|
||||
/// </summary>
|
||||
public AgentId? Sender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic associated with the message.
|
||||
/// If <c>null</c>, the message is not tied to a specific topic.
|
||||
/// </summary>
|
||||
public TopicId? Topic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this message is part of an RPC (Remote Procedure Call).
|
||||
/// </summary>
|
||||
public bool IsRpc { get; set; }
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006;IDE0130</NoWarn>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<IsAotCompatible>false</IsAotCompatible> <!-- TODO: Fix this -->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectDiagnosticAttributesOnLegacy>true</InjectDiagnosticAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" />
|
||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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:
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type Specification</see>.
|
||||
/// </summary>
|
||||
public struct TopicId : IEquatable<TopicId>
|
||||
{
|
||||
/// <summary>
|
||||
/// The default source value used when no source is explicitly provided.
|
||||
/// </summary>
|
||||
public const string DefaultSource = "default";
|
||||
|
||||
/// <summary>
|
||||
/// The separator character for the string representation of the topic.
|
||||
/// </summary>
|
||||
public const string Separator = "/";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the event that this <see cref="TopicId"/> represents.
|
||||
/// This adheres to the CloudEvents specification.
|
||||
///
|
||||
/// Must match the pattern: <c>^[\w\-\.\:\=]+$</c>.
|
||||
///
|
||||
/// Learn more here:
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type</see>.
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source that identifies the context in which an event happened.
|
||||
/// This adheres to the CloudEvents specification.
|
||||
///
|
||||
/// Learn more here:
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#source-1">CloudEvents Source</see>.
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the topic.</param>
|
||||
/// <param name="source">The source of the event. Defaults to <see cref="DefaultSource"/> if not specified.</param>
|
||||
public TopicId(string type, string source = DefaultSource)
|
||||
{
|
||||
this.Type = type;
|
||||
this.Source = source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicId"/> struct from a tuple.
|
||||
/// </summary>
|
||||
/// <param name="kvPair">A tuple containing the topic type and source.</param>
|
||||
public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.Source)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string in the format "type/source" into a <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="maybeTopicId">The topic ID string.</param>
|
||||
/// <returns>An instance of <see cref="TopicId"/>.</returns>
|
||||
/// <exception cref="FormatException">Thrown when the string is not in the valid "type/source" format.</exception>
|
||||
public static TopicId FromStr(string maybeTopicId) => new(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source)));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/source".</returns>
|
||||
public override readonly string ToString() => $"{this.Type}{Separator}{this.Source}";
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current <see cref="TopicId"/>; otherwise, <c>false</c>.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="other">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current <see cref="TopicId"/>; otherwise, <c>false</c>.</returns>
|
||||
public readonly bool Equals([NotNullWhen(true)] TopicId other)
|
||||
{
|
||||
return this.Type == other.Type && this.Source == other.Source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the current instance.</returns>
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Type, this.Source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts a string to a <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The string representation of a topic ID.</param>
|
||||
/// <returns>An instance of <see cref="TopicId"/>.</returns>
|
||||
public static explicit operator TopicId(string id) => FromStr(id);
|
||||
|
||||
// TODO: Implement < for wildcard matching (type, *)
|
||||
// == => <
|
||||
// Type == other.Type => <
|
||||
/// <summary>
|
||||
/// Determines whether the given <see cref="TopicId"/> matches another topic.
|
||||
/// </summary>
|
||||
/// <param name="other">The topic ID to compare against.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the topic types are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public readonly bool IsWildcardMatch(TopicId other)
|
||||
{
|
||||
return this.Type == other.Type;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(TopicId left, TopicId right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(TopicId left, TopicId right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code>
|
||||
/// var subscription = new TypeSubscription("t1", "a1");
|
||||
/// </code>
|
||||
/// In this case:
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`.
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`.
|
||||
/// </remarks>
|
||||
public class TypeSubscription : ISubscriptionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypeSubscription"/> class.
|
||||
/// </summary>
|
||||
/// <param name="topicType">The exact topic type to match against.</param>
|
||||
/// <param name="agentType">Agent type to handle this subscription.</param>
|
||||
/// <param name="id">Unique identifier for the subscription. If not provided, a new UUID will be generated.</param>
|
||||
public TypeSubscription(string topicType, AgentType agentType, string? id = null)
|
||||
{
|
||||
this.TopicType = topicType;
|
||||
this.AgentType = agentType;
|
||||
this.Id = id ?? Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the subscription.
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exact topic type used for matching.
|
||||
/// </summary>
|
||||
public string TopicType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent type that handles this subscription.
|
||||
/// </summary>
|
||||
public AgentType AgentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given <see cref="TopicId"/> matches the subscription based on an exact type match.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to check.</param>
|
||||
/// <returns><c>true</c> if the topic's type matches exactly, <c>false</c> otherwise.</returns>
|
||||
public bool Matches(TopicId topic)
|
||||
{
|
||||
return topic.Type == this.TopicType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="AgentId"/>. Should only be called if <see cref="Matches"/> returns true.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to map.</param>
|
||||
/// <returns>An <see cref="AgentId"/> representing the agent that should handle the topic.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the topic does not match the subscription.</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified subscription is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="other">The subscription to compare.</param>
|
||||
/// <returns><c>true</c> if the subscriptions are equal; otherwise, <c>false</c>.</returns>
|
||||
public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Id, this.AgentType, this.TopicType);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-process/in-memory implementation of the agent runtime.
|
||||
/// </summary>
|
||||
public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
{
|
||||
private readonly Dictionary<AgentType, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>>> _agentFactories = [];
|
||||
private readonly Dictionary<string, ISubscriptionDefinition> _subscriptions = [];
|
||||
private readonly ConcurrentQueue<MessageDelivery> _messageDeliveryQueue = new();
|
||||
|
||||
private CancellationTokenSource? _shutdownSource;
|
||||
private CancellationTokenSource? _finishSource;
|
||||
private Task _messageDeliveryTask = Task.CompletedTask;
|
||||
private Func<bool> _shouldContinue = () => true;
|
||||
|
||||
// Exposed for testing purposes.
|
||||
internal int messageQueueCount;
|
||||
internal readonly Dictionary<AgentId, IHostableAgent> agentInstances = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether agents should receive messages they send themselves.
|
||||
/// </summary>
|
||||
public bool DeliverToSelf { get; set; } //= false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await this.RunUntilIdleAsync().ConfigureAwait(false);
|
||||
this._shutdownSource?.Dispose();
|
||||
this._finishSource?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the runtime service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to monitor for shutdown requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the runtime is already started.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the runtime service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token to propagate when stopping the runtime.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the runtime is in the process of stopping.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will run until the message queue is empty and then stop the runtime.
|
||||
/// </summary>
|
||||
public async Task RunUntilIdleAsync()
|
||||
{
|
||||
Func<bool> oldShouldContinue = this._shouldContinue;
|
||||
this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty;
|
||||
|
||||
// TODO: Do we want detach semantics?
|
||||
await this._messageDeliveryTask.ConfigureAwait(false);
|
||||
|
||||
this._shouldContinue = oldShouldContinue;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<object?> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentId> GetAgentAsync(AgentId agentId, bool lazy = true)
|
||||
{
|
||||
if (!lazy)
|
||||
{
|
||||
await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<AgentId> GetAgentAsync(AgentType agentType, string key = AgentId.DefaultKey, bool lazy = true)
|
||||
=> this.GetAgentAsync(new AgentId(agentType, key), lazy);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<AgentId> GetAgentAsync(string agent, string key = AgentId.DefaultKey, bool lazy = true)
|
||||
=> this.GetAgentAsync(new AgentId(agent, key), lazy);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentMetadata> GetAgentMetadataAsync(AgentId agentId)
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
return agent.Metadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<TAgent> TryGetUnderlyingAgentInstanceAsync<TAgent>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state)
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
await agent.LoadStateAsync(state).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<JsonElement> SaveAgentStateAsync(AgentId agentId)
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
return await agent.SaveStateAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<JsonElement> SaveStateAsync()
|
||||
{
|
||||
Dictionary<string, JsonElement> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an agent factory with the runtime, associating it with a specific agent type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgent">The type of agent created by the factory.</typeparam>
|
||||
/// <param name="type">The agent type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the agent instance.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered agent type.</returns>
|
||||
public ValueTask<AgentType> RegisterAgentFactoryAsync<TAgent>(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<TAgent>> factoryFunc) where TAgent : IHostableAgent
|
||||
// Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask<TAgent>'
|
||||
// and recurse into the same call, causing a stack overflow.
|
||||
=> this.RegisterAgentFactoryAsync(type, async ValueTask<IHostableAgent> (agentId, runtime) => await factoryFunc(agentId, runtime).ConfigureAwait(false));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentType> RegisterAgentFactoryAsync(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc)
|
||||
{
|
||||
if (this._agentFactories.ContainsKey(type))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent with type {type} already exists.");
|
||||
}
|
||||
|
||||
this._agentFactories.Add(type, factoryFunc);
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentProxy> 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<Guid, Task> 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<Exception> 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<object?> 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<IHostableAgent> EnsureAgentAsync(AgentId agentId)
|
||||
{
|
||||
if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent))
|
||||
{
|
||||
if (!this._agentFactories.TryGetValue(agentId.Type, out Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>>? 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<T> ExecuteTracedAsync<T>(Func<ValueTask<T>> 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<ValueTask> func)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
// TODO: Bind tracing
|
||||
return func();
|
||||
}
|
||||
}
|
||||
@@ -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<MessageEnvelope, CancellationToken, ValueTask> servicer, IResultSink<object?> resultSink)
|
||||
{
|
||||
public MessageEnvelope Message { get; } = message;
|
||||
public Func<MessageEnvelope, CancellationToken, ValueTask> Servicer { get; } = servicer;
|
||||
public IResultSink<object?> ResultSink { get; } = resultSink;
|
||||
|
||||
public ValueTask InvokeAsync(CancellationToken cancellation)
|
||||
{
|
||||
return this.Servicer(this.Message, cancellation);
|
||||
}
|
||||
}
|
||||
@@ -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<MessageEnvelope, CancellationToken, ValueTask<object?>> servicer)
|
||||
{
|
||||
this.Receiver = receiver;
|
||||
|
||||
ResultSink<object?> 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<MessageEnvelope, CancellationToken, ValueTask> servicer)
|
||||
{
|
||||
this.Topic = topic;
|
||||
|
||||
ResultSink<object?> 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);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<IsAotCompatible>false</IsAotCompatible> <!-- TODO: Fix this -->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectDiagnosticAttributesOnLegacy>true</InjectDiagnosticAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Extensions.AI.Agents.Runtime.InProcess.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
|
||||
<PackageReference Include="System.Threading.Tasks.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.Runtime.Abstractions\Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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<TResult> : IValueTaskSource<TResult>
|
||||
{
|
||||
void SetResult(TResult result);
|
||||
void SetException(Exception exception);
|
||||
void SetCancelled(OperationCanceledException? exception = null);
|
||||
|
||||
ValueTask<TResult> Future { get; }
|
||||
}
|
||||
|
||||
internal sealed class ResultSink<TResult> : IResultSink<TResult>
|
||||
{
|
||||
private ManualResetValueTaskSourceCore<TResult> _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<object?> 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<TResult> Future => new(this, this._core.Version);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ internal static partial class ChatClientAgentLogMessages
|
||||
/// Logs <see cref="ChatClientAgent"/> invoking agent (started).
|
||||
/// </summary>
|
||||
[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 <see cref="ChatClientAgent"/> invoked agent (complete).
|
||||
/// </summary>
|
||||
[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 <see cref="ChatClientAgent"/> invoked streaming agent (complete).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
EventId = 0,
|
||||
Level = LogLevel.Information,
|
||||
Message = "[{MethodName}] Agent {AgentId}/{AgentName} Invoked client {ClientType}.")]
|
||||
public static partial void LogAgentChatClientInvokedStreamingAgent(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ public abstract class ChatClientAgentRunStreamingTests<TAgentFixture>(Func<TAgen
|
||||
var chatResponses = await agent.RunStreamingAsync(thread).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Join("", chatResponses.Select(x => 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<TAgentFixture>(Func<TAgen
|
||||
thread).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Join("", chatResponses.Select(x => x.Text));
|
||||
var chatResponseText = string.Concat(chatResponses.Select(x => x.Text));
|
||||
Assert.Contains(questionAndAnswer.ExpectedAnswer, chatResponseText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> 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<TAgentFixture>(Func<TAgentFixture> 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<TAgentFixture>(Func<TAgentFixture> 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<TAgentFixture>(Func<TAgentFixture> 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);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<ProjectsTargetFrameworks>net472;net9.0</ProjectsTargetFrameworks>
|
||||
<ProjectsDebugTargetFrameworks>net9.0</ProjectsDebugTargetFrameworks>
|
||||
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class AgentTests
|
||||
private readonly Mock<Agent> _agentMock;
|
||||
private readonly Mock<AgentThread> _agentThreadMock;
|
||||
private readonly ChatResponse _invokeResponse = new();
|
||||
private readonly List<ChatResponseUpdate> _invokeStreamingResponses = new();
|
||||
private readonly List<ChatResponseUpdate> _invokeStreamingResponses = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentTests"/> class.
|
||||
@@ -255,9 +255,7 @@ public class AgentTests
|
||||
/// <summary>
|
||||
/// Typed mock thread.
|
||||
/// </summary>
|
||||
public abstract class TestAgentThread : AgentThread
|
||||
{
|
||||
}
|
||||
public abstract class TestAgentThread : AgentThread;
|
||||
|
||||
/// <summary>
|
||||
/// Mock class to test the <see cref="Agent.ValidateOrCreateThreadType{TThreadType}"/> method.
|
||||
|
||||
+119
@@ -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<ArgumentException>(() => 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);
|
||||
}
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
}
|
||||
+89
@@ -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<IAgentRuntime> _mockRuntime;
|
||||
private readonly AgentId _agentId;
|
||||
private readonly AgentProxy _agentProxy;
|
||||
|
||||
public AgentProxyTests()
|
||||
{
|
||||
this._mockRuntime = new Mock<IAgentRuntime>();
|
||||
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<CancellationToken>()))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
+62
@@ -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<ArgumentException>(() => 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);
|
||||
}
|
||||
}
|
||||
+80
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.Runtime.Abstractions\Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+182
@@ -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<FormatException>(() => 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));
|
||||
}
|
||||
}
|
||||
+333
@@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(async () => await runtime.RemoveSubscriptionAsync(subscription.Id));
|
||||
|
||||
// Arrange
|
||||
await runtime.AddSubscriptionAsync(subscription);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(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<MockAgent> agents = [];
|
||||
await using InProcessRuntime runtime = new();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.GetAgentAsync(AgentType, lazy: false));
|
||||
|
||||
// Arrange
|
||||
await runtime.RegisterAgentFactoryAsync(AgentType, factoryFunc);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(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<MockAgent>(agentId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(agentId, agent.Id);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await runtime.TryGetUnderlyingAgentInstanceAsync<WrongAgent>(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<MockAgent> 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<MockAgent> 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<string, JsonElement> deserializedState = JsonSerializer.Deserialize<IDictionary<string, JsonElement>>(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<object?> OnMessageAsync(object message, MessageContext messageContext)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask<JsonElement> SaveStateAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -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<object?> 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);
|
||||
}
|
||||
}
|
||||
+202
@@ -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<BasicMessage>
|
||||
{
|
||||
private readonly IList<TopicId> _targetTopics;
|
||||
|
||||
public PublisherAgent(AgentId id, IAgentRuntime runtime, string description, IList<TopicId> 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<BasicMessage>
|
||||
{
|
||||
private readonly IList<Guid> _targetKeys;
|
||||
|
||||
public SendOnAgent(AgentId id, IAgentRuntime runtime, string description, IList<Guid> 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<BasicMessage>
|
||||
{
|
||||
public List<BasicMessage> 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<BasicMessage, BasicMessage>
|
||||
{
|
||||
private Func<string, string> ProcessFunc { get; }
|
||||
|
||||
public ProcessorAgent(AgentId id, IAgentRuntime runtime, Func<string, string> processFunc, string description)
|
||||
: base(id, runtime, description)
|
||||
{
|
||||
this.ProcessFunc = processFunc;
|
||||
}
|
||||
|
||||
public ValueTask<BasicMessage> HandleAsync(BasicMessage item, MessageContext messageContext)
|
||||
{
|
||||
BasicMessage result = new() { Content = this.ProcessFunc.Invoke(((BasicMessage)item).Content) };
|
||||
|
||||
return new(result);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CancelAgent : TestAgent, IHandle<BasicMessage>
|
||||
{
|
||||
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<BasicMessage>
|
||||
{
|
||||
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<Type, object> AgentsTypeMap { get; } = [];
|
||||
public InProcessRuntime Runtime { get; } = new();
|
||||
|
||||
public ValueTask<AgentType> RegisterFactoryMapInstances<TAgent>(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<TAgent>> factory)
|
||||
where TAgent : IHostableAgent
|
||||
{
|
||||
async ValueTask<TAgent> WrappedFactory(AgentId id, IAgentRuntime runtime)
|
||||
{
|
||||
TAgent agent = await factory(id, runtime);
|
||||
this.GetAgentInstances<TAgent>()[id] = agent;
|
||||
return agent;
|
||||
}
|
||||
|
||||
return this.Runtime.RegisterAgentFactoryAsync(type, WrappedFactory);
|
||||
}
|
||||
|
||||
public Dictionary<AgentId, TAgent> GetAgentInstances<TAgent>() where TAgent : IHostableAgent
|
||||
{
|
||||
if (!this.AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) ||
|
||||
maybeAgentMap is not Dictionary<AgentId, TAgent> 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<ReceiverAgent>(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<ErrorAgent>(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<object?> 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;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.Runtime.InProcess\Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+116
@@ -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<ReceiverAgent>().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<AggregateException>(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }));
|
||||
Assert.IsType<TestException>(Assert.Single(e.InnerExceptions));
|
||||
|
||||
var values = fixture.GetAgentInstances<ReceiverAgent>().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<AggregateException>(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }));
|
||||
Assert.Equal(2, e.InnerExceptions.Count);
|
||||
Assert.All(e.InnerExceptions, innerException => Assert.IsType<TestException>(innerException));
|
||||
|
||||
var values = fixture.GetAgentInstances<ErrorAgent>().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<AggregateException>(async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }));
|
||||
Assert.Equal(2, e.InnerExceptions.Count);
|
||||
Assert.All(e.InnerExceptions, innerException => Assert.IsType<TestException>(innerException));
|
||||
|
||||
var agents = fixture.GetAgentInstances<ReceiverAgent>().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<ErrorAgent>().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<PublisherAgent>(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<ReceiverAgent>().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");
|
||||
});
|
||||
}
|
||||
}
|
||||
+103
@@ -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<int> 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<string> 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<int> sink = new();
|
||||
InvalidOperationException expectedException = new("Test exception");
|
||||
|
||||
// Act
|
||||
sink.SetException(expectedException);
|
||||
|
||||
// Assert
|
||||
Exception exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sink.Future);
|
||||
Assert.Equal(expectedException.Message, exception.Message);
|
||||
exception = Assert.Throws<InvalidOperationException>(() => sink.GetResult(0));
|
||||
Assert.Equal(expectedException.Message, exception.Message);
|
||||
Assert.Equal(ValueTaskSourceStatus.Faulted, sink.GetStatus(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetCancelledTestAsync()
|
||||
{
|
||||
// Arrange
|
||||
ResultSink<int> sink = new();
|
||||
|
||||
// Act
|
||||
sink.SetCancelled();
|
||||
|
||||
// Assert
|
||||
Assert.True(sink.IsCancelled);
|
||||
Assert.Throws<OperationCanceledException>(() => sink.GetResult(0));
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(async () => await sink.Future);
|
||||
Assert.Equal(ValueTaskSourceStatus.Canceled, sink.GetStatus(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCompletedTest()
|
||||
{
|
||||
// Arrange
|
||||
ResultSink<int> 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");
|
||||
}
|
||||
}
|
||||
+90
@@ -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<ProcessorAgent>(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<BasicMessage>(maybeResult).Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_SendMessage_CancellationAsync()
|
||||
{
|
||||
MessagingTestFixture fixture = new();
|
||||
|
||||
await fixture.RegisterFactoryMapInstances(nameof(CancelAgent),
|
||||
(id, runtime) => new ValueTask<CancelAgent>(new CancelAgent(id, runtime, string.Empty)));
|
||||
|
||||
AgentId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString());
|
||||
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() => 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<ErrorAgent>(new ErrorAgent(id, runtime, string.Empty)));
|
||||
|
||||
AgentId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString());
|
||||
|
||||
await Assert.ThrowsAsync<TestException>(() => 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<AgentId, SendOnAgent> sendAgents = fixture.GetAgentInstances<SendOnAgent>();
|
||||
Dictionary<AgentId, ReceiverAgent> receiverAgents = fixture.GetAgentInstances<ReceiverAgent>();
|
||||
|
||||
await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent),
|
||||
(id, runtime) => new ValueTask<SendOnAgent>(new SendOnAgent(id, runtime, string.Empty, targetGuids)));
|
||||
|
||||
await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent),
|
||||
(id, runtime) => new ValueTask<ReceiverAgent>(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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<object> ReceivedMessages = [];
|
||||
|
||||
protected TestAgent(AgentId id, IAgentRuntime runtime, string description)
|
||||
: base(id, runtime, description)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A test agent that captures the messages it receives and
|
||||
/// is able to save and load its state.
|
||||
/// </summary>
|
||||
public sealed class MockAgent : TestAgent, IHandle<string>
|
||||
{
|
||||
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<JsonElement> SaveStateAsync()
|
||||
{
|
||||
JsonElement json = JsonSerializer.SerializeToElement(this.ReceivedMessages);
|
||||
return json;
|
||||
}
|
||||
|
||||
public override ValueTask LoadStateAsync(JsonElement state)
|
||||
{
|
||||
this.ReceivedMessages = JsonSerializer.Deserialize<List<object>>(state) ?? throw new InvalidOperationException("Failed to deserialize state");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -35,16 +35,16 @@ public class OpenAIAssistantFixture : IChatClientAgentFixture
|
||||
throw new InvalidOperationException("The thread must be of type ChatClientAgentThread to retrieve chat history.");
|
||||
}
|
||||
|
||||
List<ChatMessage> messages = new();
|
||||
List<ChatMessage> 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<AIContent>()
|
||||
{
|
||||
Contents =
|
||||
[
|
||||
new TextContent(agentMessage.Content[0].Text ?? string.Empty)
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user