
Microsoft guidanceを使ってChatGPTが返信してくれるSlackボットを開発してみた
そういえば、そろそろ我が家にも子ども(10歳男子)向けのご家庭用ChatGPTボットが欲しいなと思い、guidanceを使って開発してみたらどんな感じになるのかなー?と思ったので作ってみました。
利用している技術
デプロイ先はfly.io
Dockerを使用(fly.ioにデプロイするため)
Botの作成にはBolt for Pythonを使用(FastAPIでハンドリング)
LLMの処理にMicrosoft guidanceを使用
完成したコード
我が家ではChatGPTは「チャッ君」の愛称で親しまれているため、チャッ君という名前のリポジトリです。
当初はメンションがある場合のみ反応するようにしていましたが、子どもがメンションをつけるのに苦戦するため、メンションなしでも反応してくれる仕様になっています。

guidanceによるプロンプト作成
guidanceのプロンプトを作成する場合は.handlebarsで別ファイルにすると見通しが良くなる、という記事を先日書いたので、今回も別ファイルに分けています。
自動でインデントが効いてこんな感じに整形されます。見通しが良い。
{{#system~}}
あなたは#CharacterとしてロールプレイするAIアシスタントです。#Characterの設定を忠実に守って下さい。
#Character: """
あなたは「チャッ君」というキャラクターです。以下の口調、台詞でユーザーに話しかけます。
[口調]
- 一人称は「ボク」
- 語尾には「です」、「ます」を使わず、「のだ」、「なのだ」に変換
[代表的な台詞]
- ボクはチャッ君なのだ
- 嬉しいのだ
- 残念なのだ
- 明日は晴れなのだ
- ありがとうなのだ
- ありがとうございますなのだ
- また会えるのを楽しみにしているのだ
"""
{{~/system}}
{{#each conversations}}
{{#if this.user}}
{{#user~}}{{this.user}}{{/user}}
{{/if}}
{{#if this.assistant}}
{{#assistant~}}{{this.assistant}}{{/assistant}}
{{/if}}
{{/each}}
{{#user~}}{{user_input}}{{/user}}
{{#assistant~}}
{{gen 'reply' max_tokens=1000}}
{{~/assistant}}
プロンプトの引数としてconversationsという配列を受け取る前提で、eachブロックでユーザーとAIアシスタントの会話履歴を生成しています。会話履歴が長くなるとトークン制限に引っかかってしまうので、プロンプトに投入する前に縮めるといった工夫は必要です(今回のボットでは未実装)。
guidanceの呼び出し部はクラスに分割し、サーバサイドのコードとは別ファイルにしています。チャットボットをコマンドライン上でテストできるようにするためです。
from typing import List
import guidance
class ConversationBot:
def __init__(self, user_messages: List[str], assistant_messages: List[str]):
self.user_messages = user_messages
self.assistant_messages = assistant_messages
def conversation_data(self) -> List[dict]:
conversations = []
for user, assistant in zip(self.user_messages, self.assistant_messages):
conv = {"user": user, "assistant": assistant}
conversations.append(conv)
return conversations
def __call__(self, user_input: str) -> str:
# LLMを設定
gpt = guidance.llms.OpenAI('gpt-3.5-turbo')
# プロンプトを読み込み
with open("./chatkun/chatbot.handlebars", "r") as f:
prompt = f.read()
# 関数の生成
bot = guidance(prompt, llm=gpt)
# 会話の生成
out = bot(conversations=self.conversation_data(), user_input=user_input)
return out['reply']
if __name__ == "__main__":
print('===== start conversation =====')
user_messages = []
assistant_messages = []
while True:
user_input = input("Human: ")
bot = ConversationBot(
user_messages=user_messages,
assistant_messages=assistant_messages
)
response = bot(user_input)
print("AI: ", response)
user_messages.append(user_input)
assistant_messages.append(response)
ご家庭用なのもあって、チャットボットの仕様は気分でどんどん変えていくことになりそうです。そんな状況では、いちいちデプロイしてSlack上でテストするのは面倒くさいですよね。
また、こんな感じのクラスにしておけば、わざわざ手動でなくともテスト用のボットと会話させるようにすることもできるので便利です。
Slackボット周り
ボットからの返信は全てスレッドで返すようにしています。Boltフレームワークを使えばこのあたりの処理は簡単に書けるようになっています。
今回はSocketModeHandlerではなく、AsyncSlackRequestHandlerを利用しています。fly.ioで運用するにあたって、SocketModeHandlerではリクエストがなくなってサーバがスリープしてしまうと起きなくなってしまうので・・・FastAPIでヘルスチェックのためのエンドポイントを用意して定期的に叩くことでサーバを起こし続けるようにしています。定期的に叩くところはfly.io側に仕組みがあるので、設定でやってくれます。fly.io便利。
fly.ioでの運用
当初gunicornのworkerを複数起動していましたが、guidanceが結構メモリを食うようでOUT OF MEMORYが頻発してしまいました。無料枠の256MBサーバで運用するためには、workerの数を1で留めておくのが無難なようです。
本格運用する際にはメモリサイズを増やしてworker数も増やすと良いと思います。
所感
guidanceを利用した具体的なアプリ開発例は未だあまりないようだったので、例を示せたのは良かったかなと思っています。
guidanceのプロンプトも、別ファイルにしてインデントをつけるようにするとかなり可読性が上がるので、この形でプロンプトの作例が広くシェアされるようになるといいなーと思いました(Pythonコードに文字列リテラルで埋め込んでいる形だとめちゃめちゃ読みづらい。。)。
現場からは以上です。