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