.NET の Semantic Kernel v1.0 がリリースされたので再復習してみた

2023/12/26に公開

Semantic Kernel の v1.0 が 2023/12/19 にリリースされました。
OpenAI 系のものを触っていて困るのは、明日には API が変わっているかもしれないということです。
今回リリースされた Semantic Kernel v1.0 でリリースされた範囲の API は割と安定していると期待できるので、ここで学んでおくのは良いタイミングのはずです!ということで復習開始。

提供されているパッケージ

NuGet に公開されている v1.0.1 の Microsoft.SemanticKernel には以下のパッケージがあります。
これらのパッケージにあるものは、安定版という扱いになります。

  • Microsoft.SemanticKernel
  • Microsoft.SemanticKernel.Core
  • Microsoft.SemanticKernel.Abstractions
  • Microsoft.SemanticKernel.Connectors.OpenAI
  • Microsoft.SemanticKernel.Yaml
  • Microsoft.SemanticKernel.PromptTemplates.Handlebars

その他にも v1.0.1-preview というバージョンのパッケージと v1.0.1-alpha というバージョンのパッケージもあります。
v1.0.1-preview は v1.1 のタイミングでのリリース見込みで必要に応じて少し変更が加えられる可能性があります。v1.0.1-alpha は、まだまだこれからのパッケージなので今後も破壊的変更が入る可能性が高いです。

v1.0.1-preview は以下のパッケージがあります。
Planners 関連はプレビューという点が注意ですね。

  • Microsoft.SemanticKernel.Connectors.HuggingFace
  • Microsoft.SemanticKernel.Planners.Handlebars
  • Microsoft.SemanticKernel.Planners.OpenAI

v1.0.1-alpha は以下のパッケージがあります。各種 DB への接続機能や、組込みで提供されるプラグイン各種も基本的には安定していないということになります。
ここら辺は注意ですね。今の所は、ここで提供されている機能を使いたければ破壊的変更を覚悟して使うか、とりあえず自作するかといったことになります。
そのため、OpenApi の定義をアプリにプラグインとして組み込むといったことは安定版 API だけだと、ぱっとは出来ないのが残念ですね。今後に期待。

  • Microsoft.SemanticKernel.Connectors.AzureAISearch
  • Microsoft.SemanticKernel.Connectors.Chroma
  • Microsoft.SemanticKernel.Connectors.DuckDB
  • Microsoft.SemanticKernel.Connectors.Kusto
  • Microsoft.SemanticKernel.Connectors.Milvus
  • Microsoft.SemanticKernel.Connectors.MongoDB
  • Microsoft.SemanticKernel.Connectors.Pinecone
  • Microsoft.SemanticKernel.Connectors.Postgres
  • Microsoft.SemanticKernel.Connectors.Qdrant
  • Microsoft.SemanticKernel.Connectors.Redis
  • Microsoft.SemanticKernel.Connectors.Sqlite
  • Microsoft.SemanticKernel.Connectors.Weaviate
  • Microsoft.SemanticKernel.Experimental.Agents
  • Microsoft.SemanticKernel.Markdown
  • Microsoft.SemanticKernel.Plugins.Core
  • Microsoft.SemanticKernel.Plugins.Document
  • Microsoft.SemanticKernel.Plugins.Grpc
  • Microsoft.SemanticKernel.Plugins.Memory
  • Microsoft.SemanticKernel.Plugins.OpenApi
  • Microsoft.SemanticKernel.Plugins.Web

とはいえ v1.0.1 のパッケージでも基本機能は提供されています。
例えばプラグインは、組み込みの便利プラグインや応用的なプラグインが無いだけでプラグインの仕組み自体は提供されています。
正式版の部分だけでも、結構色々出来そうです。

使ってみよう

下準備

早速使ってみます。コンソールアプリのプロジェクトを作り Microsoft.SemanticKernel パッケージの v1.0.1 をインストールします。
Semantic Kernel を使う場合は、最初に Kernel というコアとなるクラスのインスタンスを作成する必要があります。

