mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Enabling Plan Edits in Copilot (#1386)
### Motivation and Context This PR enables user to edit a plan before approving or rejecting it: - User can edit any of the input value fields - User can delete step(s) (If SequentialPlanner is enabled). Deletion is disabled when there is only one step. When plan is returned to the Kernel, the chat message will be updated in the history repository to reflect the new plan state. ### Description This PR also contains optimizations for methods handling user actions on proposed plan and plan schema changes to enable the above. Chat History will also now reflect any actions the user took on the plan. Input Edit Mode: <img width="665" alt="image" src="https://github.com/microsoft/semantic-kernel/assets/125500434/31b7a5b5-9ca0-432a-bde1-3141e0e8e76f"> Editing Action Plan (inputs only):  Editing SequentialPlanner (inputs and deleting step):  Added 'NoOp' Plan state, where proposedPlan becomes unactionable if app state changes before user approves or rejects the plan. Potential reasons for unstable state: required plug-in no longer enabled, access permissions changed, etc.  > Note: there's some hallucinations with SequentialPlanner, but planner default is Action. SequentialPlanner stabilizations will continue in another PR
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a single proposed plan.
|
||||
/// </summary>
|
||||
@@ -16,12 +31,26 @@ public class ProposedPlan
|
||||
[JsonPropertyName("proposedPlan")]
|
||||
public Plan Plan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether plan is Action (single-step) or Sequential (multi-step).
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public PlanType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// State of plan
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public PlanState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new proposed plan.
|
||||
/// </summary>
|
||||
/// <param name="plan">Proposed plan object</param>
|
||||
public ProposedPlan(Plan plan)
|
||||
public ProposedPlan(Plan plan, PlanType type, PlanState state)
|
||||
{
|
||||
this.Plan = plan;
|
||||
this.Type = type;
|
||||
this.State = state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public PlanType Type { get; set; } = PlanType.Action;
|
||||
}
|
||||
|
||||
@@ -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<ProposedPlan>(
|
||||
new ProposedPlan(this._externalInformationSkill.ProposedPlan));
|
||||
return JsonSerializer.Serialize<ProposedPlan>(this._externalInformationSkill.ProposedPlan);
|
||||
}
|
||||
|
||||
// 4. Query relevant semantic memories
|
||||
@@ -380,6 +390,7 @@ public class ChatSkill
|
||||
/// </summary>
|
||||
private async Task<string> 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
|
||||
/// <summary>
|
||||
/// Helper function create the correct context variables to acquire external information.
|
||||
/// </summary>
|
||||
private Task<string> AcquireExternalInformationAsync(SKContext context, string userIntent, int tokenLimit)
|
||||
private async Task<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates previously saved response in the chat history.
|
||||
/// </summary>
|
||||
/// <param name="updatedResponse">Updated response from the chat.</param>
|
||||
/// <param name="messageId">The chat message ID</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a completion settings object for chat response. Parameters are read from the PromptSettings class.
|
||||
/// </summary>
|
||||
|
||||
+7
-1
@@ -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
|
||||
/// </summary>
|
||||
private readonly PlannerOptions? _plannerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pptions for the planner.
|
||||
/// </summary>
|
||||
public PlannerOptions? PlannerOptions => this._plannerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CopilotChatPlanner"/> class.
|
||||
/// </summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+8
-5
@@ -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
|
||||
/// <summary>
|
||||
/// Proposed plan to return for approval.
|
||||
/// </summary>
|
||||
public Plan? ProposedPlan { get; private set; }
|
||||
public ProposedPlan? ProposedPlan { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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<ProposedPlan>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ChatHistoryItemProps> = ({ 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<ChatHistoryItemProps> = ({ message, getRe
|
||||
: { name: fullName, color: 'colorful' as 'colorful' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={isMe ? mergeClasses(classes.root, classes.alignEnd) : classes.root}
|
||||
data-testid={`chat-history-item-${messageIndex}`}
|
||||
data-username={fullName}
|
||||
>
|
||||
{!isMe && <Persona className={classes.persona} avatar={avatar} presence={{ status: 'available' }} />}
|
||||
<div className={isMe ? mergeClasses(classes.item, classes.me) : classes.item}>
|
||||
<div className={classes.header}>
|
||||
{!isMe && <Text weight="semibold">{fullName}</Text>}
|
||||
<Text className={classes.time}>{timestampToDateString(message.timestamp, true)}</Text>
|
||||
{isBot && <PromptDetails message={message} />}
|
||||
</div>
|
||||
{!isPlan && (
|
||||
<div
|
||||
className={classes.content}
|
||||
dangerouslySetInnerHTML={{ __html: convertToAnchorTags(content) }}
|
||||
/>
|
||||
)}
|
||||
{isPlan && (
|
||||
<PlanViewer
|
||||
plan={plan}
|
||||
planState={message.state ?? ChatMessageState.NoOp}
|
||||
onSubmit={onPlanApproval}
|
||||
onCancel={onPlanCancel}
|
||||
/>
|
||||
)}
|
||||
<div className={isMe ? mergeClasses(classes.root, classes.alignEnd) : classes.root}>
|
||||
{!isMe && <Persona className={classes.persona} avatar={avatar} presence={{ status: 'available' }} />}
|
||||
<div className={isMe ? mergeClasses(classes.item, classes.me) : classes.item}>
|
||||
<div className={classes.header}>
|
||||
{!isMe && <Text weight="semibold">{fullName}</Text>}
|
||||
<Text className={classes.time}>{timestampToDateString(message.timestamp, true)}</Text>
|
||||
{isBot && <PromptDetails message={message} />}
|
||||
</div>
|
||||
{renderPlan ? (
|
||||
<PlanViewer message={message} messageIndex={messageIndex} getResponse={getResponse} />
|
||||
) : (
|
||||
<div
|
||||
className={classes.content}
|
||||
dangerouslySetInnerHTML={{ __html: convertToAnchorTags(content) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ const useClasses = makeStyles({
|
||||
height: '100%',
|
||||
},
|
||||
scroll: {
|
||||
...shorthands.margin('4px'),
|
||||
...shorthands.margin(tokens.spacingVerticalXS),
|
||||
...SharedStyles.scroll,
|
||||
},
|
||||
history: {
|
||||
|
||||
+128
-19
@@ -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<PlanStepCardProps> = ({ index, step }) => {
|
||||
export const PlanStepCard: React.FC<PlanStepCardProps> = ({ 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 (
|
||||
<Card className={classes.card}>
|
||||
@@ -63,23 +101,94 @@ export const PlanStepCard: React.FC<PlanStepCardProps> = ({ index, step }) => {
|
||||
<CardHeader
|
||||
header={
|
||||
<Body1>
|
||||
<b className={classes.header}>Step {stepNumber} •</b> {step.skill}.{step.function}
|
||||
<b className={classes.header}>Step {step.index + 1} •</b> {step.skill_name}.{step.name}
|
||||
<br />
|
||||
</Body1>
|
||||
}
|
||||
action={
|
||||
enableEdits && enableStepDelete ? (
|
||||
<Dialog open={openDialog}>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button
|
||||
appearance="transparent"
|
||||
icon={<Dismiss12Regular />}
|
||||
aria-label="Delete step"
|
||||
onClick={() => {
|
||||
setOpenDialog(true);
|
||||
}}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<DialogSurface className={classes.dialog}>
|
||||
<DialogBody>
|
||||
<DialogTitle>Are you sure you want to delete this step?</DialogTitle>
|
||||
<DialogContent>
|
||||
Deleting this step could disrupt the plan's initial logic and cause
|
||||
errors in subsequent steps. Make sure the next steps don't depend on
|
||||
this step's outputs.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<DialogTrigger disableButtonEnhancement>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
onClick={() => {
|
||||
setOpenDialog(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Button
|
||||
appearance="primary"
|
||||
onClick={() => {
|
||||
setOpenDialog(false);
|
||||
onDeleteStep(step.index);
|
||||
}}
|
||||
>
|
||||
Yes, Delete Step
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{step.description && (
|
||||
<div className={classes.singleLine}>
|
||||
<Text weight="semibold">About: </Text> <Text>{step.description}</Text>
|
||||
</div>
|
||||
)}
|
||||
{step.stepInputs.length > 0 && (
|
||||
<div className={classes.inputs}>
|
||||
{inputs.length > 0 && (
|
||||
<div className={classes.parameters}>
|
||||
<Text weight="semibold">Inputs: </Text>
|
||||
{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 (
|
||||
<Badge color="informative" shape="rounded" appearance="tint" key={input.Key}>
|
||||
{`${input.Key}: ${input.Value}`}
|
||||
<PlanStepInput
|
||||
input={input}
|
||||
key={input.Key}
|
||||
onEdit={onEditInput}
|
||||
enableEdits={enableEdits}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{step.outputs.length > 0 && (
|
||||
<div className={classes.parameters}>
|
||||
<Text weight="semibold">Outputs: </Text>
|
||||
{step.outputs.map((output: string) => {
|
||||
return (
|
||||
<Badge color="informative" shape="rounded" appearance="tint" key={output}>
|
||||
{output}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
|
||||
+96
@@ -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<PlanStepInputProps> = ({ 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 (
|
||||
<Badge color="informative" shape="rounded" appearance="tint" className={classes.root}>
|
||||
{`${input.Key}: `}
|
||||
{!enableEdits && input.Value}
|
||||
{enableEdits && (
|
||||
<>
|
||||
{isEditingInput ? (
|
||||
<input
|
||||
className={mergeClasses(classes.input, classes.interactable)}
|
||||
style={{ width: input.Value.length * 6, minWidth: '75px' }}
|
||||
placeholder={input.Value}
|
||||
value={inputValue}
|
||||
onChange={(event: any) => {
|
||||
setInputValue(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
onSubmitEdit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
inputValue
|
||||
)}
|
||||
<Button
|
||||
icon={isEditingInput ? <Checkmark16Regular /> : <Edit16Regular />}
|
||||
appearance="transparent"
|
||||
className={mergeClasses(classes.buttons, classes.interactable)}
|
||||
onClick={isEditingInput ? onSubmitEdit : onEditClick}
|
||||
/>
|
||||
{isEditingInput && (
|
||||
<Button
|
||||
icon={<Dismiss16Regular />}
|
||||
appearance="transparent"
|
||||
className={mergeClasses(classes.buttons, classes.interactable)}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
+118
-24
@@ -1,12 +1,18 @@
|
||||
import { Button, Text, makeStyles, mergeClasses, shorthands } from '@fluentui/react-components';
|
||||
import { CheckmarkCircle24Regular, DismissCircle24Regular } from '@fluentui/react-icons';
|
||||
import { ChatMessageState } from '../../../libs/models/ChatMessage';
|
||||
import { IPlan, IPlanStep } from '../../../libs/models/Plan';
|
||||
import { Button, Text, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components';
|
||||
import { CheckmarkCircle24Regular, DismissCircle24Regular, Info24Regular } from '@fluentui/react-icons';
|
||||
import { useState } from 'react';
|
||||
import { ChatMessageType, IChatMessage } from '../../../libs/models/ChatMessage';
|
||||
import { IPlanInput, PlanState, PlanType } from '../../../libs/models/Plan';
|
||||
import { IAskVariables } from '../../../libs/semantic-kernel/model/Ask';
|
||||
import { GetResponseOptions } from '../../../libs/useChat';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/app/hooks';
|
||||
import { RootState } from '../../../redux/app/store';
|
||||
import { updateMessageState } from '../../../redux/features/conversations/conversationsSlice';
|
||||
import { PlanStepCard } from './PlanStepCard';
|
||||
|
||||
const useClasses = makeStyles({
|
||||
container: {
|
||||
...shorthands.gap('11px'),
|
||||
...shorthands.gap(tokens.spacingVerticalM),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'baseline',
|
||||
@@ -14,12 +20,12 @@ const useClasses = makeStyles({
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginTop: '12px',
|
||||
marginBottom: '12px',
|
||||
...shorthands.gap('16px'),
|
||||
marginTop: tokens.spacingVerticalM,
|
||||
marginBottom: tokens.spacingVerticalM,
|
||||
...shorthands.gap(tokens.spacingHorizontalL),
|
||||
},
|
||||
status: {
|
||||
...shorthands.gap('10px'),
|
||||
...shorthands.gap(tokens.spacingHorizontalMNudge),
|
||||
},
|
||||
text: {
|
||||
alignSelf: 'center',
|
||||
@@ -27,49 +33,137 @@ const useClasses = makeStyles({
|
||||
});
|
||||
|
||||
interface PlanViewerProps {
|
||||
plan: IPlan;
|
||||
planState: ChatMessageState;
|
||||
onSubmit: () => Promise<void>;
|
||||
onCancel: () => Promise<void>;
|
||||
message: IChatMessage;
|
||||
messageIndex: number;
|
||||
getResponse: (options: GetResponseOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
export const PlanViewer: React.FC<PlanViewerProps> = ({ plan, planState, onSubmit, onCancel }) => {
|
||||
export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, getResponse }) => {
|
||||
const classes = useClasses();
|
||||
var stepCount = 1;
|
||||
const dispatch = useAppDispatch();
|
||||
const { selectedId } = useAppSelector((state: RootState) => state.conversations);
|
||||
|
||||
// Track original plan from user message
|
||||
const parsedContent = JSON.parse(message.content);
|
||||
const originalPlan = parsedContent.proposedPlan;
|
||||
|
||||
var planState = message.state ?? parsedContent.state;
|
||||
|
||||
// If plan came from ActionPlanner, use parameters from top-level plan state
|
||||
if (parsedContent.type === PlanType.Action) {
|
||||
originalPlan.steps[0].parameters = originalPlan.state;
|
||||
}
|
||||
|
||||
const userIntentPrefix = 'User Intent:User intent: ';
|
||||
const userIntentIndex = originalPlan.description.indexOf(userIntentPrefix);
|
||||
const description =
|
||||
userIntentIndex !== -1
|
||||
? originalPlan.description.substring(userIntentIndex + userIntentPrefix.length).trim()
|
||||
: '';
|
||||
|
||||
const [plan, setPlan] = useState(originalPlan);
|
||||
|
||||
const onPlanAction = async (planState: PlanState.PlanApproved | PlanState.PlanRejected) => {
|
||||
dispatch(
|
||||
updateMessageState({
|
||||
newMessageState: planState,
|
||||
messageIndex: messageIndex,
|
||||
chatId: selectedId,
|
||||
}),
|
||||
);
|
||||
|
||||
const contextVariables: IAskVariables[] = [
|
||||
{
|
||||
key: 'responseMessageId',
|
||||
value: message.id ?? '',
|
||||
},
|
||||
{
|
||||
key: 'proposedPlan',
|
||||
value: JSON.stringify({
|
||||
proposedPlan: plan,
|
||||
type: parsedContent.type,
|
||||
state: planState,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
contextVariables.push(
|
||||
planState === PlanState.PlanApproved
|
||||
? {
|
||||
key: 'planUserIntent',
|
||||
value: description,
|
||||
}
|
||||
: {
|
||||
key: 'userCancelledPlan',
|
||||
value: 'true',
|
||||
},
|
||||
);
|
||||
|
||||
// Invoke plan
|
||||
await getResponse({
|
||||
value: planState === PlanState.PlanApproved ? 'Yes, proceed' : 'No, cancel',
|
||||
contextVariables: contextVariables,
|
||||
messageType: ChatMessageType.Plan,
|
||||
chatId: selectedId,
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteStep = (index: number) => {
|
||||
setPlan({
|
||||
...plan,
|
||||
steps: plan.steps.filter((_step: IPlanInput, i: number) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<Text>Based on the request, Copilot Chat will run the following steps:</Text>
|
||||
<Text weight="bold">{`Goal: ${plan.description}`}</Text>
|
||||
{plan.steps.map((step: IPlanStep) => {
|
||||
const stepIndex = stepCount++;
|
||||
return <PlanStepCard key={`Plan step: ${stepIndex}`} index={stepIndex} step={step} />;
|
||||
<Text weight="bold">{`Goal: ${description}`}</Text>
|
||||
{plan.steps.map((step: any, index: number) => {
|
||||
return (
|
||||
<PlanStepCard
|
||||
key={`Plan step: ${index}`}
|
||||
step={{ ...step, index: index }}
|
||||
enableEdits={planState === PlanState.PlanApprovalRequired}
|
||||
enableStepDelete={plan.steps.length > 1}
|
||||
onDeleteStep={onDeleteStep}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{planState === ChatMessageState.PlanApprovalRequired && (
|
||||
{planState === PlanState.PlanApprovalRequired && (
|
||||
<>
|
||||
Would you like to proceed with the plan?
|
||||
<div className={classes.buttons}>
|
||||
<Button appearance="secondary" onClick={onCancel}>
|
||||
<Button appearance="secondary" onClick={() => onPlanAction(PlanState.PlanRejected)}>
|
||||
No, cancel plan
|
||||
</Button>
|
||||
<Button type="submit" appearance="primary" onClick={onSubmit}>
|
||||
<Button type="submit" appearance="primary" onClick={() => onPlanAction(PlanState.PlanApproved)}>
|
||||
Yes, proceed
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{planState === ChatMessageState.PlanApproved && (
|
||||
{planState === PlanState.PlanApproved && (
|
||||
<div className={mergeClasses(classes.buttons, classes.status)}>
|
||||
<CheckmarkCircle24Regular />
|
||||
<Text className={classes.text}> Plan Executed</Text>
|
||||
</div>
|
||||
)}
|
||||
{planState === ChatMessageState.PlanRejected && (
|
||||
{planState === PlanState.PlanRejected && (
|
||||
<div className={mergeClasses(classes.buttons, classes.status)}>
|
||||
<DismissCircle24Regular />
|
||||
<Text className={classes.text}> Plan Cancelled</Text>
|
||||
</div>
|
||||
)}
|
||||
{planState === PlanState.NoOp && (
|
||||
<div className={mergeClasses(classes.buttons, classes.status)}>
|
||||
<Info24Regular />
|
||||
<Text className={classes.text}>
|
||||
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.
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+10
-3
@@ -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<ConversationsState> = createSlice({
|
||||
name: 'conversations',
|
||||
@@ -44,7 +51,7 @@ export const conversationsSlice: Slice<ConversationsState> = 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;
|
||||
|
||||
Reference in New Issue
Block a user