diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Models/ProposedPlan.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Models/ProposedPlan.cs index 40e97eadff..f84990b88c 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Models/ProposedPlan.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Models/ProposedPlan.cs @@ -5,6 +5,21 @@ using Microsoft.SemanticKernel.Planning; namespace SemanticKernel.Service.CopilotChat.Models; +// Type of Plan +public enum PlanType +{ + Action, // single-step + Sequential, // multi-step +} + +// State of Plan +public enum PlanState +{ + NoOp, // Plan has not received any user input + Approved, + Rejected, +} + /// /// Information about a single proposed plan. /// @@ -16,12 +31,26 @@ public class ProposedPlan [JsonPropertyName("proposedPlan")] public Plan 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; } + /// /// Create a new proposed plan. /// /// Proposed plan object - public ProposedPlan(Plan plan) + public ProposedPlan(Plan plan, PlanType type, PlanState state) { this.Plan = plan; + this.Type = type; + this.State = state; } } diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Options/PlannerOptions.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Options/PlannerOptions.cs index c6a7858427..c2827035ad 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Options/PlannerOptions.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Options/PlannerOptions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel.DataAnnotations; +using SemanticKernel.Service.CopilotChat.Models; namespace SemanticKernel.Service.CopilotChat.Options; @@ -14,5 +15,5 @@ public class PlannerOptions /// Define if the planner must be Sequential or not. /// [Required] - public string Type { get; set; } = string.Empty; + public PlanType Type { get; set; } = PlanType.Action; } 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 a73699d6bf..ff1738250f 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 @@ -238,6 +238,16 @@ public class ChatSkill chatContext.Variables.Set("knowledgeCutoff", this._promptOptions.KnowledgeCutoffDate); chatContext.Variables.Set("audience", chatContext["userName"]); + // 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)) + { + 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); @@ -258,7 +268,8 @@ public class ChatSkill // Save this response to memory such that subsequent chat responses can use it try { - var botMessage = await this.SaveNewResponseAsync(response, prompt, chatId); + ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId); + context.Variables.Set("messageId", botMessage.Id); context.Variables.Set("messageType", botMessage.Type.ToString()); } catch (Exception ex) when (!ex.IsCriticalException()) @@ -310,8 +321,7 @@ public class ChatSkill // If plan is suggested, send back to user for approval before running if (this._externalInformationSkill.ProposedPlan != null) { - return JsonSerializer.Serialize( - new ProposedPlan(this._externalInformationSkill.ProposedPlan)); + return JsonSerializer.Serialize(this._externalInformationSkill.ProposedPlan); } // 4. Query relevant semantic memories @@ -380,6 +390,7 @@ public class ChatSkill /// private async Task GetUserIntentAsync(SKContext context) { + // TODO: Regenerate user intent if plan was modified if (!context.Variables.TryGetValue("planUserIntent", out string? userIntent)) { var contextVariables = new ContextVariables(); @@ -495,14 +506,10 @@ public class ChatSkill /// /// Helper function create the correct context variables to acquire external information. /// - private Task AcquireExternalInformationAsync(SKContext context, string userIntent, int tokenLimit) + private async Task AcquireExternalInformationAsync(SKContext context, string userIntent, int tokenLimit) { var contextVariables = context.Variables.Clone(); contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo())); - if (context.Variables.TryGetValue("proposedPlan", out string? proposedPlan)) - { - contextVariables.Set("proposedPlan", proposedPlan); - } var planContext = new SKContext( contextVariables, @@ -512,7 +519,7 @@ public class ChatSkill context.CancellationToken ); - var plan = this._externalInformationSkill.AcquireExternalInformationAsync(userIntent, planContext); + var plan = await this._externalInformationSkill.AcquireExternalInformationAsync(userIntent, planContext); // Propagate the error if (planContext.ErrorOccurred) @@ -570,6 +577,20 @@ public class ChatSkill return chatMessage; } + /// + /// Updates previously saved response in the chat history. + /// + /// Updated response from the chat. + /// The chat message ID + private async Task UpdateResponseAsync(string updatedResponse, string messageId) + { + // Make sure the chat exists. + var chatMessage = await this._chatMessageRepository.FindByIdAsync(messageId); + chatMessage.Content = updatedResponse; + + await this._chatMessageRepository.UpsertAsync(chatMessage); + } + /// /// Create a completion settings object for chat response. Parameters are read from the PromptSettings class. /// diff --git a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/CopilotChatPlanner.cs b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/CopilotChatPlanner.cs index 9989f61d11..f41106acee 100644 --- a/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/CopilotChatPlanner.cs +++ b/samples/apps/copilot-chat-app/webapi/CopilotChat/Skills/ChatSkills/CopilotChatPlanner.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Planning; using Microsoft.SemanticKernel.SkillDefinition; +using SemanticKernel.Service.CopilotChat.Models; using SemanticKernel.Service.CopilotChat.Options; namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills; @@ -23,6 +24,11 @@ public class CopilotChatPlanner /// private readonly PlannerOptions? _plannerOptions; + /// + /// Gets the pptions for the planner. + /// + public PlannerOptions? PlannerOptions => this._plannerOptions; + /// /// Initializes a new instance of the class. /// @@ -47,7 +53,7 @@ public class CopilotChatPlanner return Task.FromResult(new Plan(goal)); } - if (this._plannerOptions?.Type == "Sequential") + if (this._plannerOptions?.Type == PlanType.Sequential) { return new SequentialPlanner(this.Kernel).CreatePlanAsync(goal); } 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 9691633d8c..ccad3351c3 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 @@ -13,6 +13,7 @@ using Microsoft.Extensions.Options; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Planning; using Microsoft.SemanticKernel.SkillDefinition; +using SemanticKernel.Service.CopilotChat.Models; using SemanticKernel.Service.CopilotChat.Options; using SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.GitHubSkill.Model; using SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model; @@ -37,7 +38,7 @@ public class ExternalInformationSkill /// /// Proposed plan to return for approval. /// - public Plan? ProposedPlan { get; private set; } + public ProposedPlan? ProposedPlan { get; private set; } /// /// Preamble to add to the related information text. @@ -77,11 +78,13 @@ public class ExternalInformationSkill } // Check if plan exists in ask's context variables. - // If plan was returned at this point, that means it was approved and should be run - var planApproved = context.Variables.TryGetValue("proposedPlan", out string? planJson); + var planExists = context.Variables.TryGetValue("proposedPlan", out string? proposedPlanJson); + var deserializedPlan = planExists && !string.IsNullOrWhiteSpace(proposedPlanJson) ? JsonSerializer.Deserialize(proposedPlanJson) : null; - if (planApproved && !string.IsNullOrWhiteSpace(planJson)) + // Run plan if it was approved + if (deserializedPlan != null && deserializedPlan.State == PlanState.Approved) { + string planJson = JsonSerializer.Serialize(deserializedPlan.Plan); // Reload the plan with the planner's kernel so // it has full context to be executed var newPlanContext = new SKContext( @@ -130,7 +133,7 @@ public class ExternalInformationSkill Plan sanitizedPlan = this.SanitizePlan(plan, context); sanitizedPlan.State.Update(plan.State); - this.ProposedPlan = sanitizedPlan; + this.ProposedPlan = new ProposedPlan(sanitizedPlan, this._planner.PlannerOptions!.Type, PlanState.NoOp); } } diff --git a/samples/apps/copilot-chat-app/webapi/README.md b/samples/apps/copilot-chat-app/webapi/README.md index 38dc4cb0b3..08a800fa74 100644 --- a/samples/apps/copilot-chat-app/webapi/README.md +++ b/samples/apps/copilot-chat-app/webapi/README.md @@ -72,11 +72,16 @@ To enable sequential planner, 1. In [./webapi/appsettings.json](appsettings.json), set `"Type": "Sequential"` under the `Planner` section. 1. Then, set your preferred Planner model (`gpt-4` or `gpt-3.5-turbo`) under the `AIService` configuration section. 1. If using `gpt-4`, no other changes are required. - 1. If using `gpt-3.5-turbo`, change [CopilotChatPlanner.cs](CopilotChat/Skills/ChatSkills/CopilotChatPlanner.cs) to initialize SequentialPlanner with a RelevancyThreshold*. The `CreatePlanAsync` method should return the following line if `this._plannerOptions?.Type == "Sequential"` is true: - ``` - return new SequentialPlanner(this.Kernel, new SequentialPlannerConfig { RelevancyThreshold = 0.75 }).CreatePlanAsync(goal); - ``` - \* The `RelevancyThreshold` is a number from 0 to 1 that represents how similar a goal is to a function's name/description/inputs. You want to tune that value when using SequentialPlanner to help keep things scoped while not missing on on things that are relevant or including too many things that really aren't. `0.75` is an arbitrary threshold and we recommend developers play around with this number to see what best fits their scenarios. + 1. If using `gpt-3.5-turbo`: change [CopilotChatPlanner.cs](CopilotChat/Skills/ChatSkills/CopilotChatPlanner.cs) to initialize SequentialPlanner with a RelevancyThreshold*. + - Add `using` statement to top of file: + ``` + using Microsoft.SemanticKernel.Planning.Sequential; + ``` + - The `CreatePlanAsync` method should return the following line if `this._plannerOptions?.Type == "Sequential"` is true: + ``` + return new SequentialPlanner(this.Kernel, new SequentialPlannerConfig { RelevancyThreshold = 0.75 }).CreatePlanAsync(goal); + ``` + \* The `RelevancyThreshold` is a number from 0 to 1 that represents how similar a goal is to a function's name/description/inputs. You want to tune that value when using SequentialPlanner to help keep things scoped while not missing on on things that are relevant or including too many things that really aren't. `0.75` is an arbitrary threshold and we recommend developers play around with this number to see what best fits their scenarios. 1. Restart the `webapi` - Copilot Chat should be now running locally with SequentialPlanner. # (Optional) Enabling the Qdrant Memory Store diff --git a/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatHistoryItem.tsx b/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatHistoryItem.tsx index 5365587886..139b954b96 100644 --- a/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatHistoryItem.tsx +++ b/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatHistoryItem.tsx @@ -3,12 +3,11 @@ import { useMsal } from '@azure/msal-react'; import { Persona, Text, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components'; import React from 'react'; -import { AuthorRoles, ChatMessageState, ChatMessageType, IChatMessage } from '../../libs/models/ChatMessage'; +import { AuthorRoles, IChatMessage } from '../../libs/models/ChatMessage'; import { GetResponseOptions, useChat } from '../../libs/useChat'; -import { parsePlan } from '../../libs/utils/PlanUtils'; -import { useAppDispatch, useAppSelector } from '../../redux/app/hooks'; +import { isPlan } from '../../libs/utils/PlanUtils'; +import { useAppSelector } from '../../redux/app/hooks'; import { RootState } from '../../redux/app/store'; -import { updateMessageState } from '../../redux/features/conversations/conversationsSlice'; import { Breakpoints } from '../../styles'; import { convertToAnchorTags, timestampToDateString } from '../utils/TextUtils'; import { PlanViewer } from './plan-viewer/PlanViewer'; @@ -82,53 +81,10 @@ export const ChatHistoryItem: React.FC = ({ message, getRe const chat = useChat(); const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations); - const dispatch = useAppDispatch(); - const plan = parsePlan(message.content); - const isPlan = plan !== null; + const renderPlan = isPlan(message.content); - // Initializing Plan action handlers here so we don't have to drill down data the components won't use otherwise - const onPlanApproval = async () => { - dispatch( - updateMessageState({ - newMessageState: ChatMessageState.PlanApproved, - messageIndex: messageIndex, - chatId: selectedId, - }), - ); - - // Extract plan from bot response - const proposedPlan = JSON.parse(message.content).proposedPlan; - - // Invoke plan - await getResponse({ - messageType: ChatMessageType.Plan, - value: 'Yes, proceed', - chatId: selectedId, - approvedPlanJson: JSON.stringify(proposedPlan), - planUserIntent: plan?.userIntent, - }); - }; - - const onPlanCancel = async () => { - dispatch( - updateMessageState({ - newMessageState: ChatMessageState.PlanRejected, - messageIndex: messageIndex, - chatId: selectedId, - }), - ); - - // Bail out of plan - await getResponse({ - messageType: ChatMessageType.Plan, - value: 'No, cancel', - chatId: selectedId, - userCancelledPlan: true, - }); - }; - - const content = !isPlan + const content = !renderPlan ? (message.content as string) .trim() .replace(/[\u00A0-\u9999<>&]/g, function (i: string) { @@ -150,35 +106,23 @@ export const ChatHistoryItem: React.FC = ({ message, getRe : { name: fullName, color: 'colorful' as 'colorful' }; return ( - <> -
- {!isMe && } -
-
- {!isMe && {fullName}} - {timestampToDateString(message.timestamp, true)} - {isBot && } -
- {!isPlan && ( -
- )} - {isPlan && ( - - )} +
+ {!isMe && } +
+
+ {!isMe && {fullName}} + {timestampToDateString(message.timestamp, true)} + {isBot && }
+ {renderPlan ? ( + + ) : ( +
+ )}
- +
); }; diff --git a/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatRoom.tsx b/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatRoom.tsx index 5ba881f12c..a6bd657f08 100644 --- a/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatRoom.tsx +++ b/samples/apps/copilot-chat-app/webapp/src/components/chat/ChatRoom.tsx @@ -25,7 +25,7 @@ const useClasses = makeStyles({ height: '100%', }, scroll: { - ...shorthands.margin('4px'), + ...shorthands.margin(tokens.spacingVerticalXS), ...SharedStyles.scroll, }, history: { diff --git a/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepCard.tsx b/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepCard.tsx index 87723756ce..7fcabde64d 100644 --- a/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepCard.tsx +++ b/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepCard.tsx @@ -1,6 +1,27 @@ -import { Badge, Body1, Card, CardHeader, makeStyles, shorthands, Text, tokens } from '@fluentui/react-components'; -import { IPlanInput, IPlanStep } from '../../../libs/models/Plan'; +import { + Badge, + Body1, + Button, + Card, + CardHeader, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + DialogTrigger, + makeStyles, + shorthands, + Text, + tokens, +} from '@fluentui/react-components'; +import { Dismiss12Regular } from '@fluentui/react-icons'; +import { useState } from 'react'; +import { Constants } from '../../../Constants'; +import { IPlanInput } from '../../../libs/models/Plan'; import { CopilotChatTokens } from '../../../styles'; +import { PlanStepInput } from './PlanStepInput'; const useClasses = makeStyles({ card: { @@ -11,9 +32,9 @@ const useClasses = makeStyles({ header: { color: CopilotChatTokens.titleColor, }, - inputs: { + parameters: { + ...shorthands.gap(tokens.spacingHorizontalS), display: 'flex', - ...shorthands.gap('8px'), flexWrap: 'wrap', }, bar: { @@ -28,10 +49,11 @@ const useClasses = makeStyles({ flexColumn: { display: 'flex', flexDirection: 'column', - marginLeft: '8px', - marginTop: '4px', - marginBottom: '4px', - ...shorthands.gap('8px'), + width: '100%', + marginLeft: tokens.spacingHorizontalS, + marginTop: tokens.spacingVerticalXS, + marginBottom: tokens.spacingVerticalXS, + ...shorthands.gap(tokens.spacingHorizontalS), }, singleLine: { ...shorthands.overflow('hidden'), @@ -42,18 +64,34 @@ const useClasses = makeStyles({ width: '650px', fontSize: '12px', }, + dialog: { + width: '398px', + '& button': { + marginTop: tokens.spacingVerticalL, + width: 'max-content', + }, + }, }); interface PlanStepCardProps { - index: number; - step: IPlanStep; + // See Semantic Kernel's Plan object for field definitions (step === Plan). + // Plan.cs: https://github.com/microsoft/semantic-kernel/blob/df07fc6f28853a481dd6f47e60d39a52fc6c9967/dotnet/src/SemanticKernel/Planning/Plan.cs# + // Not explicitly defining the type here to avoid additional overhead of property maintenance. + step: any; + enableEdits: boolean; + enableStepDelete: boolean; + onDeleteStep: (index: number) => void; } -export const PlanStepCard: React.FC = ({ index, step }) => { +export const PlanStepCard: React.FC = ({ step, enableEdits, enableStepDelete, onDeleteStep }) => { const classes = useClasses(); + const [openDialog, setOpenDialog] = useState(false); - const numbersAsStrings = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine']; - const stepNumber = numbersAsStrings[index]; + // Omit reserved context variable names from displayed inputs + const inputs = step.parameters.filter( + (parameter: IPlanInput) => + !Constants.sk.reservedWords.includes(parameter.Key.trim()) && parameter.Value.trim() !== '', + ); return ( @@ -63,23 +101,94 @@ export const PlanStepCard: React.FC = ({ index, step }) => { - Step {stepNumber} • {step.skill}.{step.function} + Step {step.index + 1} • {step.skill_name}.{step.name}
} + action={ + enableEdits && enableStepDelete ? ( + + + + + + + + + + ) : undefined + } /> {step.description && (
About: {step.description}
)} - {step.stepInputs.length > 0 && ( -
+ {inputs.length > 0 && ( +
Inputs: - {step.stepInputs.map((input: IPlanInput) => { + {inputs.map((input: IPlanInput) => { + const onEditInput = (newValue: string) => { + const inputIndex = step.parameters.findIndex( + (element: IPlanInput) => element.Key === input.Key, + ); + step.parameters[inputIndex] = { + Key: input.Key, + Value: newValue, + }; + }; return ( - - {`${input.Key}: ${input.Value}`} + + ); + })} +
+ )} + {step.outputs.length > 0 && ( +
+ Outputs: + {step.outputs.map((output: string) => { + return ( + + {output} ); })} diff --git a/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepInput.tsx b/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepInput.tsx new file mode 100644 index 0000000000..e0234dc1d5 --- /dev/null +++ b/samples/apps/copilot-chat-app/webapp/src/components/chat/plan-viewer/PlanStepInput.tsx @@ -0,0 +1,96 @@ +import { Badge, Button, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components'; +import { Checkmark16Regular, Dismiss16Regular, Edit16Regular } from '@fluentui/react-icons'; +import { useCallback, useState } from 'react'; +import { IPlanInput } from '../../../libs/models/Plan'; + +const useClasses = makeStyles({ + root: { + ...shorthands.overflow('hidden'), + display: 'flex', + }, + buttons: { + ...shorthands.padding(tokens.spacingVerticalNone), + paddingLeft: tokens.spacingHorizontalXXS, + minWidth: '18px', + }, + input: { + ...shorthands.margin(tokens.spacingHorizontalXXS), + maxHeight: '10px', + minHeight: '10px', + fontSize: '12px', + }, + interactable: { + zIndex: '50', + }, +}); + +interface PlanStepInputProps { + input: IPlanInput; + onEdit: (newValue: string) => void; + enableEdits: boolean; +} + +export const PlanStepInput: React.FC = ({ input, onEdit, enableEdits }) => { + const classes = useClasses(); + const [isEditingInput, setIsEditingInput] = useState(false); + + const [inputValue, setInputValue] = useState(input.Value); + + const onEditClick = useCallback(() => { + setIsEditingInput(true); + }, []); + + const onSubmitEdit = useCallback(() => { + setIsEditingInput(false); + onEdit(inputValue); + }, [inputValue, onEdit]); + + const onCancel = useCallback(() => { + setIsEditingInput(false); + setInputValue(input.Value); + }, [input.Value]); + + return ( + + {`${input.Key}: `} + {!enableEdits && input.Value} + {enableEdits && ( + <> + {isEditingInput ? ( + { + setInputValue(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + onSubmitEdit(); + } + }} + /> + ) : ( + inputValue + )} + -
)} - {planState === ChatMessageState.PlanApproved && ( + {planState === PlanState.PlanApproved && (
Plan Executed
)} - {planState === ChatMessageState.PlanRejected && ( + {planState === PlanState.PlanRejected && (
Plan Cancelled
)} + {planState === PlanState.NoOp && ( +
+ + + Your app state has changed since this plan was generated, making it unreliable for the planner. + Please request a fresh plan to avoid potential conflicts. + +
+ )}
); }; diff --git a/samples/apps/copilot-chat-app/webapp/src/libs/models/ChatMessage.ts b/samples/apps/copilot-chat-app/webapp/src/libs/models/ChatMessage.ts index 62f3135d22..fc33cf06ab 100644 --- a/samples/apps/copilot-chat-app/webapp/src/libs/models/ChatMessage.ts +++ b/samples/apps/copilot-chat-app/webapp/src/libs/models/ChatMessage.ts @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +import { PlanState } from './Plan'; + /** * Role of the author of a chat message. It's a copy of AuthorRoles in the API C# code. */ @@ -38,12 +40,5 @@ export interface IChatMessage { prompt?: string; authorRole: AuthorRoles; debug?: string; - state?: ChatMessageState; // if plan needs approval -} - -export enum ChatMessageState { - NoOp, - PlanApprovalRequired, - PlanApproved, - PlanRejected, + state?: PlanState; } diff --git a/samples/apps/copilot-chat-app/webapp/src/libs/models/Plan.ts b/samples/apps/copilot-chat-app/webapp/src/libs/models/Plan.ts index 7a2f73b13d..cb9502f358 100644 --- a/samples/apps/copilot-chat-app/webapp/src/libs/models/Plan.ts +++ b/samples/apps/copilot-chat-app/webapp/src/libs/models/Plan.ts @@ -4,15 +4,14 @@ export type IPlanInput = { Value: string; }; -export type IPlanStep = { - skill: string; - function: string; - description: string; - stepInputs: IPlanInput[]; -}; +export enum PlanState { + NoOp, + PlanApproved, + PlanRejected, + PlanApprovalRequired, +} -export type IPlan = { - userIntent: string; - description: string; - steps: IPlanStep[]; -}; +export enum PlanType { + Action, // single-step + Sequential, // multi-step +} diff --git a/samples/apps/copilot-chat-app/webapp/src/libs/useChat.ts b/samples/apps/copilot-chat-app/webapp/src/libs/useChat.ts index 8cc5326bcb..5e88d9f70d 100644 --- a/samples/apps/copilot-chat-app/webapp/src/libs/useChat.ts +++ b/samples/apps/copilot-chat-app/webapp/src/libs/useChat.ts @@ -13,11 +13,15 @@ import { setSelectedConversation, updateConversation, } from '../redux/features/conversations/conversationsSlice'; +import { AuthHeaderTags } from '../redux/features/plugins/PluginsState'; import { AuthHelper } from './auth/AuthHelper'; import { AlertType } from './models/AlertType'; import { Bot } from './models/Bot'; -import { AuthorRoles, ChatMessageState, ChatMessageType, IChatMessage } from './models/ChatMessage'; +import { AuthorRoles, ChatMessageType, IChatMessage } from './models/ChatMessage'; import { IChatSession } from './models/ChatSession'; +import { IChatUser } from './models/ChatUser'; +import { PlanState } from './models/Plan'; +import { IAskVariables } from './semantic-kernel/model/Ask'; import { BotService } from './services/BotService'; import { ChatService } from './services/ChatService'; import * as PlanUtils from './utils/PlanUtils'; @@ -27,17 +31,12 @@ import botIcon2 from '../assets/bot-icons/bot-icon-2.png'; import botIcon3 from '../assets/bot-icons/bot-icon-3.png'; import botIcon4 from '../assets/bot-icons/bot-icon-4.png'; import botIcon5 from '../assets/bot-icons/bot-icon-5.png'; -import { IChatUser } from './models/ChatUser'; - -import { AuthHeaderTags } from '../redux/features/plugins/PluginsState'; export interface GetResponseOptions { messageType: ChatMessageType; value: string; chatId: string; - approvedPlanJson?: string; - planUserIntent?: string; - userCancelledPlan?: boolean; + contextVariables?: IAskVariables[]; } export const useChat = () => { @@ -103,14 +102,7 @@ export const useChat = () => { } }; - const getResponse = async ({ - messageType, - value, - chatId, - approvedPlanJson, - planUserIntent, - userCancelledPlan, - }: GetResponseOptions) => { + const getResponse = async ({ messageType, value, chatId, contextVariables }: GetResponseOptions) => { const ask = { input: value, variables: [ @@ -133,24 +125,8 @@ export const useChat = () => { ], }; - if (approvedPlanJson) { - ask.variables.push( - { - key: 'proposedPlan', - value: approvedPlanJson, - }, - { - key: 'planUserIntent', - value: planUserIntent!, - }, - ); - } - - if (userCancelledPlan) { - ask.variables.push({ - key: 'userCancelledPlan', - value: 'true', - }); + if (contextVariables) { + ask.variables.push(...contextVariables); } try { @@ -175,7 +151,8 @@ export const useChat = () => { content: result.value, prompt: result.variables.find((v) => v.key === 'prompt')?.value, authorRole: AuthorRoles.Bot, - state: PlanUtils.isPlan(result.value) ? ChatMessageState.PlanApprovalRequired : ChatMessageState.NoOp, + id: result.variables.find((v) => v.key === 'messageId')?.value, + state: PlanUtils.isPlan(result.value) ? PlanState.PlanApprovalRequired : PlanState.NoOp, }; dispatch(updateConversation({ message: messageResult, chatId: chatId })); diff --git a/samples/apps/copilot-chat-app/webapp/src/libs/utils/PlanUtils.ts b/samples/apps/copilot-chat-app/webapp/src/libs/utils/PlanUtils.ts index 714413b2c2..563aca7dc1 100644 --- a/samples/apps/copilot-chat-app/webapp/src/libs/utils/PlanUtils.ts +++ b/samples/apps/copilot-chat-app/webapp/src/libs/utils/PlanUtils.ts @@ -1,64 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -import { Constants } from '../../Constants'; -import { IPlan, IPlanInput } from '../models/Plan'; - export const isPlan = (object: string) => { // backslash has to be escaped since it's a JSON string // eslint-disable-next-line const planPrefix = `proposedPlan\":`; return object.indexOf(planPrefix) !== -1; }; - -export const parsePlan = (response: string): IPlan | null => { - if (isPlan(response)) { - try { - const parsedResponse = JSON.parse(response); - const plan = parsedResponse.proposedPlan; - const userIntentPrefix = 'User intent: '; - const index = plan.description.indexOf(userIntentPrefix); - - return { - userIntent: plan.description, - description: index !== -1 ? plan.description.substring(index + userIntentPrefix.length).trim() : '', - steps: extractPlanSteps(plan), - }; - } catch (e: any) { - console.error('Error parsing plan: ' + response); - } - } - return null; -}; - -const extractPlanSteps = (plan: any) => { - // If plan came from Actio Planner, extract step parameters from top-level plan state - const planInputs = extractParameters(plan.state); - - const planSteps = plan.steps; - return planSteps.map((step: any) => { - // If plan came from SequentialPlanner, extract step parameters from respective step object - const stepParameters = extractParameters(step.parameters); - - return { - skill: step['skill_name'], - function: step['name'], - description: step['description'], - stepInputs: planSteps.length === 1 && planInputs.length > 0 ? planInputs : stepParameters, - }; - }); -}; - -const extractParameters = (parametersArray: IPlanInput[]) => { - const parameters: IPlanInput[] = []; - for (var param in parametersArray) { - if ( - // Omit reserved context variable names - !Constants.sk.reservedWords.includes(parametersArray[param].Key.trim()) && - parametersArray[param].Value.trim() !== '' - ) { - parameters.push(parametersArray[param]); - } - } - - return parameters; -}; diff --git a/samples/apps/copilot-chat-app/webapp/src/redux/features/conversations/conversationsSlice.ts b/samples/apps/copilot-chat-app/webapp/src/redux/features/conversations/conversationsSlice.ts index ed1df1c793..6e4105ce9a 100644 --- a/samples/apps/copilot-chat-app/webapp/src/redux/features/conversations/conversationsSlice.ts +++ b/samples/apps/copilot-chat-app/webapp/src/redux/features/conversations/conversationsSlice.ts @@ -1,9 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit'; -import { ChatMessageState, IChatMessage } from '../../../libs/models/ChatMessage'; +import { IChatMessage } from '../../../libs/models/ChatMessage'; +import { PlanState } from '../../../libs/models/Plan'; import { ChatState } from './ChatState'; -import { ConversationInputChange, Conversations, ConversationsState, ConversationTitleChange, initialState } from './ConversationsState'; +import { + ConversationInputChange, + Conversations, + ConversationsState, + ConversationTitleChange, + initialState, +} from './ConversationsState'; export const conversationsSlice: Slice = createSlice({ name: 'conversations', @@ -44,7 +51,7 @@ export const conversationsSlice: Slice = createSlice({ }, updateMessageState: ( state: ConversationsState, - action: PayloadAction<{ newMessageState: ChatMessageState; messageIndex: number; chatId?: string }>, + action: PayloadAction<{ newMessageState: PlanState; messageIndex: number; chatId?: string }>, ) => { const { newMessageState, messageIndex, chatId } = action.payload; const id = chatId ?? state.selectedId;