😸

Semantic Kernel の Planner を用い、複数 Plugin を束ねた処理をする

2024/03/07に公開

長いこと https://normalian.hatenablog.com/ でブログを書いていましたが、昨今の流行に従い Zenn 側でも記事を書いてみようとこちらでも記事を投稿していきます。初回となる今回は Semantic Kernel をテーマにしようと思っています。
Semantic Kernel の概要自体は方々のブログに記事があるので割愛しますが、Microsoft 版 LangChain と思って頂ければ大きな認識祖語はないと思います。以下の公式 GitHub サイトを参照して頂ければ概要自体はつかめることに加え、C# だけでなく Python や Java も言語としてサポートしていることが分かります(各言語ごとに進捗差異はありますが)。
https://github.com/microsoft/semantic-kernel

Planner と Plugin って?

今回テーマにしたいのは掲題でもある Planner です。Semantic Kernel は OpenAI を活用して様々な複雑な問題を解決するオーケストレーションをしてくれますが、OpenAI 自体はファイル操作や HTTP リクエストを飛ばす等の IO 処理を含め、いくつかの処理はそのままではできません。
Semantic Kernel において、そういった個別の拡張的な機能を実現するのが Plugin となります。ファイル操作で有れば以下の様に FileIOPlugin が標準で提供されています。
https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs

FileIOPlugin 自体は標準で提供されている Plugin なので、NuGet で Microsoft.SemanticKernel.Plugins.Core をインストール後、以下の様に ImportPluginFromType メソッドを用いて Semantic Kernel の Kernel インスタンスに読み込むことが可能です。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning.Handlebars;
using Microsoft.SemanticKernel.Plugins.Core;
using System.Text;

using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Warning);
});

#pragma warning disable SKEXP0050, SKEXP0060
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton(loggerFactory);
builder.AddAzureOpenAIChatCompletion(deployment, endpoint, apiKey);

var kernel = builder.Build();
kernel.ImportPluginFromType<FileIOPlugin>();

更にこの読み込んだ Plugin に Planner を活用して実際の処理を行います。上記のコードに加えて以下のソースコードを実行することでテキストファイルの読み込みが可能です。

string filename = @"test.txt ";

// This code as follows can read the file directly.
Console.WriteLine("========================================== Start plan");
{
    // Create a plan
    var planner = new HandlebarsPlanner(new HandlebarsPlannerOptions() { AllowLoops = true });
    var plan = await planner.CreatePlanAsync(kernel, $"Please read {filename} file on current directory");
    Console.WriteLine($"Plan: {plan}");

    // Execute the plan
    Console.WriteLine("========================================== Start execute plan");
    var result = (await plan.InvokeAsync(kernel)).Trim();
    Console.WriteLine($"Results: {result}");
}

上記のコードのうち二つ目の Console.WriteLine($"Plan: {plan}") でどの様な Plan が作成されたかを確認できます(この時点ではファイル操作等は行われません)。実行結果例としては以下の様になります。特に Plan 部分に関して、Planner がどの様な step で Plugin 等を利用して処理を行うかが確認できます。

========================================== Start applicaion
========================================== Start plan
Plan: {{!-- Step 0: Extract Key Values --}}
{{set "path" "test.txt"}}

{{!-- Step 1: Read the file --}}
{{set "fileContent" (FileIOPlugin-Read path=path)}}

{{!-- Step 2: Print the file content --}}
{{json fileContent}}
========================================== Start execute plan
Results: You can read this sentences if your Semantic Kernel application works well.
あなたの予想に反して、この文章が見えているでしょうか?
========================================== End of Application

こちらのコードの完全版は以下にあるので必要な場合は参照ください。
https://github.com/normalian/normalian-sk-samples/blob/main/PlannerFileReadSample01/Program.cs

ちなみに ImportPluginFromType で FileIOPlugin を読み込まずにコードを実行した場合は以下の様な実行結果になります。以下の様にありもしない Example-ReadFile という Plugin を

