diff --git a/webapi/Controllers/ChatController.cs b/webapi/Controllers/ChatController.cs index ef60a7388..0d7116d2c 100644 --- a/webapi/Controllers/ChatController.cs +++ b/webapi/Controllers/ChatController.cs @@ -51,7 +51,6 @@ public class ChatController : ControllerBase, IDisposable private const string ChatPluginName = nameof(ChatPlugin); private const string ChatFunctionName = "Chat"; - private const string ProcessPlanFunctionName = "ProcessPlan"; private const string GeneratingResponseClientCall = "ReceiveBotResponseStatus"; public ChatController( @@ -74,8 +73,6 @@ public ChatController( /// /// Semantic kernel obtained through dependency injection. /// Message Hub that performs the real time relay service. - /// Planner to use to create function sequences. - /// Converter to use for converting Asks. /// Repository of chat sessions. /// Repository of chat participants. /// Auth info for the current request. @@ -100,78 +97,19 @@ public async Task ChatAsync( { this._logger.LogDebug("Chat message received."); - return await this.HandleRequest(ChatFunctionName, kernel, messageRelayHubContext, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); - } + string chatIdString = chatId.ToString(); - /// - /// Invokes the chat function to process and/or execute plan. - /// - /// Semantic kernel obtained through dependency injection. - /// Message Hub that performs the real time relay service. - /// Planner to use to create function sequences. - /// Converter to use for converting Asks. - /// Repository of chat sessions. - /// Repository of chat participants. - /// Auth info for the current request. - /// Prompt along with its parameters. - /// Chat ID. - /// Results containing the response from the model. - [Route("chats/{chatId:guid}/plan")] - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status504GatewayTimeout)] - public async Task ProcessPlanAsync( - [FromServices] Kernel kernel, - [FromServices] IHubContext messageRelayHubContext, - [FromServices] ChatSessionRepository chatSessionRepository, - [FromServices] ChatParticipantRepository chatParticipantRepository, - [FromServices] IAuthInfo authInfo, - [FromBody] ExecutePlanParameters ask, - [FromRoute] Guid chatId) - { - this._logger.LogDebug("plan request received."); - - return await this.HandleRequest(ProcessPlanFunctionName, kernel, messageRelayHubContext, chatSessionRepository, chatParticipantRepository, authInfo, ask, chatId.ToString()); - } - - /// - /// Invokes given function of ChatPlugin. - /// - /// Name of the ChatPlugin function to invoke. - /// Semantic kernel obtained through dependency injection. - /// Message Hub that performs the real time relay service. - /// Planner to use to create function sequences. - /// Converter to use for converting Asks. - /// Repository of chat sessions. - /// Repository of chat participants. - /// Auth info for the current request. - /// Prompt along with its parameters. - /// - /// Results containing the response from the model. - private async Task HandleRequest( - string functionName, - Kernel kernel, - IHubContext messageRelayHubContext, - ChatSessionRepository chatSessionRepository, - ChatParticipantRepository chatParticipantRepository, - IAuthInfo authInfo, - Ask ask, - string chatId) - { // Put ask's variables in the context we will use. - var contextVariables = GetContextVariables(ask, authInfo, chatId); + var contextVariables = GetContextVariables(ask, authInfo, chatIdString); // Verify that the chat exists and that the user has access to it. ChatSession? chat = null; - if (!(await chatSessionRepository.TryFindByIdAsync(chatId, callback: c => chat = c))) + if (!(await chatSessionRepository.TryFindByIdAsync(chatIdString, callback: c => chat = c))) { return this.NotFound("Failed to find chat session for the chatId specified in variables."); } - if (!(await chatParticipantRepository.IsUserInChatAsync(authInfo.UserId, chatId))) + if (!(await chatParticipantRepository.IsUserInChatAsync(authInfo.UserId, chatIdString))) { return this.Forbid("User does not have access to the chatId specified in variables."); } @@ -184,16 +122,7 @@ private async Task HandleRequest( await this.RegisterHostedFunctionsAsync(kernel, chat!.EnabledPlugins); // Get the function to invoke - KernelFunction? function = null; - try - { - function = kernel.Plugins.GetFunction(ChatPluginName, functionName); - } - catch (KernelException ex) - { - this._logger.LogError("Failed to find {PluginName}/{FunctionName} on server: {Exception}", ChatPluginName, functionName, ex); - return this.NotFound($"Failed to find {ChatPluginName}/{functionName} on server"); - } + KernelFunction? chatFunction = kernel.Plugins.GetFunction(ChatPluginName, ChatFunctionName); // Run the function. FunctionResult? result = null; @@ -204,20 +133,21 @@ private async Task HandleRequest( ? new CancellationTokenSource(TimeSpan.FromSeconds((double)this._serviceOptions.TimeoutLimitInS)) : null; - result = await kernel.InvokeAsync(function!, contextVariables, cts?.Token ?? default); - this._telemetryService.TrackPluginFunction(ChatPluginName, functionName, true); + result = await kernel.InvokeAsync(chatFunction!, contextVariables, cts?.Token ?? default); + this._telemetryService.TrackPluginFunction(ChatPluginName, ChatFunctionName, true); } catch (Exception ex) { if (ex is OperationCanceledException || ex.InnerException is OperationCanceledException) { // Log the timeout and return a 504 response - this._logger.LogError("The {FunctionName} operation timed out.", functionName); - return this.StatusCode(StatusCodes.Status504GatewayTimeout, $"The chat {functionName} timed out."); + this._logger.LogError("The {FunctionName} operation timed out.", ChatFunctionName); + return this.StatusCode(StatusCodes.Status504GatewayTimeout, $"The chat {ChatFunctionName} timed out."); } - this._telemetryService.TrackPluginFunction(ChatPluginName, functionName, false); - throw ex; + this._telemetryService.TrackPluginFunction(ChatPluginName, ChatFunctionName, false); + + throw; } AskResult chatAskResult = new() @@ -227,7 +157,7 @@ private async Task HandleRequest( }; // Broadcast AskResult to all users - await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, null); + await messageRelayHubContext.Clients.Group(chatIdString).SendAsync(GeneratingResponseClientCall, chatIdString, null); return this.Ok(chatAskResult); } @@ -317,7 +247,7 @@ await kernel.ImportPluginFromOpenApiAsync( { if (authHeaders.TryGetValue(plugin.AuthHeaderTag.ToUpperInvariant(), out string? PluginAuthValue)) { - // Register the ChatGPT plugin with the planner's kernel. + // Register the ChatGPT plugin with the kernel. this._logger.LogInformation("Enabling {0} plugin.", plugin.NameForHuman); // TODO: [Issue #44] Support other forms of auth. Currently, we only support user PAT or no auth. @@ -334,7 +264,7 @@ await kernel.ImportPluginFromOpenAIAsync( PluginUtils.GetPluginManifestUri(plugin.ManifestDomain), new OpenAIFunctionExecutionParameters { - HttpClient = this._httpClientFactory.CreateClient("Plugin"), + HttpClient = this._httpClientFactory.CreateClient(), IgnoreNonCompliantErrors = true, AuthCallback = requiresAuth ? authCallback : null }); @@ -383,13 +313,13 @@ Task authCallback(HttpRequestMessage request, string _, OpenAIAuthenticationConf return Task.CompletedTask; } - // Register the ChatGPT plugin with the planner's kernel. + // Register the ChatGPT plugin with the kernel. await kernel.ImportPluginFromOpenAIAsync( PluginUtils.SanitizePluginName(plugin.Name), PluginUtils.GetPluginManifestUri(plugin.ManifestDomain), new OpenAIFunctionExecutionParameters { - HttpClient = this._httpClientFactory.CreateClient("Plugin"), + HttpClient = this._httpClientFactory.CreateClient(), IgnoreNonCompliantErrors = true, AuthCallback = authCallback }); diff --git a/webapi/Controllers/PluginController.cs b/webapi/Controllers/PluginController.cs index f24c77fd7..6a795fc31 100644 --- a/webapi/Controllers/PluginController.cs +++ b/webapi/Controllers/PluginController.cs @@ -56,7 +56,7 @@ public async Task GetPluginManifest([FromQuery] Uri manifestDomai // Need to set the user agent to avoid 403s from some sites. request.Headers.Add("User-Agent", "Semantic-Kernel"); - using HttpClient client = this._httpClientFactory.CreateClient("Plugin"); + using HttpClient client = this._httpClientFactory.CreateClient(); var response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) { diff --git a/webapi/CopilotChatWebApi.csproj b/webapi/CopilotChatWebApi.csproj index 0bccca057..1bda3fda9 100644 --- a/webapi/CopilotChatWebApi.csproj +++ b/webapi/CopilotChatWebApi.csproj @@ -28,7 +28,6 @@ - diff --git a/webapi/Models/Request/ExecutePlanParameters.cs b/webapi/Models/Request/ExecutePlanParameters.cs deleted file mode 100644 index ff0bcb6d7..000000000 --- a/webapi/Models/Request/ExecutePlanParameters.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using CopilotChat.WebApi.Models.Response; - -namespace CopilotChat.WebApi.Models.Request; - -public class ExecutePlanParameters : Ask -{ - public ProposedPlan? Plan { get; set; } -} diff --git a/webapi/Models/Response/ProposedPlan.cs b/webapi/Models/Response/ProposedPlan.cs deleted file mode 100644 index 31f5f7fe9..000000000 --- a/webapi/Models/Response/ProposedPlan.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.Planning.Handlebars; - -namespace CopilotChat.WebApi.Models.Response; - -// Type of Plan -public enum PlanType -{ - Action, // single-step - Sequential, // multi-step - Stepwise, // MRKL style planning - Handlebars -} - -// State of Plan -public enum PlanState -{ - NoOp, // Plan has not received any user input - Approved, - Rejected, - Derived, // Plan has been derived from a previous plan; used when user wants to re-run a plan. -} - -/// -/// Information about a single proposed plan. -/// -public class ProposedPlan -{ - /// - /// Plan object to be approved, rejected, or executed. - /// - [JsonPropertyName("proposedPlan")] - public HandlebarsPlan Plan { get; set; } - - /// - /// Indicates whether plan is Action (single-step) or Sequential (multi-step). - /// - [JsonPropertyName("type")] - public PlanType Type { get; set; } - - /// - /// State of plan - /// - [JsonPropertyName("state")] - public PlanState State { get; set; } - - /// - /// User intent that serves as goal of plan. - /// - [JsonPropertyName("userIntent")] - public string UserIntent { get; set; } - - /// - /// Original user input that prompted this plan. - /// - [JsonPropertyName("originalUserInput")] - public string OriginalUserInput { get; set; } - - /// - /// Id tracking bot message of plan in chat history when it was first generated. - /// - [JsonPropertyName("generatedPlanMessageId")] - public string? GeneratedPlanMessageId { get; set; } = null; - - /// - /// Create a new proposed plan. - /// - /// Proposed plan object - public ProposedPlan(HandlebarsPlan plan, PlanType type, PlanState state, string userIntent, string originalUserInput, string? generatedPlanMessageId = null) - { - this.Plan = plan; - this.Type = type; - this.State = state; - this.UserIntent = userIntent; - this.OriginalUserInput = originalUserInput; - this.GeneratedPlanMessageId = generatedPlanMessageId; - } -} diff --git a/webapi/Models/Storage/CopilotChatMessage.cs b/webapi/Models/Storage/CopilotChatMessage.cs index 0a8ab6f96..7f7154f8f 100644 --- a/webapi/Models/Storage/CopilotChatMessage.cs +++ b/webapi/Models/Storage/CopilotChatMessage.cs @@ -161,7 +161,7 @@ public CopilotChatMessage( /// Total token usage of response completion public static CopilotChatMessage CreateBotResponseMessage(string chatId, string content, string prompt, IEnumerable? citations, IDictionary? tokenUsage = null) { - return new CopilotChatMessage("Bot", "Bot", chatId, content, prompt, citations, AuthorRoles.Bot, IsPlan(content) ? ChatMessageType.Plan : ChatMessageType.Message, tokenUsage); + return new CopilotChatMessage("Bot", "Bot", chatId, content, prompt, citations, AuthorRoles.Bot, ChatMessageType.Message, tokenUsage); } /// @@ -185,28 +185,13 @@ public string ToFormattedString() var messagePrefix = $"[{this.Timestamp.ToString("G", CultureInfo.CurrentCulture)}]"; switch (this.Type) { - case ChatMessageType.Plan: - var planMessageContent = "proposed a plan."; - if (this.Content.Contains("proposedPlan\":", StringComparison.InvariantCultureIgnoreCase)) - { - // Try to extract user intent from the plan proposal. - string pattern = ".*User Intent:User intent: (.*)(?=\"})"; - Match match = Regex.Match(this.Content, pattern); - if (match.Success) - { - string userIntent = match.Groups[1].Value.Trim(); - planMessageContent = $"proposed a plan to fulfill user intent: {userIntent}"; - } - } - - return $"{messagePrefix} {this.UserName} {planMessageContent}"; - case ChatMessageType.Document: var documentMessage = DocumentMessageContent.FromString(this.Content); var documentMessageContent = (documentMessage != null) ? documentMessage.ToFormattedString() : "documents"; return $"{messagePrefix} {this.UserName} uploaded: {documentMessageContent}"; + case ChatMessageType.Plan: // Fall through case ChatMessageType.Message: return $"{messagePrefix} {this.UserName} said: {this.Content}"; @@ -234,16 +219,4 @@ public override string ToString() { return JsonSerializer.Deserialize(json, SerializerSettings); } - - /// - /// Check if the response is a Plan. - /// This is a copy of the `isPlan` function on the frontend. - /// - /// The response from the bot. - /// True if the response represents Plan, false otherwise. - private static bool IsPlan(string response) - { - var planPrefix = "proposedPlan\":"; - return response.Contains(planPrefix, StringComparison.InvariantCulture); - } } diff --git a/webapi/Options/PromptsOptions.cs b/webapi/Options/PromptsOptions.cs index d6be8a336..5dd1edc6a 100644 --- a/webapi/Options/PromptsOptions.cs +++ b/webapi/Options/PromptsOptions.cs @@ -31,12 +31,6 @@ public class PromptsOptions /// internal double MemoriesResponseContextWeight { get; } = 0.6; - /// - /// Weight of information returned from planner (i.e., responses from OpenAPI functions). - /// Contextual prompt excludes all the system commands and user intent. - /// - internal double ExternalInformationContextWeight { get; } = 0.3; - /// /// Upper bound of the relevancy score of a kernel memory to be included in the final prompt. /// The actual relevancy score is determined by the memory balance. @@ -61,21 +55,6 @@ public class PromptsOptions [Required, NotEmptyOrWhitespace] public string SystemDescription { get; set; } = string.Empty; [Required, NotEmptyOrWhitespace] public string SystemResponse { get; set; } = string.Empty; - /// - /// Context bot message for meta prompt when using external information acquired from a plan. - /// - [Required, NotEmptyOrWhitespace] public string ProposedPlanBotMessage { get; set; } = string.Empty; - - /// - /// Supplement to help guide model in using data. - /// - [Required, NotEmptyOrWhitespace] public string PlanResultsDescription { get; set; } = string.Empty; - - /// - /// Supplement to help guide model in using a response from StepwisePlanner. - /// - [Required, NotEmptyOrWhitespace] public string StepwisePlannerSupplement { get; set; } = string.Empty; - internal string[] SystemAudiencePromptComponents => new string[] { this.SystemAudience, diff --git a/webapi/Plugins/Chat/ChatPlugin.cs b/webapi/Plugins/Chat/ChatPlugin.cs index b542eca91..7e33951c1 100644 --- a/webapi/Plugins/Chat/ChatPlugin.cs +++ b/webapi/Plugins/Chat/ChatPlugin.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.Linq; using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using CopilotChat.WebApi.Auth; @@ -241,23 +240,6 @@ private async Task GetAllowedChatHistoryAsync( continue; } - // Plan object is not meaningful content in generating bot response, so shorten to intent only to save on tokens - if (chatMessage.Type == CopilotChatMessage.ChatMessageType.Plan) - { - formattedMessage = "Bot proposed plan"; - - // Try to extract the user intent for more context - string pattern = @"User intent: (.*)(?=\.""})"; - Match match = Regex.Match(chatMessage.Content, pattern); - if (match.Success) - { - string userIntent = match.Groups[1].Value.Trim(); - formattedMessage = $"Bot proposed plan to help fulfill goal: {userIntent}."; - } - - formattedMessage = $"[{chatMessage.Timestamp.ToString("G", CultureInfo.CurrentCulture)}] {formattedMessage}"; - } - var promptRole = chatMessage.AuthorRole == CopilotChatMessage.AuthorRoles.Bot ? AuthorRole.System : AuthorRole.User; var tokenCount = chatHistory is not null ? TokenUtils.GetContextMessageTokenCount(promptRole, formattedMessage) : TokenUtils.TokenCount(formattedMessage); @@ -267,7 +249,7 @@ private async Task GetAllowedChatHistoryAsync( if (chatMessage.AuthorRole == CopilotChatMessage.AuthorRoles.Bot) { // Message doesn't have to be formatted for bot. This helps with asserting a natural language response from the LLM (no date or author preamble). - var botMessage = chatMessage.Type == CopilotChatMessage.ChatMessageType.Plan ? formattedMessage : chatMessage.Content; + var botMessage = chatMessage.Content; allottedChatHistory.AddAssistantMessage(botMessage.Trim()); } else diff --git a/webapi/Plugins/Utils/TokenUtils.cs b/webapi/Plugins/Utils/TokenUtils.cs index 60e7e996f..03d45e20a 100644 --- a/webapi/Plugins/Utils/TokenUtils.cs +++ b/webapi/Plugins/Utils/TokenUtils.cs @@ -24,7 +24,6 @@ public static class TokenUtils /// public static readonly Dictionary semanticFunctions = new() { - // TODO: [Issue #2106] Calculate token usage for planner dependencies. { "SystemAudienceExtraction", "audienceExtraction" }, { "SystemIntentExtraction", "userIntentExtraction" }, { "SystemMetaPrompt", "metaPromptTemplate" }, diff --git a/webapi/Program.cs b/webapi/Program.cs index cebb42543..bdbf10638 100644 --- a/webapi/Program.cs +++ b/webapi/Program.cs @@ -2,7 +2,6 @@ using System; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -69,11 +68,6 @@ public static async Task Main(string[] args) // Add named HTTP clients for IHttpClientFactory builder.Services.AddHttpClient(); - builder.Services.AddHttpClient("Plugin", httpClient => - { - int timeout = int.Parse(builder.Configuration["Planner:PluginTimeoutLimitInS"] ?? "100", CultureInfo.InvariantCulture); - httpClient.Timeout = TimeSpan.FromSeconds(timeout); - }); // Add in the rest of the services. builder.Services diff --git a/webapi/appsettings.json b/webapi/appsettings.json index b3b30d313..f11e34665 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -129,9 +129,6 @@ "SystemDescription": "This is a chat between an intelligent AI bot named Copilot and one or more participants. SK stands for Semantic Kernel, the AI platform used to build the bot. The AI was trained on data through 2021 and is not aware of events that have occurred since then. It also has no ability to access data on the Internet, so it should not claim that it can or say that it will go and look things up. Try to be concise with your answers, though it is not required. Knowledge cutoff: {{$knowledgeCutoff}} / Current date: {{TimePlugin.Now}}.", "SystemResponse": "Either return [silence] or provide a response to the last message. ONLY PROVIDE A RESPONSE IF the last message WAS ADDRESSED TO THE 'BOT' OR 'COPILOT'. If it appears the last message was not for you, send [silence] as the bot response.", "InitialBotMessage": "Hello, thank you for democratizing AI's productivity benefits with open source! How can I help you today?", - "ProposedPlanBotMessage": "As an AI language model, my knowledge is based solely on the data that was used to train me, but I can use the following functions to get fresh information: {{$planFunctions}}. Do you agree to proceed?", - "StepwisePlannerSupplement": "This result was obtained using the Stepwise Planner, which used a series of thoughts and actions to fulfill the user intent. The planner attempted to use the following functions to gather necessary information: {{$planFunctions}}.", - "PlanResultsDescription": "This is the result of invoking the functions listed after \"FUNCTIONS USED:\" to retrieve additional information outside of the data you were trained on. This information was retrieved on {{TimePlugin.Now}}. You can use this data to help answer the user's query.", "KnowledgeCutoffDate": "Saturday, January 1, 2022", "SystemAudience": "Below is a chat history between an intelligent AI bot named Copilot with one or more participants.", "SystemAudienceContinuation": "Using the provided chat history, generate a list of names of the participants of this chat. Do not include 'bot' or 'copilot'.The output should be a single rewritten sentence containing only a comma separated list of names. DO NOT offer additional commentary. DO NOT FABRICATE INFORMATION.\nParticipants:",