Files
semantic-kernel/samples/dotnet/KernelHttpServer/SemanticKernelEndpoint.cs
T
Devis Lucato 2607c14b0d Add C# simple planner (#598)
### Motivation and Context

Need for a simple planner showing how to leverage semantic functions to
implement Intent Detection pattern and turn it into a planner, selecting
a function given a goal.

The planner also allows to provide a Planning capability using less
powerful models, like GPT3 (text-davinci-002) and GPT3.5
(text-davinci-003, GPT turbo 3.5), where often the more advanced planner
can hallucinate.

Fix one bug in "WriterSkill/ShortPoem/skprompt.txt" leading to incorrect
results when the goal was missing a termination (period or new line)

### Description

* New package `Microsoft.SemanticKernel.Planning.ActionPlanner`extending
the Planning namespace. The package is automatically installed, ie it's
part of SK metapackage.
* Move SequentialPlanner into the new package
`Microsoft.SemanticKernel.Planning.SequentialPlanner` lib, part of the
metapackage
* Add new Action Planner.
* Store planner prompts to skprompt.txt, using embedded resources to
avoid files overwriting each other.
2023-04-26 20:39:41 +00:00

167 lines
5.9 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using KernelHttpServer.Model;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
namespace KernelHttpServer;
public class SemanticKernelEndpoint
{
private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private readonly IMemoryStore _memoryStore;
public SemanticKernelEndpoint(IMemoryStore memoryStore)
{
this._memoryStore = memoryStore;
}
[Function("InvokeFunction")]
public async Task<HttpResponseData> InvokeFunctionAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "skills/{skillName}/invoke/{functionName}")]
HttpRequestData req,
FunctionContext executionContext, string skillName, string functionName)
{
// in this sample we are using a per-request kernel that is created on each invocation
// once created, we feed the kernel the ask received via POST from the client
// and attempt to invoke the function with the given name
var ask = await JsonSerializer.DeserializeAsync<Ask>(req.Body, s_jsonOptions);
if (ask == null)
{
return await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, "Invalid request, unable to parse the request payload");
}
var kernel = SemanticKernelFactory.CreateForRequest(
req,
executionContext.GetLogger<SemanticKernelEndpoint>(),
ask.Skills,
this._memoryStore);
if (kernel == null)
{
return await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, "Missing one or more expected HTTP Headers");
}
var f = kernel.Skills.GetFunction(skillName, functionName);
var contextVariables = new ContextVariables(ask.Value);
foreach (var input in ask.Inputs)
{
contextVariables.Set(input.Key, input.Value);
}
var result = await kernel.RunAsync(contextVariables, f);
if (result.ErrorOccurred)
{
return await ResponseErrorWithMessageAsync(req, result);
}
var r = req.CreateResponse(HttpStatusCode.OK);
await r.WriteAsJsonAsync(new AskResult { Value = result.Result, State = result.Variables.Select(v => new AskInput { Key = v.Key, Value = v.Value }) });
return r;
}
[Function("CreatePlan")]
public async Task<HttpResponseData> CreatePlanAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "planner/createplan")]
HttpRequestData req,
FunctionContext executionContext)
{
var ask = await JsonSerializer.DeserializeAsync<Ask>(req.Body, s_jsonOptions);
if (ask == null)
{
return await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, "Invalid request, unable to parse the request payload");
}
var kernel = SemanticKernelFactory.CreateForRequest(
req,
executionContext.GetLogger<SemanticKernelEndpoint>(),
ask.Skills);
if (kernel == null)
{
return await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, "Missing one or more expected HTTP Headers");
}
var planner = new SequentialPlanner(kernel);
var goal = ask.Value;
var plan = await planner.CreatePlanAsync(goal);
var r = req.CreateResponse(HttpStatusCode.OK);
await r.WriteAsJsonAsync(new AskResult { Value = plan.ToJson() });
return r;
}
[Function("ExecutePlan")]
public async Task<HttpResponseData> ExecutePlanAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "planner/execute/{maxSteps?}")]
HttpRequestData req,
FunctionContext executionContext, int? maxSteps = 10)
{
var ask = await JsonSerializer.DeserializeAsync<Ask>(req.Body, s_jsonOptions);
if (ask == null)
{
return await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, "Invalid request, unable to parse the request payload");
}
var kernel = SemanticKernelFactory.CreateForRequest(
req,
executionContext.GetLogger<SemanticKernelEndpoint>(),
ask.Skills);
if (kernel == null)
{
return await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, "Missing one or more expected HTTP Headers");
}
var context = kernel.CreateNewContext();
var plan = Plan.FromJson(ask.Value, context);
var iterations = 1;
while (plan.HasNextStep &&
iterations < maxSteps)
{
try
{
plan = await kernel.StepAsync(context.Variables, plan);
}
catch (KernelException e)
{
context.Fail(e.Message, e);
return await ResponseErrorWithMessageAsync(req, context);
}
iterations++;
}
var r = req.CreateResponse(HttpStatusCode.OK);
await r.WriteAsJsonAsync(new AskResult { Value = plan.State.ToString() });
return r;
}
private static async Task<HttpResponseData> ResponseErrorWithMessageAsync(HttpRequestData req, SKContext result)
{
return result.LastException is AIException aiException && aiException.Detail is not null
? await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, string.Concat(aiException.Message, " - Detail: " + aiException.Detail))
: await req.CreateResponseWithMessageAsync(HttpStatusCode.BadRequest, result.LastErrorDescription);
}
}