Skip to content

Commit 6116f7d

Browse files
authored
.NET Agents - Add sample using "Prompt Function" (#12380)
### 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. --> Demonstrate and verify use of "prompt functions" with agents ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Customer raised question on _ _if_ _ supported and how to make work. Realized we didn't have a single sample showing this. ### 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 😄
1 parent aef2284 commit 6116f7d

File tree

4 files changed

+142
-19
lines changed

4 files changed

+142
-19
lines changed

dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step02_AzureAIAgent_Plugins.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,37 @@ public async Task UseAzureAgentWithPluginEnumParameter()
6262
}
6363
}
6464

65+
[Fact]
66+
public async Task UseAzureAgentWithPromptFunction()
67+
{
68+
// Define prompt function
69+
KernelFunction promptFunction =
70+
KernelFunctionFactory.CreateFromPrompt(
71+
promptTemplate:
72+
"""
73+
Count the number of vowels in INPUT and report as a markdown table.
74+
75+
INPUT:
76+
{{$input}}
77+
""",
78+
description: "Counts the number of vowels");
79+
80+
// Define the agent
81+
AzureAIAgent agent =
82+
await CreateAzureAgentAsync(
83+
KernelPluginFactory.CreateFromFunctions("AgentPlugin", [promptFunction]),
84+
instructions: "You job is to only and always analyze the vowels in the user input without confirmation.");
85+
86+
// Add a filter to the agent's kernel to log function invocations.
87+
agent.Kernel.FunctionInvocationFilters.Add(new PromptFunctionFilter());
88+
89+
// Create the chat history thread to capture the agent interaction.
90+
AzureAIAgentThread thread = new(agent.Client);
91+
92+
// Respond to user input, invoking functions where appropriate.
93+
await InvokeAgentAsync(agent, thread, "Who would know naught of art must learn, act, and then take his ease.");
94+
}
95+
6596
private async Task<AzureAIAgent> CreateAzureAgentAsync(KernelPlugin plugin, string? instructions = null, string? name = null)
6697
{
6798
// Define the agent
@@ -71,7 +102,11 @@ private async Task<AzureAIAgent> CreateAzureAgentAsync(KernelPlugin plugin, stri
71102
null,
72103
instructions);
73104

74-
AzureAIAgent agent = new(definition, this.Client);
105+
AzureAIAgent agent =
106+
new(definition, this.Client)
107+
{
108+
Kernel = this.CreateKernelWithChatCompletion(),
109+
};
75110

76111
// Add to the agent's Kernel
77112
if (plugin != null)
@@ -93,4 +128,14 @@ private async Task InvokeAgentAsync(AzureAIAgent agent, AgentThread thread, stri
93128
this.WriteAgentChatMessage(response);
94129
}
95130
}
131+
132+
private sealed class PromptFunctionFilter : IFunctionInvocationFilter
133+
{
134+
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
135+
{
136+
System.Console.WriteLine($"\nINVOKING: {context.Function.Name}");
137+
await next.Invoke(context);
138+
System.Console.WriteLine($"\nRESULT: {context.Result}");
139+
}
140+
}
96141
}

dotnet/samples/GettingStartedWithAgents/Plugins/MenuPlugin.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public MenuItem[] GetMenu()
1515
[KernelFunction, Description("Provides a list of specials from the menu.")]
1616
public MenuItem[] GetSpecials()
1717
{
18-
return s_menuItems.Where(i => i.IsSpecial).ToArray();
18+
return [.. s_menuItems.Where(i => i.IsSpecial)];
1919
}
2020

2121
[KernelFunction, Description("Provides the price of the requested menu item.")]

dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public async Task UseChatCompletionWithPlugin(bool useChatClient)
2525
name: "Host",
2626
useChatClient: useChatClient);
2727

28-
/// Create the chat history thread to capture the agent interaction.
29-
AgentThread thread = new ChatHistoryAgentThread();
28+
// Create the chat history thread to capture the agent interaction.
29+
ChatHistoryAgentThread thread = new();
3030

3131
// Respond to user input, invoking functions where appropriate.
3232
await InvokeAgentAsync(agent, thread, "Hello");
@@ -45,13 +45,46 @@ public async Task UseChatCompletionWithPluginEnumParameter(bool useChatClient)
4545
KernelPluginFactory.CreateFromType<WidgetFactory>(),
4646
useChatClient: useChatClient);
4747

48-
/// Create the chat history thread to capture the agent interaction.
49-
AgentThread thread = new ChatHistoryAgentThread();
48+
// Create the chat history thread to capture the agent interaction.
49+
ChatHistoryAgentThread thread = new();
5050

5151
// Respond to user input, invoking functions where appropriate.
5252
await InvokeAgentAsync(agent, thread, "Create a beautiful red colored widget for me.");
5353
}
5454

