Closes #5801tags/autogenstudio-v0.4.2
| @@ -25,6 +25,11 @@ public abstract class AgentMessage | |||
| /// </summary> | |||
| public required string Source { get; set; } | |||
| /// <summary> | |||
| /// The <see cref="IChatClient"/> usage incurred when producing this message. | |||
| /// </summary> | |||
| public RequestUsage? ModelUsage { get; set; } | |||
| // IMPORTANT NOTE: Unlike the ITypeMarshal<AgentMessage, WireProtocol.AgentMessage> implementation in ProtobufTypeMarshal, | |||
| // the .ToWire() call on this is intended to be used for directly converting a concrete message type to its leaf representation. | |||
| // In the context of Protobuf these may not be the same due to discriminated union types being real types, as opposed to | |||
| @@ -495,6 +500,11 @@ public class FunctionExecutionResult | |||
| /// </summary> | |||
| public required string Id { get; set; } | |||
| /// <summary> | |||
| /// The name of the function that was called. | |||
| /// </summary> | |||
| public required string Name { get; set; } | |||
| /// <summary> | |||
| /// The result of calling the function. | |||
| /// </summary> | |||
| @@ -3,6 +3,29 @@ | |||
| namespace Microsoft.AutoGen.AgentChat.Abstractions; | |||
| public static class TerminationConditionExtensions | |||
| { | |||
| /// <summary> | |||
| /// Combine this termination condition with another using a logical OR. | |||
| /// </summary> | |||
| /// <param name="other">Another termination condition.</param> | |||
| /// <returns>The combined termination condition, with appropriate short-circuiting.</returns> | |||
| public static ITerminationCondition Or(this ITerminationCondition this_, ITerminationCondition other) | |||
| { | |||
| return new CombinerCondition(CombinerCondition.Or, this_, other); | |||
| } | |||
| /// <summary> | |||
| /// Combine this termination condition with another using a logical AND. | |||
| /// </summary> | |||
| /// <param name="other">Another termination condition.</param> | |||
| /// <returns>The combined termination condition, with appropriate short-circuiting.</returns> | |||
| public static ITerminationCondition And(this ITerminationCondition this_, ITerminationCondition other) | |||
| { | |||
| return new CombinerCondition(CombinerCondition.And, this_, other); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// A stateful condition that determines when a conversation should be terminated. | |||
| /// | |||
| @@ -12,7 +35,8 @@ namespace Microsoft.AutoGen.AgentChat.Abstractions; | |||
| /// | |||
| /// Once a termination condition has been reached, it must be <see cref="Reset()"/> before it can be used again. | |||
| /// | |||
| /// Termination conditions can be combined using the <see cref="Or"/> and <see cref="And"/> methods. | |||
| /// Termination conditions can be combined using the <see cref="TerminationConditionExtensions.Or"/> and | |||
| /// <see cref="TerminationConditionExtensions.And"/> methods. | |||
| /// </summary> | |||
| public interface ITerminationCondition | |||
| { | |||
| @@ -38,23 +62,37 @@ public interface ITerminationCondition | |||
| public void Reset(); | |||
| /// <summary> | |||
| /// Combine this termination condition with another using a logical OR. | |||
| /// Combine two termination conditions with another using an associative, short-circuiting OR. | |||
| /// </summary> | |||
| /// <param name="other">Another termination condition.</param> | |||
| /// <returns>The combined termination condition, with appropriate short-circuiting.</returns> | |||
| public ITerminationCondition Or(ITerminationCondition other) | |||
| /// <param name="left"> | |||
| /// The left-hand side termination condition. If this condition is already a disjunction, the RHS condition is added to the list of clauses. | |||
| /// </param> | |||
| /// <param name="right"> | |||
| /// The right-hand side termination condition. If the LHS condition is already a disjunction, this condition is added to the list of clauses. | |||
| /// </param> | |||
| /// <returns> | |||
| /// The combined termination condition, with appropriate short-circuiting. | |||
| /// </returns> | |||
| public static ITerminationCondition operator |(ITerminationCondition left, ITerminationCondition right) | |||
| { | |||
| return new CombinerCondition(CombinerCondition.Or, this, other); | |||
| return left.Or(right); | |||
| } | |||
| /// <summary> | |||
| /// Combine this termination condition with another using a logical AND. | |||
| /// Combine two termination conditions with another using an associative, short-circuiting AND. | |||
| /// </summary> | |||
| /// <param name="other">Another termination condition.</param> | |||
| /// <returns>The combined termination condition, with appropriate short-circuiting.</returns> | |||
| public ITerminationCondition And(ITerminationCondition other) | |||
| /// <param name="left"> | |||
| /// The left-hand side termination condition. If this condition is already a conjunction, the RHS condition is added to the list of clauses. | |||
| /// </param> | |||
| /// <param name="right"> | |||
| /// The right-hand side termination condition. If the LHS condition is already a conjunction, this condition is added to the list of clauses. | |||
| /// </param> | |||
| /// <returns> | |||
| /// The combined termination condition, with appropriate short-circuiting. | |||
| /// </returns> | |||
| public static ITerminationCondition operator &(ITerminationCondition left, ITerminationCondition right) | |||
| { | |||
| return new CombinerCondition(CombinerCondition.And, this, other); | |||
| return left.And(right); | |||
| } | |||
| } | |||
| @@ -167,38 +205,4 @@ internal sealed class CombinerCondition : ITerminationCondition | |||
| return null; | |||
| } | |||
| /// <inheritdoc cref="ITerminationCondition.Or" /> | |||
| /// <remarks> | |||
| /// If this condition is already a disjunction, the new condition is added to the list of clauses. | |||
| /// </remarks> | |||
| ITerminationCondition ITerminationCondition.Or(ITerminationCondition other) | |||
| { | |||
| if (this.conjunction == Or) | |||
| { | |||
| this.clauses.Add(other); | |||
| return this; | |||
| } | |||
| else | |||
| { | |||
| return new CombinerCondition(Or, this, new CombinerCondition(Or, other)); | |||
| } | |||
| } | |||
| /// <inheritdoc cref="ITerminationCondition.And" /> | |||
| /// <remarks> | |||
| /// If this condition is already a conjunction, the new condition is added to the list of clauses. | |||
| /// </remarks> | |||
| ITerminationCondition ITerminationCondition.And(ITerminationCondition other) | |||
| { | |||
| if (this.conjunction == And) | |||
| { | |||
| this.clauses.Add(other); | |||
| return this; | |||
| } | |||
| else | |||
| { | |||
| return new CombinerCondition(And, this, new CombinerCondition(And, other)); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // Usage.cs | |||
| namespace Microsoft.AutoGen.AgentChat.Abstractions; | |||
| public struct RequestUsage | |||
| { | |||
| public int PromptTokens { get; set; } | |||
| public int CompletionTokens { get; set; } | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // ExternalTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// A <see cref="ITerminationCondition"/> that is externally controlled by calling the <see cref="Set"/> method. | |||
| /// </summary> | |||
| public sealed class ExternalTermination : ITerminationCondition | |||
| { | |||
| public ExternalTermination() | |||
| { | |||
| this.TerminationQueued = false; | |||
| this.IsTerminated = false; | |||
| } | |||
| public bool TerminationQueued { get; private set; } | |||
| public bool IsTerminated { get; private set; } | |||
| /// <summary> | |||
| /// Set the termination condition to terminated. | |||
| /// </summary> | |||
| public void Set() | |||
| { | |||
| this.TerminationQueued = true; | |||
| } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| if (this.TerminationQueued) | |||
| { | |||
| this.IsTerminated = true; | |||
| string message = "External termination requested."; | |||
| StopMessage result = new() { Content = message, Source = nameof(ExternalTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.TerminationQueued = false; | |||
| this.IsTerminated = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // FunctionCallTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation if a <see cref="ToolCallExecutionEvent"/> with a specific name is received. | |||
| /// </summary> | |||
| public sealed class FunctionCallTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="FunctionCallTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="functionName">The name of the function to look for in the messages.</param> | |||
| public FunctionCallTermination(string functionName) | |||
| { | |||
| this.FunctionName = functionName; | |||
| this.IsTerminated = false; | |||
| } | |||
| public string FunctionName { get; } | |||
| public bool IsTerminated { get; private set; } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| foreach (AgentMessage item in messages) | |||
| { | |||
| if (item is ToolCallExecutionEvent toolCallEvent && toolCallEvent.Content.Any(execution => execution.Name == this.FunctionName)) | |||
| { | |||
| this.IsTerminated = true; | |||
| string message = $"Function '{this.FunctionName}' was executed."; | |||
| StopMessage result = new() { Content = message, Source = nameof(FunctionCallTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.IsTerminated = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // HandoffTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation if a <see cref="HandoffMessage"/> with the given <see cref="HandoffMessage.Target"/> | |||
| /// is received. | |||
| /// </summary> | |||
| public sealed class HandoffTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="HandoffTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="target">The target of the handoff message.</param> | |||
| public HandoffTermination(string target) | |||
| { | |||
| this.Target = target; | |||
| this.IsTerminated = false; | |||
| } | |||
| public string Target { get; } | |||
| public bool IsTerminated { get; private set; } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| foreach (AgentMessage item in messages) | |||
| { | |||
| if (item is HandoffMessage handoffMessage && handoffMessage.Target == this.Target) | |||
| { | |||
| this.IsTerminated = true; | |||
| string message = $"Handoff to {handoffMessage.Target} from {handoffMessage.Source} detected."; | |||
| StopMessage result = new() { Content = message, Source = nameof(HandoffTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.IsTerminated = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,52 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // MaxMessageTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation after a maximum number of messages have been exchanged. | |||
| /// </summary> | |||
| public sealed class MaxMessageTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="MaxMessageTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="maxMessages">The maximum number of messages allowed in the conversation.</param> | |||
| public MaxMessageTermination(int maxMessages, bool includeAgentEvent = false) | |||
| { | |||
| this.MaxMessages = maxMessages; | |||
| this.MessageCount = 0; | |||
| this.IncludeAgentEvent = includeAgentEvent; | |||
| } | |||
| public int MaxMessages { get; } | |||
| public int MessageCount { get; private set; } | |||
| public bool IncludeAgentEvent { get; } | |||
| public bool IsTerminated => this.MessageCount >= this.MaxMessages; | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| this.MessageCount += messages.Where(m => this.IncludeAgentEvent || m is not AgentEvent).Count(); | |||
| if (this.IsTerminated) | |||
| { | |||
| StopMessage result = new() { Content = "Max message count reached", Source = nameof(MaxMessageTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.MessageCount = 0; | |||
| } | |||
| } | |||
| @@ -0,0 +1,50 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // SourceMatchTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation after a specific source responds. | |||
| /// </summary> | |||
| public sealed class SourceMatchTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="SourceMatchTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="sources">List of source names to terminate the conversation.</param> | |||
| public SourceMatchTermination(params IEnumerable<string> sources) | |||
| { | |||
| this.Sources = [.. sources]; | |||
| } | |||
| public HashSet<string> Sources { get; } | |||
| public bool IsTerminated { get; private set; } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| foreach (AgentMessage item in messages) | |||
| { | |||
| if (this.Sources.Contains(item.Source)) | |||
| { | |||
| this.IsTerminated = true; | |||
| string message = $"'{item.Source}' answered."; | |||
| StopMessage result = new() { Content = message, Source = nameof(SourceMatchTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.IsTerminated = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // TextMentionTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| using Microsoft.Extensions.AI; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation if a specific text is mentioned. | |||
| /// </summary> | |||
| public sealed class TextMentionTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="TextMentionTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="targetText">The text to look for in the messages.</param> | |||
| /// <param name="sources">Check only the messages of the specified agents for the text to look for.</param> | |||
| public TextMentionTermination(string targetText, IEnumerable<string>? sources = null) | |||
| { | |||
| this.TargetText = targetText; | |||
| this.Sources = sources != null ? [.. sources] : null; | |||
| this.IsTerminated = false; | |||
| } | |||
| public string TargetText { get; } | |||
| public HashSet<string>? Sources { get; } | |||
| public bool IsTerminated { get; private set; } | |||
| private bool CheckMultiModalData(MultiModalData data) | |||
| { | |||
| return data.ContentType == MultiModalData.Type.String && | |||
| ((TextContent)data.AIContent).Text.Contains(this.TargetText); | |||
| } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| foreach (AgentMessage item in messages) | |||
| { | |||
| if (this.Sources != null && !this.Sources.Contains(item.Source)) | |||
| { | |||
| continue; | |||
| } | |||
| bool hasMentions = item switch | |||
| { | |||
| TextMessage textMessage => textMessage.Content.Contains(this.TargetText), | |||
| MultiModalMessage multiModalMessage => multiModalMessage.Content.Any(CheckMultiModalData), | |||
| StopMessage stopMessage => stopMessage.Content.Contains(this.TargetText), | |||
| ToolCallSummaryMessage toolCallSummaryMessage => toolCallSummaryMessage.Content.Contains(this.TargetText), | |||
| _ => false | |||
| }; | |||
| if (hasMentions) | |||
| { | |||
| this.IsTerminated = true; | |||
| StopMessage result = new() { Content = "Text mention received", Source = nameof(TextMentionTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.IsTerminated = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // TextMessageTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation if a <see cref="TextMessage"/> is received. | |||
| /// | |||
| /// This termination condition checks for TextMessage instances in the message sequence. When a TextMessage is found, | |||
| /// it terminates the conversation if either: | |||
| /// | |||
| /// <list type="bullet"> | |||
| /// <item>No source was specified (terminates on any <see cref="TextMessage"/>)</item> | |||
| /// <item>The message source matches the specified source</item> | |||
| /// </list> | |||
| /// | |||
| /// </summary> | |||
| public sealed class TextMessageTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="TextMessageTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="source"> | |||
| /// The source name to match against incoming messages. If <c>null</c>, matches any source. | |||
| /// Defaults to <c>null</c>. | |||
| /// </param> | |||
| public TextMessageTermination(string? source = null) | |||
| { | |||
| this.Source = source; | |||
| this.IsTerminated = false; | |||
| } | |||
| public string? Source { get; } | |||
| public bool IsTerminated { get; private set; } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| foreach (AgentMessage item in messages) | |||
| { | |||
| if (item is TextMessage textMessage && (this.Source == null || textMessage.Source == this.Source)) | |||
| { | |||
| this.IsTerminated = true; | |||
| string message = $"Text message received from '{textMessage.Source}'."; | |||
| StopMessage result = new() { Content = message, Source = nameof(TextMessageTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.IsTerminated = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,59 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // TimeoutTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation after the specified duration has passed. | |||
| /// </summary> | |||
| public sealed class TimeoutTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="TimeoutTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="timeout">The maximum duration before terminating the conversation.</param> | |||
| public TimeoutTermination(TimeSpan timeout) | |||
| { | |||
| this.Timeout = timeout; | |||
| this.StartTime = DateTime.UtcNow; | |||
| } | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="TimeoutTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="seconds">The maximum duration in seconds before terminating the conversation.</param> | |||
| public TimeoutTermination(float seconds) : this(TimeSpan.FromSeconds(seconds)) | |||
| { | |||
| } | |||
| public TimeSpan Timeout { get; } | |||
| public DateTime StartTime { get; private set; } | |||
| public bool IsTerminated { get; private set; } | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| if (DateTime.UtcNow - this.StartTime >= this.Timeout) | |||
| { | |||
| this.IsTerminated = true; | |||
| string message = $"Timeout of {this.Timeout.TotalSeconds} seconds reached."; | |||
| StopMessage result = new() { Content = message, Source = nameof(TimeoutTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.IsTerminated = false; | |||
| this.StartTime = DateTime.UtcNow; | |||
| } | |||
| } | |||
| @@ -0,0 +1,73 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // TokenUsageTermination.cs | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| namespace Microsoft.AutoGen.AgentChat.Terminations; | |||
| /// <summary> | |||
| /// Terminate the conversation if the token usage limit is reached. | |||
| /// </summary> | |||
| public sealed class TokenUsageTermination : ITerminationCondition | |||
| { | |||
| /// <summary> | |||
| /// Initializes a new instance of the <see cref="TokenUsageTermination"/> class. | |||
| /// </summary> | |||
| /// <param name="maxTotalTokens">The maximum total number of tokens allowed in the conversation.</param> | |||
| /// <param name="maxPromptTokens">The maximum number of prompt tokens allowed in the conversation.</param> | |||
| /// <param name="maxCompletionTokens">The maximum number of completion tokens allowed in the conversation.</param> | |||
| public TokenUsageTermination(int? maxTotalTokens = null, int? maxPromptTokens = null, int? maxCompletionTokens = null) | |||
| { | |||
| this.MaxTotalTokens = maxTotalTokens; | |||
| this.MaxPromptTokens = maxPromptTokens; | |||
| this.MaxCompletionTokens = maxCompletionTokens; | |||
| this.PromptTokenCount = 0; | |||
| this.CompletionTokenCount = 0; | |||
| } | |||
| public int? MaxTotalTokens { get; } | |||
| public int? MaxPromptTokens { get; } | |||
| public int? MaxCompletionTokens { get; } | |||
| public int TotalTokenCount => this.PromptTokenCount + this.CompletionTokenCount; | |||
| public int PromptTokenCount { get; private set; } | |||
| public int CompletionTokenCount { get; private set; } | |||
| public bool IsTerminated => | |||
| (this.MaxTotalTokens != null && this.TotalTokenCount >= this.MaxTotalTokens) || | |||
| (this.MaxPromptTokens != null && this.PromptTokenCount >= this.MaxPromptTokens) || | |||
| (this.MaxCompletionTokens != null && this.CompletionTokenCount >= this.MaxCompletionTokens); | |||
| public ValueTask<StopMessage?> CheckAndUpdateAsync(IList<AgentMessage> messages) | |||
| { | |||
| if (this.IsTerminated) | |||
| { | |||
| throw new TerminatedException(); | |||
| } | |||
| foreach (AgentMessage item in messages) | |||
| { | |||
| if (item.ModelUsage is RequestUsage usage) | |||
| { | |||
| this.PromptTokenCount += usage.PromptTokens; | |||
| this.CompletionTokenCount += usage.CompletionTokens; | |||
| } | |||
| } | |||
| if (this.IsTerminated) | |||
| { | |||
| string message = $"Token usage limit reached, total token count: {this.TotalTokenCount}, prompt token count: {this.PromptTokenCount}, completion token count: {this.CompletionTokenCount}."; | |||
| StopMessage result = new() { Content = message, Source = nameof(TokenUsageTermination) }; | |||
| return ValueTask.FromResult<StopMessage?>(result); | |||
| } | |||
| return ValueTask.FromResult<StopMessage?>(null); | |||
| } | |||
| public void Reset() | |||
| { | |||
| this.PromptTokenCount = 0; | |||
| this.CompletionTokenCount = 0; | |||
| } | |||
| } | |||
| @@ -12,6 +12,7 @@ using Xunit; | |||
| namespace Microsoft.AutoGen.AgentChat.Tests; | |||
| [Trait("Category", "UnitV2")] | |||
| public class AgentChatSmokeTest | |||
| { | |||
| public class SpeakMessageAgent : ChatAgentBase | |||
| @@ -72,6 +72,7 @@ internal sealed class LifecycleObjectFixture : LifecycleObject | |||
| } | |||
| } | |||
| [Trait("Category", "UnitV2")] | |||
| public class LifecycleObjectTests | |||
| { | |||
| /* | |||
| @@ -8,6 +8,7 @@ using Xunit; | |||
| namespace Microsoft.AutoGen.AgentChat.Tests; | |||
| [Trait("Category", "UnitV2")] | |||
| public class RunContextStackTests | |||
| { | |||
| public static IRunContextLayer CreateLayer(Action<Mock<IRunContextLayer>>? setupAction = null) | |||
| @@ -0,0 +1,476 @@ | |||
| // Copyright (c) Microsoft Corporation. All rights reserved. | |||
| // TerminationConditionTests.cs | |||
| using FluentAssertions; | |||
| using Microsoft.AutoGen.AgentChat.Abstractions; | |||
| using Microsoft.AutoGen.AgentChat.Terminations; | |||
| using Microsoft.Extensions.AI; | |||
| using Xunit; | |||
| namespace Microsoft.AutoGen.AgentChat.Tests; | |||
| [Trait("Category", "UnitV2")] | |||
| public static class TerminationExtensions | |||
| { | |||
| public static async Task InvokeExpectingNullAsync<TTermination>(this TTermination termination, IList<AgentMessage> messages, bool reset = true) | |||
| where TTermination : ITerminationCondition | |||
| { | |||
| (await termination.CheckAndUpdateAsync(messages)).Should().BeNull(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| if (reset) | |||
| { | |||
| termination.Reset(); | |||
| } | |||
| } | |||
| private static readonly HashSet<string> AnonymousTerminationConditions = ["CombinerCondition", nameof(ITerminationCondition)]; | |||
| public static async Task InvokeExpectingStopAsync<TTermination>(this TTermination termination, IList<AgentMessage> messages, bool reset = true) | |||
| where TTermination : ITerminationCondition | |||
| { | |||
| StopMessage? stopMessage = await termination.CheckAndUpdateAsync(messages); | |||
| stopMessage.Should().NotBeNull(); | |||
| string name = typeof(TTermination).Name; | |||
| if (!AnonymousTerminationConditions.Contains(name)) | |||
| { | |||
| stopMessage!.Source.Should().Be(typeof(TTermination).Name); | |||
| } | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| if (reset) | |||
| { | |||
| termination.Reset(); | |||
| } | |||
| } | |||
| public static async Task InvokeExpectingFailureAsync<TTermination>(this TTermination termination, IList<AgentMessage> messages, bool reset = true) | |||
| where TTermination : ITerminationCondition | |||
| { | |||
| Func<Task> failureAction = () => termination.CheckAndUpdateAsync(messages).AsTask(); | |||
| await failureAction.Should().ThrowAsync<TerminatedException>(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| if (reset) | |||
| { | |||
| termination.Reset(); | |||
| } | |||
| } | |||
| } | |||
| public class TerminationConditionTests | |||
| { | |||
| [Fact] | |||
| public async Task Test_HandoffTermination() | |||
| { | |||
| HandoffTermination termination = new("target"); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage textMessage = new() { Content = "Hello", Source = "user" }; | |||
| HandoffMessage targetHandoffMessage = new() { Target = "target", Source = "user", Context = "Hello" }; | |||
| HandoffMessage otherHandoffMessage = new() { Target = "another", Source = "user", Context = "Hello" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([textMessage]); | |||
| await termination.InvokeExpectingStopAsync([targetHandoffMessage]); | |||
| await termination.InvokeExpectingNullAsync([otherHandoffMessage]); | |||
| await termination.InvokeExpectingStopAsync([textMessage, targetHandoffMessage], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task StopMessageTermination() | |||
| { | |||
| StopMessageTermination termination = new(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage textMessage = new() { Content = "Hello", Source = "user" }; | |||
| TextMessage otherMessage = new() { Content = "World", Source = "aser" }; | |||
| StopMessage stopMessage = new() { Content = "Stop", Source = "user" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([textMessage]); | |||
| await termination.InvokeExpectingStopAsync([stopMessage]); | |||
| await termination.InvokeExpectingNullAsync([textMessage, otherMessage]); | |||
| await termination.InvokeExpectingStopAsync([textMessage, stopMessage], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task Test_TextMesssageTermination() | |||
| { | |||
| TextMessageTermination termination = new(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage userMessage = new() { Content = "Hello", Source = "user" }; | |||
| TextMessage agentMessage = new() { Content = "World", Source = "agent" }; | |||
| StopMessage stopMessage = new() { Content = "Stop", Source = "user" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingStopAsync([userMessage]); | |||
| await termination.InvokeExpectingStopAsync([agentMessage]); | |||
| await termination.InvokeExpectingNullAsync([stopMessage]); | |||
| termination = new("user"); | |||
| await termination.InvokeExpectingNullAsync([agentMessage]); | |||
| await termination.InvokeExpectingNullAsync([stopMessage]); | |||
| await termination.InvokeExpectingStopAsync([userMessage], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task MaxMessageTermination() | |||
| { | |||
| MaxMessageTermination termination = new(2); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage textMessage = new() { Content = "Hello", Source = "user" }; | |||
| TextMessage otherMessage = new() { Content = "World", Source = "agent" }; | |||
| UserInputRequestedEvent uiRequest = new() { Source = "agent", RequestId = "1" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([textMessage]); | |||
| await termination.InvokeExpectingStopAsync([textMessage, otherMessage]); | |||
| await termination.InvokeExpectingNullAsync([textMessage, uiRequest]); | |||
| termination = new(2, includeAgentEvent: true); | |||
| await termination.InvokeExpectingStopAsync([textMessage, uiRequest], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task Test_TextMentionTermination() | |||
| { | |||
| TextMentionTermination termination = new("stop"); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage textMessage = new() { Content = "Hello", Source = "user" }; | |||
| TextMessage userStopMessage = new() { Content = "stop", Source = "user" }; | |||
| TextMessage agentStopMessage = new() { Content = "stop", Source = "agent" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([textMessage]); | |||
| await termination.InvokeExpectingStopAsync([userStopMessage]); | |||
| termination = new("stop", sources: ["agent"]); | |||
| await termination.InvokeExpectingNullAsync([textMessage]); | |||
| await termination.InvokeExpectingNullAsync([userStopMessage]); | |||
| await termination.InvokeExpectingStopAsync([agentStopMessage], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task Text_TokenUsageTermination() | |||
| { | |||
| TokenUsageTermination termination = new(10); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| RequestUsage usage_10_10 = new() { CompletionTokens = 10, PromptTokens = 10 }; | |||
| RequestUsage usage_01_01 = new() { CompletionTokens = 1, PromptTokens = 1 }; | |||
| RequestUsage usage_05_00 = new() { CompletionTokens = 5, PromptTokens = 0 }; | |||
| RequestUsage usage_00_05 = new() { CompletionTokens = 0, PromptTokens = 5 }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingStopAsync([ | |||
| new TextMessage { Content = "Hello", Source = "user", ModelUsage = usage_10_10 }, | |||
| ]); | |||
| await termination.InvokeExpectingNullAsync([ | |||
| new TextMessage { Content = "Hello", Source = "user", ModelUsage = usage_01_01 }, | |||
| new TextMessage { Content = "World", Source = "agent", ModelUsage = usage_01_01 }, | |||
| ]); | |||
| await termination.InvokeExpectingStopAsync([ | |||
| new TextMessage { Content = "Hello", Source = "user", ModelUsage = usage_05_00 }, | |||
| new TextMessage { Content = "World", Source = "agent", ModelUsage = usage_00_05 }, | |||
| ], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| public class AgentTextEvent : AgentEvent | |||
| { | |||
| public required string Content { get; set; } | |||
| public override Extensions.AI.ChatMessage ToCompletionClientMessage(ChatRole role) | |||
| { | |||
| return new Extensions.AI.ChatMessage(ChatRole.Assistant, this.Content); | |||
| } | |||
| } | |||
| [Fact] | |||
| public async Task Text_Termination_AndCombinator() | |||
| { | |||
| ITerminationCondition lhsClause = new MaxMessageTermination(2); | |||
| ITerminationCondition rhsClause = new TextMentionTermination("stop"); | |||
| ITerminationCondition termination = lhsClause & rhsClause; | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage userMessage = new() { Content = "Hello", Source = "user" }; | |||
| AgentTextEvent agentMessage = new() { Content = "World", Source = "agent" }; | |||
| TextMessage userStopMessage = new() { Content = "stop", Source = "user" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([userMessage]); | |||
| await termination.InvokeExpectingNullAsync([userMessage, agentMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeFalse(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingStopAsync([userStopMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([userMessage, agentMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeFalse(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([userMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([userMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingStopAsync([userStopMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([agentMessage, userStopMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeFalse(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingStopAsync([userMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task Test_Termination_OrCombiner() | |||
| { | |||
| ITerminationCondition lhsClause = new MaxMessageTermination(3); | |||
| ITerminationCondition rhsClause = new TextMentionTermination("stop"); | |||
| ITerminationCondition termination = lhsClause | rhsClause; | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage userMessage = new() { Content = "Hello", Source = "user" }; | |||
| AgentTextEvent agentMessage = new() { Content = "World", Source = "agent" }; | |||
| TextMessage userStopMessage = new() { Content = "stop", Source = "user" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([userMessage]); | |||
| await termination.InvokeExpectingNullAsync([userMessage, agentMessage]); | |||
| await termination.InvokeExpectingNullAsync([userMessage, agentMessage, userMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeFalse(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingStopAsync([userMessage, agentMessage, userStopMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeFalse(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| lhsClause.IsTerminated.Should().BeFalse(); | |||
| rhsClause.IsTerminated.Should().BeTrue(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingStopAsync([userMessage, userMessage, userMessage], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| lhsClause.IsTerminated.Should().BeTrue(); | |||
| rhsClause.IsTerminated.Should().BeFalse(); | |||
| termination.IsTerminated.Should().BeTrue(); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| [Fact] | |||
| public async Task Test_TimeoutTermination() | |||
| { | |||
| TextMessage userMessage = new() { Content = "Hello", Source = "user" }; | |||
| TimeoutTermination termination = new(0.15f); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await Task.Delay(TimeSpan.FromSeconds(0.20f)); | |||
| await termination.InvokeExpectingStopAsync([], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([userMessage]); | |||
| await Task.Delay(TimeSpan.FromSeconds(0.20f)); | |||
| await termination.InvokeExpectingStopAsync([], reset: false); | |||
| } | |||
| [Fact] | |||
| public async Task Test_ExternalTermination() | |||
| { | |||
| ExternalTermination termination = new(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage userMessage = new() { Content = "Hello", Source = "user" }; | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([userMessage]); | |||
| termination.Set(); | |||
| termination.IsTerminated.Should().BeFalse(); // We only terminate on the next check | |||
| await termination.InvokeExpectingStopAsync([], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| await termination.InvokeExpectingNullAsync([userMessage]); | |||
| } | |||
| private ToolCallRequestEvent CreateFunctionRequest(string functionName, string id = "1", string arguments = "") | |||
| { | |||
| ToolCallRequestEvent result = new ToolCallRequestEvent | |||
| { | |||
| Source = "agent" | |||
| }; | |||
| result.Content.Add( | |||
| new FunctionCall | |||
| { | |||
| Id = id, | |||
| Name = functionName, | |||
| Arguments = arguments, | |||
| }); | |||
| return result; | |||
| } | |||
| private ToolCallExecutionEvent CreateFunctionResponse(string functionName, string id = "1", string content = "") | |||
| { | |||
| ToolCallExecutionEvent result = new ToolCallExecutionEvent | |||
| { | |||
| Source = "agent" | |||
| }; | |||
| result.Content.Add( | |||
| new FunctionExecutionResult | |||
| { | |||
| Id = id, | |||
| Name = functionName, | |||
| Content = content, | |||
| }); | |||
| return result; | |||
| } | |||
| [Fact] | |||
| public async Task Test_FunctionCallTermination() | |||
| { | |||
| FunctionCallTermination termination = new("test_function"); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| TextMessage userMessage = new() { Content = "Hello", Source = "user" }; | |||
| ToolCallRequestEvent toolCallRequest = CreateFunctionRequest("test_function"); | |||
| ToolCallExecutionEvent testExecution = CreateFunctionResponse("test_function"); | |||
| ToolCallExecutionEvent otherExecution = CreateFunctionResponse("other_function"); | |||
| await termination.InvokeExpectingNullAsync([]); | |||
| await termination.InvokeExpectingNullAsync([userMessage]); | |||
| await termination.InvokeExpectingNullAsync([toolCallRequest]); | |||
| await termination.InvokeExpectingNullAsync([otherExecution]); | |||
| await termination.InvokeExpectingStopAsync([testExecution], reset: false); | |||
| await termination.InvokeExpectingFailureAsync([], reset: false); | |||
| termination.Reset(); | |||
| termination.IsTerminated.Should().BeFalse(); | |||
| } | |||
| } | |||