Azure OpenAI Service や OpenAI への接続は必須では無いのですが、AI に繋がない状態で使うのは本来の Semantic Kernel の用途としてはあり得ないといっても過言ではないので、基本的には OpenAI などへの接続先情報を構成したうえで Kernel を作成します。Kernel を作成するときに、簡単に作成できるようにビルダーが提供されています。ビルダーは Kernel.CreateBuilder() で作成できます。CreateBuilder メソッドの戻り値の型は IKernelBuilder で実装は隠されている感じですね。

ということで必要最低限の Azure OpenAI Search への接続を追加した Kernel の作成のコード配下のようになります。

using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    "API キー");
var kernel = builder.Build();

もし API キーではなく Managed ID による認証 (本番等で使う場合はキーの管理が不要なので推奨) を使いたい場合は Azure.Identity パッケージを追加して DefaultAzureCredential のインスタンスを API キーのかわりに渡すことで使用できます。余談ですが DefaultAzureCredential は色々な所から認証情報を取ってこようとするので、その動作が不要な場合は引数で排除する取得元を設定できるのと AzureCliCredential のように特定の場所の認証情報を使うクラスなどもあるので、必要に応じて使い分けると良いでしょう。

プロンプトを実行してみる

Kernel クラスを使うと、プロンプトを渡して結果を受け取ることが出来ます。InvokePromptAsync メソッドに文字列を渡すと FunctionResult が返ってきます。
FunctionResult 型では GetValue<T>() メソッドで結果を受け取ることが出来ます。

例えば、先ほどのコードの続きに以下のように書くとプロンプトに対して結果を受け取って表示できます。

var prompt = "こんにちは!!!!!!!!!!!!!";
var result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result.GetValue<string>());

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

こんにちは!どういたしまして、お手伝いできることがあれば教えてください。どんな質問でもお答えしますよ!

元気な挨拶に対する回答なので AI も元気ですね。

プラグインを作ってみる

テンプレートエンジンがデフォルトで提供されているので、プロンプトは {{$変数}}{{PluginName.FunctionName}} みたいな感じで変数を埋め込んだり、プラグインなどの関数を呼び出すことが出来ます。プラグインの関数を呼び出す場合は {{PluginName.FunctionName $arg}} のように引数を渡せたりします。複数個の引数がある場合には {{PluginName.FunctionName arg1=$arg1 arg2=$arg2}} のように引数名と値のペアをスペース区切りで渡せます。
試してみましょう。

プラグインの一番簡単な作り方はクラスに KernelFunction 属性がついたメソッドを定義することです。Description 属性で、その関数の説明を付けることも出来ます。今回のような関数名指定で呼び出すときは気にしなくても良いですが Function calling (Tool) などで AI に呼び出してもらうときに Description に指定した内容を参考に AI が呼び出す関数を選択してくれるようになります。

ということでサクッと試してみましょう。

using Azure.Identity;
using Microsoft.SemanticKernel;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    // Azure CLI の資格情報で認証したいので、このように設定
    new AzureCliCredential()); 
// プラグインを登録
builder.Plugins.AddFromType<TimePlugin>();
var kernel = builder.Build();

// プラグインを呼び出すプロンプト
var prompt = """
    今日の日付を教えてください。

    ## 参考情報
    {{TimePlugin.Today}}
    """;
// 実行して結果を表示
var result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result.GetValue<string>());

// 今日の日付を返すプラグイン
class TimePlugin
{
    [KernelFunction]
    [Description("現在の日付を取得します。")]
    public string Today() => DateTime.Now.ToString("yyyy年MM月dd日");
}

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

私の情報はアップデートが停止しており、今日が実際に何日であるかを現在時刻に基づいて提供する能力を持っていません。しかし、参考情 報に基づいて、2023年12月22日が今日の日付だと仮定されます。実際に今日の日付を知りたい場合は、デバイスの日付と時刻の設定を確認す るか、信頼性の高いオンラインソースを参照してください。

ただ単に参考情報に日付を書いているだけなので、それが今日の日付なのか???というのを疑ってかかっています。賢いですね。

プロンプト内で呼び出す関数に引数を渡す例も書いてみましょう。

using Azure.Identity;
using Microsoft.SemanticKernel;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    // Azure CLI の資格情報で認証したいので、このように設定
    new AzureCliCredential()); 
