Semantic Kernel の Process Framework 入門 その 2「分岐したい」
前回: Semantic Kernel の Process Framework 入門 その 1
Step に関数が複数ある場合の対応
前回に引き続き Process Framework についてやっていきます。今回は Step
に関数が複数あるケースから試していこうと思います。前回のコードでは 3 つあるステップ内に関数が 1 つしか無かったため SendEventTo
で Step
を指定するだけで動きました。関数が複数あるケースでは、関数名を指定する必要があります。関数名は ProcessFunctionTargetBuilder
のコンストラクター引数で指定します。
試しに以下のように Step2
に AddExclamation
と AddQuestion
の 2 つの関数を定義してみます。
#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
に進むときに、どちらに進むかを指定する必要があります。これは Step1
の KernelFunction
の引数で KernelProcessStepContext
を受け取って、そこで状況に応じてイベントを発行することで実現できます。Step1
を以下のように変更してみました。
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 (ここでは Event1
と Event2
) を指定します。Event1
の時は Step2
の AddExclamation
を実行し、Event2
の時は Step2
の AddQuestion
を実行するようにしてみましょう。
#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
を使って Step2
の AddExculamation
と AddQuestion
の、どちらが終了した場合でも Step3
に進むようにするには以下のように書かないといけないようです。
#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
を以下のように変更します。
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}?");
}
これで AddExclamation
と AddQuestion
の中で Appended
というイベントを発行するようになりました。これに合わせて Program.cs
の Step2
から Step3
に進む部分を以下のように変更します。
#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
でやるようにしてみます。
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
に戻るようにします。
#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
を渡しているケースでは Step2
で Reject
イベントが発生して Step1
に戻ります。その際に渡すデータも Kazuki
になっているため最終的に Hello Kazuki!
が表示されるはずです。実行してみましょう。
Hello Kazuki!
Hello Kazuki!
ちゃんと両方とも Kazuki
になっていますね。ループが出来ていることが確認できました。
まとめ
Semantic Kernel の Process Framework を使って、Step
に関数が複数ある場合の対応とループの実装をやってみました。Step
に関数が複数ある場合は、関数名を指定する必要がありました。また、ループは差し戻すイベントを発行することで実現できました。
次は Process Framework の Step
に状態を持たせることが出来る機能があるみたいなので、それを試してみようと思います。
Discussion