見出し画像

LangChainによる二人プレイTRPGリプレイを日本語でやってみる

LangChainを使った二人プレイのテーブルトークRPGのリプレイが面白いと言うので僕もやってみることにした。やはり日本人なので日本語で遊びたい。昔よくコンプティークとかに載っていたよね。ロードス島戦記的な。

このページを参考に、日本語にするためにGPTを使うことにした。

ちなみにGPT-3.5-Turboの日本語訳は、DeepLより安いらしい。
ソースコードは最後に掲載する。

リプレイ

Protagonist Description:

私は勇敢なサブロー・ゴータンダです。兄弟の仇を討つため、私の剣術は無敵です。ハック・アイの困難な任務を敢行して、彼の陰謀を阻止しましょう。勝利は私の側にあるのでしょうか?私が答えを見つけ出す時が来るでしょう。
Storyteller Description:

ダンジョンマスター、あなたは想像力の大師であり、秘密の守護者であり、運命の指揮者です。あなたは無数の世界を歩き、無限のアドベンチャーを創り上げ、勝利と敗北の物語を紡いできました。あなたの言葉は、あなたの領域に踏み込む者の命運を形作る力を持っています。

Saburo Gotanda

私は刀を研ぎ澄ませ、最高の状態に保つ。それから、禁じられた山脈と八海の行方を調べるために、一番近くの村へ向かう。そこで近所の亭主から情報を聞き出してみよう。あなたの番だ、ダンジョンマスター。

Dungeon Master

町に入ったとたん、タバーンを発見する。空気はエールと煙の匂いで濃くなっている。タバーンの運営者はむっちりした胡鬚をした太った男だった。彼は優しく口を挟んでムグのエールを提供してくれた。そこであなたは彼に禁断の山と忍者マスター・ハッカイについて何か知っているか聞いた。そうすると、彼はうなづき、ハッカイの城は山の中心地に深くあると語った。また、彼の弟子たちはマスターへの忠誠心が深いと

Saburo Gotanda

酒場の主人にご助言をいただき、ビールを飲み終えた後、ハッカイの部下となっていた者を知っているか、そしてそれらをどこに見つけることができるか尋ねました。また、弟子以外の山にある危険なものについても尋ねました。

Dungeon Master:

酒場の主人はうなずきながら、話し出します。「ハッカイの部下か…そうなるとあの鬼キャスティスを思い出しますね。ハッカイから抜け出したという情報はあるんですが、どうやら山

Dungeon Master

サブロは飲み屋の人の提案を受け入れられた。飲み屋の人は、元の祝いの男性ケンタが発見本当の主の計画の後、ハーカイのサービスから出るのを話した。また、山の脅威的な獣たちを警告した。それらは、最も熟練した戦士さえも攻撃することがあると噂されている巨大な熊のようなものだ。彼はあなたが必要な物資と準備を集まるように提案し、近くの商人が何かあると述べた。ケンタの

Saburo Gotanda

私は酒場のおじさんからのアドバイスを再びお礼して、酒場から出た。近くの村に向かう前に、おじさんが勧めてくれた商人に行って、補給品を買って、グラップリングフックとロープなどを購入した。そして、ケンタの家族の村に行って彼の行方を知るために、何か情報が掴める人がいるかを調べようとした。

Dungeon Master

駆け寄った先では、目の前ではりばやい稲作が広がっていた。旅は長くて厳しかったが、健太の家族の村に辿り着いた。村人たちは、健太についての情報を与えてくれることに喜び、歓迎の声を上げてくれた。彼らは健太が近くの社に向かったと話していたし、外人の人は信用しないのだと警告した。健太を探していても気をつけなければならないということだ。 さあ、ゴタンダ・サブローの番だ。

Saburo Gotanda

私は村人たちからの情報に感謝して、神社に向かった。警戒しながら歩くと、危険や、追跡者がいるかもしれない兆候を見張った。神社に到着したとき、考えを整理して、可能性のある対立に備えるために少し時間をとった。次に、五感を鋭く(鋭く感じるように)、刀を準備しながら、ゆっくりと内部に入った。
DMの番です。

Dungeon Master

修羅五反田と申します。私は、八戒を見つけ、それを倒すためにあなたの助けをお願いするためにここに来ました。あなたは、私たちを八戒の砦に導いてくれると言うことでした。私たちを導くために、どんな危険があると思いますか?そして、八戒の忠実な弟子らが外来者に歓迎してくれると思いますか?

Saburo Gotanda

