🤖

第9章 チャットボット

2023/11/27に公開

1 チャットボット概要

ここでは、社内文書を元に回答することができるチャットボットのコード部分について説明いたします。第3章で述べたようにBot Framework SDKを利用してチャットボットの構築を進めていきます。
今回構築するチャットボットの機能としては、FastAPIにより生成した回答の送信、会話履歴の保持、フィードバック、ログの保存があります。

2 サンプルチャットボットの動作確認

まずはMicrosoftが提供しているMicrosoft Bot Framework V4 SDK for .NETのサンプルの動作を確認します。
2.1 git clone コマンドを実行します。

git clone https://github.com/Microsoft/BotBuilder-Samples.git

2.2 以下のフォルダに移動します。

cd BotBuilder-Samples/samples/csharp_dotnetcore/02.echo-bot

2.3 チャットボットを実行します。

dotnet run

2.4 ブラウザでボットのエンドポイントURLにアクセスすると、以下の画面を確認することができます。

2.5 実際にボットの動作を確認するために、「Bot Framework Emulator」を起動します。

2.6 「Open Bot」を選択後、「Bot URL」に「 http://localhost:3978/api/messages 」を入力し、「Connect」をクリックします。

2.7 「Hello」と送信すると、「Echo: Hello」と返ってくることが確認できるかと思います。

3 Teamsチャットボットの動作確認

3.1 git clone コマンドを実行します。

git clone git@github.com:pi-poninc/document_search_bot.git

3.2 クローンが完了したら、「document_search_bot」フォルダ下にある「pipon_chatbot」フォルダを「Visual Studio Code」で開きます。

3.3 「appsettings.Development.json」にはローカルでFastAPIを実行した際のエンドポイントを設定します。

appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",
  "FastAPIEndpoint": "http://127.0.0.1:8000/conversation_history"
}

3.4 Azure上の「Cosmos DB」の「キー」に移動し、「URI」と「PRIMARY KEY」を取得します。「appsettings.json」にはCosmosDBの設定をします。

appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "MicrosoftAppType": "UserAssignedMSI",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",
  "FastAPIEndpoint": "",
  "CosmosDBOptions": {
    "Endpoint": "https://pipon-cosmos-db.documents.azure.com:443/",
    "Key": "xxxxxxxxxxxxxxx",
    "DatabaseId": "Logs",
    "MessageContainerId": "Message",
    "FeedbackContainerId": "Feedback"
  }
}

3.5 「pipon_chatbot」フォルダでチャットボットを実行します。

dotnet run

3.6 「2 サンプルチャットボットの動作確認」でエコーボットの動作を確認したのと同様の手順で「Bot Framework Emulator」で動作確認を行います。

3.7 Azure上の「Cosmos DB」の「データ エクスプローラー」に移動し、「Message」の「Items」を見ると、会話のログも保存できていることが確認できます。

4 Teamsチャットボットの解説

4.1 フォルダ構成

「Bot Framework SDK」を利用した本チャットボットにおける基本的なフォルダの構成は以下の通りです。

pipon_chatbot
.
├── AdapterWithErrorHandler.cs
├── Bot.csproj
├── Bots
│   └── Bot.cs : チャットボットのメインロジックの実装
├── Controllers
│   └── BotController.cs : ボットのリクエストとレスポンスのハンドリングを行うコントローラーの実装
├── Database
│   └── Cosmos
│       ├── Feedback.cs : フィードバックに関するモデルクラス
│       └── Message.cs : ボットとユーザーの会話に関するモデルクラス
├── Dialogs
│   └── Dialog.cs : メインの対話フローを管理するダイアログの実装
├── Program.cs
├── README.md
├── Services
│   └── LogServices
│       ├── CosmosDBOptions.cs : Cosmos DBに接続するためのオプション
│       ├── ILogService.cs : LogServiceのインターフェイス
│       └── LogService.cs : ユーザーからのフィードバックを処理するためのサービスの実装
├── Startup.cs
├── appsettings.Development.json : 環境変数の設定(開発環境)
└── appsettings.json : 環境変数の設定(本番環境)

4.2 コード解説

4.2.1 Teams上でチャットボットと会話している際は、AadObjectIdをユーザー情報として取得します。

Dialog.cs:54行目
if (stepContext.Context.Activity.ChannelId == "msteams")
{
	// ユーザーの情報を取得
	var member = await TeamsInfo.GetMemberAsync(stepContext.Context, stepContext.Context.Activity.From.Id, cancellationToken);
	// AadObjectId
	_aadObjectId = member.AadObjectId;
	} else {
	_aadObjectId = "testId";
}

4.2.2 ユーザーから画像や空白文字が送信される可能性もあるので、以下のコードでユーザーのメッセージを制御します。

Dialog.cs:67行目
// ユーザーからのメッセージが空の場合や文字列以外の場合は質問文の入力を促す
if (string.IsNullOrWhiteSpace(stepContext.Context.Activity.Text) || !stepContext.Context.Activity.Text.Any(char.IsLetterOrDigit))
{
	var options = new PromptOptions
	{
	    Prompt = MessageFactory.Text("質問を入力してください。"),
	    RetryPrompt = MessageFactory.Text("質問文が認識できませんでした。再入力をお願いします。"),
	};

	return await stepContext.PromptAsync(nameof(TextPrompt), options, cancellationToken).ConfigureAwait(false);
	}
	else
	{
	return await stepContext.NextAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}

