diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Controllers/ChatController.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Controllers/ChatController.cs index d163d08c03..f13b4aaff4 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Controllers/ChatController.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Controllers/ChatController.cs @@ -22,6 +22,7 @@ using Microsoft.SemanticKernel.Skills.MsGraph; using Microsoft.SemanticKernel.Skills.MsGraph.Connectors; using Microsoft.SemanticKernel.Skills.MsGraph.Connectors.Client; using Microsoft.SemanticKernel.Skills.OpenAPI.Authentication; +using Microsoft.SemanticKernel.Skills.OpenAPI.Extensions; using SemanticKernel.Service.CopilotChat.Hubs; using SemanticKernel.Service.CopilotChat.Models; using SemanticKernel.Service.CopilotChat.Skills.ChatSkills; @@ -162,7 +163,10 @@ public class ChatController : ControllerBase, IDisposable using HttpClient importHttpClient = new(retryHandler, false); importHttpClient.DefaultRequestHeaders.Add("User-Agent", "Microsoft.CopilotChat"); await planner.Kernel.ImportChatGptPluginSkillFromUrlAsync("KlarnaShoppingSkill", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), - importHttpClient); + new OpenApiSkillExecutionParameters + { + HttpClient = importHttpClient, + }); } // GitHub @@ -173,7 +177,10 @@ public class ChatController : ControllerBase, IDisposable await planner.Kernel.ImportOpenApiSkillFromFileAsync( skillName: "GitHubSkill", filePath: Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CopilotChat", "Skills", "OpenApiSkills/GitHubSkill/openapi.json"), - authCallback: authenticationProvider.AuthenticateRequestAsync); + new OpenApiSkillExecutionParameters + { + AuthCallback = authenticationProvider.AuthenticateRequestAsync, + }); } // Jira @@ -186,8 +193,11 @@ public class ChatController : ControllerBase, IDisposable await planner.Kernel.ImportOpenApiSkillFromFileAsync( skillName: "JiraSkill", filePath: Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CopilotChat", "Skills", "OpenApiSkills/JiraSkill/openapi.json"), - authCallback: authenticationProvider.AuthenticateRequestAsync, - serverUrlOverride: hasServerUrlOverride ? new Uri(serverUrlOverride!) : null); + new OpenApiSkillExecutionParameters + { + AuthCallback = authenticationProvider.AuthenticateRequestAsync, + ServerUrlOverride = hasServerUrlOverride ? new Uri(serverUrlOverride!) : null, + }); } // Microsoft Graph diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ChatSkill.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ChatSkill.cs index 8cede79bb7..2cf2fd0c16 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ChatSkill.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ChatSkill.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text.Json; @@ -93,10 +94,9 @@ public class ChatSkill /// Extract user intent from the conversation history. /// /// The SKContext. - [SKFunction("Extract user intent")] - [SKFunctionName("ExtractUserIntent")] - [SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to extract history from")] - [SKFunctionContextParameter(Name = "audience", Description = "The audience the chat bot is interacting with.")] + [SKFunction, Description("Extract user intent")] + [SKParameter("chatId", "Chat ID to extract history from")] + [SKParameter("audience", "The audience the chat bot is interacting with.")] public async Task ExtractUserIntentAsync(SKContext context) { var tokenLimit = this._promptOptions.CompletionTokenLimit; @@ -140,9 +140,8 @@ public class ChatSkill /// Extract the list of participants from the conversation history. /// Note that only those who have spoken will be included. /// - [SKFunction("Extract audience list")] - [SKFunctionName("ExtractAudience")] - [SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to extract history from")] + [SKFunction, Description("Extract audience list")] + [SKParameter("chatId", "Chat ID to extract history from")] public async Task ExtractAudienceAsync(SKContext context) { var tokenLimit = this._promptOptions.CompletionTokenLimit; @@ -184,15 +183,11 @@ public class ChatSkill /// Extract chat history. /// /// Contains the 'tokenLimit' controlling the length of the prompt. - [SKFunction("Extract chat history")] - [SKFunctionName("ExtractChatHistory")] - [SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to extract history from")] - [SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")] - public async Task ExtractChatHistoryAsync(SKContext context) + [SKFunction, Description("Extract chat history")] + public async Task ExtractChatHistoryAsync( + [Description("Chat ID to extract history from")] string chatId, + [Description("Maximum number of tokens")] int tokenLimit) { - var chatId = context["chatId"]; - var tokenLimit = int.Parse(context["tokenLimit"], new NumberFormatInfo()); - var messages = await this._chatMessageRepository.FindByChatIdAsync(chatId); var sortedMessages = messages.OrderByDescending(m => m.Timestamp); @@ -242,36 +237,19 @@ public class ChatSkill /// messages to memory, and fill in the necessary context variables for completing the /// prompt that will be rendered by the template engine. /// - /// - /// Contains the 'tokenLimit' and the 'contextTokenLimit' controlling the length of the prompt. - [SKFunction("Get chat response")] - [SKFunctionName("Chat")] - [SKFunctionInput(Description = "The new message")] - [SKFunctionContextParameter(Name = "userId", Description = "Unique and persistent identifier for the user")] - [SKFunctionContextParameter(Name = "userName", Description = "Name of the user")] - [SKFunctionContextParameter(Name = "chatId", Description = "Unique and persistent identifier for the chat")] - [SKFunctionContextParameter(Name = "proposedPlan", Description = "Previously proposed plan that is approved")] - [SKFunctionContextParameter(Name = "messageType", Description = "Type of the message")] - [SKFunctionContextParameter(Name = "responseMessageId", Description = "ID of the response message for planner")] - public async Task ChatAsync(string message, SKContext context) + [SKFunction, Description("Get chat response")] + public async Task ChatAsync( + [Description("The new message")] string message, + [Description("Unique and persistent identifier for the user")] string userId, + [Description("Name of the user")] string userName, + [Description("Unique and persistent identifier for the chat")] string chatId, + [Description("Type of the message")] string messageType, + [Description("Previously proposed plan that is approved"), DefaultValue(null), SKName("proposedPlan")] string? planJson, + [Description("ID of the response message for planner"), DefaultValue(null), SKName("responseMessageId")] string? messageId, + SKContext context) { - // TODO: check if user has access to the chat - var userId = context["userId"]; - var userName = context["userName"]; - var chatId = context["chatId"]; - var messageType = context["messageType"]; - // Save this new message to memory such that subsequent chat responses can use it - try - { - await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType); - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - context.Log.LogError("Unable to save new message: {0}", ex.Message); - context.Fail($"Unable to save new message: {ex.Message}", ex); - return context; - } + await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType); // Clone the context to avoid modifying the original context variables. var chatContext = Utilities.CopyContextWithVariablesClone(context); @@ -280,16 +258,15 @@ public class ChatSkill // Check if plan exists in ask's context variables. // If plan was returned at this point, that means it was approved or cancelled. // Update the response previously saved in chat history with state - if (context.Variables.TryGetValue("proposedPlan", out string? planJson) - && !string.IsNullOrWhiteSpace(planJson) - && context.Variables.TryGetValue("responseMessageId", out string? messageId)) + if (!string.IsNullOrWhiteSpace(planJson) && + !string.IsNullOrEmpty(messageId)) { await this.UpdateResponseAsync(planJson, messageId); } var response = chatContext.Variables.ContainsKey("userCancelledPlan") ? "I am sorry the plan did not meet your goals." - : await this.GetChatResponseAsync(chatContext); + : await this.GetChatResponseAsync(chatId, chatContext); if (chatContext.ErrorOccurred) { @@ -299,24 +276,14 @@ public class ChatSkill // Retrieve the prompt used to generate the response // and return it to the caller via the context variables. - var prompt = chatContext.Variables.ContainsKey("prompt") - ? chatContext.Variables["prompt"] - : string.Empty; + chatContext.Variables.TryGetValue("prompt", out string? prompt); + prompt ??= string.Empty; context.Variables.Set("prompt", prompt); // Save this response to memory such that subsequent chat responses can use it - try - { - ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId); - context.Variables.Set("messageId", botMessage.Id); - context.Variables.Set("messageType", ((int)botMessage.Type).ToString(CultureInfo.InvariantCulture)); - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - context.Log.LogError("Unable to save new response: {0}", ex.Message); - context.Fail($"Unable to save new response: {ex.Message}"); - return context; - } + ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId); + context.Variables.Set("messageId", botMessage.Id); + context.Variables.Set("messageType", ((int)botMessage.Type).ToString(CultureInfo.InvariantCulture)); // Extract semantic chat memory await SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync( @@ -336,7 +303,7 @@ public class ChatSkill /// /// The SKContext. /// A response from the model. - private async Task GetChatResponseAsync(SKContext chatContext) + private async Task GetChatResponseAsync(string chatId, SKContext chatContext) { // 0. Get the audience var audience = await this.GetAudienceAsync(chatContext); @@ -371,7 +338,7 @@ public class ChatSkill // 4. Query relevant semantic memories var chatMemoriesTokenLimit = (int)(remainingToken * this._promptOptions.MemoriesResponseContextWeight); - var chatMemories = await this.QueryChatMemoriesAsync(chatContext, userIntent, chatMemoriesTokenLimit); + var chatMemories = await this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, chatId, chatMemoriesTokenLimit, chatContext.Memory); if (chatContext.ErrorOccurred) { return string.Empty; @@ -379,7 +346,7 @@ public class ChatSkill // 5. Query relevant document memories var documentContextTokenLimit = (int)(remainingToken * this._promptOptions.DocumentContextWeight); - var documentMemories = await this.QueryDocumentsAsync(chatContext, userIntent, documentContextTokenLimit); + var documentMemories = await this._documentMemorySkill.QueryDocumentsAsync(userIntent, chatId, documentContextTokenLimit, chatContext.Memory); if (chatContext.ErrorOccurred) { return string.Empty; @@ -391,7 +358,7 @@ public class ChatSkill var chatContextTextTokenCount = remainingToken - Utilities.TokenCount(chatContextText); if (chatContextTextTokenCount > 0) { - var chatHistory = await this.GetChatHistoryAsync(chatContext, chatContextTextTokenCount); + var chatHistory = await this.ExtractChatHistoryAsync(chatId, chatContextTextTokenCount); if (chatContext.ErrorOccurred) { return string.Empty; @@ -490,62 +457,13 @@ public class ChatSkill return userIntent; } - /// - /// Helper function create the correct context variables to - /// extract chat history messages from the conversation history. - /// - private Task GetChatHistoryAsync(SKContext context, int tokenLimit) - { - var contextVariables = new ContextVariables(); - contextVariables.Set("chatId", context["chatId"]); - contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo())); - - var chatHistoryContext = new SKContext( - contextVariables, - context.Memory, - context.Skills, - context.Log, - context.CancellationToken - ); - - var chatHistory = this.ExtractChatHistoryAsync(chatHistoryContext); - - // Propagate the error - if (chatHistoryContext.ErrorOccurred) - { - context.Fail(chatHistoryContext.LastErrorDescription); - } - - return chatHistory; - } - /// /// Helper function create the correct context variables to /// query chat memories from the chat memory store. /// private Task QueryChatMemoriesAsync(SKContext context, string userIntent, int tokenLimit) { - var contextVariables = new ContextVariables(); - contextVariables.Set("chatId", context["chatId"]); - contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo())); - - var chatMemoriesContext = new SKContext( - contextVariables, - context.Memory, - context.Skills, - context.Log, - context.CancellationToken - ); - - var chatMemories = this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, chatMemoriesContext); - - // Propagate the error - if (chatMemoriesContext.ErrorOccurred) - { - context.Fail(chatMemoriesContext.LastErrorDescription); - } - - return chatMemories; + return this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, context["chatId"], tokenLimit, context.Memory); } /// @@ -554,27 +472,7 @@ public class ChatSkill /// private Task QueryDocumentsAsync(SKContext context, string userIntent, int tokenLimit) { - var contextVariables = new ContextVariables(); - contextVariables.Set("chatId", context["chatId"]); - contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo())); - - var documentMemoriesContext = new SKContext( - contextVariables, - context.Memory, - context.Skills, - context.Log, - context.CancellationToken - ); - - var documentMemories = this._documentMemorySkill.QueryDocumentsAsync(userIntent, documentMemoriesContext); - - // Propagate the error - if (documentMemoriesContext.ErrorOccurred) - { - context.Fail(documentMemoriesContext.LastErrorDescription); - } - - return documentMemories; + return this._documentMemorySkill.QueryDocumentsAsync(userIntent, context["chatId"], tokenLimit, context.Memory); } /// diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/DocumentMemorySkill.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/DocumentMemorySkill.cs index 01475eedbc..ac9f879380 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/DocumentMemorySkill.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/DocumentMemorySkill.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Globalization; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; using SemanticKernel.Service.CopilotChat.Options; @@ -43,15 +42,13 @@ public class DocumentMemorySkill /// /// Query to match. /// The SkContext. - [SKFunction("Query documents in the memory given a user message")] - [SKFunctionName("QueryDocuments")] - [SKFunctionInput(Description = "Query to match.")] - [SKFunctionContextParameter(Name = "chatId", Description = "ID of the chat that owns the documents")] - [SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")] - public async Task QueryDocumentsAsync(string query, SKContext context) + [SKFunction, Description("Query documents in the memory given a user message")] + public async Task QueryDocumentsAsync( + [Description("Query to match.")] string query, + [Description("ID of the chat that owns the documents")] string chatId, + [Description("Maximum number of tokens")] int tokenLimit, + ISemanticTextMemory textMemory) { - string chatId = context.Variables["chatId"]; - int tokenLimit = int.Parse(context.Variables["tokenLimit"], new NumberFormatInfo()); var remainingToken = tokenLimit; // Search for relevant document snippets. @@ -64,7 +61,7 @@ public class DocumentMemorySkill List relevantMemories = new(); foreach (var documentCollection in documentCollections) { - var results = context.Memory.SearchAsync( + var results = textMemory.SearchAsync( documentCollection, query, limit: 100, diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs index 37a9e6f3f9..9fea94f622 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text.Json; @@ -64,12 +65,12 @@ public class ExternalInformationSkill /// /// Extract relevant additional knowledge using a planner. /// - [SKFunction("Acquire external information")] - [SKFunctionName("AcquireExternalInformation")] - [SKFunctionInput(Description = "The intent to whether external information is needed")] - [SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")] - [SKFunctionContextParameter(Name = "proposedPlan", Description = "Previously proposed plan that is approved")] - public async Task AcquireExternalInformationAsync(string userIntent, SKContext context) + [SKFunction, Description("Acquire external information")] + [SKParameter("tokenLimit", "Maximum number of tokens")] + [SKParameter("proposedPlan", "Previously proposed plan that is approved")] + public async Task AcquireExternalInformationAsync( + [Description("The intent to whether external information is needed")] string userIntent, + SKContext context) { FunctionsView functions = this._planner.Kernel.Skills.GetFunctionsView(true, true); if (functions.NativeFunctions.IsEmpty && functions.SemanticFunctions.IsEmpty) diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/SemanticChatMemorySkill.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/SemanticChatMemorySkill.cs index 6358a0cb18..b9d7efaeb1 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/SemanticChatMemorySkill.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/SemanticChatMemorySkill.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Globalization; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.SkillDefinition; using SemanticKernel.Service.CopilotChat.Options; @@ -37,22 +36,20 @@ public class SemanticChatMemorySkill /// Query to match. /// The SKContext /// A string containing the relevant memories. - [SKFunction("Query chat memories")] - [SKFunctionName("QueryMemories")] - [SKFunctionInput(Description = "Query to match.")] - [SKFunctionContextParameter(Name = "chatId", Description = "Chat ID to query history from")] - [SKFunctionContextParameter(Name = "tokenLimit", Description = "Maximum number of tokens")] - public async Task QueryMemoriesAsync(string query, SKContext context) + [SKFunction, Description("Query chat memories")] + public async Task QueryMemoriesAsync( + [Description("Query to match.")] string query, + [Description("Chat ID to query history from")] string chatId, + [Description("Maximum number of tokens")] int tokenLimit, + ISemanticTextMemory textMemory) { - var chatId = context["chatId"]; - var tokenLimit = int.Parse(context["tokenLimit"], new NumberFormatInfo()); var remainingToken = tokenLimit; // Search for relevant memories. List relevantMemories = new(); foreach (var memoryName in this._promptOptions.MemoryMap.Keys) { - var results = context.Memory.SearchAsync( + var results = textMemory.SearchAsync( SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName), query, limit: 100, diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj b/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj index dea8e5e98e..95f4dd6f79 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj +++ b/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj @@ -11,13 +11,13 @@ - - - - - - - + + + + + + + diff --git a/samples/apps/copilot-chat-app/webapi/SemanticKernelExtensions.cs b/samples/apps/copilot-chat-app/webapi/SemanticKernelExtensions.cs index 703ba93743..5432bccfde 100644 --- a/samples/apps/copilot-chat-app/webapi/SemanticKernelExtensions.cs +++ b/samples/apps/copilot-chat-app/webapi/SemanticKernelExtensions.cs @@ -12,8 +12,8 @@ using Microsoft.SemanticKernel.AI.Embeddings; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch; using Microsoft.SemanticKernel.Connectors.Memory.Qdrant; -using Microsoft.SemanticKernel.CoreSkills; using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Skills.Core; using Microsoft.SemanticKernel.TemplateEngine; using SemanticKernel.Service.CopilotChat.Extensions; using SemanticKernel.Service.CopilotChat.Storage;