🍣

Semantic Kernel のスキルを外部ファイルで定義して読み込む

2023/04/30に公開

Azure OpenAI Service を便利に使うための Semantic Kernel を試してみよう on C# で簡単に Semantic Kernel を試してみました。この記事では C# のコード内にプロンプトやスキルのパラメーターなどを全部書いていましたが、実際には外部ファイルに書いて読み込むことができます。今回は、そのやり方を試してみたメモです。

スキルを外部ファイルに書く

スキルは以下の 2 ファイルで定義します。

  • skprompt.txt
    • プロンプトのテキスト (UTF-8 で保存するんだよ!!!)
  • config.json
    • スキルのパラメーター (UTF-8 で保存するんだよ!!!)

skprompt.txt は本当にただのテキストファイルです。プロンプトを書いておくだけです。config.json は CreateSemanticFunctionconfig 引数で渡したパラメーターに相当する json ファイルです。
例として以下のようなコードで定義している 2 つのスキルを外部ファイルに書き換えることをやってみようと思います。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;

IKernel kernel = Kernel.Builder
    .Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    new AzureCliCredential());

var marketer = kernel.CreateSemanticFunction("""
    あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
    キーワード: {{$keywords}}
    紹介文:

    """,
    config: new()
    {
        Completion = new()
        {
            FrequencyPenalty = 0,
            MaxTokens = 200,
            PresencePenalty = 0,
            StopSequences = { },
            Temperature = 0.7,
            TopP = 0,
        },
        Description = "マーケティングの担当者",
        Input = new()
        {
            Parameters =
            {
                new()
                {
                    Name = "target",
                    Description = "マーケティング対象の商品名",
                },
                new()
                {
                    Name = "keywords",
                    Description = "紹介文に含めるキーワード",
                }
            }
        }
    });

var consultant = kernel.CreateSemanticFunction("""
    あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
    観点: {{$viewpoints}}
    紹介文:
    {{$input}}
    改善後の紹介文:

    """,
    config: new()
    {
        Completion = new()
        {
            FrequencyPenalty = 0,
            MaxTokens = 200,
            PresencePenalty = 0,
            StopSequences = { },
            Temperature = 0.7,
            TopP = 0,
        },
        Description = "マーケティングの担当者",
        Input = new()
        {
            Parameters =
            {
                new()
                {
                    Name = "target",
                    Description = "マーケティング対象の商品名",
                },
                new()
                {
                    Name = "viewpoints",
                    Description = "改善する際の観点",
                }
            }
        }
    });

var variables = new ContextVariables();
variables["target"] = "ワイン";
variables["keywords"] = "一陣の風、芳醇な香り、命を吹き込んだ";
variables["viewpoints"] = "高齢者へのリーチ";

var context = await kernel.RunAsync(variables, marketer, consultant);
Console.WriteLine("## 結果");
Console.WriteLine(context);

まず、プロジェクトに Skills フォルダーを作って、その下に SampleSkill というフォルダーを作成して、その下に Marketer と Consultant という名前のフォルダーを作成します。Marketer と Consultant フォルダーの中に skprompt.txt と config.json を作成します。

Marketer の skprompt.txt は以下のようになります。

Skills/SampleSkill/Marketer/skprompt.txt
あなたは{{$target}}のマーケティングの担当者です。以下のキーワードを含めた詩的な紹介文を作成してください。
キーワード: {{$keywords}}
紹介文:

同じ要領で Consultant の skprompt.txt は以下のようになります。

Skills/SampleSkill/Consultant/skprompt.txt
あなたはコンサルタントです。以下の{{$target}}の紹介文を改善する必要があります。改善の際に以下の観点を加える必要があります。
観点: {{$viewpoints}}
紹介文:
{{$input}}
改善後の紹介文:

次に config.json ですが、これは以下のようなプログラムを作って実際の C# で指定したものを JSON 形式で出力するのが楽です。

using Microsoft.SemanticKernel.SemanticFunctions;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

JsonSerializerOptions options = new()
{
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
    WriteIndented = true,
};

PromptTemplateConfig c1 = new()
{
    Completion = new()
    {
        FrequencyPenalty = 0,
        MaxTokens = 200,
        PresencePenalty = 0,
        StopSequences = { },
        Temperature = 0.7,
        TopP = 0,
    },
    Description = "マーケティングの担当者",
    Input = new()
    {
        Parameters =
            {
                new()
                {
                    Name = "target",
                    Description = "マーケティング対象の商品名",
                },
                new()
                {
                    Name = "keywords",
                    Description = "紹介文に含めるキーワード",
                }
            }
    }
};

Console.WriteLine(JsonSerializer.Serialize(c1, options));
Console.WriteLine();

