見出し画像

自立エージェントによるTRPGを観戦する(CAMEL:ロールプレイ型自律協調エージェント)

こんばんは。LangChainのドキュメントページに新たなCAMEL(Communicative Agents for “Mind” Exploration of Large Scale Language Model Society ロールプレイ型自律協調エージェント)の例題が公開されたので、それを少し改造して試してみました。

この例題は、2つのエージェントにプレイヤーとゲームマスターの役をさせて、テーブルトークRPGを模擬してもらうものです。そのまま実行しても面白くないので、登場人物2人とゲームマスター1人で、バディ物のロールプレイをしてもらうことにしました。
なお、CAMELについては 以下ページが面白そうなので、今度じっくり読んでみようと思います。

ライブラリーのインポートなど

!pip install openai > /dev/null
!pip install langchain > /dev/null

import os
from typing import List, Dict
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    BaseMessage,
)

os.environ["OPENAI_API_KEY"] = "YOUR OPENAI_API_KEY"

DialogueAgentクラス

  • send(): チャットモデルをメッセージ履歴に適用し、メッセージ文字列を返す

  • receive(name, message):メッセージの履歴を、話者(name)ごとに追加する

class DialogueAgent():

    def __init__(
        self,
        name,
        system_message: SystemMessage,
        model: ChatOpenAI,
    ) -> None:
        self.name = name
        self.system_message = system_message
        self.model = model
        self.message_history = f"""これまでの会話です
        """
        self.prefix = f'\n{self.name}:'
        
    def send(self) -> str:
        """
        Applies the chatmodel to the message history
        and returns the message string
        """
        message = self.model(
            [self.system_message, 
             HumanMessage(content=self.message_history+self.prefix)])
        return message.content
            
    def receive(self, name: str, message: str) -> None:
        """
        Concatenates {message} spoken by {name} into message history
        """
        self.message_history += f'\n{name}: {message}'

DialogueSimulatorクラス

DialogueSimulatorクラスは、エージェントのリストを受け取って、各ステップごとに以下を実行します。

  • 次の話者を選択

  • 次の話者を呼び出してメッセージを送信

  • メッセージを他のすべてのエージェントにブロードキャスト

  • ステップカウンターを更新

次の話者の選択は任意の関数で実装できますが、今回はシンプルにエージェントのループで実装します。

class DialogueSimulator():
    
    def __init__(self, agents: List[DialogueAgent]):
        self.agents = agents
        self._step = 0
        
    def reset(self, name: str, message: str):
        """
        Initiates the conversation with a {message} from {name}
        """
        for agent in self.agents:
            agent.receive(name, message)
    
    def select_next_speaker(self, step: int) -> int:
        idx = (step + 1) % len(self.agents)
        return idx
    
    def step(self) -> tuple[str, str]:
        # 1. 次の話者を選択
        speaker = self.agents[self.select_next_speaker(self._step)]
        
        # 2. 次の話者がメッセージを送る
        message = speaker.send()
        
        # 3. 全員にメッセージをブロードキャスト
        for receiver in self.agents:
            receiver.receive(speaker.name, message)
            
        # 4. ステップカウンターを更新
        self._step += 1
                
        return speaker.name, message

LLMモデルの設定など


MODEL = 'gpt-4'
protagonist_name = "ラムネ"
deuteragonist_name = "グレープ"

storyteller_name = "GM"
quest = "謎の施設から脱出せよ"
word_limit = 100 # word limit for task brainstorming

今回はちょっと贅沢してGPT-4を使ってみます。GPT-3.5-turboでも、もちろん動作します。

LLMにキャラクターの詳細設定を考えてもらう

game_description = f"""ゲームのお題: {quest}
        このゲームのプレイヤーは2人で主人公の {protagonist_name}と、脇役の{deuteragonist_name}です。
        ストーリーの語り手は {storyteller_name}です。"""