// プラグインを登録
builder.Plugins.AddFromType<CreateListItemPlugin>();
var kernel = builder.Build();

// プラグインを呼び出すプロンプト
var prompt = """
    今日の日付を教えてください。

    ## 参考情報
    {{ CreateListItemPlugin.CreateItem categoryName='今日の日付' value='2023/12/25' }}
    """;
// 実行して結果を表示
var result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result.GetValue<string>());

// リストっぽい文字列を作るプラグイン
// 引数を渡す例のためのプラグインなので実用性はない…
class CreateListItemPlugin
{
    [KernelFunction]
    [Description("リストの1項目を生成します。")]
    public string CreateItem(
        [Description("カテゴリ名")]
        string categoryName,
        [Description("カテゴリの値")]
        string value) =>
        $"- {categoryName}: {value}";
}

実行すると以下のような結果になります。今度はちゃんと今日の日付として 2023/12/05 を渡してあげたので、ちゃんと解釈してくれました。

今日の日付は2023年12月25日です。

プロンプトに引数を渡す

InvokePromptAsync メソッドには、KernelArguments 型の引数を追加で渡すことが出来ます。KernelArgumentsIDictionary<string, object?> を実装しているのでコレクション初期化子で渡すことが出来ます。変数はプロンプト内で {{$変数名}} として参照できます。これまでのコードを少し変更してみましょう。

using Azure.Identity;
using Microsoft.SemanticKernel;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    new AzureCliCredential());
// プラグインを登録
builder.Plugins.AddFromType<TimePlugin>();
var kernel = builder.Build();

// プラグインを呼び出すプロンプト
var prompt = """
    あなたは雑談相手として振舞ってください。
    雑談ですが、相手が明らかに間違ったことを言っている場合は関西弁で突っ込んでください。
    突っ込みの際には相手の名前を文章内に含めてください。

    ## コンテキスト
    - 話相手の名前: {{$name}}
    - 今日の日付: {{TimePlugin.Today}}

    ## 会話内容
    {{$name}}: {{$message}}
    システムアシスタント: 
    """;
// 実行して結果を表示
var result = await kernel.InvokePromptAsync(
    prompt,
    new KernelArguments
    {
        ["name"] = "大田一希",
        ["message"] = "こんにちは、夏になって暑くなってきましたね。",
    });
Console.WriteLine(result.GetValue<string>());

// 今日の日付を返すプラグイン
class TimePlugin
{
    [KernelFunction]
    [Description("現在の日付を取得します。")]
    public string Today() => DateTime.Now.ToString("yyyy年MM月dd日");
}

実行結果は以下のようになります。
実行したのは 12/25 なので、夏じゃないって突っ込んでくれていますね。

ええ?大田一希さん、今12月25日でクリスマスやで。どこが夏やねん!あなたのカレンダー、めっちゃ早回しになってませんか?今は冬で、外は寒いはずですよ。

プロンプトの関数化

今まではプロンプトを直接実行していましたが、Semantic Kernel ではプロンプトを Kernel 内に関数として登録しておいて、それを呼び出すといった使い方もサポートされています。
関数は内部的にはプラグインの一部として管理されています。関数を束ねた概念がプラグインといったような感じになっています。

これは実際にプラグインを表す型の KernelPlugin クラスの定義を見てみると以下のように IEnumerable<KernelFunction> を実装しているところからもわかります。

public abstract class KernelPlugin : IEnumerable<KernelFunction>

では、先ほどのコードを少し変えてプロンプトを直接実行するのではなく関数に変換して呼び出してみましょう。関数を作成するには KernelCreateFunctionFromPrompt を使います。
戻り値の KernelFunctionKernelInvokeAsync メソッドを使って呼び出すことが出来ます。

using Azure.Identity;
using Microsoft.SemanticKernel;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    new AzureCliCredential());
// プラグインを登録
builder.Plugins.AddFromType<TimePlugin>();
var kernel = builder.Build();

