Microsoft.Bot.Framework で作成される EchoBot.cs をいじってみた

自分はどちらかと言うとインフラ寄りの CI/CD エンジニアなのですが、専門外の私にも ChatGPT との連携を検証する日がやって来ました。いちユーザーとして ChatGPT を使うだけの幸せな日常から、仕事として携わる日も近いかもしれません。という事で趣味がてら EchoBot.cs をいじって OpenAI の REST API に会話を送ったり、CosmosDB にチャット履歴を保存したりを試してみました。

## C# VSCode CLI でボットアプリを作成

https://learn.microsoft.com/ja-jp/azure/bot-service/bot-service-quickstart-create-bot?view=azure-bot-service-4.0&tabs=csharp%2Cvscode

こちらのドキュメントを参考にボットアプリを作成します。

```bash:bash
$ dotnet --version
6.0.300

$ dotnet new -i Microsoft.Bot.Framework.CSharp.EchoBot
次のパッケージがインストールされます:
   Microsoft.Bot.Framework.CSharp.EchoBot

Microsoft.Bot.Framework.CSharp.EchoBot は既にインストールされています。バージョン: 4.17.1、バージョン  に置き換えられます。
Microsoft.Bot.Framework.CSharp.EchoBot::4.17.1 が正常にアンインストールされました。
成功: Microsoft.Bot.Framework.CSharp.EchoBot::4.17.1により次のテンプレートがインストールされました。
テンプレート名                    短い名前  言語  タグ                                           
--------------------------------  --------  ----  -----------------------------------------------
Bot Framework Echo Bot (v4.17.1)  echobot   [C#]  Bot/Bot Framework/Echo Bot/Conversational AI/AI

$ dotnet new echobot -n mnrechobot
テンプレート "Bot Framework Echo Bot (v4.17.1)" が正常に作成されました。

作成後の操作を処理しています...
復元するプライマリ出力はありません。

$ cd mnrechobot

$ dotnet run
```

## テンプレートが生成する EchoBot.cs を確認

```cs:Bots/EchoBots.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Generated with EchoBot .NET Template version v4.17.1

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;

namespace EchoBot.Bots
{
    public class EchoBot : ActivityHandler
    {
        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            var replyText = $"Echo: {turnContext.Activity.Text}";
            await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);
        }

        protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
        {
            var welcomeText = "Hello and welcome!";
            foreach (var member in membersAdded)
            {
                if (member.Id != turnContext.Activity.Recipient.Id)
                {
                    await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText, welcomeText), cancellationToken);
                }
            }
        }
    }
}
```

## 検証のために色々いじった EchoBots.cs

OpenAI の REST API に会話を送ったり、CosmosDB にチャット履歴を保存したり、チャット履歴をクリアしたりする機能を検証してみました。

`dotnet add package Microsoft.Azure.Cosmos` とかは必要に応じて。

```cs:Bots/EchoBots.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Generated with EchoBot .NET Template version v4.17.1

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using System.Net.Http;
using System.Net.Http.Headers;
using System;
using System.Text.Json;
using System.Text;
using System.Net;
using Newtonsoft.Json.Linq;
using Microsoft.Azure.Cosmos;

namespace EchoBot.Bots
{
    public class EchoBot : ActivityHandler
    {
        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            // デフォルトで返す値
            var replyText = $"You Say: {turnContext.Activity.Text}";

            // OpenAI の REST API 準備
            HttpClient httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
            JObject postData = JObject.Parse(@"{
                ""model"": ""gpt-3.5-turbo"",
                ""messages"": []
            }");
            JArray messages = (JArray)postData["messages"];

            // CosmosDB 準備
            var client = new CosmosClient(Environment.GetEnvironmentVariable("CDB_ENDPOINT"), Environment.GetEnvironmentVariable("CDB_KEY"));
            var databaseResponse = await client.CreateDatabaseIfNotExistsAsync("chat");
            var containerProperties = new ContainerProperties(turnContext.Activity.From.Id, "/id");
            var containerResponse = await databaseResponse.Database.CreateContainerIfNotExistsAsync(containerProperties);
            var database = client.GetDatabase("chat");
            var container = database.GetContainer(turnContext.Activity.From.Id);

            // チャット履歴を削除
            if (turnContext.Activity.Text == "clear")
            {
                await container.DeleteContainerAsync();
                containerResponse = await databaseResponse.Database.CreateContainerIfNotExistsAsync(containerProperties);
                container = database.GetContainer(turnContext.Activity.From.Id);
                await turnContext.SendActivityAsync(MessageFactory.Text("チャット履歴をクリアしました！", replyText), cancellationToken);
                return;
            }

            // CosmosDB からチャット履歴を取得
            var sqlQueryText = "SELECT * FROM c";
            var queryDefinition = new QueryDefinition(sqlQueryText);
            var results = container.GetItemQueryIterator<dynamic>(queryDefinition);
            while (results.HasMoreResults)
            {
                var items = await results.ReadNextAsync();
                foreach (var item in items)
                {
                    // OpenAI のリクエストに追加
                    JObject history = new JObject();
                    history.Add("role", item["role"]);
                    history.Add("content", item["content"]);
                    messages.Add(history);
                }
            }

            // OpenAI のリクエストの最後に今回のプロンプトを追加
            JObject message = new JObject();
            message.Add("role", "user");
            message.Add("content", turnContext.Activity.Text);
            messages.Add(message);

            // OpenAI の REST API を実行
            var post = postData.ToString();
            var response = await httpClient.PostAsync("https://api.openai.com/v1/chat/completions",
                new StringContent(post, Encoding.UTF8, "application/json"));
            if (response.StatusCode == HttpStatusCode.OK)
            {
                // 実行結果のテキストを格納
                replyText = "";
                JObject completions = JObject.Parse(await response.Content.ReadAsStringAsync());
                foreach (var value in completions.GetValue("choices"))
                {
                    replyText += value["message"]?["content"];
                }

                // CosmosDB にチャット履歴を保存
                var newItem = new {
                    id = Guid.NewGuid().ToString(),
                    role = "user",
                    content = turnContext.Activity.Text
                };
                await container.CreateItemAsync(newItem);
                var addItem = new {
                    id = Guid.NewGuid().ToString(),
                    role = "assistant",
                    content = replyText
                };
                await container.CreateItemAsync(addItem);
            }

            // チャット返信
            await turnContext.SendActivityAsync(MessageFactory.Text(replyText, replyText), cancellationToken);
        }

        protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
        {
            var welcomeText = "Hello and welcome!";
            foreach (var member in membersAdded)
            {
                if (member.Id != turnContext.Activity.Recipient.Id)
                {
                    await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText, welcomeText), cancellationToken);
                }
            }
        }
    }
}
```

## Teams でチャットしてる例

![teams-botframework-sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675885/5661391a-8b5b-edba-d837-2f76b712105e.png)


## 参考

https://platform.openai.com/docs/api-reference/chat

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.bot.schema.imessageactivity?view=botbuilder-dotnet-stable

https://github.com/Azure/azure-cosmos-dotnet-v3