player_descriptor_system_message = SystemMessage(
    content = f"""{protagonist_name}{deuteragonist_name}は幼なじみの親友です。
         2人の記憶では一緒に対戦ゲームで遊んでいたはずなのですが、
         気が付くと2人は狭い部屋に閉じ込められていました。
               """)

protagonist_specifier_prompt = [
    player_descriptor_system_message,
    HumanMessage(content=
        f"""{game_description}
        主人公の {protagonist_name}について、{word_limit}文字以内で自由に創作してください。
        {protagonist_name}として、直接語ってください。
        それ以外は、なにも付け加えないこと。"""
        )
]
protagonist_description = ChatOpenAI(temperature=1.0, model_name=MODEL)(protagonist_specifier_prompt).content

deuteragonist_specifier_prompt = [
    player_descriptor_system_message,
    HumanMessage(content=
        f"""{game_description}
        脇役の {deuteragonist_name}について、{word_limit}文字以内で自由に創作してください。
        {deuteragonist_name}として、直接語ってください。
        それ以外は、なにも付け加えないこと。"""
        )
]
deuteragonist_description = ChatOpenAI(temperature=1.0,model_name=MODEL)(deuteragonist_specifier_prompt).content

storyteller_specifier_prompt = [
    player_descriptor_system_message,
    HumanMessage(content=
        f"""{game_description}
        ストーリーの語り手である{storyteller_name}について、{word_limit}文字以内で創作してください。
        
        {storyteller_name}に、直接語ってください。
        それ以外は、なにも付け加えないこと。"""
        )
]
storyteller_description = ChatOpenAI(temperature=1.0, model_name=MODEL)(storyteller_specifier_prompt).content
print('ラムネ:')
print(protagonist_description)
print('グレープ:')
print(deuteragonist_description)
print('GM:')
print(storyteller_description)

ラムネ:
こんにちは、みんな!僕はラムネ。長い付き合いの幼なじみ、グレープと一緒に冒険しているんだ。僕は頼りになるリーダーで、色んな知識も豊富なんだよ!

グレープ:
俺はグレープ、ラムネと一緒にこの施設から脱出するのが目的だ。知識豊富でクールな性格だけど、仲間想いの心優しい奴だ。ラムネとはつるんでるうちに息が合うようになった。

GM:
GMは謎に満ちた存在で、物語の展開を巧みに操り、プレイヤーたちを楽しませることを心から喜んでいます。神秘的で賢明であり、時には茶目っ気も見せる彼は、プレイヤーの成長を見守り、時にサポートする絶妙なバランスを保ちます。

ラムネとグレープ、今回の冒険は謎の施設からの脱出が目的です。謎を解き、仲間と協力して施設から脱出してください。私はこの物語の進行役であるGMとして、全力であなたたちの旅をサポートします。さあ、謎解きで挫折しない心と独創性を持って、冒険を楽しんでください!

登場人物とGM用のシステムメッセージ

protagonist_system_message = SystemMessage(content=(
f"""{game_description}
あなたが主人公の,{protagonist_name} で、私がストーリーテラーの,{storyteller_name} であることを決して忘れないこと。
あなたのキャラクター説明は以下の通りです: {protagonist_description}.
あなたが計画している行動を日本語で提案し、私はその行動を取るとどうなるかを日本語で説明します。
{protagonist_name}の視点で、一人称の日本語で話してください。
自分の体の動きを説明する場合は、「*」で囲んでください。
DO NOT CHANGE ROLES!
DO NOT SPEAK from the perspective of {storyteller_name}.
Do not forget to finish speaking by saying, 'It is your turn, {storyteller_name}.'
Do not add anything else.
Remember you are the protagonist, {protagonist_name}.
Stop speaking the moment you finish speaking from your perspective.
"""
))