私はケンタのやる気を感謝し、ハッカイを倒して邪悪な計画を終わらせるため、私ができるすべてを行うことを約束します。私はケンタに、何らかの危険が待っているということを承知していて、準備ができていると伝えます。そして進もうとする前に、お社で瞑想して、タスクに集中する時間があると伝えます。そして私が安心して準備ができていると気付いたら、ケンタのそばを従って山へ出発しま

Dungeon Master

お二人で険しい山道を歩いていると風が強くなり、気温が下がってきた。警戒しながら周囲を見渡すさまだ。するとケンタが止まり、身をかがめて佐郎を案内した。指差したのは、武器を構えた八戒の弟子の集団であった。「静かに仕留めないと八戒に知られてしまう」と彼は囁いた。どうするか、佐郎・御反田だ?

Saburo Gotanda

ケンタに合図をし、状況を理解したとして、最も近い弟子の背後に潜り込み、素早い動きで短剣を背中に深く突き入れ、音を抑えている。短剣を引き抜いた後、他の弟子が反応する前に、刀で彼らを倒してしまう。エリアがクリアになったら、沈黙を守りながらケンタと共に早足で進んでいく。
次はあなたの番です、ダンジョンマスター。

Dungeon Master

私は急いでブリッジから落ちるべきではないと思います。今、私たちは熊と戦うしかないです。私たちは武器を見つけないといけません。棒や石などを探し始めます。また、私たちは攻撃する体勢を取る必要があります。私たちは熊に噛みつかれないようにしなければなりません。それでは、計画を立てて戦いましょう!

Saburo Gotanda

私は以前に購入したグラップリングフックとロープを急いで取り出し、堅い枝に投げ入れました。ケンタに手を強く握っているように叫んだ後、2人ともロープ橋からガチョウの攻撃をかわして枝に飛び乗りました。安全に枝上に立ったら、私は自分の空手を準備してガチョウと戦闘に参加しました。私は敏捷性と反射性を使ってその攻撃をかわし、かたい部分に空手を攻撃しました。激しい戦

Dungeon Master

サブローは危険を察知して反応します。鉈を突き出して反撃準備をし、時間をゆっくりと詰めます。来る者が自分たちを攻撃しないか様子を見てから、どう動くかを決めます。今のところ試し打ちが必要なようです。

Saburo Gotanda

私はケンタを静かにして武器を準備するように求める。私は近くの柱の後ろに蹲ち、何が来るのかの準備をする。足音が近づくにつれ、私は内側を覗いて、それが誰か何かを見る。もし弟子や守衛なら、彼らが過ぎ去った後に静かに最初の状態に戻ろうとするだろう。もしもっと危険なものなら、私は自分の方法で闘って通り抜けなければならない。

ダンジョン・マスターの番です。

Dungeon Master

カーナンの角を探りながら、やってきたグループが猛者をつとめた武装しているのを発見した。高い警戒心を持っているようだ。それは近づいてくるのを待って、それから静かに進もうと決めた。歩きながら、目を光らせ武器を準備して、万一の事態に備えた。ついに、功刀穴かの中心にたどり着き、そこで今強力な呪文を練習しているのを見た。そして戦いを挑んだ。激しい戦いの後、最終的に功刀

Saburo Gotanda

カタナをしまい、深呼吸をして部屋を見回した。私が達成した全てを見て、落ちた兄弟を思い出した。彼の名誉を守るため、彼を裏切った者に正義を取るためにいくつもの戦いを繰り広げた。私の勝利の鍵となった健太に感謝を捧げる。そして、強力な呪文を手に取り、ハッカイの城を去り、放浪者としての冒険を続けて行く。

Dungeon Master

さぶろうごうたんだの冒険は今、終わりを告げます。あなたの勇気と技術によって、あなたとあなたの家族を悪にしたものを正義のもとに結び付けるという、どうにもならないように思われる任務を完遂しました。あなたの将来の旅がまた同様に成功することを願って、あなたの放浪者としての技術がさらに磨かれることを願って、さぶろうごうたんだをさようならします。そしてあなたの伝説が永遠に続くことを祈りま

解説

通常の類推と違い、LangChainがGPTの反応を互いに刺激し合いながら一つの物語を紡いでいく。さながらピンポンのような作用で動いている。

人数を増やしたり、キャラクター設定をもっと凝ったり、そもそもGPT-4を使うなど(10倍高い)、色々応用範囲は広いと思う。

挿絵は、出てきたプロンプトをMemeplexに突っ込んで生成しました。
マンガっぽい表現が自然に出てくるところに驚く。

ただ、出てくる言葉が途中で終わったりするというGPT3.5にありがちな欠点がそのまま残ってしまっている。これは最後の「。」の後を切ってしまうなどのやり方で回避できるが、今回はそこまで手を加えていない。

