🐥

Semantic Kernel を使ってアプリ内に AI を組み込んでみた

2023/07/19に公開

やってみたこと

こちらのツイートに埋め込まれている動画にあるように、ユーザーがやりたいことを記入するとアプリ内のページから一番目的に近いものを教えてくれるような機能を Semantic Kernel を使って実装してみました。

動画には音声があるので音量注意

https://twitter.com/okazuki/status/1681501853069242368?s=20

動画の内容だと、部分一致とか駆使しても出来そうですが、こちらのように「クッキークリッカーをしたい」といったリクエストに対してカウンターのページを出すというのは AI 使ってる感が出るなと個人的に感じました。

https://twitter.com/okazuki/status/1681516170514690050?s=20

実装

実装ですが、Semantic Kernel の ActionPlanner を使って実装しています。
ActionPlanner は Semantic Kernel の Kernel に登録したプラグイン (Skills) から、ユーザーがやりたいことに対して最適なものを選んでくれる機能を提供します。今回のコードでは、Kernel にページの数だけページの情報を提供するというプラグインを登録しておき、ActionPlanner で一番いいページを選んでもらっています。

ページの情報の管理を行うクラスは以下のように定義しました。単純なリストのラッパーですね。

namespace BlazorApp12.AIPlugins;

public class PageInfoProvider
{
    private readonly List<PageInfo> _pages = new();

    public void AddPage(string name, string path, string description)
    {
        _pages.Add(new (name, path, description));
    }

    public IEnumerable<PageInfo> GetPages() { return _pages.AsEnumerable(); }
}

public record PageInfo(string Name, string Path, string Description);

そして、このクラスにある情報を使って Kernel にプラグインを登録して ActionPlanner を使って最適なものを選ぶというロジックを持ったクラスを作成しました。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.SkillDefinition;
using System.Text.Json;

namespace BlazorApp12.AIPlugins;

public class PageSuggestionService
{
    private IKernel _kernel;

    public PageSuggestionService(IKernel kernel, PageInfoProvider pageInfoProvider)
    {
        ImportPagePlugin(kernel, pageInfoProvider.GetPages());
        _kernel = kernel;
    }

    public static void ImportPagePlugin(IKernel kernel, IEnumerable<PageInfo> pages)
    {
        // ページの情報をもとに、ページの情報を返す Semantic 関数を登録しておく。
        foreach (var page in pages)
        {
            var json = JsonSerializer.Serialize(page);
            kernel.RegisterCustomFunction(
                SKFunction.FromNativeFunction(
                    () => json,
                    nameof(PageInfoProvider),
                    page.Name,
                    page.Description));
        }
    }

    public async Task<PageInfo?> SuggestPageAsync(string goal)
    {
        // ActionPlanner を使って最適なページを選んでもらう。
        var planner = new ActionPlanner(_kernel);
        var plan = await planner.CreatePlanAsync(goal);
        if (!plan.HasNextStep)
        {
            return null;
        }

        // 結果は JSON 文字列なのでオブジェクトにして返す
        var planResult = await plan.InvokeAsync();
        return JsonSerializer.Deserialize<PageInfo>(planResult.Result);
    }
}

コンストラクタで IKernel を受け取り PageInfoProvider に登録された情報を使って RegisterCustomFunction で関数を登録しておきます。
このとき、きちんと Description を指定しておくことで ActionPlanner に選んでもらうときのヒントとして使ってもらえるようになります。

これが出来たら、あとは Program.cs で以下のように各種サービスを登録します。

Program.cs
// テンプレートエンジンは1個あればいいのでシングルトン
builder.Services.AddSingleton<IPromptTemplateEngine, PromptTemplateEngine>();

// IKernel は毎回インスタンス化したいので Transient
builder.Services.AddTransient(sp =>
{
    var section = builder.Configuration.GetSection("AzureChatCompletionService");
    return Kernel.Builder
        .WithAzureChatCompletionService(
            section.GetValue<string>("DeployName")!,
            section.GetValue<string>("Endpoint")!,
            new DefaultAzureCredential(new DefaultAzureCredentialOptions
            {
                ExcludeVisualStudioCredential = true,
            }))
        .WithLogger(sp.GetRequiredService<ILogger<Kernel>>())
        .WithPromptTemplateEngine(sp.GetRequiredService<IPromptTemplateEngine>())
        .Build();
});

// ページの情報を管理するクラスを登録
builder.Services.AddSingleton(sp =>
{
    var pageNavigationPlugins = new PageInfoProvider();
    pageNavigationPlugins.AddPage("WeatherForecast", "showdata", "気温の予報の一覧を表示するページです。");
    pageNavigationPlugins.AddPage("StockForecast", "showdata2", "株価の予測の一覧を表示するページです。");
    pageNavigationPlugins.AddPage("Counter", "counter", "ボタンを押した回数をカウントするページ");
    return pageNavigationPlugins;
});

// ページのお勧めをしてくれるクラスを登録
builder.Services.AddScoped<PageSuggestionService>();

ここまでお膳立てが出来たら、ページ内で PageSuggestionService を使って画面を組み立てるだけです。先ほどの動画の画面のコードは以下のようになっています。

Index.razor
@page "/"
@using BlazorApp12.AIPlugins;
@inject PageSuggestionService PageSuggestionService
@attribute [RenderModeServer]

<PageTitle>Index</PageTitle>

<h1>やりたいことが出来るページを探す Copilot サンプル</h1>

<p>やりたいことを入力して探すボタンを押してください。</p>

<input type="text" @bind-value="_goal" />
<button @onclick="SearchAsync">探す</button>

<hr />

@if (_loading)
{
    <div>探しています...</div>
}
else
{
    @if (_suggestedPageInfo is null)
    {
        <div>やりたいことに対するページはまだ見つかりませんでした。</div>
    }
    else
    {
        <div>
            <h3>見つかったページ</h3>
            <dl>
                <dt>@_suggestedPageInfo.Name</dt>
                <dd>@_suggestedPageInfo.Description</dd>
            </dl>
            <a href="@_suggestedPageInfo.Path">ページに移動する</a>
        </div>
    }
}

@code {
    private bool _loading = false;
    private string _goal = "";
    private PageInfo? _suggestedPageInfo;
    private async Task SearchAsync()
    {
        _loading = true;
        try
        {
            _suggestedPageInfo = await PageSuggestionService.SuggestPageAsync(_goal);
        }
        finally
        {
            _loading = false;
        }
    }
}

これはそんなに難しくないですね。PageSuggestionService に Semantic Kernel 成分は全て閉じ込めているのでページ内では特別なことはしていません。

まとめ

ざくっとですが Copilot っぽいことをしてくれるようなアプリを作りました。
ページの提示だけなので、Copilot と呼ぶには弱いかも…とは思っていますが画面がそこそこ多いアプリなどでは、同じような仕組みでユーザーが迷わないように使うべきページを提案するといった機能として使うことが出来るかもしれません。

Copilot っぽさを出そうと思ったら、もう少しユーザーの要望も長文で打ち込んで、そこからパラメーターを抜き出して予め画面に設定しておくといったところまでやってくれたらいいと思うのですが、それも ActionPlanner を使うと出来そうな気がするので気が向いたらやってみようかなぁと思います。

ソースコードは GitHub に上げています。
.NET 8 Preview 6 で作ってしまったので、動かす場合は環境構築に注意してください。.NET 7 では動きません。

https://github.com/runceel/AddCopilotFeaturesApp

Microsoft (有志)

Discussion