💭

Semantic Kernel の Process Framework 入門 その 2「分岐したい」

に公開

前回: Semantic Kernel の Process Framework 入門 その 1

Step に関数が複数ある場合の対応

前回に引き続き Process Framework についてやっていきます。今回は Step に関数が複数あるケースから試していこうと思います。前回のコードでは 3 つあるステップ内に関数が 1 つしか無かったため SendEventToStep を指定するだけで動きました。関数が複数あるケースでは、関数名を指定する必要があります。関数名は ProcessFunctionTargetBuilder のコンストラクター引数で指定します。

試しに以下のように Step2AddExclamationAddQuestion の 2 つの関数を定義してみます。

Steps.cs
#pragma warning disable SKEXP0080
using Microsoft.SemanticKernel;

namespace ConsoleApp6;
class Step1 : KernelProcessStep
{
    [KernelFunction]
    public string Execute(string input) => $"Hello {input}";
}

class Step2 : KernelProcessStep
{
    [KernelFunction]
    public string AddExclamation(string input) => $"{input}!";
    [KernelFunction]
    public string AddQuestion(string input) => $"{input}?";
}

class Step3 : KernelProcessStep
{
    [KernelFunction]
    public void Execute(string input) => Console.WriteLine(input);
}

次に Step1 から Step2 に進むときに、どちらに進むかを指定する必要があります。これは Step1KernelFunction の引数で KernelProcessStepContext を受け取って、そこで状況に応じてイベントを発行することで実現できます。Step1 を以下のように変更してみました。

Steps.cs
class Step1 : KernelProcessStep
{
    [KernelFunction]
    public async Task Execute(KernelProcessStepContext context, string input)
    {
        var message = $"Hello {input}";
        if (input == "Kazuki")
        {
            await context.EmitEventAsync("Event1", message);
        }
        else
        {
            await context.EmitEventAsync("Event2", message);
        }
    }
}

私の場合のみ Event1 を発行しています。それ以外の人は Event2 を発行しています。どちらも、パラメーターは Hello {input} を渡しています。
これで状況に応じて Step1 を実行したときに発生するイベントが 2 種類になりました。では、これに対応するようにプロセスも変更していきます。

前回は各 Step の次の Step を指定する際に OnFunctionResult を使っていましたが、これは関数が成功したときのを表します。今回はイベントが発生したときに、次に何を実行するかを指定するため OnEvent を使います。OnEvent にはイベントの ID (ここでは Event1Event2) を指定します。Event1 の時は Step2AddExclamation を実行し、Event2 の時は Step2AddQuestion を実行するようにしてみましょう。

Program.cs
#pragma warning disable SKEXP0080
using ConsoleApp6;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Process.Tools;


var processBuilder = new ProcessBuilder("Sample");
var step1 = processBuilder.AddStepFromType<Step1>();
var step2 = processBuilder.AddStepFromType<Step2>();
var step3 = processBuilder.AddStepFromType<Step3>();

processBuilder
    .OnInputEvent("Start")
    .SendEventTo(new ProcessFunctionTargetBuilder(step1));

// ここがポイント!!分岐!
step1.OnEvent("Event1")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddExclamation)));
step1.OnEvent("Event2")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddQuestion)));

step2.OnFunctionResult()
    .SendEventTo(new ProcessFunctionTargetBuilder(step3));

var process = processBuilder.Build();

// 別のパラメーターでプロセスを起動してみる
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Kazuki" });
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Taro" });

Console.WriteLine(process.ToMermaid());

Kazuki を渡しているほうは末尾が ! になり、Taro を渡しているほうは末尾が ? になるはずです。実行してみましょう。

エラーになりました…。

どうも OnFunctionResult も関数が複数個ある場合は関数名を指定しないといけないみたいです。OnFunctionResult を使って Step2AddExculamationAddQuestion の、どちらが終了した場合でも Step3 に進むようにするには以下のように書かないといけないようです。

Program.cs
#pragma warning disable SKEXP0080
using ConsoleApp6;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Process.Tools;


var processBuilder = new ProcessBuilder("Sample");
var step1 = processBuilder.AddStepFromType<Step1>();
var step2 = processBuilder.AddStepFromType<Step2>();
var step3 = processBuilder.AddStepFromType<Step3>();

processBuilder
    .OnInputEvent("Start")
    .SendEventTo(new ProcessFunctionTargetBuilder(step1));

// ここがポイント!!分岐!
step1.OnEvent("Event1")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddExclamation)));
step1.OnEvent("Event2")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddQuestion)));

// それぞれの関数が終わった後に、Step3にイベントを送る
step2.OnFunctionResult(nameof(Step2.AddExclamation))
    .SendEventTo(new ProcessFunctionTargetBuilder(step3));
step2.OnFunctionResult(nameof(Step2.AddQuestion))
    .SendEventTo(new ProcessFunctionTargetBuilder(step3));

var process = processBuilder.Build();

// 別のパラメーターでプロセスを起動してみる
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Kazuki" });
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Taro" });

Console.WriteLine(process.ToMermaid());

これで実行すると以下のような結果になりました。