PromptTemplateConfig c2 = new()
{
    Completion = new()
    {
        FrequencyPenalty = 0,
        MaxTokens = 200,
        PresencePenalty = 0,
        StopSequences = { },
        Temperature = 0.7,
        TopP = 0,
    },
    Description = "マーケティングの担当者",
    Input = new()
    {
        Parameters =
            {
                new()
                {
                    Name = "target",
                    Description = "マーケティング対象の商品名",
                },
                new()
                {
                    Name = "viewpoints",
                    Description = "改善する際の観点",
                }
            }
    }
};

Console.WriteLine(JsonSerializer.Serialize(c2, options));

実行すると以下のような 2 つの JSON が出力されます。

{
  "schema": 1,
  "type": "completion",
  "description": "マーケティングの担当者",
  "completion": {
    "temperature": 0.7,
    "top_p": 0,
    "presence_penalty": 0,
    "frequency_penalty": 0,
    "max_tokens": 200,
    "stop_sequences": []
  },
  "default_services": [],
  "input": {
    "parameters": [
      {
        "name": "target",
        "description": "マーケティング対象の商品名",
        "defaultValue": ""
      },
      {
        "name": "keywords",
        "description": "紹介文に含めるキーワード",
        "defaultValue": ""
      }
    ]
  }
}

{
  "schema": 1,
  "type": "completion",
  "description": "マーケティングの担当者",
  "completion": {
    "temperature": 0.7,
    "top_p": 0,
    "presence_penalty": 0,
    "frequency_penalty": 0,
    "max_tokens": 200,
    "stop_sequences": []
  },
  "default_services": [],
  "input": {
    "parameters": [
      {
        "name": "target",
        "description": "マーケティング対象の商品名",
        "defaultValue": ""
      },
      {
        "name": "viewpoints",
        "description": "改善する際の観点",
        "defaultValue": ""
      }
    ]
  }
}

最初の JSON を Skills/SampleSkill/Marketing/config.json に、2 番目の JSON を Skills/SampleSkill/Consultant/config.json にコピーします。
そして、Skills フォルダー以下のファイルを全て出力フォルダーにコピーするように csproj ファイルに以下の定義を追加しておきます。

<ItemGroup>
  <Content Include="Skills\**\*">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>

このように準備できたら IKernelpublic static IDictionary<string, ISKFunction> ImportSemanticSkillFromDirectory(this IKernel kernel, string parentDirectory, string skillDirectoryName) メソッドで読み込みます。

試しに読みこんでみて読み込まれたスキルを画面に出してみます。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.KernelExtensions;
using Microsoft.SemanticKernel.Orchestration;

IKernel kernel = Kernel.Builder
    .Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    new AzureCliCredential());

IDictionary<string, ISKFunction> sampleSkills = kernel.ImportSemanticSkillFromDirectory(@".\Skills", "SampleSkill");

foreach (var name in sampleSkills.Keys)
{
    Console.WriteLine(name);
}

実行結果は以下のようになります。ちゃんと読み込めてますね。

Consultant
Marketer

あとは RunAsync を呼ぶだけです。読み込んだスキルを渡してあげるだけなので簡単ですね。コードは以下のようになります。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.KernelExtensions;
using Microsoft.SemanticKernel.Orchestration;

IKernel kernel = Kernel.Builder
    .Build();
kernel.Config.AddAzureTextCompletionService("sample",
    "kaota-text-davinci-003",
    "https://aikaota.openai.azure.com/",
    new AzureCliCredential());

var sampleSkills = kernel.ImportSemanticSkillFromDirectory(@".\Skills", "SampleSkill");

var variables = new ContextVariables();
variables["target"] = "ワイン";
variables["keywords"] = "一陣の風、芳醇な香り、命を吹き込んだ";
variables["viewpoints"] = "高齢者へのリーチ";

var context = await kernel.RunAsync(
    variables, 
    sampleSkills["Marketer"], 
    sampleSkills["Consultant"]);

Console.WriteLine("## 結果");
Console.WriteLine(context);

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

## 結果
古くから伝わる伝統的な製法で作られた、深みのある味わいを持つワイン。一陣の風が吹き抜け、芳醇な香りが空気を満たし、そして命を吹き込んだワインです。高齢者の方にも安心してお楽しみいただける、安全なワインです。そのワインをお楽しみください。

ちゃんとできてますね。

まとめ

ということで、外部ファイルからスキルを読み込んで実行する方法を試してみました。
実際のプログラムでやるときは、もしかしたらファイル以外に DB とかから読み込むようにしたいかもしれませんが、その場合はどうするのがいいんでしょうね?
愚直に CreateSemanticFunction 使うのがよいかな?と思っていますが、もっといい方法があるかもしれません。

Microsoft (有志)

Discussion