Skip to content

Suggestion to add sequential plan executor to library #1451

Description

@mattmazzola

I was learning about the planner capabilities by going through the Book creator sample and the Example12_SequentialPlanner

I thought it was odd that this type of code

private static async Task<Plan> ExecutePlanAsync(
IKernel kernel,
Plan plan,
string input = "",
int maxSteps = 10)
{
Stopwatch sw = new();
sw.Start();
// loop until complete or at most N steps
try
{
for (int step = 1; plan.HasNextStep && step < maxSteps; step++)
{
if (string.IsNullOrEmpty(input))
{
await plan.InvokeNextStepAsync(kernel.CreateNewContext());
// or await kernel.StepAsync(plan);
}
else
{
plan = await kernel.StepAsync(input, plan);
input = string.Empty;
}
if (!plan.HasNextStep)
{
Console.WriteLine($"Step {step} - COMPLETE!");
Console.WriteLine(plan.State.ToString());
break;
}
Console.WriteLine($"Step {step} - Results so far:");
Console.WriteLine(plan.State.ToString());
}
}
catch (KernelException e)
{
Console.WriteLine("Step - Execution failed:");
Console.WriteLine(e.Message);
}

was not part of the library. I expected these common use cases to be abstracted.

I actually expected the first PoetrySamplesAsync which uses a different method of running the plan to run it to completion, but from what I saw it only ran the first step of the plan.

The other EmailSamplesAsync and BookSamplesAsync using a different method of calling ExecutePlanAsync

ExecutePlanAsync mixes stopwatch timing, step counting, and printing of log messages which can distract from the core purpose of the example which is to teach developers how to sequentially execute a plan.

I would propose the library has a built in sequential execution implemented as a generator like so

async IAsyncEnumerable<(ContextVariables, Plan, int)> ExecutePlanSequentiallyAsync(
    IKernel kernel,
    Plan plan,
    string input = "")
{
    // for each plan step, invoke step and yield state
    for (int stepIndex = 0; plan.HasNextStep; stepIndex = +1)
    {
        if (string.IsNullOrEmpty(input))
        {
            plan = await kernel.StepAsync(plan);
        }
        else
        {
            plan = await kernel.StepAsync(input, plan);
        }

        yield return (plan.State, plan.Steps.ElementAt(stepIndex), stepIndex);
    }
}

You can then separate the SK responsibility of plan execution from the Application responsibilities (in the case of example) such as writing logs before and after, or breaking after desired step max

PrintStep("Execute Plan:");
var planExecutor = ExecutePlanSequentiallyAsync(kernel, plan);
await foreach (var (state, step, stepIndex) in planExecutor)
{
    PrintStep($"Step {stepIndex + 1}: {plan.Name} {step.Name}");
    Console.WriteLine(state.ToString());
    Console.WriteLine();
}

PrintStep($"Plan Complete");

What do you think about an approach like this?

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

Status
Sprint: Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions