Closes #5801tags/autogenstudio-v0.4.2
| @@ -25,6 +25,11 @@ public abstract class AgentMessage | |||||
| /// </summary> | /// </summary> | ||||
| public required string Source { get; set; } | 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, | // 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. | // 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 | // 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> | /// </summary> | ||||
| public required string Id { get; set; } | public required string Id { get; set; } | ||||
| /// <summary> | |||||
| /// The name of the function that was called. | |||||
| /// </summary> | |||||
| public required string Name { get; set; } | |||||
| /// <summary> | /// <summary> | ||||
| /// The result of calling the function. | /// The result of calling the function. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -3,6 +3,29 @@ | |||||
| namespace Microsoft.AutoGen.AgentChat.Abstractions; | 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> | /// <summary> | ||||
| /// A stateful condition that determines when a conversation should be terminated. | /// 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. | /// 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> | /// </summary> | ||||
| public interface ITerminationCondition | public interface ITerminationCondition | ||||
| { | { | ||||
| @@ -38,23 +62,37 @@ public interface ITerminationCondition | |||||
| public void Reset(); | public void Reset(); | ||||
| /// <summary> | /// <summary> | ||||
| /// Combine this termination condition with another using a logical OR. | |||||
| /// Combine two termination conditions with another using an associative, short-circuiting OR. | |||||
| /// </summary> | /// </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> | /// <summary> | ||||
| /// Combine this termination condition with another using a logical AND. | |||||
| /// Combine two termination conditions with another using an associative, short-circuiting AND. | |||||
| /// </summary> | /// </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; | 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; | namespace Microsoft.AutoGen.AgentChat.Tests; | ||||
| [Trait("Category", "UnitV2")] | |||||
| public class AgentChatSmokeTest | public class AgentChatSmokeTest | ||||
| { | { | ||||
| public class SpeakMessageAgent : ChatAgentBase | public class SpeakMessageAgent : ChatAgentBase | ||||
| @@ -72,6 +72,7 @@ internal sealed class LifecycleObjectFixture : LifecycleObject | |||||
| } | } | ||||
| } | } | ||||
| [Trait("Category", "UnitV2")] | |||||
| public class LifecycleObjectTests | public class LifecycleObjectTests | ||||
| { | { | ||||
| /* | /* | ||||
| @@ -8,6 +8,7 @@ using Xunit; | |||||
| namespace Microsoft.AutoGen.AgentChat.Tests; | namespace Microsoft.AutoGen.AgentChat.Tests; | ||||
| [Trait("Category", "UnitV2")] | |||||
| public class RunContextStackTests | public class RunContextStackTests | ||||
| { | { | ||||
| public static IRunContextLayer CreateLayer(Action<Mock<IRunContextLayer>>? setupAction = null) | 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(); | |||||
| } | |||||
| } | |||||