
自立エージェントによる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人の絆は、これからの冒険でさらに強くなっていくことだろう。どんな困難な試練が待ち受けていても、ラムネとグレープはお互いを信じ、力を合わせて乗り越えていく。それが、彼らの冒険の物語だ。さあ、次の冒険が始まる!
(グレープ): それじゃあ、次の冒険に向かおう!ラムネ、一緒に頑張ろう! *意気揚々と次の冒険に向かう*
(ラムネ): グレープ、もちろんだよ!一緒に頑張ろう! *意気揚々と次の冒険に向かう*
感想
どうでしょうか。各エージェントが生き生きとロールプレイの役目してくれて、思いのほかうまく動いてくれた気がします。皆さんも設定を色々変えて面白いストーリーができましたら、ぜひ教えてもらえると嬉しいです。
お読みいただき、ありがとうございました。