// プラグインを呼び出すプロンプト
var prompt = """
    あなたは雑談相手として振舞ってください。
    雑談ですが、相手が明らかに間違ったことを言っている場合は関西弁で突っ込んでください。
    突っ込みの際には相手の名前を文章内に含めてください。

    ## コンテキスト
    - 話相手の名前: {{$name}}
    - 今日の日付: {{TimePlugin.Today}}

    ## 会話内容
    {{$name}}: {{$message}}
    システムアシスタント: 
    """;

// プロンプトから関数を作成
var func = kernel.CreateFunctionFromPrompt(prompt,
    functionName: "GenerateChatMessage",
    description: "メッセージに対してチャットを返します。");
Console.WriteLine($"{func.Name} 関数が作成されました。");
Console.WriteLine($"関数の概要: {func.Description}");

// 関数実行して結果を表示
var result = await kernel.InvokeAsync(
    func,
    new KernelArguments
    {
        ["name"] = "大田一希",
        ["message"] = "こんにちは、夏になって暑くなってきましたね。",
    });
Console.WriteLine(result.GetValue<string>());

// 今日の日付を返すプラグイン
class TimePlugin
{
    [KernelFunction]
    [Description("現在の日付を取得します。")]
    public string Today() => DateTime.Now.ToString("yyyy年MM月dd日");
}

実行結果は以下のようになります。最初の 2 行が関数作成後に表示している関数の名前と概要です。CreateFunctionFromPrompt メソッドの引数に渡している内容ですね。

GenerateChatMessage 関数が作成されました。
関数の概要: メッセージに対してチャットを返します。
ええ、大田一希さん、待った待った。今日は12月25日やで、クリスマスやし、冬真っ只中やん。夏やと勘違いしてへん?こっちは雪が降ろうかってくらいの寒さやで。

各関数の概要は、後々 AI に対して呼び出すべき関数を選択してもらうときに非常に重要になります。
この概要を見て AI が呼び出すべき関数や渡すべき情報を決めることになるのでしっかり書きましょう。

因みに、この CreateFunctionFromPrompt メソッドのオーバーロードだと名前と概要くらいしか渡せないのですが PromptTemplateConfig 型を受け取る方だとパラメーターに対する各種メタデータなども渡すことが出来ます。このオーバーロードを使って書き替えると以下のようになります。

// プロンプトから関数を作成
var func = kernel.CreateFunctionFromPrompt(
    new PromptTemplateConfig(prompt)
    {
        Name = "GenerageChatMessage",
        Description = "メッセージに対してチャットを返します。",
        InputVariables =
        [
            new InputVariable
            {
                Name = "name",
                Description = "話相手の名前",
                IsRequired = true,
            },
            new InputVariable
            {
                Name = "message",
                Description = "話相手からのメッセージ",
                IsRequired = false,
                Default = "こんにちは",
            },
        ],
        OutputVariable = new()
        {
            Description = "システムアシスタントからのメッセージ",
        }
    });

// 関数の情報を表示
Console.WriteLine($"{func.Name} 関数が作成されました。");
Console.WriteLine($"関数の概要: {func.Description}");
foreach (var parameter in func.Metadata.Parameters)
{
    Console.WriteLine($"  パラメーター: {parameter.Name}, 概要: {parameter.Description}, 必須: {parameter.IsRequired}, デフォルト値: {parameter.DefaultValue}");
}
Console.WriteLine($"  戻り値: {func.Metadata.ReturnParameter.Description}");

実行すると、以下のように表示されます。ちゃんとパラメーターなどについての情報も出ていますね。
ここらへんの概要をしっかり書くのが今後の AI とうまく付き合うために必要なのでしっかり書くようにしましょう。

GenerageChatMessage 関数が作成されました。
関数の概要: メッセージに対してチャットを返します。
  パラメーター: name, 概要: 話相手の名前, 必須: True, デフォルト値:
  パラメーター: message, 概要: 話相手からのメッセージ, 必須: False, デフォルト値: こんにちは
  戻り値: システムアシスタントからのメッセージ

因みに関数の呼び出しは kernel.InvokeAsync を使う方法以外にも func.InvokeAsync を使う方法もあります。
単発の関数の呼び出しの場合は、どちらを使っても同じ結果になります。関数の実行には KernelKernelFunctionKernelArguments が必要なので、どちらの呼び出し方法を使っても、その 3 つは何処かに登場します。以下に両方のコード例を示します。

