|
| 1 | +// Copyright (c) Microsoft. All rights reserved. |
| 2 | + |
| 3 | +using Microsoft.SemanticKernel; |
| 4 | +using Microsoft.SemanticKernel.ChatCompletion; |
| 5 | +using Microsoft.SemanticKernel.Connectors.OpenAI; |
| 6 | + |
| 7 | +namespace ChatCompletion; |
| 8 | + |
| 9 | +/// <summary> |
| 10 | +/// This example shows how to access <see cref="ChatHistory"/> object in Semantic Kernel functions using |
| 11 | +/// <see cref="Kernel.Data"/> and <see cref="KernelArguments"/>. |
| 12 | +/// This scenario can be useful with auto function calling, |
| 13 | +/// when logic in SK functions depends on results from previous messages in the same chat history. |
| 14 | +/// </summary> |
| 15 | +public sealed class ChatHistoryInFunctions(ITestOutputHelper output) : BaseTest(output) |
| 16 | +{ |
| 17 | + /// <summary> |
| 18 | + /// This method passes an instance of <see cref="ChatHistory"/> to SK function using <see cref="Kernel.Data"/> property. |
| 19 | + /// This approach should be used with caution for cases when Kernel is registered in application as singleton. |
| 20 | + /// For singleton Kernel, check examples <see cref="UsingKernelArgumentsAndFilterOption1Async"/> and <see cref="UsingKernelArgumentsAndFilterOption2Async"/>. |
| 21 | + /// </summary> |
| 22 | + [Fact] |
| 23 | + public async Task UsingKernelDataAsync() |
| 24 | + { |
| 25 | + // Initialize kernel. |
| 26 | + var kernel = GetKernel(); |
| 27 | + |
| 28 | + // Import plugin. |
| 29 | + kernel.ImportPluginFromObject(new DataPlugin(this.Output)); |
| 30 | + |
| 31 | + // Get chat completion service. |
| 32 | + var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>(); |
| 33 | + |
| 34 | + // Initialize chat history with prompt. |
| 35 | + var chatHistory = new ChatHistory(); |
| 36 | + chatHistory.AddUserMessage("I want to get an information about featured products, product reviews and daily summary."); |
| 37 | + |
| 38 | + // Initialize execution settings with enabled auto function calling. |
| 39 | + var executionSettings = new OpenAIPromptExecutionSettings |
| 40 | + { |
| 41 | + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() |
| 42 | + }; |
| 43 | + |
| 44 | + // Set chat history in kernel data to access it in a function. |
| 45 | + kernel.Data[nameof(ChatHistory)] = chatHistory; |
| 46 | + |
| 47 | + // Send a request. |
| 48 | + var result = await chatCompletionService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel); |
| 49 | + |
| 50 | + // Each function will receive a greater number of messages in chat history, because chat history is populated |
| 51 | + // with results of previous functions. |
| 52 | + Console.WriteLine($"Result: {result}"); |
| 53 | + |
| 54 | + // Output: |
| 55 | + // GetFeaturedProducts - Chat History Message Count: 2 |
| 56 | + // GetProductReviews - Chat History Message Count: 3 |
| 57 | + // GetDailySalesSummary - Chat History Message Count: 4 |
| 58 | + // Result: Here's the information you requested... |
| 59 | + } |
| 60 | + |
| 61 | + /// <summary> |
| 62 | + /// This method passes an instance of <see cref="ChatHistory"/> to SK function using |
| 63 | + /// <see cref="KernelArguments"/> and <see cref="IAutoFunctionInvocationFilter"/> filter. |
| 64 | + /// The plugin has access to <see cref="KernelArguments"/>, so it's possible to find a chat history in arguments by property name. |
| 65 | + /// </summary> |
| 66 | + [Fact] |
| 67 | + public async Task UsingKernelArgumentsAndFilterOption1Async() |
| 68 | + { |
| 69 | + // Initialize kernel. |
| 70 | + var kernel = GetKernel(); |
| 71 | + |
| 72 | + // Import plugin. |
| 73 | + kernel.ImportPluginFromObject(new DataPlugin(this.Output)); |
| 74 | + |
| 75 | + // Add filter. |
| 76 | + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter()); |
| 77 | + |
| 78 | + // Initialize execution settings with enabled auto function calling. |
| 79 | + var executionSettings = new OpenAIPromptExecutionSettings |
| 80 | + { |
| 81 | + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() |
| 82 | + }; |
| 83 | + |
| 84 | + // Send a request. |
| 85 | + var result = await kernel.InvokePromptAsync("I want to get an information about featured products, product reviews and daily summary.", new(executionSettings)); |
| 86 | + |
| 87 | + // Each function will receive a greater number of messages in chat history, because chat history is populated |
| 88 | + // with results of previous functions. |
| 89 | + Console.WriteLine($"Result: {result}"); |
| 90 | + |
| 91 | + // Output: |
| 92 | + // GetFeaturedProducts - Chat History Message Count: 2 |
| 93 | + // GetProductReviews - Chat History Message Count: 3 |
| 94 | + // GetDailySalesSummary - Chat History Message Count: 4 |
| 95 | + // Result: Here's the information you requested... |
| 96 | + } |
| 97 | + |
| 98 | + /// <summary> |
| 99 | + /// This method passes an instance of <see cref="ChatHistory"/> to SK function using |
| 100 | + /// <see cref="KernelArguments"/> and <see cref="IAutoFunctionInvocationFilter"/> filter. |
| 101 | + /// The plugin has access to <see cref="ChatHistory"/> directly, since it's automatically injected from <see cref="KernelArguments"/> |
| 102 | + /// into the function by argument name. |
| 103 | + /// </summary> |
| 104 | + [Fact] |
| 105 | + public async Task UsingKernelArgumentsAndFilterOption2Async() |
| 106 | + { |
| 107 | + // Initialize kernel. |
| 108 | + var kernel = GetKernel(); |
| 109 | + |
| 110 | + // Import plugin. |
| 111 | + kernel.ImportPluginFromObject(new EmailPlugin(this.Output)); |
| 112 | + |
| 113 | + // Add filter. |
| 114 | + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter()); |
| 115 | + |
| 116 | + // Initialize execution settings with enabled auto function calling. |
| 117 | + var executionSettings = new OpenAIPromptExecutionSettings |
| 118 | + { |
| 119 | + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() |
| 120 | + }; |
| 121 | + |
| 122 | + // Send a request. |
| 123 | + var result = await kernel.InvokePromptAsync("Send email to [email protected]", new(executionSettings)); |
| 124 | + |
| 125 | + Console.WriteLine($"Result: {result}"); |
| 126 | + |
| 127 | + // Output: |
| 128 | + // SendEmail - Chat History Message Count: 2 |
| 129 | + // Result: Email has been sent to [email protected]. |
| 130 | + } |
| 131 | + |
| 132 | + #region private |
| 133 | + |
| 134 | + /// <summary> |
| 135 | + /// Implementation of <see cref="IAutoFunctionInvocationFilter"/> to set chat history in <see cref="KernelArguments"/> |
| 136 | + /// before invoking a function. |
| 137 | + /// </summary> |
| 138 | + private sealed class AutoFunctionInvocationFilter : IAutoFunctionInvocationFilter |
| 139 | + { |
| 140 | + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next) |
| 141 | + { |
| 142 | + // Set chat history in kernel arguments. |
| 143 | + if (context.Arguments is not null) |
| 144 | + { |
| 145 | + // nameof(ChatHistory) is used for demonstration purposes. |
| 146 | + // Any name can be used here, as long as it is effective for the intended purpose. |
| 147 | + // However, the same name must be used when retrieving chat history from the KernelArguments instance |
| 148 | + // or when the ChatHistory parameter is directly injected into a function. |
| 149 | + context.Arguments[nameof(ChatHistory)] = context.ChatHistory; |
| 150 | + } |
| 151 | + |
| 152 | + // Invoke next filter in pipeline or function. |
| 153 | + await next(context); |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + /// <summary> |
| 158 | + /// Data plugin for demonstration purposes, where methods accept <see cref="Kernel"/> and <see cref="KernelArguments"/> |
| 159 | + /// as parameters. |
| 160 | + /// </summary> |
| 161 | + private sealed class DataPlugin(ITestOutputHelper output) |
| 162 | + { |
| 163 | + [KernelFunction] |
| 164 | + public List<string> GetFeaturedProducts(Kernel kernel, KernelArguments arguments) |
| 165 | + { |
| 166 | + var chatHistory = GetChatHistory(kernel.Data) ?? GetChatHistory(arguments); |
| 167 | + |
| 168 | + if (chatHistory is not null) |
| 169 | + { |
| 170 | + output.WriteLine($"{nameof(GetFeaturedProducts)} - Chat History Message Count: {chatHistory.Count}"); |
| 171 | + } |
| 172 | + |
| 173 | + return ["Laptop", "Smartphone", "Smartwatch"]; |
| 174 | + } |
| 175 | + |
| 176 | + [KernelFunction] |
| 177 | + public Dictionary<string, List<string>> GetProductReviews(Kernel kernel, KernelArguments arguments) |
| 178 | + { |
| 179 | + var chatHistory = GetChatHistory(kernel.Data) ?? GetChatHistory(arguments); |
| 180 | + |
| 181 | + if (chatHistory is not null) |
| 182 | + { |
| 183 | + output.WriteLine($"{nameof(GetProductReviews)} - Chat History Message Count: {chatHistory.Count}"); |
| 184 | + } |
| 185 | + |
| 186 | + return new() |
| 187 | + { |
| 188 | + ["Laptop"] = ["Excellent performance!", "Battery life could be better."], |
| 189 | + ["Smartphone"] = ["Amazing camera!", "Very responsive."], |
| 190 | + ["Smartwatch"] = ["Stylish design", "Could use more apps."], |
| 191 | + }; |
| 192 | + } |
| 193 | + |
| 194 | + [KernelFunction] |
| 195 | + public string GetDailySalesSummary(Kernel kernel, KernelArguments arguments) |
| 196 | + { |
| 197 | + var chatHistory = GetChatHistory(kernel.Data) ?? GetChatHistory(arguments); |
| 198 | + |
| 199 | + if (chatHistory is not null) |
| 200 | + { |
| 201 | + output.WriteLine($"{nameof(GetDailySalesSummary)} - Chat History Message Count: {chatHistory.Count}"); |
| 202 | + } |
| 203 | + |
| 204 | + const int OrdersProcessed = 50; |
| 205 | + const decimal TotalRevenue = 12345.67m; |
| 206 | + |
| 207 | + return $"Today's Sales: {OrdersProcessed} orders processed, total revenue: ${TotalRevenue}."; |
| 208 | + } |
| 209 | + |
| 210 | + private static ChatHistory? GetChatHistory(IDictionary<string, object?> data) |
| 211 | + { |
| 212 | + if (data.TryGetValue(nameof(ChatHistory), out object? chatHistoryObj) && |
| 213 | + chatHistoryObj is ChatHistory chatHistory) |
| 214 | + { |
| 215 | + return chatHistory; |
| 216 | + } |
| 217 | + |
| 218 | + return null; |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + /// <summary> |
| 223 | + /// Email plugin for demonstration purposes, where method accepts <see cref="ChatHistory"/> as parameter. |
| 224 | + /// </summary> |
| 225 | + private sealed class EmailPlugin(ITestOutputHelper output) |
| 226 | + { |
| 227 | + [KernelFunction] |
| 228 | + public string SendEmail(string to, ChatHistory? chatHistory = null) |
| 229 | + { |
| 230 | + if (chatHistory is not null) |
| 231 | + { |
| 232 | + output.WriteLine($"{nameof(SendEmail)} - Chat History Message Count: {chatHistory.Count}"); |
| 233 | + } |
| 234 | + |
| 235 | + // Simulate the email-sending process by notifying the AI model that the email was sent. |
| 236 | + return $"Email has been sent to {to}"; |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + /// <summary> |
| 241 | + /// Helper method to initialize <see cref="Kernel"/>. |
| 242 | + /// </summary> |
| 243 | + private static Kernel GetKernel() |
| 244 | + { |
| 245 | + return Kernel.CreateBuilder() |
| 246 | + .AddOpenAIChatCompletion( |
| 247 | + modelId: "gpt-4o", |
| 248 | + apiKey: TestConfiguration.OpenAI.ApiKey) |
| 249 | + .Build(); |
| 250 | + } |
| 251 | + |
| 252 | + #endregion |
| 253 | +} |
0 commit comments