-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathPlannerPlugin.cs
238 lines (203 loc) · 10.7 KB
/
PlannerPlugin.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
using System.ComponentModel;
using DocumentFormat.OpenXml.Bibliography;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning.Handlebars;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Spectre.Console;
/*
PlannerPlugin: can be used to create a plan for steps that are needed for a process or task
Exposed functions:
- CreateProcessPlan: Create or adjust the plan for the task based on provided details.
- ExecuteProcessPlan: Execute the plan that was created earlier.
- LoadPlanFromFile: Load a plan from a file.
- SavePlanToFile: Save the plan to a file.
- GetPlansList: Gets a list of plans that have been saved.
- GenerateFlowChartForPlan: Displays the plan in a flow chart (using Mermaid chart format and Mermaid.live)
*/
namespace SemanticKernelConsoleCopilotDemo
{
public sealed class PlannerPlugin
{
private HandlebarsPlanner planner;
private Kernel kernel;
private HandlebarsPlan? plan;
private DocuRAGPlugin docuPlugin;
private bool consultCookbookForPlan;
private bool autoExecutePlanAfterCreation;
private bool enableChartGeneration;
private string assistantLanguage;
private KernelFunction mermaidConverterFunction;
// ----------------- Plugin functions -----------------
[KernelFunction, Description("Create or adjust an existing process plan for a given task. ")]
public async Task<string> CreateProcessPlan(
[Description("The task to perform, that can involve multiple steps. Describe the plan based on details provided earlier if relevant. If a plan changed is requested, extend the previous plan. ")] string task,
[Description("Set if the user has requested an adjustment of an existing plan")] bool planChangeRequested = false)
{
var enhancedTask = $"{task}. Don't use the GetProcessGuidance, ExecuteProcessPlan or the CreateProcessPlan function in the Handlebars template.";
var loadText = "Creating a plan...";
if (consultCookbookForPlan && planChangeRequested == false)
{
var guidance = await docuPlugin.GetProcessGuidance(task);
var guidanceDebugOutput = guidance.ToString().Truncate(250);
AnsiConsole.MarkupLineInterpolated($"[dim grey30][dim grey30 underline]Guidance:[/] {guidanceDebugOutput}[/]");
var noHallucinatedHelpers = "Don't use Handlebars helpers that does not exist, e.g. split, substring, indexOf, includes, replace.";
enhancedTask = $"{enhancedTask}. For coming up with a plan, here is some guidance: {System.Environment.NewLine + guidance} {noHallucinatedHelpers}";
}
if (planChangeRequested && plan != null)
{
enhancedTask = $"{enhancedTask}. The user has requested a change in the plan. The previous plan was: {System.Environment.NewLine + plan.ToString()}";
loadText = "Adjusting the plan...";
}
plan = await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.StartAsync(loadText, async (context) =>
{
return await planner.CreatePlanAsync(kernel, enhancedTask);
});
DisplayPlan();
if (autoExecutePlanAfterCreation)
{
return await ExecutePlan();
}
else
{
var planString = plan.ToString();
return $"Plan was created. {System.Environment.NewLine + planString + System.Environment.NewLine} Please check and revise the plan as needed before executing it.";
}
}
[KernelFunction, Description("Execute the plan that was created earlier.")]
public async Task<string> ExecuteProcessPlan()
{
return await ExecutePlan();
}
[KernelFunction, Description("Load a plan from a file. ")]
public async Task<string> LoadPlanFromFile(
[Description("The file path to the plan file. ")] string filePath)
{
var planJson = await System.IO.File.ReadAllTextAsync(filePath);
plan = new HandlebarsPlan(planJson);
DisplayPlan();
return "Plan has been loaded from the file: " + filePath;
}
[KernelFunction, Description("Save the plan to a file. ")]
public async Task<string> SavePlanToFile(
[Description("The filename with a .hbp file extension. The filename should reflect the task for the plan in a few words with underscores. ")] string fileName)
{
var dateYYYMMDDHHMM = System.DateTime.Now.ToString("yyyyMMddHHmm");
var filePath = "output/" + dateYYYMMDDHHMM + '-' + fileName;
if (plan == null)
{
return "No plan has been created yet. Please provide instructions for a plan first.";
};
var planString = plan.ToString();
await System.IO.File.WriteAllTextAsync(filePath, planString);
return "Plan has been saved to the file: " + filePath;
}
[KernelFunction, Description("Gets a list of plans that have been saved ")]
public string[] GetPlansList()
{
var folderPath = "output";
var files = System.IO.Directory.GetFiles(folderPath, "*.hbp");
return files;
}
[KernelFunction, Description("Displays the plan in a flow chart")]
public async Task<string> GenerateFlowChartForPlan()
{
if (!enableChartGeneration)
{
return "";
}
if (plan == null)
{
return "No plan has been created yet. Please provide instructions for a plan first.";
};
var planString = plan.ToString();
var funcResult = await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.StartAsync("Converting plan to Mermaid Chart...", async (context) =>
{
return await kernel.InvokeAsync(mermaidConverterFunction, new() { ["planToConvert"] = planString });
});
var planInMermaidFormat = funcResult.GetValue<string>();
var link = Utils.GenMermaidLiveLink(planInMermaidFormat??"");
AnsiConsole.MarkupLineInterpolated($" [bold][link={link}]Display Flowchart for Plan[/][/] {System.Environment.NewLine}");
return "The chart has been generated. Click the above link to view the chart.";
}
// ----------------- Helper methods & initialization -----------------
private async Task<string> ExecutePlan()
{
if (plan == null)
{
return "No plan has been created yet. Please provide instructions for a plan first.";
};
var result = await plan.InvokeAsync(kernel);
var resultDebugOutput = result.ToString().Truncate(250);
AnsiConsole.MarkupLineInterpolated($"[dim]Plan result: {System.Environment.NewLine + resultDebugOutput}[/]");
AnsiConsole.WriteLine();
return result.ToString();
}
private void DisplayPlan()
{
if (plan == null)
{
return;
};
AnsiConsole.WriteLine();
var planPanel = new Panel(new Markup($"[dim]{Markup.Escape(plan.ToString())}[/]"))
.Border(BoxBorder.Rounded)
.BorderColor(Spectre.Console.Color.Grey50)
.Header("Plan", Justify.Center);
planPanel.Expand = true;
AnsiConsole.Write(planPanel);
}
public PlannerPlugin(Kernel kernel, DocuRAGPlugin docuPlugin,
bool consultCookbookForPlan = false,
bool autoExecutePlanAfterCreation = false,
bool enableChartGeneration = true,
string assistantLanguage = "English")
{
planner = InitPlanner();
this.kernel = kernel;
this.docuPlugin = docuPlugin;
this.consultCookbookForPlan = consultCookbookForPlan;
this.autoExecutePlanAfterCreation = autoExecutePlanAfterCreation;
this.enableChartGeneration = enableChartGeneration;
this.assistantLanguage = assistantLanguage;
this.mermaidConverterFunction = CreateMermaidConverterFunction();
}
private KernelFunction CreateMermaidConverterFunction()
{
string converterPrompt = @"
Convert the plan that uses the Handlebars template syntax to Mermaid flow chart format.
Don't keep any specific details, e.g. names, email addresses, personal details in the Mermaid chart, generalize the plan.
Use proper formatting and tabulators for the Mermaid chart. Should be a TD chart.
You can represent iterations in the plan, e.g. the #each function in the Handlebars template, for example:
flowchart TD
step1-->step2-->step3-->|Loop on step2| step2
step3-->step4
Conditions however should be represented as separate paths in the chart. If there's a condition in the plan, create an expicit true and false path in the chart.
The steps shouldn't just be like step1, step2, but should contain a short description of the action, e.g. 'Send email'.
RETURN JUST THE MERMAID CHART STRING, NO OTHER TEXT OR EXPLANATION. DON'T USE quotes or code blocks in the returned Mermaid chart
Don't use ```mermaid or ``` in the returned Mermaid chart, just the string.
Again, just the string, no other text or explanation or no code blocks.
Process plan to convert:
{{$planToConvert}}
";
return kernel.CreateFunctionFromPrompt(promptTemplate: converterPrompt, functionName: "PlanToMermaidConverter", executionSettings: new OpenAIPromptExecutionSettings() { Temperature = 0.0, TopP = 0.2 });
}
private static HandlebarsPlanner InitPlanner()
{
HandlebarsPlanner planner = new(new HandlebarsPlannerOptions()
{
ExecutionSettings = new OpenAIPromptExecutionSettings()
{
Temperature = 0.0,
TopP = 0.1,
},
AllowLoops = true,
//ExcludedFunctions = new HashSet<string> { "GetProcessGuidance", "ExecuteProcessPlan", "CreateProcessPlan", "GenerateFlowChartForPlan" },
});
return planner;
}
}
}