Skip to content

Commit

Permalink
.Net - Agents KernelFunction Strategies (#5895)
Browse files Browse the repository at this point in the history
### 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.
-->

Users of `AgentGroupChat` will likely require strategies that rely on AI
processing. Text processing alone is insufficient, but developers need
other options than calling an AI model. With this update, our strategy
story supports either.

### Description
<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

Introducing support `SelectionStrategy` and `TerminationStrategy` that
utilize LLM processing with support for result processing.

- `KernelFunctionSelectionStrategy`
- `KernelFunctionTerminationStrategy`

### 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 Apr 26, 2024
1 parent 0296329 commit 4af7dfc
Show file tree
Hide file tree
Showing 13 changed files with 603 additions and 10 deletions.
6 changes: 1 addition & 5 deletions dotnet/samples/Concepts/AgentSyntax/AgentSyntax.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<!-- Suppress: "Declare types in namespaces", "Require ConfigureAwait", "Experimental" -->
<NoWarn>CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0010,SKEXP0110</NoWarn>
<NoWarn>IDE0009,VSTHRD111,CS0612,CS1591,CS8618,CA1050,CA1051,CA1707,CA2007,CA5394,RCS1110,SKEXP0001,SKEXP0010,SKEXP0110</NoWarn>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
Expand Down Expand Up @@ -48,8 +48,4 @@
</EmbeddedResource>
<None Remove="Resources\*" />
</ItemGroup>
<ItemGroup>
<Folder Include="MixedAgents\" />
<Folder Include="OpenAIAssistant\" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion dotnet/samples/Concepts/AgentSyntax/BaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public abstract class BaseTest
TestConfiguration.OpenAI.ChatModelId :
TestConfiguration.AzureOpenAI.ChatDeploymentName;

protected Kernel CreateEmptyKernel() => Kernel.CreateBuilder().Build();
protected Kernel CreateEmptyKernel() => new();

protected Kernel CreateKernelWithChatCompletion()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class Step3_Chat(ITestOutputHelper output) : BaseTest(output)
private const string ReviewerInstructions =
"""
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
The goal is to determine is the given copy is acceptable to print.
The goal is to determine if the given copy is acceptable to print.
If so, state that it is approved.
If not, provide insight on how to refine suggested copy without example.
""";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Examples;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;
using Xunit;
using Xunit.Abstractions;

namespace GettingStarted;

/// <summary>
/// Demonstrate usage of <see cref="KernelFunctionTerminationStrategy"/> and <see cref="KernelFunctionSelectionStrategy"/>
/// to manage <see cref="AgentGroupChat"/> execution.
/// </summary>
public class Step4_KernelFunctionStrategies(ITestOutputHelper output) : BaseTest(output)
{
private const string ReviewerName = "ArtDirector";
private const string ReviewerInstructions =
"""
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
The goal is to determine if the given copy is acceptable to print.
If so, state that it is approved.
If not, provide insight on how to refine suggested copy without examples.
""";

private const string CopyWriterName = "Writer";
private const string CopyWriterInstructions =
"""
You are a copywriter with ten years of experience and are known for brevity and a dry humor.
You're laser focused on the goal at hand. Don't waste time with chit chat.
The goal is to refine and decide on the single best copy as an expert in the field.
Consider suggestions when refining an idea.
""";

[Fact]
public async Task RunAsync()
{
// Define the agents
ChatCompletionAgent agentReviewer =
new()
{
Instructions = ReviewerInstructions,
Name = ReviewerName,
Kernel = this.CreateKernelWithChatCompletion(),
};

ChatCompletionAgent agentWriter =
new()
{
Instructions = CopyWriterInstructions,
Name = CopyWriterName,
Kernel = this.CreateKernelWithChatCompletion(),
};

KernelFunction terminationFunction =
KernelFunctionFactory.CreateFromPrompt(
"""
Determine if the copy has been approved. If so, respond with a single word: yes

History:
{{$history}}
""");

KernelFunction selectionFunction =
KernelFunctionFactory.CreateFromPrompt(
"""
You are in a role playing game.
Carefully read the conversation history and carry on the conversation by specifying only the name of player to take the next turn.

The available names are:
{{$agents}}

History:
{{$history}}
""");

// Create a chat for agent interaction.
AgentGroupChat chat =
new(agentWriter, agentReviewer)
{
ExecutionSettings =
new()
{
// Here KernelFunctionTerminationStrategy will terminate
// when the art-director has given their approval.
TerminationStrategy =
new KernelFunctionTerminationStrategy(terminationFunction, CreateKernelWithChatCompletion())
{
// Only the art-director may approve.
Agents = [agentReviewer],
// Customer result parser to determine if the response is "yes"
ResultParser = (result) => result.GetValue<string>()?.Contains("yes", StringComparison.OrdinalIgnoreCase) ?? false,
// The prompt variable name for the history argument.
HistoryVariableName = "history",
// Limit total number of turns
MaximumIterations = 10,
},
// Here a KernelFunctionSelectionStrategy selects agents based on a prompt function.
SelectionStrategy =
new KernelFunctionSelectionStrategy(selectionFunction, CreateKernelWithChatCompletion())
{
// Returns the entire result value as a string.
ResultParser = (result) => result.GetValue<string>() ?? string.Empty,
// The prompt variable name for the agents argument.
AgentsVariableName = "agents",
// The prompt variable name for the history argument.
HistoryVariableName = "history",
},
}
};

// Invoke chat and display messages.
string input = "concept: maps made out of egg cartons.";
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
this.WriteLine($"# {AuthorRole.User}: '{input}'");

await foreach (var content in chat.InvokeAsync())
{
this.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
}

this.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Examples;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;
using Resources;
using Xunit;
using Xunit.Abstractions;

namespace GettingStarted;

/// <summary>
/// Demonstrate parsing JSON response.
/// </summary>
public class Step5_JsonResult(ITestOutputHelper output) : BaseTest(output)
{
private const string TutorName = "Tutor";
private const string TutorInstructions =
"""
Think step-by-step and rate the user input on creativity and expressivness from 1-100.

Respond in JSON format with the following JSON schema:

{
"score": "integer (1-100)",
"notes": "the reason for your score"
}
""";

[Fact]
public async Task RunAsync()
{
// Define the agents
ChatCompletionAgent agent =
new()
{
Instructions = TutorInstructions,
Name = TutorName,
Kernel = this.CreateKernelWithChatCompletion(),
};

// Create a chat for agent interaction.
AgentGroupChat chat =
new()
{
ExecutionSettings =
new()
{
// Here a TerminationStrategy subclass is used that will terminate when
// the response includes a score that is greater than or equal to 70.
TerminationStrategy = new ThresholdTerminationStrategy()
}
};

// Respond to user input
await InvokeAgentAsync("The sunset is very colorful.");
await InvokeAgentAsync("The sunset is setting over the mountains.");
await InvokeAgentAsync("The sunset is setting over the mountains and filled the sky with a deep red flame, setting the clouds ablaze.");

// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));

this.WriteLine($"# {AuthorRole.User}: '{input}'");

await foreach (var content in chat.InvokeAsync(agent))
{
this.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
this.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
}
}
}

private record struct InputScore(int score, string notes);

private sealed class ThresholdTerminationStrategy : TerminationStrategy
{
protected override Task<bool> ShouldAgentTerminateAsync(Agent agent, IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken)
{
string lastMessageContent = history[history.Count - 1].Content ?? string.Empty;

InputScore? result = JsonResultTranslator.Translate<InputScore>(lastMessageContent);

return Task.FromResult((result?.score ?? 0) >= 70);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.SemanticKernel;

namespace Resources;
/// <summary>
/// Supports parsing json from a text block that may contain literals delimiters:
/// <list type="table">
/// <item>
/// <code>
/// [json]
/// </code>
/// </item>
/// <item>
/// <code>
/// ```
/// [json]
/// ```
/// </code>
/// </item>
/// <item>
/// <code>
/// ```json
/// [json]
/// ```
/// </code>
/// </item>
/// </list>
/// </summary>
/// <remarks>
/// Encountering json with this form of delimiters is not uncommon for agent scenarios.
/// </remarks>
public static class JsonResultTranslator
{
private const string LiteralDelimiter = "```";
private const string JsonPrefix = "json";

/// <summary>
/// Utility method for extracting a JSON result from an agent response.
/// </summary>
/// <param name="result">A text result</param>
/// <typeparam name="TResult">The target type of the <see cref="FunctionResult"/>.</typeparam>
/// <returns>The JSON translated to the requested type.</returns>
public static TResult? Translate<TResult>(string result)
{
string rawJson = ExtractJson(result);

return JsonSerializer.Deserialize<TResult>(rawJson);
}

private static string ExtractJson(string result)
{
// Search for initial literal delimiter: ```
int startIndex = result.IndexOf(LiteralDelimiter, System.StringComparison.Ordinal);
if (startIndex < 0)
{
// No initial delimiter, return entire expression.
return result;
}

startIndex += LiteralDelimiter.Length;

// Accommodate "json" prefix, if present.
if (JsonPrefix.Equals(result.Substring(startIndex, JsonPrefix.Length), System.StringComparison.OrdinalIgnoreCase))
{
startIndex += JsonPrefix.Length;
}

// Locate final literal delimiter
int endIndex = result.IndexOf(LiteralDelimiter, startIndex, System.StringComparison.OrdinalIgnoreCase);
if (endIndex < 0)
{
endIndex = result.Length;
}

// Extract JSON
return result.Substring(startIndex, endIndex - startIndex);
}
}
2 changes: 1 addition & 1 deletion dotnet/src/Agents/Abstractions/KernelAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ public abstract class KernelAgent : Agent
/// <remarks>
/// Defaults to empty Kernel, but may be overridden.
/// </remarks>
public Kernel Kernel { get; init; } = Kernel.CreateBuilder().Build();
public Kernel Kernel { get; init; } = new Kernel();
}
1 change: 1 addition & 0 deletions dotnet/src/Agents/Core/Agents.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<ItemGroup>
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Diagnostics/*" Link="%(RecursiveDir)Utilities/%(Filename)%(Extension)" />
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/System/TypeConverterFactory.cs" Link="%(RecursiveDir)Utilities/%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 4af7dfc

Please sign in to comment.