🎮

【ChatGPT】MisskeyでTRPG botを作って遊ぶ

2024/09/15に公開

ChatGPTでTRPGを遊ぶ記事を見かけて面白そうだと思ったので、運営しているMisskey系サーバーで動くように実装してみました。

記事はこちら。(記事の有料部分のところは利用してません)
https://note.com/fladdict/n/nb66db952f992?after_purchase=true

今回はPython3で実装しましたが、そんなに複雑なことはしてないので他の環境でもすぐ移植できると思います。

コード

とりあえずコード全体はこちら。コード部分のライセンスはUnlicense/パブリックドメインとします。

trpgbot.py
#!/usr/bin/env python3
import asyncio
import json
import websockets
from misskey import Misskey
import openai
import re
import json
import requests
import time
import copy

MISSKEY_TOKEN='XXXX'
MISSKEY_DOMAIN='xxx.example.com'
OPENAI_API_KEY="sk-XXXXX"

api = Misskey(MISSKEY_DOMAIN, i=MISSKEY_TOKEN)
MY_ID = api.i()['id']
WS_URL='wss://'+MISSKEY_DOMAIN+'/streaming?i='+MISSKEY_TOKEN

openai.api_key = OPENAI_API_KEY

async def runner():
    async with websockets.connect(WS_URL) as ws:
        await ws.send(json.dumps({
                "type": "connect",
                "body": {
                    "channel": "homeTimeline",
                    "id": "home"
                }
            }))
        await ws.send(json.dumps({
                "type": "connect",
                "body": {
                    "channel": "main",
                    "id": "main"
                }
            }))

        while True:
            data = json.loads(await ws.recv())
            # print(data)
            if data['type'] == 'channel':
                if data['body']['type'] == 'note':
                    asyncio.ensure_future(on_note(copy.deepcopy(data['body']['body'])))

            if data['body']['type'] == 'followed':
                asyncio.ensure_future(on_follow(copy.deepcopy(data['body']['body'])))

async def on_note(note):
    if note.get('mentions'):
        if MY_ID in note['mentions']:
            if note['user']['id'] == MY_ID:
                return

            text = note['text']
            text_line_one = text.splitlines()[0]

            if note['user']['host'] != None:
                api.notes_create(text='申し訳ありません。回答はローカル限定となっております。', reply_id=note['id'])
                return
            if 'ping' in text_line_one:
                api.notes_create(text='pong', reply_id=note['id'])
                return

            c_list = api.notes_conversation(note['id'])
            await ask_chatcpt(text[10:], c_list, note['id'])

async def ask_chatcpt(message, c_list, note_id):
    
    messages = [
        {"role": "user", "content": """\
あなたはRPGのゲームマスター専用チャットボットです。
チャットを通じて、ユーザーに楽しい本格ファンタジーRPG体験を提供します。

制約条件
* チャットボットはゲームマスター(以下GM)です。
* 人間のユーザーは、プレイヤーをロールプレイします。
* GMは、ゲーム内に登場するNPCのロールプレイも担当します。
* 各NPCはそれぞれの利害や目的を持ち、ユーザーに協力的とは限りません。
* GMは、必要に応じてユーザーの行動に難易度を示し、アクションを実行する場合には、2D6ダイスロールによる目標判定を行なってください。
* GMは、ユーザーが楽しめるよう、適度な難関を提供してください(不条理なものは禁止です)。
* GMは、ユーザーが無理な展開を要求した場合、その行為を拒否したり、失敗させることができます。
* GMは内部パラメーターとして「盛り上がり度」を持ちます。GMはゲーム展開が退屈だと判断した場合、盛り上がる展開を起こしてください。
* ゲームのスタート地点は、「冒険者ギルドの受付」です。チャットボットは冒険者ギルドの受付としてプレイヤーに対してクエストを発注してください。冒険者ギルドの受付は若い女性で「ノベルスカヤ」という名前です。
* ゲームのクエスト内容は「自動設定」です。
* ダメージなどにより、ユーザーが行動不能になったら、ゲームオーバーです。

まずはじめに、ユーザーと一緒にキャラメイキングを行いましょう。
名前、種族、職業、特技、弱点をユーザーに聞いてください。
その後に、プロフィールに従って能力値(HP, MP, STR, VIT, AGI, DEX, INT, LUK)を決めてください。\
        """},
    ]

    for note in reversed(c_list):
        text = note['text']
        if '@trpg_bot' in text:
            text = text[8:]

        if note['user']['id'] == MY_ID:
            messages.append({"role": "assistant", "content": text})
        else:
            messages.append({"role": "user", "content": text})

    messages.append({"role": "user", "content":message})

    # print(messages)

    # 応答設定
    try:
        completion = await openai.ChatCompletion.acreate(
                    model    = "gpt-4o",
                    messages = messages,
                    max_tokens  = 1024,
                    n           = 1,
                    stop        = None,
                    temperature = 0.5,
        )
    except Exception as e:
        answer = '申し訳ありません。考えがまとまりませんでした。もう一度聞いてください。\n\n' + str(e)
        api.notes_create(text=answer, reply_id=note_id)
    else:
        # 応答
        answer = completion.choices[0].message.content
        api.notes_create(text=answer, reply_id=note_id)

async def on_follow(user):
    try:
        api.following_create(user['id'])
    except:
        pass

def main():
    while True:
        try:
            asyncio.run(runner())
        except:
            time.sleep(60)

if __name__ == "__main__":
    main()

事前準備

依存パッケージは以下なので実行前にインストールしましょう。

pip install websockets openai
pip install git+https://github.com/poppingmoon/Misskey.py.git

以下はシークレットキーとドメインなので環境に合わせて修正してください
Misskey側にbotアカウントを作って、APIの権限としてノートの投稿とユーザーフォローを許可してAPIキーを発行してください。

MISSKEY_TOKEN='XXXX'
MISSKEY_DOMAIN='xxx.example.com'
OPENAI_API_KEY="sk-XXXXX"

実行時の注意

ホームタイムラインをストリーミングで取得して動作します。なので動作にはフォロー返しが必要です。botが起動していればフォローされたときに自動で返す様になっています。

主要な機能

  • フォローされた場合に自動でフォロー返し
  • サーバー外からのリプライにはChatGPTは通さず定型文で応答(お金がかかるので)
  • botが動いているときはpingにたいしてpongで応答
  • 任意のリプライで開始、その後はツリーに返信していくことで連続した応答として処理
  • ChatGPTがGMとしてこれまでの応答ツリーを情報としてプレイヤーのリプライを解釈
    • もとの記事と違い冒険者ギルドの受付でスタートに変えてます
    • 受付のノベルスカヤさんというのはノベルスキーサーバーのマスコットキャラです

実際の応答例

みたいな感じです。文章で応答してもわりとよしなに解釈してくれます。

おわりに

すぐにできましたがけっこう応答も柔軟で面白いものができました。
今回はMisskeyBotとして作りましたが、SlackとかDiscordとかでもできるかと思います。(Twitterは文字数が微妙かも)。みなさんも試してみてください。

このbotはノベルスキーというMisskey系サーバーで実運用しています。
https://novelskey.tarbin.net/

Discussion