-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Expand file tree
/
Copy pathStep03a_GroupChatWithHumanInTheLoop.cs
More file actions
138 lines (124 loc) · 6.35 KB
/
Step03a_GroupChatWithHumanInTheLoop.cs
File metadata and controls
138 lines (124 loc) · 6.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Orchestration;
using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
using Microsoft.SemanticKernel.ChatCompletion;
namespace GettingStarted.Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/> with human in the loop
/// </summary>
public class Step03a_GroupChatWithHumanInTheLoop(ITestOutputHelper output) : BaseOrchestrationTest(output)
{
[Fact]
public async Task GroupChatWithHumanAsync()
{
// Define the agents
ChatCompletionAgent writer =
this.CreateChatCompletionAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
"""
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.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Consider suggestions when refining an idea.
""");
ChatCompletionAgent editor =
this.CreateChatCompletionAgent(
name: "Reviewer",
description: "An editor.",
instructions:
"""
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: "I Approve".
If not, provide insight on how to refine suggested copy without example.
""");
// Create a monitor to capturing agent responses (via ResponseCallback)
// to display at the end of this sample. (optional)
// NOTE: Create your own callback to capture responses in your application or service.
OrchestrationMonitor monitor = new();
// Define the orchestration
bool didUserRespond = false;
GroupChatOrchestration orchestration =
new(
new HumanInTheLoopChatManager(writer.Name!, editor.Name!)
{
MaximumInvocationCount = 5,
InteractiveCallback = () =>
{
// Simlulate user input that first replies "No" and then "Yes"
ChatMessageContent input = new(AuthorRole.User, didUserRespond ? "Yes" : "More pizzazz");
didUserRespond = true;
Console.WriteLine($"\n# INPUT: {input.Content}\n");
return ValueTask.FromResult(input);
}
},
writer,
editor)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
};
// Start the runtime
InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
string input = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
}
/// <summary>
/// Define a custom group chat manager that enables user input.
/// </summary>
/// <remarks>
/// User input is achieved by overriding the default round robin manager
/// to allow user input after the reviewer agent's message.
/// </remarks>
private sealed class HumanInTheLoopChatManager(string authorName, string criticName) : RoundRobinGroupChatManager
{
public override ValueTask<GroupChatManagerResult<string>> FilterResults(ChatHistory history, CancellationToken cancellationToken = default)
{
ChatMessageContent finalResult = history.Last(message => message.AuthorName == authorName);
return ValueTask.FromResult(new GroupChatManagerResult<string>($"{finalResult}") { Reason = "The approved copy." });
}
/// <inheritdoc/>
public override async ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(ChatHistory history, CancellationToken cancellationToken = default)
{
// Has the maximum invocation count been reached?
GroupChatManagerResult<bool> result = await base.ShouldTerminate(history, cancellationToken);
if (!result.Value)
{
// If not, check if the reviewer has approved the copy.
ChatMessageContent? lastMessage = history.LastOrDefault();
if (lastMessage is not null && lastMessage.Role == AuthorRole.User && $"{lastMessage}".Contains("Yes", StringComparison.OrdinalIgnoreCase))
{
// If the reviewer approves, we terminate the chat.
result = new GroupChatManagerResult<bool>(true) { Reason = "The user is satisfied with the copy." };
}
}
return result;
}
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default)
{
ChatMessageContent? lastMessage = history.LastOrDefault();
if (lastMessage is null)
{
return ValueTask.FromResult(new GroupChatManagerResult<bool>(false) { Reason = "No agents have spoken yet." });
}
if (lastMessage is not null && lastMessage.AuthorName == criticName && $"{lastMessage}".Contains("I Approve", StringComparison.OrdinalIgnoreCase))
{
return ValueTask.FromResult(new GroupChatManagerResult<bool>(true) { Reason = "User input is needed after the reviewer's message." });
}
return ValueTask.FromResult(new GroupChatManagerResult<bool>(false) { Reason = "User input is not needed until the reviewer's message." });
}
}
}