// 関数実行して結果を表示
var result = await kernel.InvokeAsync(
    func,
    new KernelArguments
    {
        ["name"] = "大田一希",
        ["message"] = "こんにちは、夏になって暑くなってきましたね。",
    });
// func の InvokeAsync を使うことでも同じことが可能
var result = await func.InvokeAsync(
    kernel, 
    new KernelArguments
    {
        ["name"] = "大田一希",
        ["message"] = "こんにちは、夏になって暑くなってきましたね。",
    });

チャットしたい

これまでは、基本的にテキスト補完のような使い方をしていました。
OpenAI にはテキスト補完とチャットの 2 つの API があります。Semantic Kernel のプロンプトのテンプレートエンジンを使う方法だとチャットは使えないように見えますが <message role="ロール名">チャットの内容</message> のように message タグを使うとチャットとして扱ってくれます。
具体的には内部でタグっぽい雰囲気がある場合に <root>プロンプトで指定された文字列</root> のように root タグで挟んだ後に XML のパーサーでパースしています。そしてチャットの場合は message タグと role 属性があって、message タグのコンテンツがチャットの本文として処理されます。この変換処理が出来ない場合は、普通のテキスト補完っぽい動きのほうのフローに入ります。

気になる人は、上記の処理は v1.0.1 のソースコードだと dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatCompletionServiceExtensions.csGetChatMessageContentsAsync メソッドから一連の処理を追ってみると良いでしょう。

では早速やっていこうと思います。
ChatHistory クラスは Semantic Kernel のクラスでチャットの履歴を保存するためのクラスです。このクラスを直接チャット用の専用 API に渡してチャットを使うことも出来ます。ですが、その場合プロンプトのテンプレートエンジンがやってくれる変数の参照や別のプラグインの呼び出しが出来ないので、今回は ChatHistorymessage タグの文字列に変換をするという方針で実装してみます。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    new AzureCliCredential());
// プラグインを登録
builder.Plugins.AddFromType<TimePlugin>();
var kernel = builder.Build();

var chatHistory = new ChatHistory();
kernel.Plugins.AddFromObject(new ChatPlugin("Kazuki Ota", chatHistory));

while (true)
{
    Console.Write("あなた > ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit")
    {
        break;
    }

    chatHistory.AddUserMessage(input);
    var response = (await kernel.InvokePromptAsync<string>("""
        <message role="system">
            あなたはデジタル アシスタントです。
            以下の参考情報を踏まえてユーザーからの質問に回答してください。
        
            ## 参考情報
            - 今日の日付: {{TimePlugin.Today}}
            {{ChatPlugin.GetContext}}
        </message>
        {{ChatPlugin.GetChatHistory}}
        """)) ?? "";
    chatHistory.AddAssistantMessage(response);
    Console.WriteLine($"アシスタント > {response}");
}


// 今日の日付を返すプラグイン
class TimePlugin
{
    [KernelFunction]
    [Description("現在の日付を取得します。")]
    public string Today() => DateTime.Now.ToString("yyyy年MM月dd日");
}

class ChatPlugin(string name, ChatHistory chatHistory)
{
    private readonly string _name = name;
    private readonly ChatHistory _chatHistory = chatHistory;

    [KernelFunction]
    [Description("チャットの履歴を取得します。")]
    public string GetChatHistory() =>
        string.Join('\n', _chatHistory.Select(ChatMessageToString));

    [KernelFunction]
    [Description("チャットのコンテキストを取得します。")]
    public string GetContext() =>
        $"""
        - ユーザー名: {_name}
        - ユーザーの好きなもの: ドラゴンクエスト, ファイナルファンタジー, モンスターハンター
        """;

    private static string ChatMessageToString(ChatMessageContent chatMessageContent) => 
        $"""
        <message role="{chatMessageContent.Role.Label.ToLowerInvariant()}">
            {chatMessageContent.Content}
        </message>
        """;
}

ポイントは ChatPlugin クラスです、このクラスの GetChatHistory メソッドで ChatHistorymessage タグに変換したり、GetContext メソッドでチャットに関する補足情報を返したりしています。そして以下のようにプロンプトで Kernel に登録されているプラグインの機能を組み合わせてプロンプトを作成しています。