55+
[Theory]
56+
[InlineData(true)]
57+
[InlineData(false)]
58+
public async Task UseChatCompletionWithPromptFunction(bool useChatClient)
59+
{
60+
// Define prompt function
61+
KernelFunction promptFunction =
62+
KernelFunctionFactory.CreateFromPrompt(
63+
promptTemplate:
64+
"""
65+
Count the number of vowels in INPUT and report as a markdown table.
66+
67+
INPUT:
68+
{{$input}}
69+
""",
70+
description: "Counts the number of vowels");
71+
72+
// Define the agent
73+
ChatCompletionAgent agent = CreateAgentWithPlugin(
74+
KernelPluginFactory.CreateFromFunctions("AgentPlugin", [promptFunction]),
75+
instructions: "You job is to only and always analyze the vowels in the user input without confirmation.",
76+
useChatClient: useChatClient);
77+
78+
// Add a filter to the agent's kernel to log function invocations.
79+
agent.Kernel.FunctionInvocationFilters.Add(new PromptFunctionFilter());
80+
81+
// Create the chat history thread to capture the agent interaction.
82+
ChatHistoryAgentThread thread = new();
83+
84+
// Respond to user input, invoking functions where appropriate.
85+
await InvokeAgentAsync(agent, thread, "Who would know naught of art must learn, act, and then take his ease.");
86+
}
87+
5588
[Theory]
5689
[InlineData(true)]
5790
[InlineData(false)]
@@ -72,45 +105,90 @@ public async Task UseChatCompletionWithTemplateExecutionSettings(bool useChatCli
72105

73106
agent.Kernel.Plugins.AddFromType<WidgetFactory>();
74107

75-
/// Create the chat history thread to capture the agent interaction.
76-
AgentThread thread = new ChatHistoryAgentThread();
108+
// Create the chat history thread to capture the agent interaction.
109+
ChatHistoryAgentThread thread = new();
77110

78111
// Respond to user input, invoking functions where appropriate.
79112
await InvokeAgentAsync(agent, thread, "Create a beautiful red colored widget for me.");
80113

81114
chatClient?.Dispose();
82115
}
83116

117+
[Theory]
118+
[InlineData(true)]
119+
[InlineData(false)]
120+
public async Task UseChatCompletionWithManualFunctionCalling(bool useChatClient)
121+
{
122+
// Define the agent
123+
ChatCompletionAgent agent = CreateAgentWithPlugin(
124+
KernelPluginFactory.CreateFromType<MenuPlugin>(),
125+
functionChoiceBehavior: FunctionChoiceBehavior.Auto(autoInvoke: false),
126+
useChatClient: useChatClient);
127+
128+
/// Create the chat history thread to capture the agent interaction.
129+
ChatHistoryAgentThread thread = new();
130+
131+
// Respond to user input, invoking functions where appropriate.
132+
await InvokeAgentAsync(agent, thread, "What is the special soup and its price?");
133+
await InvokeAgentAsync(agent, thread, "What is the special drink and its price?");
134+
}
135+
84136
private ChatCompletionAgent CreateAgentWithPlugin(
85137
KernelPlugin plugin,
86138
string? instructions = null,
87139
string? name = null,
140+
FunctionChoiceBehavior? functionChoiceBehavior = null,
88141
bool useChatClient = false)
89142
{
90143
ChatCompletionAgent agent =
91-
new()
92-
{
93-
Instructions = instructions,
94-
Name = name,
95-
Kernel = this.CreateKernelWithChatCompletion(useChatClient, out _),
96-
Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
97-
};
144+
new()
145+
{
146+
Instructions = instructions,
147+
Name = name,
148+
Kernel = this.CreateKernelWithChatCompletion(useChatClient, out _),
149+
Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = functionChoiceBehavior ?? FunctionChoiceBehavior.Auto() }),
150+
};
98151

99152
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
100153
agent.Kernel.Plugins.Add(plugin);
101154

102155
return agent;
103156
}
104157

105-
// Local function to invoke agent and display the conversation messages.
106-
private async Task InvokeAgentAsync(ChatCompletionAgent agent, AgentThread thread, string input)
158+
private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistoryAgentThread thread, string input)
107159
{
108160
ChatMessageContent message = new(AuthorRole.User, input);
109161
this.WriteAgentChatMessage(message);
110162

111163
await foreach (ChatMessageContent response in agent.InvokeAsync(message, thread))
112164
{
113165
this.WriteAgentChatMessage(response);
166+
167+
Task<FunctionResultContent>[] functionResults = await ProcessFunctionCalls(response, agent.Kernel).ToArrayAsync();
168+
thread.ChatHistory.Add(response);
169+
foreach (ChatMessageContent functionResult in functionResults.Select(result => result.Result.ToChatMessage()))
170+
{
171+
this.WriteAgentChatMessage(functionResult);
172+
thread.ChatHistory.Add(functionResult);
173+
}
174+
}
175+
}
176+
177+
private async IAsyncEnumerable<Task<FunctionResultContent>> ProcessFunctionCalls(ChatMessageContent response, Kernel kernel)
178+
{
179+
foreach (FunctionCallContent functionCall in response.Items.OfType<FunctionCallContent>())
180+
{
181+
yield return functionCall.InvokeAsync(kernel);
182+
}
183+
}
184+
185+
private sealed class PromptFunctionFilter : IFunctionInvocationFilter
186+
{
187+
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
188+
{
189+
System.Console.WriteLine($"INVOKING: {context.Function.Name}");
190+
await next.Invoke(context);
191+
System.Console.WriteLine($"RESULT: {context.Result}");
114192
}
115193
}
116194
}

dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ protected IChatClient AddChatClientToKernel(IKernelBuilder builder)
120120
.AsIChatClient();
121121
}
122122

123-
var functionCallingChatClient = chatClient!.AsBuilder().UseKernelFunctionInvocation().Build();
124-
builder.Services.AddTransient<IChatClient>((sp) => functionCallingChatClient);
123+
IChatClient functionCallingChatClient = chatClient.AsBuilder().UseKernelFunctionInvocation().Build();
124+
builder.Services.AddSingleton(functionCallingChatClient);
125125
return functionCallingChatClient;
126126
#pragma warning restore CA2000 // Dispose objects before losing scope
127127
}

0 commit comments

Comments
 (0)