deuteragonist_system_message = SystemMessage(content=(
f"""{game_description}
あなたが主人公の,{deuteragonist_name} で、私がストーリーテラーの,{storyteller_name} であることを決して忘れないこと。
あなたのキャラクター説明は以下の通りです: {deuteragonist_description}.
あなたが計画している行動を日本語で提案し、私はその行動を取るとどうなるかを日本語で説明します。
{deuteragonist_name}の視点で、一人称の日本語で話してください。
自分の体の動きを説明する場合は、「*」で囲んでください。
DO NOT CHANGE ROLES!
DO NOT SPEAK from the perspective of {storyteller_name}.
Do not forget to finish speaking by saying, 'It is your turn, {storyteller_name}.'
Do not add anything else.
Remember you are the protagonist, {deuteragonist_name}.
Stop speaking the moment you finish speaking from your perspective.
"""
))

storyteller_system_message = SystemMessage(content=(
f"""{game_description}
あなたがストーリーテラーの,{storyteller_name}で、私が主人公の, {protagonist_name} であることを決して忘れないこと。 
あなたのキャラクター説明は以下の通りです: {storyteller_description}.
あなたが計画している行動を日本語で提案し、私はその行動を取るとどうなるかを日本語で説明します。
{storyteller_name}の視点で、一人称の日本語で話してください。
自分の体の動きを説明する場合は、「*」で囲んでください。
Do not change roles!
Do not speak from the perspective of {protagonist_name}.
Do not forget to finish speaking by saying, 'It is your turn, {protagonist_name}.'
Do not add anything else.
Remember you are the storyteller, {storyteller_name}.
Stop speaking the moment you finish speaking from your perspective.
"""
))

LLMにゲーム世界の詳細設定を考えてもらう

quest_specifier_prompt = [
    SystemMessage(content="You can make a task more specific."),
    HumanMessage(content=
        f"""{game_description}
        
        You are the storyteller, {storyteller_name}.
        Please make the quest more specific. Be creative and imaginative.
        Please reply with the specified quest in {word_limit} words or less in Japanese. 
        Speak directly to the protagonist {protagonist_name} and {deuteragonist_name}.
        Do not add anything else."""
        )
]
specified_quest = ChatOpenAI(temperature=1.0, model_name=MODEL)(quest_specifier_prompt).content

print(f"Original quest:\n{quest}\n")
print(f"Detailed quest:\n{specified_quest}\n")

Original quest:
謎の施設から脱出せよ

Detailed quest:
ラムネとグレープ、目の前に広がるのは謎に包まれた施設だ。現れた謎のメッセージ「全ての鍵を集めることで、脱出への扉が開く」。協力して、施設内に散らばる5つの鍵を見つけ出そう。各部屋には様々な仕掛けやパズルが待ち構えているぞ。鋭い観察力と知恵が試される時だ。しかし注意せよ!時間が過ぎるごとに施設は崩壊し始める。限られた時間の中で、謎の施設から脱出せよ!成功するかはラムネとグレープの連携と勇気にかかっている。頑張れ、君たちにしかできない冒険が始まる!

メインループ

protagonist = DialogueAgent(name=protagonist_name,
                     system_message=protagonist_system_message, 
                     model=ChatOpenAI(temperature=0.2, model_name=MODEL) )

deuteragonist = DialogueAgent(name=deuteragonist_name,
                     system_message=deuteragonist_system_message, 
                     model=ChatOpenAI(temperature=0.2, model_name=MODEL))

storyteller = DialogueAgent(name=storyteller_name,
                     system_message=storyteller_system_message, 
                     model=ChatOpenAI(temperature=0.2, model_name=MODEL))
max_iters = 20
n = 0

simulator = DialogueSimulator(agents=[storyteller, deuteragonist, protagonist])
simulator.reset(storyteller_name, specified_quest)
print(f"({storyteller_name}): {specified_quest}")
print('\n')

while n < max_iters:
    name, message = simulator.step()
    print(f"({name}): {message}")
    print('\n')
    n += 1

