Azure Functions で Semantic Kernel を使用する

更新事項
2023/11/08 
.NET 版の Semantic Kernel の v1.0.0-beta5 に対応した書き方に変更しました。

# やってみたこと
Semantic Kernel をアプリに組み込むために、Azure Functions に Semantic Kernel を組み込んで API を作成してみました。
デモとして、文章を要約させる機能を作りました。

ローカルで、Postman で API 実行してみると、要約された文章が返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/2a01b1d2-f318-013a-3153-024ac2532773.png)

# Azure Functions の設定
* .NET7 の Isolated モード
* Http トリガー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/be1f61b5-5b96-95a2-c697-59cdc794be08.png)

# 実装
DIの設定
* Semantic Kernel
* Azure OpenAI
```C#:Program.cs
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureAppConfiguration(config =>
{
    config
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables();
})
.ConfigureServices((context, services) =>
{
    var configuration = context.Configuration;
    services.AddSingleton<IKernel>(serviceProvider => Kernel.Builder.WithAzureChatCompletionService(
                    configuration.GetSection("AzureOpenAI").GetValue<string>("ModelName"),
                    configuration.GetSection("AzureOpenAI").GetValue<string>("Endpoint"),
                    configuration.GetSection("AzureOpenAI").GetValue<string>("APIKey")
                ).Build());
    services.AddAzureClients(builder =>
            {
                builder.AddOpenAIClient(
                    new Uri(configuration.GetSection("AzureOpenAI").GetValue<string>("Endpoint")),
                    new AzureKeyCredential(configuration.GetSection("AzureOpenAI").GetValue<string>("APIKey"))
                    );
            });
})
.Build();

host.Run();
```
ポイントは以下になります。
* 今回は簡単のためネイティブ関数（コードでプラグインを作成）は使わず、セマンティック関数（要約するプラグイン）のみを使用
* パスの指定を書いて、Skills フォルダを指定してあげる（※注意点後述）
```C#:SKFunction
namespace SKDemo
{
    public class SKFunction
    {
        private readonly ILogger _logger;
        private readonly IKernel kernel;

        public SKFunction(ILoggerFactory loggerFactory, IKernel kernel)
        {
            _logger = loggerFactory.CreateLogger<SKFunction>();
            this.kernel = kernel;
        }

        [Function("SummarizeFunction")]
        public async Task<SKResponseModel> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
        {
            var input = await req.ReadFromJsonAsync<SKRequestModel>();

            string exePath = AppDomain.CurrentDomain.BaseDirectory;
            string parentDirectory = Directory.GetParent(exePath).Parent.Parent.Parent.FullName;
            string skillsDirectory = Path.Combine(parentDirectory, "Skills");
            IDictionary<string, ISKFunction>? semanticPlugins = kernel.ImportSemanticSkillFromDirectory(skillsDirectory, "Plugins");

            SKContext context = kernel.CreateNewContext();
            context.Variables["INPUT"] = input.Text;
            SKContext answer = await semanticPlugins["SummarizeFunction"].InvokeAsync(context);
            context["answer"] = answer.ToString();

            var result = new SKResponseModel
            {
                Answer = answer.ToString()
            };

            return result;
        }
    }
}

```
```C#
public class SKRequestModel
{
    [JsonPropertyName("text")]
    public string? Text { get; set; }
}

public class SKResponseModel
{
    [JsonPropertyName("answer")]
    public string? Answer { get; set; }
}
```

```:config.json
{
  "schema": 1,
  "type": "completion",
  "description": "文章（ユーザーの入力値）を要約する",
  "completion": {
    "max_tokens": 4000,
    "temperature": 0.0,
    "top_p": 1.0,
    "presence_penalty": 0.0,
    "frequency_penalty": 0.0
  },
  "input": {
    "parameters": [
      {
        "name": "INPUT",
        "description": "ユーザーの入力値",
        "defaultValue": ""
      }
    ]
  }
}
```

```:skprompt.txt
以下の文章を要約してください。以下の条件に従ってください。
・日本語で回答してください
・文章の長さは、200文字以内にしてください

文章:
{{$INPUT}}
```
# 注意点
Skills フォルダについて以下 2 つ注意点があります。
1. フォルダ構造
Skills フォルダ内に Plugins フォルダを作成しており、その中に各種セマンティック関数を置いています。今回は要約するために使う SummarizeFunction というセマンティック関数を作って置いてあります。コード内でフォルダ指定するので、フォルダ名は何でもいいです。
（ネイティブ関数を使う場合は、この場合 Skills フォルダ配下に置きます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/a113adbb-144b-49e0-cf5c-0ce23685a743.png)

1. プロパティ設定
ローカルで実行する場合は気にしなくてよいのですが、Azure リソースへデプロイして使う場合に必要な設定があります。以下の項目をプロジェクトファイルに追加してください。この設定をしないと Azure 上で動きません。
```C#:SKDemo.csproj
<ItemGroup>
    <Content Include="Skills\**\*">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>
```