Hello Kazuki!
Hello Taro?
flowchart LR
    Start["Start"]
    End["End"]
    Step1["Step1"]
    Step1["Step1"] --> Step2["Step2"]
    Step1["Step1"] --> Step2["Step2"]
    Step2["Step2"]
    Step2["Step2"] --> Step3["Step3"]
    Step2["Step2"] --> Step3["Step3"]
    Step3["Step3"]
    Start --> Step1["Step1"]
    Step3["Step3"] --> End

最初の 2 行が今回の注目したい部分で、ちゃんと !? に分かれています。ついでなので mermaid の部分の図も以下に示します。

イベント名が出てないのが何かイマイチですね…。

今回の Step2 から Step3 に進むときに関数ごとに定義をするのは非常にメンドクサイです。ということで実際には Step2 でもイベントを発行して、そのイベントが発行されたら Step3 に進むようにすることで、定義が簡単になると思います。やってみましょう。まず、Step2 を以下のように変更します。

Steps.cs
class Step2 : KernelProcessStep
{
    [KernelFunction]
    public async Task AddExclamation(KernelProcessStepContext context, string input) => 
        await context.EmitEventAsync("Appended", $"{input}!");

    [KernelFunction]
    public async Task AddQuestion(KernelProcessStepContext context, string input) => 
        await context.EmitEventAsync("Appended", $"{input}?");
}

これで AddExclamationAddQuestion の中で Appended というイベントを発行するようになりました。これに合わせて Program.csStep2 から Step3 に進む部分を以下のように変更します。

Program.cs
#pragma warning disable SKEXP0080
using ConsoleApp6;
using Microsoft.SemanticKernel;


var processBuilder = new ProcessBuilder("Sample");
var step1 = processBuilder.AddStepFromType<Step1>();
var step2 = processBuilder.AddStepFromType<Step2>();
var step3 = processBuilder.AddStepFromType<Step3>();

processBuilder
    .OnInputEvent("Start")
    .SendEventTo(new ProcessFunctionTargetBuilder(step1));

// ここがポイント!!分岐!
step1.OnEvent("Event1")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddExclamation)));
step1.OnEvent("Event2")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddQuestion)));

// Appended イベントが起きたら、Step3 に送信する
step2.OnEvent("Appended")
    .SendEventTo(new ProcessFunctionTargetBuilder(step3));

var process = processBuilder.Build();

// 別のパラメーターでプロセスを起動してみる
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Kazuki" });
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Taro" });

ついでに mermaid の出力処理も消しました。今回はいらないので。これで実行すると以下のような結果になりました。

Hello Kazuki!
Hello Taro?

結果は変わらずですね!

ループをしてみよう

Process Framework では、分岐を使用してループが出来ます。単に差し戻すイベントを発行すればよいだけです。例えば絶対に Kazuki しか受け入れないプロセスにしてみようと思います。そして、それは Step2 でやるようにしてみます。

Steps.cs
class Step2 : KernelProcessStep
{
    [KernelFunction]
    public async Task AddExclamation(KernelProcessStepContext context, string input) => 
        await context.EmitEventAsync("Appended", $"{input}!");

    [KernelFunction]
    public async Task AddQuestion(KernelProcessStepContext context) => 
        await context.EmitEventAsync("Reject", "Kazuki");
}

メソッド名とやっていることが違う感じになってしまいましたが、今回は気にしないでおきましょう。次に Program.cs のプロセスを組み立てているところで Reject イベントが発生したら Step1 に戻るようにします。

Program.cs
#pragma warning disable SKEXP0080
using ConsoleApp6;
using Microsoft.SemanticKernel;


var processBuilder = new ProcessBuilder("Sample");
var step1 = processBuilder.AddStepFromType<Step1>();
var step2 = processBuilder.AddStepFromType<Step2>();
var step3 = processBuilder.AddStepFromType<Step3>();

processBuilder
    .OnInputEvent("Start")
    .SendEventTo(new ProcessFunctionTargetBuilder(step1));

// ここがポイント!!分岐!
step1.OnEvent("Event1")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddExclamation)));
step1.OnEvent("Event2")
    .SendEventTo(new ProcessFunctionTargetBuilder(step2, nameof(Step2.AddQuestion)));

// Reject イベントが起きたら、Step1 に戻す
step2.OnEvent("Reject")
    .SendEventTo(new ProcessFunctionTargetBuilder(step1));
// Appended イベントが起きたら、Step3 に送信する
step2.OnEvent("Appended")
    .SendEventTo(new ProcessFunctionTargetBuilder(step3));

var process = processBuilder.Build();

// 別のパラメーターでプロセスを起動してみる
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Kazuki" });
await process.StartAsync(new Kernel(), new KernelProcessEvent { Id = "Start", Data = "Taro" });

この状態で実行すると、Taro を渡しているケースでは Step2Reject イベントが発生して Step1 に戻ります。その際に渡すデータも Kazuki になっているため最終的に Hello Kazuki! が表示されるはずです。実行してみましょう。

Hello Kazuki!
Hello Kazuki!

ちゃんと両方とも Kazuki になっていますね。ループが出来ていることが確認できました。

まとめ

Semantic Kernel の Process Framework を使って、Step に関数が複数ある場合の対応とループの実装をやってみました。Step に関数が複数ある場合は、関数名を指定する必要がありました。また、ループは差し戻すイベントを発行することで実現できました。

次は Process Framework の Step に状態を持たせることが出来る機能があるみたいなので、それを試してみようと思います。

Microsoft (有志)

Discussion