From 05d99d6e998d358f7b377c83c1b7fa7a2e7aa5e0 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:12:27 -0700 Subject: [PATCH] .Net Agents - Update KernelFunction Based Strategies for `AgentGroupChat` (#8913) ### Motivation and Context Refine handling of history for `KernelFunctionSelectionStrategy` and `KernelFunctionTerminationStrategy` based on customer input. Fixes: https://github.com/microsoft/semantic-kernel/issues/8898 Fixes: https://github.com/microsoft/semantic-kernel/issues/8914 ``` Determine which participant takes the next turn in a conversation based on the the most recent participant. State only the name of the participant to take the next turn. No participant should take more than one turn in a row. Choose only from these participants: - ArtDirector - CopyWriter Always follow these rules when selecting the next participant: - After CopyWriter, it is ArtDirector's turn. - After ArtDirector, it is CopyWriter's turn. History: [ { "Role": "user", "Content": "concept: maps made out of egg cartons." }, { "Role": "Assistant", "Name": "CopyWriter", "Content": "Navigate your world, one carton at a time." } { "Role": "Assistant", "Name": "ArtDirector", "Content": "Approved. The copy effectively conveys the concept with a clever and concise tagline." } ] ``` ### Description - Introduce convenience method (static) to create a `KernelFunction` from prompt-template and specifiy "safe" parameters. - Internally utilize `ChatMessageForPrompt` wrapper for `ChatMessageContent` to eliminate the inclusion of extraneous metadata in the strategy prompt. - Update `KernelFunctionSelectionStrategy` and `KernelFunctionTerminationStrategy` to call `ChatMessageForPrompt.Format` to format history for strategy prompt. - Add optional `HistoryReducer` property to `KernelFunctionSelectionStrategy` and `KernelFunctionTerminationStrategy` to limit how much history is included in the strategy prompt. - Updated sample ### Contribution Checklist - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone :smile: --- .../Step04_KernelFunctionStrategies.cs | 21 ++++++--- dotnet/src/Agents/Core/AgentGroupChat.cs | 25 +++++++++++ .../Chat/KernelFunctionSelectionStrategy.cs | 24 ++++++---- .../Chat/KernelFunctionTerminationStrategy.cs | 20 ++++++--- .../src/Agents/Core/ChatHistoryKernelAgent.cs | 8 +++- .../History/ChatHistoryReducerExtensions.cs | 17 +++++++ .../Core/Internal/ChatMessageForPrompt.cs | 44 +++++++++++++++++++ 7 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 dotnet/src/Agents/Core/Internal/ChatMessageForPrompt.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs index f97c6e733421..f3916ad1e583 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs @@ -2,6 +2,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.Agents.History; using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted; @@ -27,6 +28,7 @@ public class Step04_KernelFunctionStrategies(ITestOutputHelper output) : BaseAge You are a copywriter with ten years of experience and are known for brevity and a dry humor. The goal is to refine and decide on the single best copy as an expert in the field. Only provide a single proposal per response. + Never delimit the response with quotation marks. You're laser focused on the goal at hand. Don't waste time with chit chat. Consider suggestions when refining an idea. @@ -53,16 +55,17 @@ public async Task UseKernelFunctionStrategiesWithAgentGroupChatAsync() }; KernelFunction terminationFunction = - KernelFunctionFactory.CreateFromPrompt( + AgentGroupChat.CreatePromptFunctionForStrategy( """ Determine if the copy has been approved. If so, respond with a single word: yes History: {{$history}} - """); + """, + safeParameterNames: "history"); KernelFunction selectionFunction = - KernelFunctionFactory.CreateFromPrompt( + AgentGroupChat.CreatePromptFunctionForStrategy( $$$""" Determine which participant takes the next turn in a conversation based on the the most recent participant. State only the name of the participant to take the next turn. @@ -78,7 +81,11 @@ No participant should take more than one turn in a row. History: {{$history}} - """); + """, + safeParameterNames: "history"); + + // Limit history used for selection and termination to the most recent message. + ChatHistoryTruncationReducer strategyReducer = new(1); // Create a chat for agent interaction. AgentGroupChat chat = @@ -100,6 +107,8 @@ No participant should take more than one turn in a row. HistoryVariableName = "history", // Limit total number of turns MaximumIterations = 10, + // Save tokens by not including the entire history in the prompt + HistoryReducer = strategyReducer, }, // Here a KernelFunctionSelectionStrategy selects agents based on a prompt function. SelectionStrategy = @@ -109,10 +118,10 @@ No participant should take more than one turn in a row. InitialAgent = agentWriter, // Returns the entire result value as a string. ResultParser = (result) => result.GetValue() ?? CopyWriterName, - // The prompt variable name for the agents argument. - AgentsVariableName = "agents", // The prompt variable name for the history argument. HistoryVariableName = "history", + // Save tokens by not including the entire history in the prompt + HistoryReducer = strategyReducer, }, } }; diff --git a/dotnet/src/Agents/Core/AgentGroupChat.cs b/dotnet/src/Agents/Core/AgentGroupChat.cs index 09c2aa959a81..3b2a2c9ba788 100644 --- a/dotnet/src/Agents/Core/AgentGroupChat.cs +++ b/dotnet/src/Agents/Core/AgentGroupChat.cs @@ -177,6 +177,31 @@ public async IAsyncEnumerable InvokeStreamingAsync( this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); } + /// + /// Convenience method to create a for a given strategy without HTML encoding the specified parameters. + /// + /// The prompt template string that defines the prompt. + /// + /// On optional to use when interpreting the . + /// The default factory will be used when none is provided. + /// + /// The parameter names to exclude from being HTML encoded. + /// A created via using the specified template. + /// + /// This is particularly targeted to easily avoid encoding the history used by + /// or . + /// + public static KernelFunction CreatePromptFunctionForStrategy(string template, IPromptTemplateFactory? templateFactory = null, params string[] safeParameterNames) + { + PromptTemplateConfig config = + new(template) + { + InputVariables = safeParameterNames.Select(parameterName => new InputVariable { Name = parameterName, AllowDangerouslySetContent = true }).ToList() + }; + + return KernelFunctionFactory.CreateFromPrompt(config, promptTemplateFactory: templateFactory); + } + /// /// Initializes a new instance of the class. /// diff --git a/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs b/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs index 00ea8c1e2965..ca73ab5ccc8b 100644 --- a/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs +++ b/dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs @@ -2,9 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.History; +using Microsoft.SemanticKernel.Agents.Internal; namespace Microsoft.SemanticKernel.Agents.Chat; @@ -16,12 +17,12 @@ namespace Microsoft.SemanticKernel.Agents.Chat; public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel kernel) : SelectionStrategy { /// - /// The default value for . + /// The default value for . /// public const string DefaultAgentsVariableName = "_agents_"; /// - /// The default value for . + /// The default value for . /// public const string DefaultHistoryVariableName = "_history_"; @@ -42,20 +43,25 @@ public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel ker /// public KernelArguments? Arguments { get; init; } + /// + /// The used when invoking . + /// + public Kernel Kernel => kernel; + /// /// The invoked as selection criteria. /// public KernelFunction Function { get; } = function; /// - /// When set, will use in the event of a failure to select an agent. + /// Optionally specify a to reduce the history. /// - public bool UseInitialAgentAsFallback { get; init; } + public IChatHistoryReducer? HistoryReducer { get; init; } /// - /// The used when invoking . + /// When set, will use in the event of a failure to select an agent. /// - public Kernel Kernel => kernel; + public bool UseInitialAgentAsFallback { get; init; } /// /// A callback responsible for translating the @@ -66,12 +72,14 @@ public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel ker /// protected sealed override async Task SelectAgentAsync(IReadOnlyList agents, IReadOnlyList history, CancellationToken cancellationToken = default) { + history = await history.ReduceAsync(this.HistoryReducer, cancellationToken).ConfigureAwait(false); + KernelArguments originalArguments = this.Arguments ?? []; KernelArguments arguments = new(originalArguments, originalArguments.ExecutionSettings?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)) { { this.AgentsVariableName, string.Join(",", agents.Select(a => a.Name)) }, - { this.HistoryVariableName, JsonSerializer.Serialize(history) }, // TODO: GitHub Task #5894 + { this.HistoryVariableName, ChatMessageForPrompt.Format(history) }, }; this.Logger.LogKernelFunctionSelectionStrategyInvokingFunction(nameof(NextAsync), this.Function.PluginName, this.Function.Name); diff --git a/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs b/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs index e86cf9b5a09f..622366bc768d 100644 --- a/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs +++ b/dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs @@ -2,9 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.History; +using Microsoft.SemanticKernel.Agents.Internal; namespace Microsoft.SemanticKernel.Agents.Chat; @@ -43,14 +44,14 @@ public class KernelFunctionTerminationStrategy(KernelFunction function, Kernel k public KernelArguments? Arguments { get; init; } /// - /// The invoked as termination criteria. + /// The used when invoking . /// - public KernelFunction Function { get; } = function; + public Kernel Kernel => kernel; /// - /// The used when invoking . + /// The invoked as termination criteria. /// - public Kernel Kernel => kernel; + public KernelFunction Function { get; } = function; /// /// A callback responsible for translating the @@ -58,15 +59,22 @@ public class KernelFunctionTerminationStrategy(KernelFunction function, Kernel k /// public Func ResultParser { get; init; } = (_) => true; + /// + /// Optionally specify a to reduce the history. + /// + public IChatHistoryReducer? HistoryReducer { get; init; } + /// protected sealed override async Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken = default) { + history = await history.ReduceAsync(this.HistoryReducer, cancellationToken).ConfigureAwait(false); + KernelArguments originalArguments = this.Arguments ?? []; KernelArguments arguments = new(originalArguments, originalArguments.ExecutionSettings?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)) { { this.AgentVariableName, agent.Name ?? agent.Id }, - { this.HistoryVariableName, JsonSerializer.Serialize(history) }, // TODO: GitHub Task #5894 + { this.HistoryVariableName, ChatMessageForPrompt.Format(history) }, }; this.Logger.LogKernelFunctionTerminationStrategyInvokingFunction(nameof(ShouldAgentTerminateAsync), this.Function.PluginName, this.Function.Name); diff --git a/dotnet/src/Agents/Core/ChatHistoryKernelAgent.cs b/dotnet/src/Agents/Core/ChatHistoryKernelAgent.cs index aa28682173c7..b7beae216f36 100644 --- a/dotnet/src/Agents/Core/ChatHistoryKernelAgent.cs +++ b/dotnet/src/Agents/Core/ChatHistoryKernelAgent.cs @@ -23,7 +23,13 @@ public abstract class ChatHistoryKernelAgent : KernelAgent /// public KernelArguments? Arguments { get; init; } - /// + /// + /// Optionally specify a to reduce the history. + /// + /// + /// This is automatically applied to the history before invoking the agent, only when using + /// an . It must be explicitly applied via . + /// public IChatHistoryReducer? HistoryReducer { get; init; } /// diff --git a/dotnet/src/Agents/Core/History/ChatHistoryReducerExtensions.cs b/dotnet/src/Agents/Core/History/ChatHistoryReducerExtensions.cs index c884846baafa..f7b243e99013 100644 --- a/dotnet/src/Agents/Core/History/ChatHistoryReducerExtensions.cs +++ b/dotnet/src/Agents/Core/History/ChatHistoryReducerExtensions.cs @@ -163,4 +163,21 @@ public static async Task ReduceAsync(this ChatHistory history, IChatHistor return true; } + + /// + /// Reduce the history using the provided reducer without mutating the source history. + /// + /// The source history + /// The target reducer + /// The to monitor for cancellation requests. The default is . + public static async Task> ReduceAsync(this IReadOnlyList history, IChatHistoryReducer? reducer, CancellationToken cancellationToken) + { + if (reducer != null) + { + IEnumerable? reducedHistory = await reducer.ReduceAsync(history, cancellationToken).ConfigureAwait(false); + history = reducedHistory?.ToArray() ?? history; + } + + return history; + } } diff --git a/dotnet/src/Agents/Core/Internal/ChatMessageForPrompt.cs b/dotnet/src/Agents/Core/Internal/ChatMessageForPrompt.cs new file mode 100644 index 000000000000..2ec91664ce4b --- /dev/null +++ b/dotnet/src/Agents/Core/Internal/ChatMessageForPrompt.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Agents.Internal; + +/// +/// Present a for serialization without metadata. +/// +/// The referenced message +internal sealed class ChatMessageForPrompt(ChatMessageContent message) +{ + private static readonly JsonSerializerOptions s_jsonOptions = new() { WriteIndented = true }; + + /// + /// The string representation of the property. + /// + public string Role => message.Role.Label; + + /// + /// The referenced property. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name => message.AuthorName; + + /// + /// The referenced property. + /// + public string Content => message.Content ?? string.Empty; + + /// + /// Convenience method to reference a set of messages. + /// + public static IEnumerable Prepare(IEnumerable messages) => + messages.Where(m => !string.IsNullOrWhiteSpace(m.Content)).Select(m => new ChatMessageForPrompt(m)); + + /// + /// Convenience method to format a set of messages for use in a prompt. + /// + public static string Format(IEnumerable messages) => + JsonSerializer.Serialize(Prepare(messages).ToArray(), s_jsonOptions); +}