🔥

Semantic Kernel で Plan を進捗情報も含めて永続化したい

2023/05/20に公開

Semantic Kernel の Plan は複数ステップのプランを 1 ステップずつ実行することが出来ます。
その時に Plan をいったん途中で止めて後で続きをやりたいというケースがあると思います。Semantic Kernel のサンプルでは JSON 形式にシリアライズしているケースがありますが実はデシリアライズもサポートされています。

シリアライズは plan.ToJson() を使い、デシリアライズは Plan Plan.FromJson(string json, SKContext context) を使います。JSON から Plan オブジェクトを組み立てた後に context から ISKFunction を取得して設定してくれます。context は省略可能ですが、きちんとデシリアライズしたい場合は context を渡してください。

1 つ前の記事で作ったお手製の Plan を少し書き換えてシリアライズ、デシリアライズを試してみたいと思います。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.SkillDefinition;

var kernel = KernelBuilder.Create();
// デバッグ用のネイティブスキルを登録
kernel.ImportSkill(new DebugSkill("test1", "Test1 result"), "test1");
kernel.ImportSkill(new DebugSkill("test2", "Test2 result"), "test2");
kernel.ImportSkill(new DebugSkill("test3", "Test3 result"), "test3");

// import したスキルの関数をラップしたプランを作成
var test1 = new Plan(kernel.Func("test1", "invoke"))
{
    // 出力を test1Result という名前で Context に格納する
    Outputs = { "test1Result" },
};
var test2 = new Plan(kernel.Func("test2", "invoke"))
{
    // 出力を test2Result という名前で Context に格納する
    Outputs = { "test2Result" },
    // 入力として test1Result を受け取る
    Parameters = new ContextVariables()
    {
        ["test1Result"] = "",
    },
};

var test3 = new Plan(kernel.Func("test3", "invoke"))
{
    // 入力として test1Result と test2Result を受け取る。
    // test2Result は customNameParameter という名前で受け取る 
    Parameters = new ContextVariables()
    {
        ["test1Result"] = "",
        ["customNameParameter"] = "$test2Result",
    },
};

// その 3 つのプランを順番に実行するプランを作成
var plan = new Plan("Planの動きを確認する。", test1, test2, test3);

var context = kernel.CreateNewContext();
while (plan.HasNextStep)
{
    await plan.InvokeNextStepAsync(context);
    var json = plan.ToJson();
    plan = Plan.FromJson(json, context);
}

Console.WriteLine($"Result: {plan.State.Input}");


// ログを出力して指定した結果を返すだけのデバッグ目的のスキル
class DebugSkill
{
    private readonly string _name;
    private readonly string _skillResult;

    public DebugSkill(string name, string skillResult)
    {
        _name = name;
        _skillResult = skillResult;
    }

    [SKFunction("テスト用関数")]
    public string Invoke(string input, SKContext context)
    {
        Console.WriteLine($"{_name} was invoked with {input}.");
        foreach (var variable in context.Variables)
        {
            Console.WriteLine($"  {variable.Key}: {variable.Value}");
        }
        Console.WriteLine($"  Result value: {_skillResult}");

        return _skillResult;
    }
}

実行結果は以下のようになります。

test1 was invoked with Planの動きを確認する。.
  INPUT: Planの動きを確認する。
  Result value: Test1 result
test2 was invoked with Test1 result.
  INPUT: Test1 result
  test1Result: Test1 result
  Result value: Test2 result
test3 was invoked with Test2 result.
  INPUT: Test2 result
  test1Result: Test1 result
  customNameParameter: Test2 result
  Result value: Test3 result
Result: Test3 result

Plan の実行を InvokeNextStepAsync で 1 ステップずつ進めています。1 ステップ進めるたびに ToJson でシリアライズして Plan.FromJson でデシリアライズしています。
その状態でも、ちゃんと状態を保持した状態で Plan を進めることが出来ています。

まとめ

非常に簡単にですが Plan のシリアライズとデシリアライズについて紹介しました。
これを使うと Plan を DB に保存しておいて後でやるということも出来ますし、色々つかいみちがありそうです。

Microsoft (有志)

Discussion