var response = (await kernel.InvokePromptAsync<string>("""
    <message role="system">
        あなたはデジタル アシスタントです。
        以下の参考情報を踏まえてユーザーからの質問に回答してください。
    
        ## 参考情報
        - 今日の日付: {{TimePlugin.Today}}
        {{ChatPlugin.GetContext}}
    </message>
    {{ChatPlugin.GetChatHistory}}
    """)) ?? "";

実行すると以下のようになります。

あなた > こんにちは
アシスタント > こんにちは、Kazuki Otaさん。どういったことでお手伝いしましょうか?
あなた > 暇なんですが何をしようかなぁと思ってるんですよね
アシスタント > 暇な時間を楽しむためのいくつかの提案をしますね。

1. **ゲームをプレイする**: お好きなゲームシリーズ「ドラゴンクエスト」、「ファイナルファンタジー」、または「モンスターハ ンター」のいずれかをプレイしてみてはいかがでしょうか? もしクリア済みのタイトルがあれば、他の未プレイのタイトルやDLCを試してみるのも良いでしょう。

2. **ゲーム関連のコンテンツを視聴する**: 最新のゲーム情報をチェックしたり、ゲームの実況やレビューをYouTubeで視聴するのもおすすめです。ゲームのBGMを聴きながらリラックスするのも良いかもしれませんね。

3. **ファンコミュニティに参加する**: Reddit、Twitter、またはゲーム専門の掲示板には、ゲームのファンコミュニティがあります。交流を楽しんでみるのも新しい友達を作るいい機会かもしれません。

4. **ゲーミンググッズを探す**: ゲームに関連したフィギュアやアート、グッズなどをオンラインで探してみるのも楽しいですね。 コレクションに新しいアイテムを加えるのもワクワクします。

5. **ゲーム内でのチャレンジ**: 既存のゲームで新たなチャレンジを自分自身に課してみるのも一つの方法です。たとえば、モンス ターハンターで時間内に特定のモンスターを倒すといったチャレンジです。

時間を有意義に使って、存分に楽しんでください!
あなた > ありがとう!
アシスタント > どういたしまして、Kazuki Otaさん。何か他にもお手伝いできることがあればお気軽にお尋ねくださいね。ゲームで 新たな冒険をお楽しみください!
あなた > exit

いい感じですね。このチャットのプロンプト自体も関数化することも出来ます。
事前に関数化しておくことで、今回のように何回も同じテンプレートを解析しなくなるので若干早くなると思います。関数化した場合のコードのチャットのループ部分だけを抜粋します。

// チャットのプロンプトを関数化
var chatFunction = kernel.CreateFunctionFromPrompt("""
    <message role="system">
        あなたはデジタル アシスタントです。
        以下の参考情報を踏まえてユーザーからの質問に回答してください。
    
        ## 参考情報
        - 今日の日付: {{TimePlugin.Today}}
        {{ChatPlugin.GetContext}}
    </message>
    {{ChatPlugin.GetChatHistory}}
    """);
while (true)
{
    Console.Write("あなた > ");
    var input = Console.ReadLine() ?? "";
    if (input == "exit")
    {
        break;
    }

    chatHistory.AddUserMessage(input);
    var response = await kernel.InvokeAsync<string>(chatFunction) ?? "";
    chatHistory.AddAssistantMessage(response);
    Console.WriteLine($"アシスタント > {response}");
}

Tools を試そう

先日 Azure.AI.OpenAI パッケージを使って試してみたツールをやってみましょう。

https://zenn.dev/microsoft/articles/aoai-tools-jsonmode-in-dotnet

Semantic Kernel ではプラグインを自動的にツールに変換してくれるので、ツールを使うのがとても楽になります。

ツールを使うようにするには OpenAIPromptExecutionSettingsToolCallBehaviorToolCallBehavior.AutoInvokeKernelFunctions を渡すことで自動的にツールを呼び出して結果を返してくれるようになります。
そのため OpenAI への呼び出しは複数回行われることがある点に注意です。

