Skip to content

Commit

Permalink
.Net Agents - Update KernelFunction Based Strategies for `AgentGroupC…
Browse files Browse the repository at this point in the history
…hat` (microsoft#8913)

### Motivation and Context
<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Refine handling of history for `KernelFunctionSelectionStrategy` and
`KernelFunctionTerminationStrategy` based on customer input.

Fixes: microsoft#8898
Fixes: microsoft#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
<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

- 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
<!-- Before submitting this PR, please make sure: -->

- [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 😄
  • Loading branch information
crickman authored Sep 20, 2024
1 parent 6bba2b6 commit 05d99d6
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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 =
Expand All @@ -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 =
Expand All @@ -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<string>() ?? 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,
},
}
};
Expand Down
25 changes: 25 additions & 0 deletions dotnet/src/Agents/Core/AgentGroupChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,31 @@ public async IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamingAsync(
this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete);
}

/// <summary>
/// Convenience method to create a <see cref="KernelFunction"/> for a given strategy without HTML encoding the specified parameters.
/// </summary>
/// <param name="template">The prompt template string that defines the prompt.</param>
/// <param name="templateFactory">
/// On optional <see cref="IPromptTemplateFactory"/> to use when interpreting the <paramref name="template"/>.
/// The default factory will be used when none is provided.
/// </param>
/// <param name="safeParameterNames">The parameter names to exclude from being HTML encoded.</param>
/// <returns>A <see cref="KernelFunction"/> created via <see cref="KernelFunctionFactory"/> using the specified template.</returns>
/// <remarks>
/// This is particularly targeted to easily avoid encoding the history used by <see cref="KernelFunctionSelectionStrategy"/>
/// or <see cref="KernelFunctionTerminationStrategy"/>.
/// </remarks>
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);
}