========================================== Start applicaion
========================================== Start plan
Plan: {{!-- Step 0: Extract key values --}}
{{!-- No key values needed for this goal --}}

{{!-- Step 1: Read test.txt file --}}
{{set "fileContent" (Example-ReadFile path="test.txt")}}

{{!-- Step 2: Print the file content --}}
{{json fileContent}}
========================================== Start execute plan
fail: Microsoft.SemanticKernel.Planning.Handlebars.HandlebarsPlan[0]
      Plan execution failed. Error: [HallucinatedHelpers] The plan references hallucinated helpers: Helper 'Example-ReadFile'
      Microsoft.SemanticKernel.KernelException: [HallucinatedHelpers] The plan references hallucinated helpers: Helper 'Example-ReadFile'
       ---> HandlebarsDotNet.HandlebarsRuntimeException: Template references a helper that cannot be resolved. Helper 'Example-ReadFile'
         at HandlebarsDotNet.Helpers.MissingHelperDescriptor.Invoke(HelperOptions& options, Context& context, Arguments& arguments)
         at HandlebarsDotNet.Helpers.MissingHelperDescriptor.HandlebarsDotNet.Helpers.IHelperDescriptor<HandlebarsDotNet.HelperOptions>.Invoke(HelperOptions& options, Context& context, Arguments& arguments)
         at HandlebarsDotNet.Helpers.LateBindHelperDescriptor.Invoke(HelperOptions& options, Context& context, Arguments& arguments)

Planner で複数の Plugin を呼ぶ

上記だけでは「単にファイルを読む操作を使っただけだよね?」になってしまうので、もう少し複雑なシナリオを考えてみましょう。次は以下を同時に行います。

  • 特定の URL に HTTP リクエストを飛ばす
  • 結果をテキストファイルに保存する

上記での二つの処理は、それぞれ OpenAI 単独では難しい処理で、自分でのソースコードで記載する処理と組み合わせてる必要があります。こちらに関しては以下のソースコードを参照下さい。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning.Handlebars;
using Microsoft.SemanticKernel.Plugins.Core;
using System.Text;

Console.WriteLine("========================================== Start applicaion");

Console.OutputEncoding = Encoding.GetEncoding("utf-8");
var configuration = new ConfigurationBuilder()
    .AddUserSecrets("90cdbb2e-9f8f-4322-8837-f59e9e4b4703")
    .Build();
var deployment = configuration["AzureOpenAI:Deployment"];
var endpoint = configuration["AzureOpenAI:Endpoint"];
var apiKey = configuration["AzureOpenAI:ApiKey"];
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Warning);
});

#pragma warning disable SKEXP0050, SKEXP0060
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton(loggerFactory);
builder.AddAzureOpenAIChatCompletion(deployment, endpoint, apiKey);

var kernel = builder.Build();
kernel.ImportPluginFromType<HttpPlugin>();
kernel.ImportPluginFromType<FileIOPlugin>();

var uri = "https://raw.githubusercontent.com/normalian/My-Azure-Portal-ChromeExtension/master/README.md";
var filename = "response_message.txt";

Console.WriteLine("========================================== Start make plan");
{
    // Create a plan
    var planner = new HandlebarsPlanner(new HandlebarsPlannerOptions() { AllowLoops = true });
    var plan = await planner.CreatePlanAsync(kernel, $"Please send http request to {uri} and save the response as {filename} file on current directory.");
    Console.WriteLine($"Plan: {plan}");

    // Execute the plan
    Console.WriteLine("========================================== Start execute plan");
    var result = (await plan.InvokeAsync(kernel)).Trim();
    Console.WriteLine($"Results: {result}");
}
#pragma warning restore SKEXP0050, SKEXP0060
Console.WriteLine("========================================== End of Application ");

こちらのソースコードの完全版は以下に配置したので、ご参考になれば幸いです。
https://github.com/normalian/normalian-sk-samples/tree/main/PlannerWith2PluginsSample01

GitHubで編集を提案
Microsoft (有志)

Discussion