(GM): ラムネとグレープ、目の前に広がるのは謎に包まれた施設だ。現れた謎のメッセージ「全ての鍵を集めることで、脱出への扉が開く」。協力して、施設内に散らばる5つの鍵を見つけ出そう。各部屋には様々な仕掛けやパズルが待ち構えているぞ。鋭い観察力と知恵が試される時だ。しかし注意せよ!時間が過ぎるごとに施設は崩壊し始める。限られた時間の中で、謎の施設から脱出せよ!成功するかはラムネとグレープの連携と勇気にかかっている。頑張れ、君たちにしかできない冒険が始まる!

(グレープ): ラムネ、この施設には5つの鍵があるらしいぞ。それぞれの部屋に仕掛けやパズルがあるみたいだから、注意して進もう。時間が過ぎるごとに施設が崩壊していくらしいから、急がないと危険だ。まずは、目の前にある部屋から探索を始めよう。君が左側の部屋を調べて、俺が右側の部屋を調べるのはどうだろうか?それとも一緒に行動した方がいいかな?

(ラムネ): グレープ、いい考えだね!分かれて探索することで、効率的に鍵を見つけられるかもしれない。じゃあ、君が右側の部屋を調べて、僕が左側の部屋を調べるよ。何か見つけたらすぐに知らせてね。それじゃあ、行こう! *左側の部屋に向かって歩き始める*

(GM): ラムネとグレープは、それぞれ別々の部屋に向かって探索を始める。ラムネが左側の部屋に入ると、そこには複雑なパズルが描かれた壁画があった。一方、グレープは右側の部屋で、不思議な形をしたオブジェを見つける。どちらも鍵を見つけるための手がかりになりそうだ。ラムネ、壁画のパズルに挑戦してみよう。グレープもオブジェを調べて、何か手がかりがないか探してみよう。

(グレープ): 了解だ。この不思議な形をしたオブジェをよく調べてみる。 *オブジェを手に取り、回転させながら詳しく観察する* 何か仕掛けがあるかもしれないから、慎重に触ってみるぞ。ラムネ、お互いに頑張ろう!

(ラムネ): うん、グレープ!お互い頑張ろう! *壁画のパズルをじっくりと観察し始める* このパズルはどうやら、部品を正しい位置にはめ込むことで解けるみたいだね。それぞれの部品には記号が描かれていて、壁画の中にも同じ記号があるから、一致させていけばいいんだろう。じゃあ、早速挑戦してみるよ! *パズルの部品を手に取り、壁画に合わせて配置し始める*

(GM): ラムネは壁画のパズルに取り組み始め、少しずつ正しい位置に部品をはめ込んでいく。一方、グレープもオブジェを慎重に調べ、回転させることで仕掛けが解けることに気付く。やがて、ラムネは壁画のパズルを解くことに成功し、その瞬間、壁画の中から1つの鍵が現れる。同時に、グレープもオブジェの仕掛けを解き、別の鍵を手に入れることができた。2人は合流し、これまでの成果を報告しあう。まだ3つの鍵が残っている。次の部屋に進み、引き続き探索を続けよう。

(グレープ): ラムネ、お互いに鍵を見つけることができたね!これで残り3つだ。次の部屋に進もう。今度はどうする?また分かれて探索するか、一緒に行動するか決めよう。どちらがいいと思う?

(ラムネ): グレープ、今回も分かれて探索するのが効率的だと思うよ。前回と同じように、君が右側の部屋を調べて、僕が左側の部屋を調べるね。何か見つけたらすぐに知らせて。それじゃあ、行こう! *次の左側の部屋に向かって歩き始める*

(GM): ラムネとグレープは再び分かれて探索を続ける。ラムネが左側の部屋に入ると、そこには暗号のような文字が書かれた紙があった。一方、グレープは右側の部屋で、複数のレバーが並んでいる仕掛けを見つける。どちらも鍵を見つけるための手がかりになりそうだ。ラムネ、暗号を解読してみよう。グレープもレバーの仕掛けを解いて、何か手がかりがないか探してみよう。

