Azure Cognitive Search（Azure AI Search）× Semantic Kernel でドキュメントの内容を要約する

### 更新事項
2023/11/17
Azure Cognitive Search の名前を Azure AI Search に変更しました。
2023/11/08
.NET 版の Semantic Kernel の v1.0.0-beta5 に対応した書き方に変更しました。

# やってみたこと
[前回記事](https://qiita.com/kkubo124/items/4f1bb5724df79c4e14ac)に引き続き、Azure Functions と Semantic Kernel の組み合わせに、Azure Cognitive Search  （Azure AI Search）を追加して検証してみました。

ドキュメント参照し Q＆A を行える API を作成してみました。
Cognitive Search （Azure AI Search）でインデックスしているドキュメントに関する質問をすると、それに関するドキュメントを参照し要約して回答を生成してくれます。ついでに参照したドキュメントのファイル名も出すようにしてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/dd451314-0ce4-b1ab-d8ba-16fd13b9bb68.png)

# 実装
## Azure Cognitive Search （Azure AI Search）
インデックスを作成するのみなので、こちらの説明は省略します。
今回は以下のサンプルを用いて作成しました。
[Azure-Cognitive-Search-Workshop ノーコード全文検索インデックスの作成](https://github.com/nohanaga/Azure-Cognitive-Search-Workshop/blob/main/CreateIndex.md)

## Azure Functions
DI の設定をします。
* Azure OpenAI
* Semantic Kernel
* Cognitive Search （Azure AI Search）
```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"))
                    );
                builder.AddSearchClient(configuration.GetSection("SearchClient"));
            });
})
.Build();

host.Run();
```
処理の流れとしては、以下のようになります。
1. 質問受ける
1. 質問に関するドキュメントを検索する
1. ドキュメントの内容を要約して回答する



セマンティック関数とネイティブ関数を 1 つずつインポートしています。
セマンティック関数は、取得したドキュメント内容を要約して回答する関数を作りました。ネイティブ関数は Cognitive Search  （Azure AI Search）の機能で、質問文からそれに関するドキュメントを取得する関数を作りました。
```C#:SKFunction.cs
public class SKFunction
{
    private readonly IKernel kernel;
    private readonly ICognitiveSearchRepository cognitiveSearchRepository;

    public SKFunction(ICognitiveSearchRepository cognitiveSearchRepository, IKernel kernel)
    {
        this.cognitiveSearchRepository = cognitiveSearchRepository;
        this.kernel = kernel;
    }

    [Function("SemanticKernel")]
    public async Task<SKResponseModel> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
    {
        var input = await req.ReadFromJsonAsync<SKRequestModel>();
        
        string skillsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Skills");
        IDictionary<string, ISKFunction>? semanticPlugins = kernel.ImportSemanticSkillFromDirectory(skillsDirectory, "Plugins");
        IDictionary<string, ISKFunction>? docSearchPlugins = kernel.ImportSkill(
            new CognitiveSearchSkill(cognitiveSearchRepository),
            "CognitiveSearchSkill");

        SKContext context = kernel.CreateNewContext();
        context.Variables["INPUT"] = input.Message ?? "";

        SKContext docsResult = await kernel.RunAsync(new ContextVariables(input.Message), docSearchPlugins["GetDocumetContent"]);
        var docsTextModel = JsonSerializer.Deserialize<SearchResultOutputModel>(docsResult.ToString());
        context.Variables["raw_text"] = docsTextModel?.Text ?? "";

        SKContext answer = await semanticPlugins["QAFunction"].InvokeAsync(context);
        context.Variables["answer"] = answer.ToString();

        var result = new SKResponseModel
        {
            Text = answer.ToString(),
            FileName = docsTextModel?.FileName ?? ""
        };

        return result;
    }
}
```
## ネイティブ関数の実装
こちらは Cognitive Search  （Azure AI Search）の SDK を使って実装をしています。 
[Azure.Search.Documents](https://www.nuget.org/packages/Azure.Search.Documents/)
```C#
[SKFunction]
[Description("ユーザーの入力値からドキュメント検索して、ドキュメント文章を返す")]
public async Task<string> GetDocumetContent(string input)
{
    var searchResults = await cognitiveSearchRepository.GetDocuments(input, 1);

    var result = new SearchResultOutputModel()
    {
        Text = searchResults[0].Content,
        FileName = searchResults[0].FileName
    };

    var serializeOptions = new JsonSerializerOptions
    {
        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
        WriteIndented = true
    };
    var resultString = JsonSerializer.Serialize(result, serializeOptions);

    return resultString;
}
```
```C#:CognitiveSearchRepository.cs
public async Task<List<Document>> GetDocuments(string input, int size)
{
    SearchOptions options = new SearchOptions()
    {
        OrderBy = { "" },
        Size = size
    };
    SearchResults<SearchDocument> response = await searchClient.SearchAsync<SearchDocument>(input, options);

    List<Document> searchResults = new List<Document>();
    await foreach (SearchResult<SearchDocument> item in response.GetResultsAsync())
    {
        var searchResult = JsonSerializer.Deserialize<Document>(item.Document.ToString());
        searchResults.Add(searchResult);
    }

    return searchResults;
}
```
## セマンティック関数の実装
ドキュメントの文字数が大きいことを想定し、gpt-3.5-turbo-16k を使用しているため、大きめの max_tokens を設定しています。
```config.json
{
  "schema": 1,
  "type": "completion",
  "description": "ドキュメント文章を参考に質問（ユーザーの入力値）に回答する",
  "completion": {
    "max_tokens": 8000,
    "temperature": 0.0,
    "top_p": 1.0,
    "presence_penalty": 0.0,
    "frequency_penalty": 0.0
  },
  "input": {
    "parameters": [
      {
        "name": "raw_text",
        "description": "ドキュメント文章",
        "defaultValue": ""
      },
      {
        "name": "INPUT",
        "description": "ユーザーの入力値",
        "defaultValue": ""
      }
    ]
  }
}
```

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

質問:
{{$INPUT}}

文章:
{{$raw_text}}
```

# 課題
主にエラー処理が課題になると思いました。
ドキュメントと関係ない質問を受けた場合や、トークンの上限を超えた場合の処理など工夫が必要で、各モデルはトークン制限があるので、チャンク分割などを上手く活用することが必要になってくるのかなと思いました。

# 今後試したいこと
* Bot に組み込んで、社内 Q&A ボットを作ってみる（Teams などで使えるように）
* Azure Cognitive Search  （Azure AI Search）のベクトル検索