/// <summary>
/// Initializes a new instance of the <see cref="AgentGroupChat"/> class.
/// </summary>
Expand Down
24 changes: 16 additions & 8 deletions dotnet/src/Agents/Core/Chat/KernelFunctionSelectionStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,12 +17,12 @@ namespace Microsoft.SemanticKernel.Agents.Chat;
public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel kernel) : SelectionStrategy
{
/// <summary>
/// The default value for <see cref="KernelFunctionTerminationStrategy.AgentVariableName"/>.
/// The default value for <see cref="KernelFunctionSelectionStrategy.AgentsVariableName"/>.
/// </summary>
public const string DefaultAgentsVariableName = "_agents_";

/// <summary>
/// The default value for <see cref="KernelFunctionTerminationStrategy.HistoryVariableName"/>.
/// The default value for <see cref="KernelFunctionSelectionStrategy.HistoryVariableName"/>.
/// </summary>
public const string DefaultHistoryVariableName = "_history_";

Expand All @@ -42,20 +43,25 @@ public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel ker
/// </summary>
public KernelArguments? Arguments { get; init; }

/// <summary>
/// The <see cref="Microsoft.SemanticKernel.Kernel"/> used when invoking <see cref="KernelFunctionSelectionStrategy.Function"/>.
/// </summary>
public Kernel Kernel => kernel;

/// <summary>
/// The <see cref="KernelFunction"/> invoked as selection criteria.
/// </summary>
public KernelFunction Function { get; } = function;

/// <summary>
/// When set, will use <see cref="SelectionStrategy.InitialAgent"/> in the event of a failure to select an agent.
/// Optionally specify a <see cref="IChatHistoryReducer"/> to reduce the history.
/// </summary>
public bool UseInitialAgentAsFallback { get; init; }
public IChatHistoryReducer? HistoryReducer { get; init; }

/// <summary>
/// The <see cref="Microsoft.SemanticKernel.Kernel"/> used when invoking <see cref="KernelFunctionSelectionStrategy.Function"/>.
/// When set, will use <see cref="SelectionStrategy.InitialAgent"/> in the event of a failure to select an agent.
/// </summary>
public Kernel Kernel => kernel;
public bool UseInitialAgentAsFallback { get; init; }

/// <summary>
/// A callback responsible for translating the <see cref="FunctionResult"/>
Expand All @@ -66,12 +72,14 @@ public class KernelFunctionSelectionStrategy(KernelFunction function, Kernel ker
/// <inheritdoc/>
protected sealed override async Task<Agent> SelectAgentAsync(IReadOnlyList<Agent> agents, IReadOnlyList<ChatMessageContent> 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);
Expand Down
20 changes: 14 additions & 6 deletions dotnet/src/Agents/Core/Chat/KernelFunctionTerminationStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -43,30 +44,37 @@ public class KernelFunctionTerminationStrategy(KernelFunction function, Kernel k
public KernelArguments? Arguments { get; init; }

/// <summary>
/// The <see cref="KernelFunction"/> invoked as termination criteria.
/// The <see cref="Microsoft.SemanticKernel.Kernel"/> used when invoking <see cref="KernelFunctionTerminationStrategy.Function"/>.
/// </summary>
public KernelFunction Function { get; } = function;
public Kernel Kernel => kernel;

/// <summary>
/// The <see cref="Microsoft.SemanticKernel.Kernel"/> used when invoking <see cref="KernelFunctionTerminationStrategy.Function"/>.
/// The <see cref="KernelFunction"/> invoked as termination criteria.
/// </summary>
public Kernel Kernel => kernel;
public KernelFunction Function { get; } = function;

/// <summary>
/// A callback responsible for translating the <see cref="FunctionResult"/>
/// to the termination criteria.
/// </summary>
public Func<FunctionResult, bool> ResultParser { get; init; } = (_) => true;

/// <summary>
/// Optionally specify a <see cref="IChatHistoryReducer"/> to reduce the history.
/// </summary>
public IChatHistoryReducer? HistoryReducer { get; init; }

/// <inheritdoc/>
protected sealed override async Task<bool> ShouldAgentTerminateAsync(Agent agent, IReadOnlyList<ChatMessageContent> 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);
Expand Down
8 changes: 7 additions & 1 deletion dotnet/src/Agents/Core/ChatHistoryKernelAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ public abstract class ChatHistoryKernelAgent : KernelAgent
/// </summary>
public KernelArguments? Arguments { get; init; }

/// <inheritdoc/>
/// <summary>
/// Optionally specify a <see cref="IChatHistoryReducer"/> to reduce the history.
/// </summary>
/// <remarks>
/// This is automatically applied to the history before invoking the agent, only when using
/// an <see cref="AgentChat"/>. It must be explicitly applied via <see cref="ReduceAsync"/>.
/// </remarks>
public IChatHistoryReducer? HistoryReducer { get; init; }

/// <inheritdoc/>
Expand Down
17 changes: 17 additions & 0 deletions dotnet/src/Agents/Core/History/ChatHistoryReducerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,21 @@ public static async Task<bool> ReduceAsync(this ChatHistory history, IChatHistor

return true;
}

/// <summary>
/// Reduce the history using the provided reducer without mutating the source history.
/// </summary>
/// <param name="history">The source history</param>
/// <param name="reducer">The target reducer</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
public static async Task<IReadOnlyList<ChatMessageContent>> ReduceAsync(this IReadOnlyList<ChatMessageContent> history, IChatHistoryReducer? reducer, CancellationToken cancellationToken)
{
if (reducer != null)
{
IEnumerable<ChatMessageContent>? reducedHistory = await reducer.ReduceAsync(history, cancellationToken).ConfigureAwait(false);
history = reducedHistory?.ToArray() ?? history;
}

return history;
}
}
44 changes: 44 additions & 0 deletions dotnet/src/Agents/Core/Internal/ChatMessageForPrompt.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Present a <see cref="ChatMessageForPrompt"/> for serialization without metadata.
/// </summary>
/// <param name="message">The referenced message</param>
internal sealed class ChatMessageForPrompt(ChatMessageContent message)
{
private static readonly JsonSerializerOptions s_jsonOptions = new() { WriteIndented = true };

/// <summary>
/// The string representation of the <see cref="ChatMessageContent.Role"/> property.
/// </summary>
public string Role => message.Role.Label;

/// <summary>
/// The referenced <see cref="ChatMessageContent.AuthorName"/> property.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Name => message.AuthorName;

/// <summary>
/// The referenced <see cref="ChatMessageContent.Content"/> property.
/// </summary>
public string Content => message.Content ?? string.Empty;

/// <summary>
/// Convenience method to reference a set of messages.
/// </summary>
public static IEnumerable<ChatMessageForPrompt> Prepare(IEnumerable<ChatMessageContent> messages) =>
messages.Where(m => !string.IsNullOrWhiteSpace(m.Content)).Select(m => new ChatMessageForPrompt(m));

/// <summary>
/// Convenience method to format a set of messages for use in a prompt.
/// </summary>
public static string Format(IEnumerable<ChatMessageContent> messages) =>
JsonSerializer.Serialize(Prepare(messages).ToArray(), s_jsonOptions);
}

0 comments on commit 05d99d6

Please sign in to comment.