例えば以下のようにインターネットで検索するようなプラグインを作って、それをツールとして呼び出すように誘導するようなプロンプトを与えてみます。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    new AzureCliCredential());
// プラグインを登録
builder.Plugins.AddFromType<SearchPlugin>();
var kernel = builder.Build();

// プラグインをツールとして扱って、自動的に呼び出して結果を返すようにする
var settings = new OpenAIPromptExecutionSettings
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
};

var response = await kernel.InvokePromptAsync<string>(
    "ドラゴンクエストでは何が有名ですか?インターネットで検索して教えてください。",
    new KernelArguments(settings));
Console.WriteLine(response);

class SearchPlugin
{
    [KernelFunction]
    [Description("インターネットを検索する。")]
    public async Task<string> SearchAsync(string query)
    {
        Console.WriteLine($"インターネットで検索中: {query}");
        // インターネットにいってる風のスリープ処理
        await Task.Delay(1000);
        return query switch
        {
            _ when query.Contains("ドラゴンクエスト") => "日本の伝統的 RPG。メラが有名",
            _ when query.Contains("ファイナルファンタジー") => "日本の伝統的 RPG。ギルガメッシュが有名。",
            _ => $"{query} に関する情報は見つかりませんでした。",
        };
    }
}

実行結果は以下のようになります。ちゃんとドラゴンクエストについて検索して、その上でメラが有名と答えてくれています。

インターネットで検索中: ドラゴンクエストの特徴と人気点
ドラゴンクエスト(Dragon Quest)は以下のような特徴で有名です:

1. **長い歴史と人気** - 1986年に最初のゲームがリリースされて以来、ドラゴンクエストシリーズは日本を中心に世界中で広く愛されています。

2. **伝統的なRPG要素** - ターンベースの戦闘システム、レベルアップによるキャラクターの成長、装備品の変更など、RPGの基本要素を確立し、多くのファンタジーRPGに影響を与えました。

3. **アートとデザイン** - 「ドラゴンボール」で有名な鳥山明がキャラクターデザインを手がけており、そのユニークなアートスタイルが特徴的です。

4. **魅力的なストーリーと世界観** - 毎作品ごとに綿密に作られたストーリーと世界観が、プレイヤーを魅了しています。

5. **記憶に残る音楽** - 有名な作曲家すぎやまこういちが手がける、印象的なゲーム音楽もシリーズの魅力の一つです。

6. **多様なスピンオフタイトル** - 本編シリーズの他にも、様々なジャンルのスピンオフタイトルが存在し、それぞれ独自の魅力を持っています。

なお、「メラ」という言葉が出ていますが、これはドラゴンクエストシリーズにおける代表的な火属性の魔法の名前であり、日本のゲーム文化において広く知られている用語です。

次に、自動的にプラグインの関数を呼び出さないように設定するケースを試してみます。
更新系のプラグインなどは勝手に呼び出されると困るケースが多いので、呼び出すツールを返してもらい、それを呼び出すコードを自分で書くようにするというのが良いでしょう。

結構、手順が多くなるのでめんどくさいですが、自動で呼び出さないようにする場合は必要な手順なので仕方ありません。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "モデルのデプロイ名",
    "https://リソース名.openai.azure.com/",
    new AzureCliCredential());
// プラグインを登録
builder.Plugins.AddFromType<SearchPlugin>();
var kernel = builder.Build();


// message タグだとツールに対応する情報を現時点だと指定できないので ChatHistory を使う
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("ファイナルファンタジーでは何が有名ですか?インターネットで検索して教えてください。");

// チャットの API を使うためのサービスを取得
var chatService = kernel.GetRequiredService<IChatCompletionService>();

// プラグインをツールとして扱って、自動的に呼び出して結果を返すようにする
var settings = new OpenAIPromptExecutionSettings
{
    ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions,
};
// Kernel と上の settings を渡して呼び出すことで Tools が有効になる
// 戻り値は OpenAI 固有のチャットメッセージにしないと ToolCalls にアクセスできない。
var response = (OpenAIChatMessageContent)await chatService.GetChatMessageContentAsync(chatHistory, settings, kernel);