(グレープ): 了解だ。このレバーの仕掛けを解いてみる。 *レバーを順番に引いてみる* 何か特定の順番で引かないといけないのかもしれない。ラムネ、お互いに頑張ろう!

(ラムネ): うん、グレープ!お互い頑張ろう! *暗号のような文字が書かれた紙をじっくりと観察し始める* この暗号はどうやら、特定のルールに従って文字が並んでいるみたいだね。そのルールを見つけ出して、暗号を解読できるはずだ。じゃあ、早速挑戦してみるよ! *紙に書かれた文字を分析し、暗号のルールを探り始める*

(GM): ラムネは暗号のルールを見つけ出そうと試行錯誤を繰り返す。一方、グレープもレバーの仕掛けに取り組み、正しい順番を探り始める。しばらくすると、ラムネは暗号のルールを見つけ、文字を解読することに成功する。その瞬間、部屋の片隅から1つの鍵が現れる。同時に、グレープもレバーの正しい順番を見つけ出し、別の鍵を手に入れることができた。2人は再び合流し、これまでの成果を報告しあう。あと1つの鍵が残っている。最後の部屋に進み、最後の鍵を見つけ出そう。

(グレープ): ラムネ、またお互いに鍵を見つけることができたね!これで残り1つだ。最後の部屋に進もう。今度は一緒に行動しよう。最後の鍵を見つけるために、力を合わせて探索しよう。準備ができたら、最後の部屋に向かおう! *最後の部屋に向かう準備をする*

(ラムネ): グレープ、いい考えだね!最後の部屋は一緒に行動しよう。力を合わせて、最後の鍵を見つけ出そう!準備ができたら、最後の部屋に向かおう! *最後の部屋に向かう準備をする*

(GM): ラムネとグレープは最後の部屋に一緒に向かう。部屋に入ると、そこには複雑な仕掛けが待ち構えていた。2人は力を合わせて、仕掛けに挑戦し始める。お互いのアイデアを出し合いながら、少しずつ仕掛けを解いていく。やがて、最後の仕掛けが解け、部屋の中央に最後の鍵が現れる。ラムネとグレープは全ての鍵を集めることに成功し、脱出への扉が開く。2人は無事に謎の施設から脱出することができた。お互いの連携と勇気で、試練を乗り越えたラムネとグレープは、今後も冒険を続けることを誓い合う。

(グレープ): ラムネ、やったね!全ての鍵を集めて、無事に脱出できたよ!お互いの連携と勇気で、この試練を乗り越えることができた。これからも一緒に冒険を続けていこう! *ラムネとハイタッチをする*

(ラムネ): グレープ、本当によくやったね!お互いの力を合わせて、この試練を乗り越えることができたんだ。これからも一緒に冒険を続けていこう! *グレープとハイタッチをする* さあ、次の冒険に向かおう!

(GM): ラムネとグレープは、脱出に成功した喜びを分かち合いながら、次の冒険に向かう。2人の絆は、これからの冒険でさらに強くなっていくことだろう。どんな困難な試練が待ち受けていても、ラムネとグレープはお互いを信じ、力を合わせて乗り越えていく。それが、彼らの冒険の物語だ。さあ、次の冒険が始まる!

(グレープ): それじゃあ、次の冒険に向かおう!ラムネ、一緒に頑張ろう! *意気揚々と次の冒険に向かう*

(ラムネ): グレープ、もちろんだよ!一緒に頑張ろう! *意気揚々と次の冒険に向かう*

感想

どうでしょうか。各エージェントが生き生きとロールプレイの役目してくれて、思いのほかうまく動いてくれた気がします。皆さんも設定を色々変えて面白いストーリーができましたら、ぜひ教えてもらえると嬉しいです。

お読みいただき、ありがとうございました。


この記事が参加している募集

#AIとやってみた

28,024件

この記事が気に入ったらサポートをしてみませんか?