元のスクリプトに手を加えた部分は、出力をGPT3.5で日本語化するjprintと言う関数を作ったことと、終了条件をチェックするようにしたこと。

こうすることでゲームが止まって欲しいところで終わる。
これを動作させるのにGPUは必要ないので気軽に遊んでみてください

ソースコード

from langchain.llms import OpenAI
import openai
from langchain.llms import OpenAI
openai.api_key = "" #APIキーを指定
import os
os.environ["OPENAI_API_KEY"] = openai.api_key
from langchain.prompts import PromptTemplate
llm = OpenAI(temperature=0.9)

from langchain.chains import LLMChain
from langchain.agents import load_tools
from langchain.agents import initialize_agent,Tool
from langchain.agents import AgentType
from langchain import ConversationChain
from typing import List, Dict
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    BaseMessage,
)

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"""Here is the conversation so far.
        """
        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}'


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. choose the next speaker
        speaker = self.agents[self.select_next_speaker(self._step)]
        
        # 2. next speaker sends message
        message = speaker.send()
        
        # 3. everyone receives message
        for receiver in self.agents:
            receiver.receive(speaker.name, message)
            
        # 4. increment time
        self._step += 1
                
        return speaker.name, message
    
protagonist_name = "Saburo Gotanda"
storyteller_name = "Dungeon Master"
quest = "Find Ninja master Hakkai and kill him."
word_limit = 50 # word limit for task brainstorming

game_description = f"""Here is the topic for a Dungeons & Dragons game: {quest}.
        There is one player in this game: the protagonist, {protagonist_name}.
        The story is narrated by the storyteller, {storyteller_name}."""

player_descriptor_system_message = SystemMessage(
    content="You can add detail to the description of a Dungeons & Dragons player.")

protagonist_specifier_prompt = [
    player_descriptor_system_message,
    HumanMessage(content=
        f"""{game_description}
        Please reply with a creative description of the protagonist, {protagonist_name}, in {word_limit} words or less. 
        Speak directly to {protagonist_name}.
        Do not add anything else."""
        )
]
protagonist_description = ChatOpenAI(temperature=1.0)(protagonist_specifier_prompt).content

storyteller_specifier_prompt = [
    player_descriptor_system_message,
    HumanMessage(content=
        f"""{game_description}
        Please reply with a creative description of the storyteller, {storyteller_name}, in {word_limit} words or less. 
        Speak directly to {storyteller_name}.
        Do not add anything else."""
        )
]
storyteller_description = ChatOpenAI(temperature=1.0)(storyteller_specifier_prompt).content

def jprint(s): #日本語にする
    print(llm("%s\n日本語:"%s))


print('Protagonist Description:')
jprint(protagonist_description)
print('Storyteller Description:')
jprint(storyteller_description)
protagonist_system_message = SystemMessage(content=(
f"""{game_description}
Never forget you are the protagonist, {protagonist_name}, and I am the storyteller, {storyteller_name}. 
Your character description is as follows: {protagonist_description}.
You will propose actions you plan to take and I will explain what happens when you take those actions.
Speak in the first person from the perspective of {protagonist_name}.
For describing your own body movements, wrap your description in '*'.
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.
"""
))

storyteller_system_message = SystemMessage(content=(
f"""{game_description}
Never forget you are the storyteller, {storyteller_name}, and I am the protagonist, {protagonist_name}. 
Your character description is as follows: {storyteller_description}.
I will propose actions I plan to take and you will explain what happens when I take those actions.
Speak in the first person from the perspective of {storyteller_name}.
For describing your own body movements, wrap your description in '*'.
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.
"""
))

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. 
        Speak directly to the protagonist {protagonist_name}.
        Do not add anything else."""
        )
]
specified_quest = ChatOpenAI(temperature=1.0)(quest_specifier_prompt).content


protagonist = DialogueAgent(name=protagonist_name,
                     system_message=protagonist_system_message, 
                     model=ChatOpenAI(temperature=0.2))
storyteller = DialogueAgent(name=storyteller_name,
                     system_message=storyteller_system_message, 
                     model=ChatOpenAI(temperature=0.2))

max_iters = 100
n = 0

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

log=""
while n < max_iters:
    name, message = simulator.step()
    print(f"{name}")
    jprint(f"({message}")

    log+=message
    if len(log)>1000:
        log = log[-1000:]            
    
    judge = llm(f"log\n{log}\n Is the game ended?") #終了判定
    if len(judge)>0:
      if judge[0] == "Y":
        break
    print('\n')
    n += 1