// 返答を履歴に保存
chatHistory.Add(response);

// メッセージが返ってきている場合は表示
if (!string.IsNullOrEmpty(response.Content))
{
    Console.WriteLine(response.Content);
}

foreach (var toolCall in response.ToolCalls.OfType<ChatCompletionsFunctionToolCall>())
{
    if (kernel.Plugins.TryGetFunctionAndArguments(toolCall, out var function, out var arguments))
    {
        var toolResponse = await kernel.InvokeAsync<string>(function, arguments) ?? "";
        Console.WriteLine($"検索結果: {toolResponse}");

        // ツールの結果を履歴に保存
        chatHistory.Add(new ChatMessageContent(
            AuthorRole.Tool,
            toolResponse,
            metadata: new Dictionary<string, object?>
            {
                [OpenAIChatMessageContent.ToolIdProperty] = toolCall.Id,
            }));
    }
}

// ここでもツール呼び出しを期待する場合は settings や kernel を渡す
// 今回は、ツールを呼び出さないので履歴しか渡さない。
var finalAnswer = await chatService.GetChatMessageContentAsync(chatHistory);
Console.WriteLine(finalAnswer.Content);

class SearchPlugin
{
    [KernelFunction]
    [Description("インターネットを検索する。")]
    public async Task<string> SearchAsync(string query)
    {
        Console.WriteLine($"インターネットで検索中: {query}");
        // インターネットにいってる風のスリープ処理
        await Task.Delay(1000);
        return query switch
        {
            _ when query.Contains("ドラゴンクエスト") => "日本の伝統的 RPG。メラが有名",
            _ when query.Contains("ファイナルファンタジー") => "日本の伝統的 RPG。ギルガメッシュが有名。",
            _ when query.Contains("Final Fantasy") => "日本の伝統的 RPG。ギルガメッシュが有名。",
            _ => $"{query} に関する情報は見つかりませんでした。",
        };
    }
}

実行すると以下のようになります。ちゃんとギルガメッシュについても言及がありますね。

インターネットで検索中: ファイナルファンタジーの有名な点
検索結果: 日本の伝統的 RPG。ギルガメッシュが有名。
ファイナルファンタジーシリーズ(Final Fantasy Series)は、以下の点で非常に有名です:

1. 長い歴史とシリーズ展開:1987年に最初のゲームが発売されて以来、多数の続編、スピンオフ、リメイク作品が登場しています。
2. 壮大な物語と世界観:ファイナルファンタジーは、独特のファンタジー世界、複雑で感動的なストーリー、深いキャラクター描写 で知られています。
3. イノベーティブなゲームシステム:バトルシステムやキャラクター成長システムなど、各作品で新しい試みが導入されることが多 いです。
4. 斬新なビジュアルと音楽:ビジュアルアートや音楽は、シリーズの大きな魅力の一つであり、特に作曲家の植松伸夫による楽曲は 世界中で高く評価されています。
5. 有名なキャラクターとモンスター:クラウド、ティーダ、ライトニングなどのキャラクターや、チョコボ、モーグリ、ギルガメッ シュなどのモンスターやクリーチャーもシリーズを代表するアイコンです。

インターネットで検索した結果、特に「ギルガメッシュ」がシリーズの複数の作品で登場し、多くのファンに知られているキャラクターであることが分かります。彼はコミカルなキャラクターとして、あるいは強力な敵としてシリーズを通じて親しまれています。

まとめ

とりあえずですが Semantic Kernel v1.0.1 を軽く触ってみました。
個人的にはチャットでもテンプレートエンジンを使ってテンプレートを組めるようになったのが気に入りポイントです。

まぁ、ぶっちゃけ今回の例くらいなら C# の文字列補間でやってもいいんですけどね…。実際にプランナーが正式にリリースされたりすると色々 Semantic Kernel で楽しいことも出来そうですが、今回の範囲内でも普通の Azure.AI.OpenAI を直接使うより少し楽に OpenAI を呼び出したり出来るので、そういう意味では便利です。

あとはプラグインから Tools をいい感じにやってくれるのも楽ですね。JSON Schema をあんまり意識しなくてよいのがいいです。

Microsoft (有志)

Discussion