4.2.3 ユーザーの発言(または、ボットの発言)を「Cosmos DB」に保存します。

Dialog.cs:87行目
// ユーザーの発言をCosmosDBに保存
var userMessageLog = new Message
{
	MessageId = Guid.NewGuid().ToString(),
	Timestamp = DateTime.Now,
	AadObjectId = _aadObjectId,
	ConversationId = _conversationId,
	TextMessage = userMessage,
	IsUser = true
};
await _logService.SaveConversationAsync(userMessageLog);

4.2.4 FastAPIからチャットボットの回答を取得し、ユーザーに送信します。

Dialog.cs:102行目
// FastAPIエンドポイントにGETリクエストを送信
var content = new StringContent(JsonConvert.SerializeObject(new { messages = conversationHistory }), Encoding.UTF8, "application/json");
var fastAPIEndpoint = _configuration["FastAPIEndpoint"];
var response = await httpClient.PostAsync(fastAPIEndpoint, content);

if (response.IsSuccessStatusCode)
{
	using var responseStream = await response.Content.ReadAsStreamAsync();
	using var jsonDocument = await JsonDocument.ParseAsync(responseStream);

	if (jsonDocument.RootElement.TryGetProperty("bot", out var botProperty))
	{
	    var botMessage = "";
	    var bot = botProperty.GetString();
	    if (jsonDocument.RootElement.TryGetProperty("metadata", out var metadataProperty))
	    {
		var metadataArray = metadataProperty.EnumerateArray();
		var notionURLs = new List<string>();

		foreach (var metadataElement in metadataArray)
		{
		    if (metadataElement.TryGetProperty("notion_id", out var notionIdProperty))
		    {
			var notionURL = notionIdProperty.GetString();
			var notionURLMarkdown = $"[{notionURL}]({notionURL})";
			notionURLs.Add(notionURLMarkdown);
		    }
		}

		var joinedNotionIds = string.Join("\n\n", notionURLs);

		botMessage = $"{bot}\n\n{joinedNotionIds}";
	    }
	    // ボットの発言を会話履歴に追加
	    conversationHistory.Add(new { bot = botMessage });
	    // ボットの発言を返信
	    await stepContext.Context.SendActivityAsync(MessageFactory.Text(botMessage), cancellationToken);
	    // ボットの発言をCosmosDBに保存
	    var botMessageLog = new Message
	    {
		MessageId = Guid.NewGuid().ToString(),
		Timestamp = DateTime.Now,
		AadObjectId = _aadObjectId,
		ConversationId = _conversationId,
		TextMessage = botMessage,
		IsUser = false
	    };
	    await _logService.SaveConversationAsync(botMessageLog);

	} else {
	    // botプロパティがない場合はエラーを返信
	    await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Error: bot property not found"), cancellationToken);
	}
	}
	else
	{
	// FastAPIエンドポイントからのレスポンスがエラーの場合はエラーを返信
	await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Error: {response.StatusCode}"), cancellationToken);
}

4.2.5 ユーザーからの質問に回答した後、ユーザーのフィードバックを確認します。

Dialog.cs:162行目
// ユーザーのフィードバックを確認
return await stepContext
	.PromptAsync(nameof(ConfirmPrompt),
	    new()
	    {
		Prompt = MessageFactory.Text("こちらの回答で解決しましたか?"),
	    },
	    cancellationToken)
	.ConfigureAwait(false);

4.2.6 フィードバックの結果に応じて、チャットボットがメッセージを送信します。

Dialog.cs:175行目
var isGoodFeedback = (bool)stepContext.Result;

if (isGoodFeedback)
{
	await stepContext.Context.SendActivityAsync(MessageFactory.Text("お役に立てて光栄です。また何でも聞いてください。"), cancellationToken);
}
else
{
	await stepContext.Context.SendActivityAsync(MessageFactory.Text("お探しの情報が見つからずに申し訳ありません。"), cancellationToken);
}

4.2.7 ユーザーのフィードバックを会話のIDに紐づけてCosmosDBに保存します。

Dialog.cs:185行目
// ユーザーのフィードバックをCosmosDBに保存
var feedback = new Feedback
{
	FeedbackId = Guid.NewGuid().ToString(),
	Timestamp = DateTime.Now,
	ConversationId = _conversationId,
	IsGoodFeedback = isGoodFeedback
};
await _logService.SaveFeedbackAsync(feedback);

return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);

お知らせ

もちろん、株式会社piponでも技術でお困りのことがある方はオンライン相談が可能です。
こちらから会社概要資料をDLできます!
お問い合わせ内容に「オンライン相談希望」とご記載ください。
お問い合わせはこちら

株式会社piponでは定期的に技術勉強会を開催しています。
ChatGPT・AI・データサイエンスについてご興味がある方は是非、ご参加ください。
技術勉強会についてはこちら

株式会社piponではChatGPT・AI・データサイエンスについて業界ごとの事例を紹介しています。ご興味ある方はこちらのオウンドメディアをご覧ください。
オウンドメディアはこちら

株式会社piponのテックブログ

Discussion