From 49b8d3ad82b1d35ab613dc299ea1ed24857a89dc Mon Sep 17 00:00:00 2001 From: Teresa Hoang <125500434+teresaqhoang@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:00:19 -0700 Subject: [PATCH 1/6] Integrating Stepwise planner (#121) ### Motivation and Context This PR adds support for Stepwise Planner to enable MRKL style planning in Chat Copilot. See https://github.com/microsoft/semantic-kernel/pull/1468 for details on how Stepwise Planner works. - Stepwise Planner will only be called if a plugin has been enabled. This follows the same logic as Action/Sequential Planner. - Added `StepwiseStepView` and `StepwiseThoughtProcess` components to render friendly view of Stepwise thought process in prompt dialog. ### Description If Planner returns with thought process: ![image](https://github.com/microsoft/chat-copilot/assets/125500434/ef8b2dee-4bfb-44f8-a363-b8469196ebce) ![image](https://github.com/microsoft/chat-copilot/assets/125500434/88cdc5a9-ba9f-4341-bd13-80d3f1532951) ![image](https://github.com/microsoft/chat-copilot/assets/125500434/72027814-7085-4346-944d-999284e84a12) If Planner returns with suggested response or information ![image](https://github.com/microsoft/chat-copilot/assets/125500434/31a2b011-7590-4d82-86a3-a1e5799e019b) Specific Changes: - CopilotChatPlanner class: - The planner now includes support for the new Stepwise Planner and StepwiseStep model. - The planner now includes bailout functionality, which allows the planner to request additional user input if needed. - ProposedPlan model: - Updated to include a new PlanType called Stepwise. - PlannerOptions: - Updated to include a new StepwisePlannerConfig property. - ChatMemoryController: - Updated to sanitize log input by removing new line characters. - ExternalInformationSkill class: - Updated to use the Stepwise Planner if the PlannerOptions.Type is set to Stepwise. - appsettings.json file: - Updated to include a new configuration section, StepwisePlannerConfig, which includes settings for the Stepwise Planner feature. - TextUtils file: - Added utility function to format text containing `\n` line breaks into paragraphs. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts) raises no violations ~~- [ ] All unit tests pass, and I have added new tests where possible~~ - [x] I didn't break anyone :smile: --- webapi/CopilotChatWebApi.csproj | 1 + webapi/Models/Response/ProposedPlan.cs | 1 + webapi/Options/PlannerOptions.cs | 7 ++ .../Skills/ChatSkills/CopilotChatPlanner.cs | 68 ++++++++++++-- .../ChatSkills/ExternalInformationSkill.cs | 12 ++- webapi/appsettings.json | 15 ++- webapp/src/Constants.ts | 1 + .../chat/prompt-dialog/PromptDialog.tsx | 13 ++- .../stepwise-planner/StepwiseStepView.tsx | 94 +++++++++++++++++++ .../StepwiseThoughtProcess.tsx | 42 +++++++++ .../utils/{TextUtils.ts => TextUtils.tsx} | 28 ++++++ webapp/src/libs/models/BotResponsePrompt.ts | 2 +- webapp/src/libs/models/StepwiseStep.ts | 19 ++++ webapp/src/libs/models/TokenUsage.ts | 4 +- 14 files changed, 286 insertions(+), 21 deletions(-) create mode 100644 webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseStepView.tsx create mode 100644 webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseThoughtProcess.tsx rename webapp/src/components/utils/{TextUtils.ts => TextUtils.tsx} (76%) create mode 100644 webapp/src/libs/models/StepwiseStep.ts diff --git a/webapi/CopilotChatWebApi.csproj b/webapi/CopilotChatWebApi.csproj index bd586a492..0ce24a154 100644 --- a/webapi/CopilotChatWebApi.csproj +++ b/webapi/CopilotChatWebApi.csproj @@ -13,6 +13,7 @@ + diff --git a/webapi/Models/Response/ProposedPlan.cs b/webapi/Models/Response/ProposedPlan.cs index c8e580936..a2070e68b 100644 --- a/webapi/Models/Response/ProposedPlan.cs +++ b/webapi/Models/Response/ProposedPlan.cs @@ -10,6 +10,7 @@ public enum PlanType { Action, // single-step Sequential, // multi-step + Stepwise, // MRKL style planning } // State of Plan diff --git a/webapi/Options/PlannerOptions.cs b/webapi/Options/PlannerOptions.cs index 0e24cdbc6..291533159 100644 --- a/webapi/Options/PlannerOptions.cs +++ b/webapi/Options/PlannerOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel.DataAnnotations; using CopilotChat.WebApi.Models.Response; +using Microsoft.SemanticKernel.Planning.Stepwise; namespace CopilotChat.WebApi.Options; @@ -52,4 +53,10 @@ public class MissingFunctionErrorOptions /// Whether to retry plan creation if LLM returned response that doesn't contain valid plan (e.g., invalid XML or JSON, contains missing function, etc.). /// public bool AllowRetriesOnInvalidPlan { get; set; } = true; + + /// + /// The configuration for the stepwise planner. + /// + [RequiredOnPropertyValue(nameof(Type), PlanType.Stepwise)] + public StepwisePlannerConfig StepwisePlannerConfig { get; set; } = new StepwisePlannerConfig(); } diff --git a/webapi/Skills/ChatSkills/CopilotChatPlanner.cs b/webapi/Skills/ChatSkills/CopilotChatPlanner.cs index 17d8abf00..9f4202b99 100644 --- a/webapi/Skills/ChatSkills/CopilotChatPlanner.cs +++ b/webapi/Skills/ChatSkills/CopilotChatPlanner.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using CopilotChat.WebApi.Options; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Planning; using Microsoft.SemanticKernel.Planning.Sequential; using Microsoft.SemanticKernel.SkillDefinition; @@ -39,7 +41,7 @@ public class CopilotChatPlanner /// Flag to indicate that a variable is unknown and needs to be filled in by the user. /// This is used to flag any inputs that had dependencies from removed steps. /// - private const string UNKNOWN_VARIABLE_FLAG = "$???"; + private const string UnknownVariableFlag = "$???"; /// /// Regex to match variable names from plan parameters. @@ -47,7 +49,13 @@ public class CopilotChatPlanner /// Matches: $variableName, $variable_name, $variable-name, $some_variable_Name, $variableName123, $variableName_123, $variableName-123 /// Does not match: $123variableName, $100 $200 /// - private const string VARIABLE_REGEX = @"\$([A-Za-z]+[_-]*[\w]+)"; + private const string VariableRegex = @"\$([A-Za-z]+[_-]*[\w]+)"; + + /// + /// Supplemental text to add to the plan goal if PlannerOptions.Type is set to Stepwise. + /// Helps the planner know when to bail out to request additional user input. + /// + private const string StepwisePlannerSupplement = "If you need more information to fulfill this request, return with a request for additional user input."; /// /// Initializes a new instance of the class. @@ -74,8 +82,12 @@ public async Task CreatePlanAsync(string goal, ILogger logger) return new Plan(goal); } - Plan plan = this._plannerOptions?.Type == PlanType.Sequential - ? await new SequentialPlanner( + Plan plan; + + switch (this._plannerOptions?.Type) + { + case PlanType.Sequential: + plan = await new SequentialPlanner( this.Kernel, new SequentialPlannerConfig { @@ -83,12 +95,52 @@ public async Task CreatePlanAsync(string goal, ILogger logger) // Allow plan to be created with missing functions AllowMissingFunctions = this._plannerOptions?.MissingFunctionError.AllowRetries ?? false } - ).CreatePlanAsync(goal) - : await new ActionPlanner(this.Kernel).CreatePlanAsync(goal); + ).CreatePlanAsync(goal); + break; + default: + plan = await new ActionPlanner(this.Kernel).CreatePlanAsync(goal); + break; + } return this._plannerOptions!.MissingFunctionError.AllowRetries ? this.SanitizePlan(plan, plannerFunctionsView, logger) : plan; } + /// + /// Run the stepwise planner. + /// + /// The goal containing user intent and ask context. + /// The context to run the plan in. + public async Task RunStepwisePlannerAsync(string goal, SKContext context) + { + var config = new Microsoft.SemanticKernel.Planning.Stepwise.StepwisePlannerConfig() + { + MaxTokens = this._plannerOptions?.StepwisePlannerConfig.MaxTokens ?? 2048, + MaxIterations = this._plannerOptions?.StepwisePlannerConfig.MaxIterations ?? 15, + MinIterationTimeMs = this._plannerOptions?.StepwisePlannerConfig.MinIterationTimeMs ?? 1500 + }; + + Stopwatch sw = new(); + sw.Start(); + + try + { + var plan = new StepwisePlanner( + this.Kernel, + config + ).CreatePlan(string.Join("\n", goal, StepwisePlannerSupplement)); + var result = await plan.InvokeAsync(context); + + sw.Stop(); + result.Variables.Set("timeTaken", sw.Elapsed.ToString()); + return result; + } + catch (Exception e) + { + context.Log.LogError(e, "Error running stepwise planner"); + throw; + } + } + #region Private /// @@ -112,7 +164,7 @@ private Plan SanitizePlan(Plan plan, FunctionsView availableFunctions, ILogger l availableOutputs.AddRange(step.Outputs); // Regex to match variable names - Regex variableRegEx = new(VARIABLE_REGEX, RegexOptions.Singleline); + Regex variableRegEx = new(VariableRegex, RegexOptions.Singleline); // Check for any inputs that may have dependencies from removed steps foreach (var input in step.Parameters) @@ -133,7 +185,7 @@ private Plan SanitizePlan(Plan plan, FunctionsView availableFunctions, ILogger l && inputVariableMatch.Groups[1].Captures.Count == 1 && !unavailableOutputs.Any(output => string.Equals(output, inputVariableValue, StringComparison.OrdinalIgnoreCase)) ? "$PLAN.RESULT" // TODO: [Issue #2256] Extract constants from Plan class, requires change on kernel team - : UNKNOWN_VARIABLE_FLAG; + : UnknownVariableFlag; step.Parameters.Set(input.Key, Regex.Replace(input.Value, variableRegEx.ToString(), overrideValue)); } } diff --git a/webapi/Skills/ChatSkills/ExternalInformationSkill.cs b/webapi/Skills/ChatSkills/ExternalInformationSkill.cs index d08bfd3b0..bfa23b211 100644 --- a/webapi/Skills/ChatSkills/ExternalInformationSkill.cs +++ b/webapi/Skills/ChatSkills/ExternalInformationSkill.cs @@ -85,6 +85,15 @@ public async Task AcquireExternalInformationAsync( return string.Empty; } + var contextString = string.Join("\n", context.Variables.Where(v => v.Key != "userIntent").Select(v => $"{v.Key}: {v.Value}")); + var goal = $"Given the following context, accomplish the user intent.\nContext:\n{contextString}\nUser Intent:{userIntent}"; + if (this._planner.PlannerOptions?.Type == PlanType.Stepwise) + { + var newPlanContext = context.Clone(); + newPlanContext = await this._planner.RunStepwisePlannerAsync(goal, context); + return $"{PromptPreamble}\n{newPlanContext.Variables.Input.Trim()}\n{PromptPostamble}\n"; + } + // Check if plan exists in ask's context variables. var planExists = context.Variables.TryGetValue("proposedPlan", out string? proposedPlanJson); var deserializedPlan = planExists && !string.IsNullOrWhiteSpace(proposedPlanJson) ? JsonSerializer.Deserialize(proposedPlanJson) : null; @@ -127,7 +136,6 @@ public async Task AcquireExternalInformationAsync( else { // Create a plan and set it in context for approval. - var contextString = string.Join("\n", context.Variables.Where(v => v.Key != "userIntent").Select(v => $"{v.Key}: {v.Value}")); Plan? plan = null; // Use default planner options if planner options are null. var plannerOptions = this._planner.PlannerOptions ?? new PlannerOptions(); @@ -139,7 +147,7 @@ public async Task AcquireExternalInformationAsync( { // TODO: [Issue #2256] Remove InvalidPlan retry logic once Core team stabilizes planner try { - plan = await this._planner.CreatePlanAsync($"Given the following context, accomplish the user intent.\nContext:\n{contextString}\nUser Intent:{userIntent}", context.Logger); + plan = await this._planner.CreatePlanAsync(goal, context.Logger); } catch (Exception e) when (this.IsRetriableError(e)) { diff --git a/webapi/appsettings.json b/webapi/appsettings.json index 256434543..d3a27c729 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -49,15 +49,17 @@ // // Planner can determine which skill functions, if any, need to be used to fulfill a user's request. // https://learn.microsoft.com/en-us/semantic-kernel/concepts-sk/planner - // - Set Planner:Type to "Action" to use the single-step ActionPlanner (default) + // - Set Planner:Type to "Action" to use the single-step ActionPlanner // - Set Planner:Type to "Sequential" to enable the multi-step SequentialPlanner // Note: SequentialPlanner works best with `gpt-4`. See the "Enabling Sequential Planner" section in webapi/README.md for configuration instructions. // - Set Planner:RelevancyThreshold to a decimal between 0 and 1.0. // "Planner": { "Type": "Sequential", - // Set RelevancyThreshold to a value >= 0.50 if using the SequentialPlanner with gpt-3.5-turbo. Ignored when Planner:Type is "Action" - "RelevancyThreshold": "0.80", + // The minimum relevancy score for a function to be considered. + // Set RelevancyThreshold to a value between 0 and 1 if using the SequentialPlanner or Stepwise planner with gpt-3.5-turbo. + // Ignored when Planner:Type is "Action" + "RelevancyThreshold": "0.25", // Whether to allow missing functions in the plan on creation then sanitize output. Functions are considered missing if they're not available in the planner's kernel's context. // If set to true, the plan will be created with missing functions as no-op steps that are filtered from the final proposed plan. // If this is set to false, the plan creation will fail if any functions are missing. @@ -66,7 +68,12 @@ "MaxRetriesAllowed": "3" // Max retries allowed on MissingFunctionsError. If set to 0, no retries will be attempted. }, // Whether to retry plan creation if LLM returned response with invalid plan. - "AllowRetriesOnInvalidPlan": "true" + "AllowRetriesOnInvalidPlan": "true", + "StepwisePlannerConfig": { + "MaxTokens": "2048", + "MaxIterations": "15", + "MinIterationTimeMs": "1500" + } }, // // Optional Azure Speech service configuration for providing Azure Speech access tokens. diff --git a/webapp/src/Constants.ts b/webapp/src/Constants.ts index 0e8c70794..136499450 100644 --- a/webapp/src/Constants.ts +++ b/webapp/src/Constants.ts @@ -53,4 +53,5 @@ export const Constants = { MANIFEST_PATH: '/.well-known/ai-plugin.json', }, KEYSTROKE_DEBOUNCE_TIME_MS: 250, + STEPWISE_RESULT_NOT_FOUND_REGEX: /(Result not found, review _stepsTaken to see what happened\.)\s+(\[{.*}])/g, }; diff --git a/webapp/src/components/chat/prompt-dialog/PromptDialog.tsx b/webapp/src/components/chat/prompt-dialog/PromptDialog.tsx index 454c0fe07..50459ca0b 100644 --- a/webapp/src/components/chat/prompt-dialog/PromptDialog.tsx +++ b/webapp/src/components/chat/prompt-dialog/PromptDialog.tsx @@ -19,10 +19,13 @@ import { } from '@fluentui/react-components'; import { Info16Regular } from '@fluentui/react-icons'; import React from 'react'; +import { Constants } from '../../../Constants'; import { BotResponsePrompt, PromptSectionsNameMap } from '../../../libs/models/BotResponsePrompt'; import { IChatMessage } from '../../../libs/models/ChatMessage'; import { useDialogClasses } from '../../../styles'; import { TokenUsageGraph } from '../../token-usage/TokenUsageGraph'; +import { formatParagraphTextContent } from '../../utils/TextUtils'; +import { StepwiseThoughtProcess } from './stepwise-planner/StepwiseThoughtProcess'; const useClasses = makeStyles({ prompt: { @@ -50,18 +53,20 @@ export const PromptDialog: React.FC = ({ message }) => { } catch (e) { prompt = message.prompt ?? ''; } - let promptDetails; if (typeof prompt === 'string') { promptDetails = prompt.split('\n').map((paragraph, idx) =>

{paragraph}

); } else { promptDetails = Object.entries(prompt).map(([key, value]) => { + const isStepwiseThoughtProcess = Constants.STEPWISE_RESULT_NOT_FOUND_REGEX.test(value as string); return value ? (
{PromptSectionsNameMap[key]} - {(value as string).split('\n').map((paragraph, idx) => ( -

{paragraph}

- ))} + {isStepwiseThoughtProcess ? ( + + ) : ( + formatParagraphTextContent(value as string) + )}
) : null; }); diff --git a/webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseStepView.tsx b/webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseStepView.tsx new file mode 100644 index 000000000..160526939 --- /dev/null +++ b/webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseStepView.tsx @@ -0,0 +1,94 @@ +import { + AccordionHeader, + AccordionItem, + AccordionPanel, + Body1, + makeStyles, + shorthands, + tokens, +} from '@fluentui/react-components'; +import { StepwiseStep } from '../../../../libs/models/StepwiseStep'; +import { formatParagraphTextContent } from '../../../utils/TextUtils'; + +const useClasses = makeStyles({ + root: { + display: 'flex', + ...shorthands.gap(tokens.spacingHorizontalM), + }, + accordionItem: { + width: '99%', + }, + header: { + width: '100%', + /* Styles for the button within the header */ + '& button': { + alignItems: 'flex-start', + minHeight: '-webkit-fill-available', + paddingLeft: tokens.spacingHorizontalNone, + }, + }, +}); + +interface IStepwiseStepViewProps { + step: StepwiseStep; + index: number; +} + +export const StepwiseStepView: React.FC = ({ step, index }) => { + const classes = useClasses(); + + let header = `[OBSERVATION] ${step.observation}`; + let details: string | undefined; + + if (step.thought) { + const thoughtRegEx = /\[(THOUGHT|QUESTION|ACTION)](\s*(.*))*/g; + let thought = step.thought.match(thoughtRegEx)?.[0] ?? `[THOUGHT] ${step.thought}`; + + // Only show the first sentence of the thought in the header. + // Show the rest as details. + const firstSentenceIndex = thought.indexOf('. '); + if (firstSentenceIndex > 0) { + details = thought.substring(firstSentenceIndex + 2); + thought = thought.substring(0, firstSentenceIndex + 1); + } + + header = thought; + } + + if (step.action) { + header = `[ACTION] ${step.action}`; + + // Format the action variables and observation. + const variables = step.action_variables + ? 'Action variables: \n' + + Object.entries(step.action_variables) + .map(([key, value]) => `\r${key}: ${value}`) + .join('\n') + : ''; + + // Remove the [ACTION] tag from the thought and remove any code block formatting. + details = step.thought.replace('[ACTION]', '').replaceAll('```', '') + '\n'; + + // Parse any unicode quotation characters in the observation. + const observation = step.observation?.replaceAll(/\\{0,2}u0022/g, '"'); + details = details.concat(variables, `\nObservation: \n\r${observation}`); + } + + return ( +
+ {index + 1}. + + {details ? ( + <> + + {header} + + {formatParagraphTextContent(details)} + + ) : ( + {header} + )} + +
+ ); +}; diff --git a/webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseThoughtProcess.tsx b/webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseThoughtProcess.tsx new file mode 100644 index 000000000..709f20d64 --- /dev/null +++ b/webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseThoughtProcess.tsx @@ -0,0 +1,42 @@ +import { Accordion, Body1, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components'; +import { Constants } from '../../../../Constants'; +import { StepwiseStep } from '../../../../libs/models/StepwiseStep'; +import { StepwiseStepView } from './StepwiseStepView'; + +const useClasses = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap(tokens.spacingHorizontalSNudge), + }, + header: { + paddingTop: tokens.spacingVerticalS, + }, +}); + +interface IStepwiseThoughtProcessProps { + stepwiseResult: string; +} + +export const StepwiseThoughtProcess: React.FC = ({ stepwiseResult }) => { + const classes = useClasses(); + + const matches = stepwiseResult.matchAll(Constants.STEPWISE_RESULT_NOT_FOUND_REGEX); + const matchGroups = Array.from(matches); + if (matchGroups.length > 0) { + const steps = JSON.parse(matchGroups[0][2]) as StepwiseStep[]; + return ( +
+ {matchGroups[0][1]} + [THOUGHT PROCESS] + + {steps.map((step, index) => { + return ; + })} + +
+ ); + } + + return; +}; diff --git a/webapp/src/components/utils/TextUtils.ts b/webapp/src/components/utils/TextUtils.tsx similarity index 76% rename from webapp/src/components/utils/TextUtils.ts rename to webapp/src/components/utils/TextUtils.tsx index 7a588a754..ec986de25 100644 --- a/webapp/src/components/utils/TextUtils.ts +++ b/webapp/src/components/utils/TextUtils.tsx @@ -1,3 +1,5 @@ +import { Body1, tokens } from '@fluentui/react-components'; + /* * Function to detect and convert URLs within a string into clickable links. * It wraps each link matched with anchor tags and applies safe href attributes. @@ -66,3 +68,29 @@ export function formatChatTextContent(messageContent: string) { .replace(/^!sk:.*$/gm, (match: string) => createCommandLink(match)); return contentAsString; } + +/* + * Formats text containing `\n` or `\r` into paragraphs. + */ +export function formatParagraphTextContent(messageContent: string) { + messageContent = messageContent.replaceAll('\r\n', '\n\r'); + return ( + + {messageContent.split('\n').map((paragraph, idx) => ( +

+ {paragraph} +

+ ))} +
+ ); +} diff --git a/webapp/src/libs/models/BotResponsePrompt.ts b/webapp/src/libs/models/BotResponsePrompt.ts index 4714685d4..b266b61e8 100644 --- a/webapp/src/libs/models/BotResponsePrompt.ts +++ b/webapp/src/libs/models/BotResponsePrompt.ts @@ -27,7 +27,7 @@ export const PromptSectionsNameMap: Record = { audience: 'Audience', userIntent: 'User Intent', chatMemories: 'Chat Memories', - externalInformation: 'External Information', + externalInformation: 'Planner Results', chatHistory: 'Chat History', systemChatContinuation: 'System Chat Continuation', }; diff --git a/webapp/src/libs/models/StepwiseStep.ts b/webapp/src/libs/models/StepwiseStep.ts new file mode 100644 index 000000000..08a7c9410 --- /dev/null +++ b/webapp/src/libs/models/StepwiseStep.ts @@ -0,0 +1,19 @@ +export interface StepwiseStep { + // The step number + thought: string; + + // The action of the step + action?: string; + + // The variables for the action + action_variables?: Record; + + // The output of the action + observation?: string; + + // The output of the system + final_answer?: string; + + // The raw response from the action + original_response: string; +} diff --git a/webapp/src/libs/models/TokenUsage.ts b/webapp/src/libs/models/TokenUsage.ts index 01e5c33d1..5d2c1d90d 100644 --- a/webapp/src/libs/models/TokenUsage.ts +++ b/webapp/src/libs/models/TokenUsage.ts @@ -20,6 +20,6 @@ export const TokenUsageFunctionNameMap: Record = { userIntentExtraction: 'User Intent Extraction', metaPromptTemplate: 'Meta Prompt Template', responseCompletion: 'Response Completion', - workingMemoryExtraction: 'Working Memory Extraction', - longTermMemoryExtraction: 'Long Term Memory Extraction', + workingMemoryExtraction: 'Working Memory Generation', + longTermMemoryExtraction: 'Long Term Memory Generation', }; From 298985d74bf20a3bba03e19dbd27ea4c5f58c8d4 Mon Sep 17 00:00:00 2001 From: Desmond Howard Date: Tue, 8 Aug 2023 19:07:55 -0400 Subject: [PATCH 2/6] Update permissions on deploy scripts + only run Github build workflows on relevant paths (#135) ### Motivation and Context updates deploy scripts to have execute permissions, so that they can run correctly in Github, and changes build workflows to run only when the relevant paths are updated. ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .github/workflows/copilot-build-backend.yml | 4 ++-- .github/workflows/copilot-build-frontend.yml | 2 ++ scripts/deploy/deploy-azure.sh | 0 scripts/deploy/deploy-webapp.sh | 0 scripts/deploy/package-webapi.ps1 | 0 5 files changed, 4 insertions(+), 2 deletions(-) mode change 100644 => 100755 scripts/deploy/deploy-azure.sh mode change 100644 => 100755 scripts/deploy/deploy-webapp.sh mode change 100644 => 100755 scripts/deploy/package-webapi.ps1 diff --git a/.github/workflows/copilot-build-backend.yml b/.github/workflows/copilot-build-backend.yml index 0eed8fb3f..e82da790e 100644 --- a/.github/workflows/copilot-build-backend.yml +++ b/.github/workflows/copilot-build-backend.yml @@ -3,8 +3,8 @@ name: copilot-build-backend on: pull_request: branches: ["main"] - merge_group: - branches: ["main"] + paths: + - "webapi/**" workflow_call: outputs: artifact: diff --git a/.github/workflows/copilot-build-frontend.yml b/.github/workflows/copilot-build-frontend.yml index c74e7e4cf..7c8fb46ce 100644 --- a/.github/workflows/copilot-build-frontend.yml +++ b/.github/workflows/copilot-build-frontend.yml @@ -4,6 +4,8 @@ on: workflow_dispatch: pull_request: branches: ["main"] + paths: + - "webapp/**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/scripts/deploy/deploy-azure.sh b/scripts/deploy/deploy-azure.sh old mode 100644 new mode 100755 diff --git a/scripts/deploy/deploy-webapp.sh b/scripts/deploy/deploy-webapp.sh old mode 100644 new mode 100755 diff --git a/scripts/deploy/package-webapi.ps1 b/scripts/deploy/package-webapi.ps1 old mode 100644 new mode 100755 From b560cd871a99423f3f75d06c98728a7a454bd016 Mon Sep 17 00:00:00 2001 From: Desmond Howard Date: Tue, 8 Aug 2023 22:52:32 -0400 Subject: [PATCH 3/6] Fix relative paths in deploy scripts (#136) ### Motivation and Context fixing paths now that the scripts have been moved to `deploy/`. ### Description ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- scripts/deploy/deploy-webapp.ps1 | 10 +++++----- scripts/deploy/deploy-webapp.sh | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/deploy/deploy-webapp.ps1 b/scripts/deploy/deploy-webapp.ps1 index dd05a520b..88b3d1c99 100644 --- a/scripts/deploy/deploy-webapp.ps1 +++ b/scripts/deploy/deploy-webapp.ps1 @@ -58,7 +58,7 @@ Write-Host "webapiUrl: $webapiUrl" # Set ASCII as default encoding for Out-File $PSDefaultParameterValues['Out-File:Encoding'] = 'ascii' -$envFilePath = "$PSScriptRoot/../webapp/.env" +$envFilePath = "$PSScriptRoot/../../webapp/.env" Write-Host "Writing environment variables to '$envFilePath'..." "REACT_APP_BACKEND_URI=https://$webapiUrl/" | Out-File -FilePath $envFilePath "REACT_APP_AAD_AUTHORITY=$Authority" | Out-File -FilePath $envFilePath -Append @@ -66,16 +66,16 @@ Write-Host "Writing environment variables to '$envFilePath'..." "REACT_APP_SK_API_KEY=$webapiApiKey" | Out-File -FilePath $envFilePath -Append Write-Host "Generating SWA config..." -$swaConfig = $(Get-Content "$PSScriptRoot/../webapp/template.swa-cli.config.json" -Raw) +$swaConfig = $(Get-Content "$PSScriptRoot/../../webapp/template.swa-cli.config.json" -Raw) $swaConfig = $swaConfig.Replace("{{appDevserverUrl}}", "https://$webappUrl") $swaConfig = $swaConfig.Replace("{{appName}}", "$webappName") $swaConfig = $swaConfig.Replace("{{resourceGroup}}", "$ResourceGroupName") $swaConfig = $swaConfig.Replace("{{subscription-id}}", "$Subscription") -$swaConfig | Out-File -FilePath "$PSScriptRoot/../webapp/swa-cli.config.json" -Write-Host $(Get-Content "$PSScriptRoot/../webapp/swa-cli.config.json" -Raw) +$swaConfig | Out-File -FilePath "$PSScriptRoot/../../webapp/swa-cli.config.json" +Write-Host $(Get-Content "$PSScriptRoot/../../webapp/swa-cli.config.json" -Raw) -Push-Location -Path "$PSScriptRoot/../webapp" +Push-Location -Path "$PSScriptRoot/../../webapp" Write-Host "Installing yarn dependencies..." yarn install if ($LASTEXITCODE -ne 0) { diff --git a/scripts/deploy/deploy-webapp.sh b/scripts/deploy/deploy-webapp.sh index 77200321c..66ef188cc 100755 --- a/scripts/deploy/deploy-webapp.sh +++ b/scripts/deploy/deploy-webapp.sh @@ -91,7 +91,7 @@ echo "WEB_API_NAME: $WEB_API_NAME" echo "Getting webapi key..." eval WEB_API_KEY=$(az webapp config appsettings list --name $WEB_API_NAME --resource-group $RESOURCE_GROUP | jq '.[] | select(.name=="Authorization:ApiKey").value') -ENV_FILE_PATH="$SCRIPT_ROOT/../webapp/.env" +ENV_FILE_PATH="$SCRIPT_ROOT/../../webapp/.env" echo "Writing environment variables to '$ENV_FILE_PATH'..." echo "REACT_APP_BACKEND_URI=https://$WEB_API_URL/" > $ENV_FILE_PATH echo "REACT_APP_AAD_AUTHORITY=$AUTHORITY" >> $ENV_FILE_PATH @@ -99,8 +99,8 @@ echo "REACT_APP_AAD_CLIENT_ID=$APPLICATION_ID" >> $ENV_FILE_PATH echo "REACT_APP_SK_API_KEY=$WEB_API_KEY" >> $ENV_FILE_PATH echo "Writing swa-cli.config.json..." -SWA_CONFIG_FILE_PATH="$SCRIPT_ROOT/../webapp/swa-cli.config.json" -SWA_CONFIG_TEMPLATE_FILE_PATH="$SCRIPT_ROOT/../webapp/template.swa-cli.config.json" +SWA_CONFIG_FILE_PATH="$SCRIPT_ROOT/../../webapp/swa-cli.config.json" +SWA_CONFIG_TEMPLATE_FILE_PATH="$SCRIPT_ROOT/../../webapp/template.swa-cli.config.json" swaConfig=`cat $SWA_CONFIG_TEMPLATE_FILE_PATH` swaConfig=$(echo $swaConfig | sed "s/{{appDevserverUrl}}/https:\/\/${WEB_APP_URL}/") swaConfig=$(echo $swaConfig | sed "s/{{appName}}/$WEB_API_NAME/") @@ -108,7 +108,7 @@ swaConfig=$(echo $swaConfig | sed "s/{{resourceGroup}}/$RESOURCE_GROUP/") swaConfig=$(echo $swaConfig | sed "s/{{subscription-id}}/$SUBSCRIPTION/") echo $swaConfig > $SWA_CONFIG_FILE_PATH -pushd "$SCRIPT_ROOT/../webapp" +pushd "$SCRIPT_ROOT/../../webapp" echo "Installing yarn dependencies..." yarn install From 0bcc2769facf5e6e24ff554d4650f66f45f5f55d Mon Sep 17 00:00:00 2001 From: Teresa Hoang <125500434+teresaqhoang@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:09:17 -0700 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20connection=20error=20h?= =?UTF-8?q?andling=20+=20uncaught=20exceptions=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation and Context Fix connection errors in useChat and appSlice + fix red screens of death ### Description - Added an ID to Alerts in AppState, add logic to update connection status in appSlice, and update SignalRMiddleware to add an ID to connection alerts. - Added more error handling to reduce red screens of death - Move all getAccessToken calls into try catch blocks - Added some missing details around Stepwise Planner ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts) raises no violations ~~- [ ] All unit tests pass, and I have added new tests where possible~~ - [x] I didn't break anyone :smile: --- webapi/Options/PlannerOptions.cs | 2 +- webapi/appsettings.json | 1 + webapp/src/Constants.ts | 1 + webapp/src/checkEnv.ts | 5 +- .../token-usage/TokenUsageGraph.tsx | 3 +- webapp/src/components/views/BackendProbe.tsx | 4 +- webapp/src/libs/hooks/useChat.ts | 34 +++++++------- webapp/src/redux/features/app/AppState.ts | 1 + webapp/src/redux/features/app/appSlice.ts | 46 +++++++++++++++++-- .../message-relay/signalRMiddleware.ts | 21 +++++++-- 10 files changed, 91 insertions(+), 27 deletions(-) diff --git a/webapi/Options/PlannerOptions.cs b/webapi/Options/PlannerOptions.cs index 291533159..7d9c79d3f 100644 --- a/webapi/Options/PlannerOptions.cs +++ b/webapi/Options/PlannerOptions.cs @@ -33,7 +33,7 @@ public class MissingFunctionErrorOptions public const string PropertyName = "Planner"; /// - /// Define if the planner must be Sequential or not. + /// The type of planner to used to create plan. /// [Required] public PlanType Type { get; set; } = PlanType.Action; diff --git a/webapi/appsettings.json b/webapi/appsettings.json index d3a27c729..f7d7790f4 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -52,6 +52,7 @@ // - Set Planner:Type to "Action" to use the single-step ActionPlanner // - Set Planner:Type to "Sequential" to enable the multi-step SequentialPlanner // Note: SequentialPlanner works best with `gpt-4`. See the "Enabling Sequential Planner" section in webapi/README.md for configuration instructions. + // - Set Planner:Type to "Stepwise" to enable MRKL style planning // - Set Planner:RelevancyThreshold to a decimal between 0 and 1.0. // "Planner": { diff --git a/webapp/src/Constants.ts b/webapp/src/Constants.ts index 136499450..73db5cfde 100644 --- a/webapp/src/Constants.ts +++ b/webapp/src/Constants.ts @@ -4,6 +4,7 @@ export const Constants = { app: { name: 'Copilot', updateCheckIntervalSeconds: 60 * 5, + CONNECTION_ALERT_ID: 'connection-alert', }, msal: { method: 'redirect', // 'redirect' | 'popup' diff --git a/webapp/src/checkEnv.ts b/webapp/src/checkEnv.ts index afa5a8afd..f3b322c3a 100644 --- a/webapp/src/checkEnv.ts +++ b/webapp/src/checkEnv.ts @@ -1,7 +1,10 @@ +/** + * Checks if all required environment variables are defined + * @returns {string[]} An array of missing environment variables + */ export const getMissingEnvVariables = () => { // Should be aligned with variables defined in .env.example const envVariables = ['REACT_APP_BACKEND_URI', 'REACT_APP_AAD_AUTHORITY', 'REACT_APP_AAD_CLIENT_ID']; - const missingVariables = []; for (const variable of envVariables) { diff --git a/webapp/src/components/token-usage/TokenUsageGraph.tsx b/webapp/src/components/token-usage/TokenUsageGraph.tsx index 697ab01be..4ba0529cb 100644 --- a/webapp/src/components/token-usage/TokenUsageGraph.tsx +++ b/webapp/src/components/token-usage/TokenUsageGraph.tsx @@ -63,7 +63,8 @@ const contrastColors = [ export const TokenUsageGraph: React.FC = ({ promptView, tokenUsage }) => { const classes = useClasses(); const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations); - const loadingResponse = conversations[selectedId].botResponseStatus && Object.entries(tokenUsage).length === 0; + const loadingResponse = + selectedId !== '' && conversations[selectedId].botResponseStatus && Object.entries(tokenUsage).length === 0; const responseGenerationView: TokenUsageView = {}; const memoryGenerationView: TokenUsageView = {}; diff --git a/webapp/src/components/views/BackendProbe.tsx b/webapp/src/components/views/BackendProbe.tsx index a4eec1239..266963b86 100644 --- a/webapp/src/components/views/BackendProbe.tsx +++ b/webapp/src/components/views/BackendProbe.tsx @@ -20,7 +20,9 @@ const BackendProbe: FC = ({ uri, onBackendFound }) => { } }; - void fetchAsync(); + fetchAsync().catch(() => { + // Ignore - this page is just a probe, so we don't need to show any errors if backend is not found + }); }, 3000); return () => { diff --git a/webapp/src/libs/hooks/useChat.ts b/webapp/src/libs/hooks/useChat.ts index e4751107f..095a5b2f2 100644 --- a/webapp/src/libs/hooks/useChat.ts +++ b/webapp/src/libs/hooks/useChat.ts @@ -72,10 +72,9 @@ export const useChat = () => { const createChat = async () => { const chatTitle = `Copilot @ ${new Date().toLocaleString()}`; - const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); try { await chatService - .createChatAsync(userId, chatTitle, accessToken) + .createChatAsync(userId, chatTitle, await AuthHelper.getSKaaSAccessToken(instance, inProgress)) .then((result: ICreateChatSessionResponse) => { const newChat: ChatState = { id: result.chatSession.id, @@ -148,8 +147,8 @@ export const useChat = () => { }; const loadChats = async () => { - const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); try { + const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); const chatSessions = await chatService.getAllChatsAsync(userId, accessToken); if (chatSessions.length > 0) { @@ -201,10 +200,9 @@ export const useChat = () => { }; const uploadBot = async (bot: Bot) => { - const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); - botService - .uploadAsync(bot, userId, accessToken) - .then(async (chatSession: IChatSession) => { + try { + const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); + await botService.uploadAsync(bot, userId, accessToken).then(async (chatSession: IChatSession) => { const chatMessages = await chatService.getChatMessagesAsync(chatSession.id, 0, 100, accessToken); const newChat = { @@ -217,11 +215,11 @@ export const useChat = () => { }; dispatch(addConversation(newChat)); - }) - .catch((e: any) => { - const errorMessage = `Unable to upload the bot. Details: ${getErrorDetails(e)}`; - dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); }); + } catch (e: any) { + const errorMessage = `Unable to upload the bot. Details: ${getErrorDetails(e)}`; + dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); + } }; const getBotProfilePicture = (index: number): string => { @@ -282,8 +280,8 @@ export const useChat = () => { }; const joinChat = async (chatId: string) => { - const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); try { + const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); await chatService.joinChatAsync(userId, chatId, accessToken).then(async (result: IChatSession) => { // Get chat messages const chatMessages = await chatService.getChatMessagesAsync(result.id, 0, 100, accessToken); @@ -315,9 +313,14 @@ export const useChat = () => { }; const editChat = async (chatId: string, title: string, syetemDescription: string, memoryBalance: number) => { - const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); try { - await chatService.editChatAsync(chatId, title, syetemDescription, memoryBalance, accessToken); + await chatService.editChatAsync( + chatId, + title, + syetemDescription, + memoryBalance, + await AuthHelper.getSKaaSAccessToken(instance, inProgress), + ); } catch (e: any) { const errorMessage = `Error editing chat ${chatId}. Details: ${getErrorDetails(e)}`; dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); @@ -325,9 +328,8 @@ export const useChat = () => { }; const getServiceOptions = async () => { - const accessToken = await AuthHelper.getSKaaSAccessToken(instance, inProgress); try { - return await chatService.getServiceOptionsAsync(accessToken); + return await chatService.getServiceOptionsAsync(await AuthHelper.getSKaaSAccessToken(instance, inProgress)); } catch (e: any) { const errorMessage = `Error getting service options. Details: ${getErrorDetails(e)}`; dispatch(addAlert({ message: errorMessage, type: AlertType.Error })); diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index a1664f69e..11cd7ecb0 100644 --- a/webapp/src/redux/features/app/AppState.ts +++ b/webapp/src/redux/features/app/AppState.ts @@ -13,6 +13,7 @@ export interface ActiveUserInfo { export interface Alert { message: string; type: AlertType; + id?: string; } interface Feature { diff --git a/webapp/src/redux/features/app/appSlice.ts b/webapp/src/redux/features/app/appSlice.ts index ff95e23b3..1d4ef00f2 100644 --- a/webapp/src/redux/features/app/appSlice.ts +++ b/webapp/src/redux/features/app/appSlice.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { Constants } from '../../../Constants'; import { ServiceOptions } from '../../../libs/models/ServiceOptions'; import { TokenUsage } from '../../../libs/models/TokenUsage'; import { ActiveUserInfo, Alert, AppState, FeatureKeys, initialState } from './AppState'; @@ -13,10 +14,14 @@ export const appSlice = createSlice({ state.alerts = action.payload; }, addAlert: (state: AppState, action: PayloadAction) => { - if (state.alerts.length === 3) { - state.alerts.shift(); + if ( + action.payload.id == Constants.app.CONNECTION_ALERT_ID || + isServerConnectionError(action.payload.message) + ) { + updateConnectionStatus(state, action.payload); + } else { + addNewAlert(state.alerts, action.payload); } - state.alerts.push(action.payload); }, removeAlert: (state: AppState, action: PayloadAction) => { state.alerts.splice(action.payload, 1); @@ -89,3 +94,38 @@ const getTotalTokenUsage = (previousSum?: number, current?: number) => { return previousSum + current; }; + +const isServerConnectionError = (message: string) => { + return ( + message.includes(`Cannot send data if the connection is not in the 'Connected' State.`) || + message.includes(`Server timeout elapsed without receiving a message from the server.`) + ); +}; + +const addNewAlert = (alerts: Alert[], newAlert: Alert) => { + if (alerts.length === 3) { + alerts.shift(); + } + + alerts.push(newAlert); +}; + +const updateConnectionStatus = (state: AppState, statusUpdate: Alert) => { + if (isServerConnectionError(statusUpdate.message)) { + statusUpdate.message = + // Constant message so alert UI doesn't feel glitchy on every connection error from SignalR + 'Cannot send data due to lost connection or server timeout. Try refreshing this page to restart the connection.'; + } + + // There should only ever be one connection alert at a time, + // so we tag the alert with a unique ID so we can remove if needed + statusUpdate.id ??= Constants.app.CONNECTION_ALERT_ID; + + // Remove the existing connection alert if it exists + const connectionAlertIndex = state.alerts.findIndex((alert) => alert.id === Constants.app.CONNECTION_ALERT_ID); + if (connectionAlertIndex !== -1) { + state.alerts.splice(connectionAlertIndex, 1); + } + + addNewAlert(state.alerts, statusUpdate); +}; diff --git a/webapp/src/redux/features/message-relay/signalRMiddleware.ts b/webapp/src/redux/features/message-relay/signalRMiddleware.ts index 8ffc237ef..6beaac3fd 100644 --- a/webapp/src/redux/features/message-relay/signalRMiddleware.ts +++ b/webapp/src/redux/features/message-relay/signalRMiddleware.ts @@ -2,6 +2,7 @@ import * as signalR from '@microsoft/signalr'; import { AnyAction, Dispatch } from '@reduxjs/toolkit'; +import { Constants } from '../../../Constants'; import { AlertType } from '../../../libs/models/AlertType'; import { IChatUser } from '../../../libs/models/ChatUser'; import { PlanState } from '../../../libs/models/Plan'; @@ -65,7 +66,13 @@ const registerCommonSignalConnectionEvents = (store: Store) => { hubConnection.onclose((error) => { if (hubConnection.state === signalR.HubConnectionState.Disconnected) { const errorMessage = 'Connection closed due to error. Try refreshing this page to restart the connection'; - store.dispatch(addAlert({ message: String(errorMessage), type: AlertType.Error })); + store.dispatch( + addAlert({ + message: String(errorMessage), + type: AlertType.Error, + id: Constants.app.CONNECTION_ALERT_ID, + }), + ); console.log(errorMessage, error); } }); @@ -73,15 +80,21 @@ const registerCommonSignalConnectionEvents = (store: Store) => { hubConnection.onreconnecting((error) => { if (hubConnection.state === signalR.HubConnectionState.Reconnecting) { const errorMessage = 'Connection lost due to error. Reconnecting...'; - store.dispatch(addAlert({ message: String(errorMessage), type: AlertType.Info })); + store.dispatch( + addAlert({ + message: String(errorMessage), + type: AlertType.Info, + id: Constants.app.CONNECTION_ALERT_ID, + }), + ); console.log(errorMessage, error); } }); hubConnection.onreconnected((connectionId = '') => { if (hubConnection.state === signalR.HubConnectionState.Connected) { - const message = 'Connection reestablished.'; - store.dispatch(addAlert({ message, type: AlertType.Success })); + const message = 'Connection reestablished. Please refresh the page to ensure you have the latest data.'; + store.dispatch(addAlert({ message, type: AlertType.Success, id: Constants.app.CONNECTION_ALERT_ID })); console.log(message + ` Connected with connectionId ${connectionId}`); } }); From 1ed63be54cdf4cc2d347ee301054b7b3019c008a Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:57:54 -0700 Subject: [PATCH 5/6] Fix app registration link in README (#142) ### Motivation and Context The link to the application registration section has become zombied. ### Description Re-targeted existing reference. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/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 :smile: --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a5d88d2c..bb02be1d1 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,8 @@ You will need the following items to run the sample: ## (Optional) Enable backend authorization via Azure AD -1. Ensure you created the required application registration mentioned in [Start the WebApp FrontEnd application](#start-the-webapp-frontend-application) +1. Ensure you created the required application registration mentioned in [Register an application](#register-an-application) + 2. Create a second application registration to represent the web api > For more details on creating an application registration, go [here](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). From 6b6f369e7f70d974fde669fafad76ffba06858c0 Mon Sep 17 00:00:00 2001 From: Gil LaHaye Date: Wed, 9 Aug 2023 15:24:00 -0600 Subject: [PATCH 6/6] Rename "MemoriesStore" to "MemoryStore" (#138) ### Motivation and Context Just making this grammatically correct in English. (It was driving me nuts) ### Description Rename "MemoriesStore" to "MemoryStore" ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- scripts/deploy/main.bicep | 12 ++-- scripts/deploy/main.json | 12 ++-- .../Controllers/ServiceOptionsController.cs | 12 ++-- webapi/Extensions/SemanticKernelExtensions.cs | 20 +++---- webapi/Extensions/ServiceExtensions.cs | 4 +- .../Models/Response/ServiceOptionsResponse.cs | 14 ++--- webapi/Options/MemoriesStoreOptions.cs | 60 ------------------- webapi/Options/MemoryStoreOptions.cs | 60 +++++++++++++++++++ .../Skills/ChatSkills/CopilotChatPlanner.cs | 2 +- webapi/appsettings.json | 10 ++-- .../src/components/chat/tabs/DocumentsTab.tsx | 6 +- webapp/src/libs/models/ServiceOptions.ts | 4 +- webapp/src/redux/features/app/AppState.ts | 2 +- 13 files changed, 109 insertions(+), 109 deletions(-) delete mode 100644 webapi/Options/MemoriesStoreOptions.cs create mode 100644 webapi/Options/MemoryStoreOptions.cs diff --git a/scripts/deploy/main.bicep b/scripts/deploy/main.bicep index 2434d58e7..569ebc312 100644 --- a/scripts/deploy/main.bicep +++ b/scripts/deploy/main.bicep @@ -227,27 +227,27 @@ resource appServiceWebConfig 'Microsoft.Web/sites/config@2022-09-01' = { value: deployCosmosDB ? cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString : '' } { - name: 'MemoriesStore:Type' + name: 'MemoryStore:Type' value: memoryStore } { - name: 'MemoriesStore:Qdrant:Host' + name: 'MemoryStore:Qdrant:Host' value: memoryStore == 'Qdrant' ? 'https://${appServiceQdrant.properties.defaultHostName}' : '' } { - name: 'MemoriesStore:Qdrant:Port' + name: 'MemoryStore:Qdrant:Port' value: '443' } { - name: 'MemoriesStore:AzureCognitiveSearch:UseVectorSearch' + name: 'MemoryStore:AzureCognitiveSearch:UseVectorSearch' value: 'true' } { - name: 'MemoriesStore:AzureCognitiveSearch:Endpoint' + name: 'MemoryStore:AzureCognitiveSearch:Endpoint' value: memoryStore == 'AzureCognitiveSearch' ? 'https://${azureCognitiveSearch.name}.search.windows.net' : '' } { - name: 'MemoriesStore:AzureCognitiveSearch:Key' + name: 'MemoryStore:AzureCognitiveSearch:Key' value: memoryStore == 'AzureCognitiveSearch' ? azureCognitiveSearch.listAdminKeys().primaryKey : '' } { diff --git a/scripts/deploy/main.json b/scripts/deploy/main.json index 21a0c157d..8687527fd 100644 --- a/scripts/deploy/main.json +++ b/scripts/deploy/main.json @@ -336,27 +336,27 @@ "value": "[if(parameters('deployCosmosDB'), listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('cosmos-{0}', variables('uniqueName')))), '2023-04-15').connectionStrings[0].connectionString, '')]" }, { - "name": "MemoriesStore:Type", + "name": "MemoryStore:Type", "value": "[parameters('memoryStore')]" }, { - "name": "MemoriesStore:Qdrant:Host", + "name": "MemoryStore:Qdrant:Host", "value": "[if(equals(parameters('memoryStore'), 'Qdrant'), format('https://{0}', reference(resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName'))), '2022-09-01').defaultHostName), '')]" }, { - "name": "MemoriesStore:Qdrant:Port", + "name": "MemoryStore:Qdrant:Port", "value": "443" }, { - "name": "MemoriesStore:AzureCognitiveSearch:UseVectorSearch", + "name": "MemoryStore:AzureCognitiveSearch:UseVectorSearch", "value": "true" }, { - "name": "MemoriesStore:AzureCognitiveSearch:Endpoint", + "name": "MemoryStore:AzureCognitiveSearch:Endpoint", "value": "[if(equals(parameters('memoryStore'), 'AzureCognitiveSearch'), format('https://{0}.search.windows.net', format('acs-{0}', variables('uniqueName'))), '')]" }, { - "name": "MemoriesStore:AzureCognitiveSearch:Key", + "name": "MemoryStore:AzureCognitiveSearch:Key", "value": "[if(equals(parameters('memoryStore'), 'AzureCognitiveSearch'), listAdminKeys(resourceId('Microsoft.Search/searchServices', format('acs-{0}', variables('uniqueName'))), '2022-09-01').primaryKey, '')]" }, { diff --git a/webapi/Controllers/ServiceOptionsController.cs b/webapi/Controllers/ServiceOptionsController.cs index 7a76f5219..28adf3758 100644 --- a/webapi/Controllers/ServiceOptionsController.cs +++ b/webapi/Controllers/ServiceOptionsController.cs @@ -20,14 +20,14 @@ public class ServiceOptionsController : ControllerBase { private readonly ILogger _logger; - private readonly MemoriesStoreOptions _memoriesStoreOptions; + private readonly MemoryStoreOptions _memoryStoreOptions; public ServiceOptionsController( ILogger logger, - IOptions memoriesStoreOptions) + IOptions memoryStoreOptions) { this._logger = logger; - this._memoriesStoreOptions = memoriesStoreOptions.Value; + this._memoryStoreOptions = memoryStoreOptions.Value; } // TODO: [Issue #95] Include all service options in a single response. @@ -42,10 +42,10 @@ public IActionResult GetServiceOptions() return this.Ok( new ServiceOptionsResponse() { - MemoriesStore = new MemoriesStoreOptionResponse() + MemoryStore = new MemoryStoreOptionResponse() { - Types = Enum.GetNames(typeof(MemoriesStoreOptions.MemoriesStoreType)), - SelectedType = this._memoriesStoreOptions.Type.ToString() + Types = Enum.GetNames(typeof(MemoryStoreOptions.MemoryStoreType)), + SelectedType = this._memoryStoreOptions.Type.ToString() } } ); diff --git a/webapi/Extensions/SemanticKernelExtensions.cs b/webapi/Extensions/SemanticKernelExtensions.cs index 82fce1721..034968784 100644 --- a/webapi/Extensions/SemanticKernelExtensions.cs +++ b/webapi/Extensions/SemanticKernelExtensions.cs @@ -22,7 +22,7 @@ using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Skills.Core; using Microsoft.SemanticKernel.TemplateEngine; -using static CopilotChat.WebApi.Options.MemoriesStoreOptions; +using static CopilotChat.WebApi.Options.MemoryStoreOptions; namespace CopilotChat.WebApi.Extensions; @@ -155,18 +155,18 @@ private static Task RegisterSkillsAsync(IServiceProvider sp, IKernel kernel) ///
private static void AddSemanticTextMemory(this IServiceCollection services) { - MemoriesStoreOptions config = services.BuildServiceProvider().GetRequiredService>().Value; + MemoryStoreOptions config = services.BuildServiceProvider().GetRequiredService>().Value; switch (config.Type) { - case MemoriesStoreType.Volatile: + case MemoryStoreType.Volatile: services.AddSingleton(); break; - case MemoriesStoreType.Qdrant: + case MemoryStoreType.Qdrant: if (config.Qdrant == null) { - throw new InvalidOperationException("MemoriesStore type is Qdrant and Qdrant configuration is null."); + throw new InvalidOperationException("MemoryStore type is Qdrant and Qdrant configuration is null."); } services.AddSingleton(sp => @@ -189,10 +189,10 @@ private static void AddSemanticTextMemory(this IServiceCollection services) }); break; - case MemoriesStoreType.AzureCognitiveSearch: + case MemoryStoreType.AzureCognitiveSearch: if (config.AzureCognitiveSearch == null) { - throw new InvalidOperationException("MemoriesStore type is AzureCognitiveSearch and AzureCognitiveSearch configuration is null."); + throw new InvalidOperationException("MemoryStore type is AzureCognitiveSearch and AzureCognitiveSearch configuration is null."); } services.AddSingleton(sp => @@ -201,10 +201,10 @@ private static void AddSemanticTextMemory(this IServiceCollection services) }); break; - case MemoriesStoreOptions.MemoriesStoreType.Chroma: + case MemoryStoreOptions.MemoryStoreType.Chroma: if (config.Chroma == null) { - throw new InvalidOperationException("MemoriesStore type is Chroma and Chroma configuration is null."); + throw new InvalidOperationException("MemoryStore type is Chroma and Chroma configuration is null."); } services.AddSingleton(sp => @@ -222,7 +222,7 @@ private static void AddSemanticTextMemory(this IServiceCollection services) break; default: - throw new InvalidOperationException($"Invalid 'MemoriesStore' type '{config.Type}'."); + throw new InvalidOperationException($"Invalid 'MemoryStore' type '{config.Type}'."); } services.AddScoped(sp => new SemanticTextMemory( diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs index 3e0f3fa09..e3a2fd966 100644 --- a/webapi/Extensions/ServiceExtensions.cs +++ b/webapi/Extensions/ServiceExtensions.cs @@ -53,8 +53,8 @@ public static IServiceCollection AddOptions(this IServiceCollection services, Co .PostConfigure(TrimStringProperties); // Memory store configuration - services.AddOptions() - .Bind(configuration.GetSection(MemoriesStoreOptions.PropertyName)) + services.AddOptions() + .Bind(configuration.GetSection(MemoryStoreOptions.PropertyName)) .ValidateDataAnnotations() .ValidateOnStart() .PostConfigure(TrimStringProperties); diff --git a/webapi/Models/Response/ServiceOptionsResponse.cs b/webapi/Models/Response/ServiceOptionsResponse.cs index e6a124409..d5eda9cba 100644 --- a/webapi/Models/Response/ServiceOptionsResponse.cs +++ b/webapi/Models/Response/ServiceOptionsResponse.cs @@ -9,25 +9,25 @@ namespace CopilotChat.WebApi.Models.Response; public class ServiceOptionsResponse { /// - /// The memories store that is configured. + /// Configured memory store. /// - [JsonPropertyName("memoriesStore")] - public MemoriesStoreOptionResponse MemoriesStore { get; set; } = new MemoriesStoreOptionResponse(); + [JsonPropertyName("memoryStore")] + public MemoryStoreOptionResponse MemoryStore { get; set; } = new MemoryStoreOptionResponse(); } /// -/// Response to memoriesStoreType request. +/// Response to memoryStoreType request. /// -public class MemoriesStoreOptionResponse +public class MemoryStoreOptionResponse { /// - /// All the available memories store types. + /// All the available memory store types. /// [JsonPropertyName("types")] public IEnumerable Types { get; set; } = Enumerable.Empty(); /// - /// The selected memories store type. + /// The selected memory store type. /// [JsonPropertyName("selectedType")] public string SelectedType { get; set; } = string.Empty; diff --git a/webapi/Options/MemoriesStoreOptions.cs b/webapi/Options/MemoriesStoreOptions.cs deleted file mode 100644 index 7486b4cba..000000000 --- a/webapi/Options/MemoriesStoreOptions.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace CopilotChat.WebApi.Options; - -/// -/// Configuration settings for the memories store. -/// -public class MemoriesStoreOptions -{ - public const string PropertyName = "MemoriesStore"; - - /// - /// The type of memories store to use. - /// - public enum MemoriesStoreType - { - /// - /// Non-persistent memories store. - /// - Volatile, - - /// - /// Qdrant based persistent memories store. - /// - Qdrant, - - /// - /// Azure Cognitive Search persistent memories store. - /// - AzureCognitiveSearch, - - /// - /// Chroma DB persistent memories store. - /// - Chroma - } - - /// - /// Gets or sets the type of memories store to use. - /// - public MemoriesStoreType Type { get; set; } = MemoriesStoreType.Volatile; - - /// - /// Gets or sets the configuration for the Qdrant memories store. - /// - [RequiredOnPropertyValue(nameof(Type), MemoriesStoreType.Qdrant)] - public QdrantOptions? Qdrant { get; set; } - - /// - /// Gets or sets the configuration for the Chroma memories store. - /// - [RequiredOnPropertyValue(nameof(Type), MemoriesStoreType.Chroma)] - public VectorMemoryWebOptions? Chroma { get; set; } - - /// - /// Gets or sets the configuration for the Azure Cognitive Search memories store. - /// - [RequiredOnPropertyValue(nameof(Type), MemoriesStoreType.AzureCognitiveSearch)] - public AzureCognitiveSearchOptions? AzureCognitiveSearch { get; set; } -} diff --git a/webapi/Options/MemoryStoreOptions.cs b/webapi/Options/MemoryStoreOptions.cs new file mode 100644 index 000000000..a55da230a --- /dev/null +++ b/webapi/Options/MemoryStoreOptions.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration settings for the memory store. +/// +public class MemoryStoreOptions +{ + public const string PropertyName = "MemoryStore"; + + /// + /// The type of memory store to use. + /// + public enum MemoryStoreType + { + /// + /// Non-persistent memory store. + /// + Volatile, + + /// + /// Qdrant based persistent memory store. + /// + Qdrant, + + /// + /// Azure Cognitive Search persistent memory store. + /// + AzureCognitiveSearch, + + /// + /// Chroma DB persistent memory store. + /// + Chroma + } + + /// + /// Gets or sets the type of memory store to use. + /// + public MemoryStoreType Type { get; set; } = MemoryStoreType.Volatile; + + /// + /// Gets or sets the configuration for the Qdrant memory store. + /// + [RequiredOnPropertyValue(nameof(Type), MemoryStoreType.Qdrant)] + public QdrantOptions? Qdrant { get; set; } + + /// + /// Gets or sets the configuration for the Chroma memory store. + /// + [RequiredOnPropertyValue(nameof(Type), MemoryStoreType.Chroma)] + public VectorMemoryWebOptions? Chroma { get; set; } + + /// + /// Gets or sets the configuration for the Azure Cognitive Search memory store. + /// + [RequiredOnPropertyValue(nameof(Type), MemoryStoreType.AzureCognitiveSearch)] + public AzureCognitiveSearchOptions? AzureCognitiveSearch { get; set; } +} diff --git a/webapi/Skills/ChatSkills/CopilotChatPlanner.cs b/webapi/Skills/ChatSkills/CopilotChatPlanner.cs index 9f4202b99..4841e0d55 100644 --- a/webapi/Skills/ChatSkills/CopilotChatPlanner.cs +++ b/webapi/Skills/ChatSkills/CopilotChatPlanner.cs @@ -136,7 +136,7 @@ public async Task RunStepwisePlannerAsync(string goal, SKContext cont } catch (Exception e) { - context.Log.LogError(e, "Error running stepwise planner"); + context.Logger.LogError(e, "Error running stepwise planner"); throw; } } diff --git a/webapi/appsettings.json b/webapi/appsettings.json index f7d7790f4..c4b65326d 100644 --- a/webapi/appsettings.json +++ b/webapi/appsettings.json @@ -127,12 +127,12 @@ // Memory stores are used for storing new memories and retrieving semantically similar memories. // - Supported Types are "volatile", "qdrant", "azurecognitivesearch", or "chroma". // - When using Qdrant or Azure Cognitive Search, see ./README.md for deployment instructions. - // - Set "MemoriesStore:AzureCognitiveSearch:Key" using dotnet's user secrets (see above) - // (i.e. dotnet user-secrets set "MemoriesStore:AzureCognitiveSearch:Key" "MY_AZCOGSRCH_KEY") - // - Set "MemoriesStore:Qdrant:Key" using dotnet's user secrets (see above) if you are using a Qdrant Cloud instance. - // (i.e. dotnet user-secrets set "MemoriesStore:Qdrant:Key" "MY_QDRANTCLOUD_KEY") + // - Set "MemoryStore:AzureCognitiveSearch:Key" using dotnet's user secrets (see above) + // (i.e. dotnet user-secrets set "MemoryStore:AzureCognitiveSearch:Key" "MY_AZCOGSRCH_KEY") + // - Set "MemoryStore:Qdrant:Key" using dotnet's user secrets (see above) if you are using a Qdrant Cloud instance. + // (i.e. dotnet user-secrets set "MemoryStore:Qdrant:Key" "MY_QDRANTCLOUD_KEY") // - "MemoriesStore": { + "MemoryStore": { "Type": "volatile", "Qdrant": { "Host": "http://localhost", diff --git a/webapp/src/components/chat/tabs/DocumentsTab.tsx b/webapp/src/components/chat/tabs/DocumentsTab.tsx index b4e89c315..fd0afefd3 100644 --- a/webapp/src/components/chat/tabs/DocumentsTab.tsx +++ b/webapp/src/components/chat/tabs/DocumentsTab.tsx @@ -181,14 +181,14 @@ export const DocumentsTab: React.FC = () => { {/* Hardcode vector database as we don't support switching vector store dynamically now. */}
- - {serviceOptions.memoriesStore.types.map((storeType) => { + + {serviceOptions.memoryStore.types.map((storeType) => { return ( ); })} diff --git a/webapp/src/libs/models/ServiceOptions.ts b/webapp/src/libs/models/ServiceOptions.ts index 79172ac5f..f275ba5fb 100644 --- a/webapp/src/libs/models/ServiceOptions.ts +++ b/webapp/src/libs/models/ServiceOptions.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -export interface MemoriesStore { +export interface MemoryStore { types: string[]; selectedType: string; } export interface ServiceOptions { - memoriesStore: MemoriesStore; + memoryStore: MemoryStore; } diff --git a/webapp/src/redux/features/app/AppState.ts b/webapp/src/redux/features/app/AppState.ts index 11cd7ecb0..1baa327ba 100644 --- a/webapp/src/redux/features/app/AppState.ts +++ b/webapp/src/redux/features/app/AppState.ts @@ -134,5 +134,5 @@ export const initialState: AppState = { tokenUsage: {}, features: Features, settings: Settings, - serviceOptions: { memoriesStore: { types: [], selectedType: '' } }, + serviceOptions: { memoryStore: { types: [], selectedType: '' } }, };