{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Two-Player Dungeons & Dragons\n", "\n", "In this notebook, we show how we can use concepts from [CAMEL](https://www.camel-ai.org/) to simulate a role-playing game with a protagonist and a dungeon master. To simulate this game, we create a `TwoAgentSimulator` class that coordinates the dialogue between the two agents." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Import LangChain related modules " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from typing import List\n", "from langchain.chat_models import ChatOpenAI\n", "from langchain.schema import (\n", " AIMessage,\n", " HumanMessage,\n", " SystemMessage,\n", " BaseMessage,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `Player` class\n", "The `Player` class is a simple wrapper around the `ChatOpenAI` model that stores the message history from the `player`'s point of view. Specifically, it treats incoming messages as `HumanMessage`s and outgoing messages as `AIMessage`s." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class Player():\n", "\n", " def __init__(\n", " self,\n", " system_message: SystemMessage,\n", " model: ChatOpenAI,\n", " ) -> None:\n", " self.system_message = system_message\n", " self.model = model\n", " self.message_history = [self.system_message]\n", "\n", " def reset(self, message: BaseMessage=None) -> None:\n", " \"\"\"\n", " Initialize the player with an optional message to\n", " append to its message history.\n", " \"\"\"\n", " if message is not None:\n", " self.message_history.append(message)\n", " return self.message_history\n", "\n", " def _update_messages(self, message: BaseMessage) -> List[BaseMessage]:\n", " \"\"\"\n", " Append message to message history\n", " \"\"\"\n", " self.message_history.append(message)\n", " return self.message_history\n", "\n", " def step(\n", " self,\n", " input_message: HumanMessage,\n", " ) -> AIMessage:\n", " \"\"\"\n", " Compute agent response to input message\n", " \"\"\"\n", " messages = self._update_messages(input_message)\n", " output_message = self.model(messages)\n", " self._update_messages(output_message)\n", " return output_message" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `TwoAgentSimulator` class\n", "The `TwoAgentSimulator` class takes in two agents, the `first_speaker` and the `second_speaker`. It initializes the simulation using `reset()` with an utterance from the first speaker. The method `step()` takes an utterance from the `first_speaker` to the `second_speaker` as input and returns the messages from a single exchange between the `first_speaker` and `second_speaker`." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class TwoAgentSimulator():\n", " \n", " def __init__(self, first_speaker, second_speaker):\n", " self.first_speaker = first_speaker\n", " self.second_speaker = second_speaker\n", " \n", " def reset(self, msg_from_first_speaker): \n", " \"\"\"\n", " Initialize the simulation with an utterance from the first speaker.\n", " \"\"\"\n", " self.first_speaker.reset(\n", " AIMessage(content=msg_from_first_speaker))\n", " self.second_speaker.reset()\n", " \n", " return HumanMessage(content=msg_from_first_speaker)\n", " \n", " def step(self, msg_to_second_speaker):\n", " \"\"\"\n", " Simulates a single back-and-forth exchange between the speakers\n", " \"\"\"\n", " msg_from_second_speaker = self.second_speaker.step(msg_to_second_speaker) \n", " msg_to_first_speaker = HumanMessage(content=msg_from_second_speaker.content)\n", "\n", " msg_from_first_speaker = self.first_speaker.step(msg_to_first_speaker)\n", " msg_to_second_speaker = HumanMessage(content=msg_from_first_speaker.content)\n", "\n", " return msg_to_second_speaker, msg_to_first_speaker" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define roles and quest" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "protagonist_name = \"Harry Potter\"\n", "storyteller_name = \"Dungeon Master\"\n", "quest = \"Find all of Lord Voldemort's seven horcruxes.\"\n", "word_limit = 50 # word limit for task brainstorming" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Ask an LLM to add detail to the game description" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "game_description = f\"\"\"Here is the topic for a Dungeons & Dragons game: {quest}.\n", " There is one player in this game: the protagonist, {protagonist_name}.\n", " The story is narrated by the storyteller, {storyteller_name}.\"\"\"\n", "\n", "player_descriptor_system_message = SystemMessage(\n", " content=\"You can add detail to the description of a Dungeons & Dragons player.\")\n", "\n", "protagonist_specifier_prompt = [\n", " player_descriptor_system_message,\n", " HumanMessage(content=\n", " f\"\"\"{game_description}\n", " Please reply with a creative description of the protagonist, {protagonist_name}, in {word_limit} words or less. \n", " Speak directly to {protagonist_name}.\n", " Do not add anything else.\"\"\"\n", " )\n", "]\n", "protagonist_description = ChatOpenAI(temperature=1.0)(protagonist_specifier_prompt).content\n", "\n", "storyteller_specifier_prompt = [\n", " player_descriptor_system_message,\n", " HumanMessage(content=\n", " f\"\"\"{game_description}\n", " Please reply with a creative description of the storyteller, {storyteller_name}, in {word_limit} words or less. \n", " Speak directly to {storyteller_name}.\n", " Do not add anything else.\"\"\"\n", " )\n", "]\n", "storyteller_description = ChatOpenAI(temperature=1.0)(storyteller_specifier_prompt).content" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Protagonist Description:\n", "Harry Potter, you are the chosen one. Your lightning scar and piercing green eyes hint at the bravery and determination that will drive you to fulfill your quest. Wield your wand and trust in your friends as you embark on this perilous journey to defeat Lord Voldemort once and for all.\n", "Storyteller Description:\n", "As the Dungeon Master, you have the power to bring this story to life. You hold the keys to every door, every creature, and every treasure in the wizarding world. Your words weave a tapestry of adventure, magic, and danger that will test Harry Potter's courage and resourcefulness.\n" ] } ], "source": [ "print('Protagonist Description:')\n", "print(protagonist_description)\n", "print('Storyteller Description:')\n", "print(storyteller_description)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Protagonist and dungeon master system messages" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "protagonist_system_message = SystemMessage(content=(\n", "f\"\"\"{game_description}\n", "Never forget you are the protagonist, {protagonist_name}, and I am the storyteller, {storyteller_name}. \n", "Your character description is as follows: {protagonist_description}.\n", "You will propose actions you plan to take and I will explain what happens when you take those actions.\n", "Speak in the first person from the perspective of {protagonist_name}.\n", "To describe body movements, wrap your description in '*'.\n", "Do not change roles!\n", "Finish speaking by saying, 'It is your turn, {storyteller_name}.'\n", "\"\"\"\n", "))\n", "\n", "storyteller_system_message = SystemMessage(content=(\n", "f\"\"\"{game_description}\n", "Never forget you are the storyteller, {storyteller_name}, and I am the protagonist, {protagonist_name}. \n", "Your character description is as follows: {storyteller_description}.\n", "I will propose actions I plan to take and you will explain what happens when I take those actions.\n", "Speak in the first person from the perspective of {storyteller_name}.\n", "To describe body movements, wrap your description in '*'.\n", "Do not change roles!\n", "Finish speaking by saying, 'It is your turn, {protagonist_name}.'\n", "\"\"\"\n", "))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create AI assistant agent and AI user agent" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "protagonist = Player(protagonist_system_message, ChatOpenAI(temperature=0.2))\n", "storyteller = Player(storyteller_system_message, ChatOpenAI(temperature=0.2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Main Loop" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Original quest:\n", "Find all of Lord Voldemort's seven horcruxes.\n", "\n", "Detailed quest:\n", "Harry Potter, you have received word from the Order of the Phoenix that one of Voldemort's horcruxes, the Snake's Fang, is hidden within the cursed ruins of the Temple of Vistra. Journey through the dangerous swamps, battle the cursed undead, and retrieve the horcrux before it's too late.\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I gather my wand and my courage, and set out towards the Temple of Vistra. As I make my way through the swamps, I keep my eyes peeled for any signs of danger. I stay alert, ready to defend myself against any cursed undead that might cross my path.\n", "\n", "As I approach the temple, I take a moment to survey the area. I look for any signs of traps or obstacles that might hinder my progress. Once I'm sure it's safe, I cautiously make my way inside.\n", "\n", "I move slowly, keeping my wand at the ready. I listen carefully for any sounds that might indicate the presence of cursed undead or other dangers. As I explore the temple, I search for any clues that might lead me to the Snake's Fang.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*As you step inside the temple, you notice that the air is thick with the scent of decay. The walls are covered in moss and vines, and the floor is slick with slime. Suddenly, you hear a low growling sound coming from the shadows.*\n", "\n", "You turn your wand towards the sound, ready to defend yourself. Out of the darkness emerges a pack of cursed undead wolves, their eyes glowing with an eerie green light. They snarl and bare their teeth, ready to attack.\n", "\n", "*Roll for initiative.*\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I quickly assess the situation and prepare to defend myself. I cast a spell to create a shield around me, protecting me from the wolves' attacks.\n", "\n", "Then, I aim my wand at the wolves and cast a spell to immobilize them. I hope to buy myself some time to figure out my next move.\n", "\n", "If the immobilization spell is successful, I will quickly search the area for any clues that might lead me to the Snake's Fang. If not, I will have to defend myself against the wolves.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*You cast the immobilization spell, and it hits the wolves with a bright flash of light. The wolves are frozen in place, unable to move. You take a moment to catch your breath and survey the area.*\n", "\n", "As you look around, you notice a faint glow coming from a nearby altar. You cautiously approach the altar and see that it is covered in ancient runes. You recognize the runes as belonging to an old language of magic that you studied at Hogwarts.\n", "\n", "You decipher the runes and realize that they are a clue to the location of the Snake's Fang. The clue leads you to a hidden chamber deep within the temple.\n", "\n", "*You make your way to the hidden chamber and find the Snake's Fang resting on a pedestal. You carefully pick it up, feeling its power coursing through your veins.*\n", "\n", "Congratulations, Harry Potter! You have found one of Voldemort's horcruxes. But be warned, the journey ahead will only get more dangerous from here on out.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I take a deep breath and steel myself for the challenges ahead. I know that finding the remaining horcruxes won't be easy, but I'm determined to see this through to the end.\n", "\n", "I carefully stow the Snake's Fang away in my bag and make my way out of the hidden chamber. As I exit the temple, I keep my wand at the ready, knowing that there may be more cursed undead or other dangers lurking in the swamps.\n", "\n", "I make my way back to the Order of the Phoenix to report my success and to receive my next mission. I know that the fate of the wizarding world rests on my shoulders, and I'm ready to do whatever it takes to defeat Voldemort once and for all.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*As you make your way back to the Order of the Phoenix, you encounter a group of Death Eaters who have been sent to stop you. They are armed with wands and are ready to do whatever it takes to prevent you from finding the remaining horcruxes.*\n", "\n", "Roll for initiative.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I quickly assess the situation and prepare to defend myself. I cast a spell to create a shield around me, protecting me from the Death Eaters' attacks.\n", "\n", "Then, I aim my wand at the Death Eaters and cast a spell to disarm them. I hope to buy myself some time to figure out my next move.\n", "\n", "If the disarmament spell is successful, I will quickly try to escape and make my way back to the Order of the Phoenix. If not, I will have to defend myself against the Death Eaters.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*You cast the disarmament spell, and it hits the Death Eaters with a bright flash of light. Their wands fly out of their hands, and they are momentarily stunned.*\n", "\n", "You take advantage of the moment and quickly make your escape. You run through the swamps, dodging obstacles and avoiding any other dangers that might cross your path.\n", "\n", "Eventually, you make it back to the Order of the Phoenix, where you report your success in finding the Snake's Fang. The members of the Order congratulate you on your bravery and determination, and they give you your next mission.\n", "\n", "You must now journey to the Forbidden Forest to find the next horcrux, the Raven's Claw. The journey ahead will be perilous, but you know that you have the support of the Order of the Phoenix and the power of magic on your side.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I thank the members of the Order of the Phoenix for their support and guidance, and I set out towards the Forbidden Forest. As I make my way through the forest, I keep my wand at the ready, knowing that danger could be lurking around every corner.\n", "\n", "I search for any clues that might lead me to the Raven's Claw. I keep my eyes peeled for any signs of Voldemort's followers or other dangers that might be in my path.\n", "\n", "As I journey deeper into the forest, I begin to feel a sense of unease. The trees seem to be closing in around me, and the air is thick with an eerie silence. I know that I must stay alert and focused if I hope to find the Raven's Claw and make it out of the forest alive.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*As you make your way through the Forbidden Forest, you suddenly hear a rustling in the bushes. You turn your wand towards the sound, ready to defend yourself.*\n", "\n", "Out of the bushes emerges a group of acromantulas, their eyes gleaming with a malevolent hunger. They are massive spiders, each one the size of a small car. They hiss and bare their fangs, ready to attack.\n", "\n", "*Roll for initiative.*\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I take a deep breath and prepare to defend myself against the acromantulas. I cast a spell to create a shield around me, protecting me from their attacks.\n", "\n", "Then, I aim my wand at the acromantulas and cast a spell to immobilize them. I hope to buy myself some time to figure out my next move.\n", "\n", "If the immobilization spell is successful, I will quickly search the area for any clues that might lead me to the Raven's Claw. If not, I will have to defend myself against the acromantulas.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*You cast the immobilization spell, and it hits the acromantulas with a bright flash of light. The acromantulas are frozen in place, unable to move. You take a moment to catch your breath and survey the area.*\n", "\n", "As you look around, you notice a faint glow coming from a nearby tree. You cautiously approach the tree and see that it is covered in ancient runes. You recognize the runes as belonging to an old language of magic that you studied at Hogwarts.\n", "\n", "You decipher the runes and realize that they are a clue to the location of the Raven's Claw. The clue leads you to a hidden cave deep within the forest.\n", "\n", "*You make your way to the hidden cave and find the Raven's Claw resting on a pedestal. You carefully pick it up, feeling its power coursing through your veins.*\n", "\n", "Congratulations, Harry Potter! You have found another one of Voldemort's horcruxes. But be warned, the journey ahead will only get more dangerous from here on out.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Protagonist (Harry Potter):\n", "\n", "I take a deep breath and stow the Raven's Claw away in my bag. I know that I must remain focused and vigilant if I hope to find the remaining horcruxes and defeat Voldemort once and for all.\n", "\n", "I make my way out of the Forbidden Forest and back to the Order of the Phoenix to report my success. I know that I must continue to rely on my friends and allies if I hope to succeed in my mission.\n", "\n", "I am ready for whatever challenges lie ahead, and I will not rest until Voldemort is defeated and the wizarding world is safe once again.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*As you make your way back to the Order of the Phoenix, you encounter a group of dementors who have been sent to stop you. They are floating ominously in the air, their tattered robes billowing in the wind. You feel their icy breath on the back of your neck, and you know that you must act quickly to defend yourself.*\n", "\n", "Roll for initiative.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I quickly assess the situation and prepare to defend myself against the dementors. I cast a Patronus charm to create a shield around me, protecting me from their attacks.\n", "\n", "Then, I aim my wand at the dementors and cast a spell to repel them. I hope to buy myself some time to figure out my next move.\n", "\n", "If the repelling spell is successful, I will quickly try to escape and make my way back to the Order of the Phoenix. If not, I will have to defend myself against the dementors.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*You cast the repelling spell, and it hits the dementors with a bright flash of light. The dementors are pushed back, giving you a moment to catch your breath.*\n", "\n", "You take advantage of the moment and quickly make your escape. You run through the forest, dodging obstacles and avoiding any other dangers that might cross your path.\n", "\n", "Eventually, you make it back to the Order of the Phoenix, where you report your success in finding the Raven's Claw. The members of the Order congratulate you on your bravery and determination, and they give you your next mission.\n", "\n", "You must now journey to the depths of Gringotts Bank to find the next horcrux, the Dragon's Heartstring. The journey ahead will be perilous, but you know that you have the support of the Order of the Phoenix and the power of magic on your side.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I thank the members of the Order of the Phoenix for their support and guidance, and I set out towards Gringotts Bank. As I make my way through the streets of Diagon Alley, I keep my wand at the ready, knowing that danger could be lurking around every corner.\n", "\n", "I search for any clues that might lead me to the Dragon's Heartstring. I keep my eyes peeled for any signs of Voldemort's followers or other dangers that might be in my path.\n", "\n", "As I journey deeper into Gringotts Bank, I begin to feel a sense of unease. The bank is heavily guarded, and I know that I must stay alert and focused if I hope to find the Dragon's Heartstring and make it out of the bank alive.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*As you make your way through Gringotts Bank, you suddenly hear a loud alarm ringing. You turn your wand towards the sound, ready to defend yourself.*\n", "\n", "Out of the shadows emerges a group of goblins, armed with swords and shields. They are the bank's security force, and they are ready to do whatever it takes to protect the bank's treasures.\n", "\n", "*Roll for initiative.*\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n", "Protagonist (Harry Potter):\n", "\n", "I quickly assess the situation and prepare to defend myself against the goblins. I cast a spell to create a shield around me, protecting me from their attacks.\n", "\n", "Then, I aim my wand at the goblins and cast a spell to stun them. I hope to buy myself some time to figure out my next move.\n", "\n", "If the stunning spell is successful, I will quickly search the area for any clues that might lead me to the Dragon's Heartstring. If not, I will have to defend myself against the goblins.\n", "\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "Storyteller (Dungeon Master):\n", "\n", "*You cast the stunning spell, and it hits the goblins with a bright flash of light. The goblins are momentarily stunned, giving you a moment to catch your breath.*\n", "\n", "You take advantage of the moment and quickly make your way deeper into the bank. You search for any clues that might lead you to the Dragon's Heartstring.\n", "\n", "As you explore the bank, you come across a hidden vault. You recognize the vault as belonging to Bellatrix Lestrange, one of Voldemort's most loyal followers. You know that the Dragon's Heartstring must be inside.\n", "\n", "*You make your way into the vault and find the Dragon's Heartstring resting on a pedestal. You carefully pick it up, feeling its power coursing through your veins.*\n", "\n", "Congratulations, Harry Potter! You have found another one of Voldemort's horcruxes. But be warned, the journey ahead will only get more dangerous from here on out.\n", "\n", "It is your turn, Harry Potter.\n", "\n", "\n" ] } ], "source": [ "quest_specifier_prompt = [\n", " SystemMessage(content=\"You can make a task more specific.\"),\n", " HumanMessage(content=\n", " f\"\"\"{game_description}\n", " \n", " You are the storyteller, {storyteller_name}.\n", " Please make the quest more specific. Be creative and imaginative.\n", " Please reply with the specified quest in {word_limit} words or less. \n", " Speak directly to the protagonist {protagonist_name}.\n", " Do not add anything else.\"\"\"\n", " )\n", "]\n", "specified_quest = ChatOpenAI(temperature=1.0)(quest_specifier_prompt).content\n", "\n", "print(f\"Original quest:\\n{quest}\\n\")\n", "print(f\"Detailed quest:\\n{specified_quest}\\n\")\n", "\n", "max_iters = 10\n", "n = 0\n", "\n", "simulator = TwoAgentSimulator(\n", " first_speaker=storyteller, \n", " second_speaker=protagonist)\n", "\n", "msg_to_protagonist = simulator.reset(specified_quest)\n", "\n", "while n < max_iters:\n", " msg_to_protagonist, msg_to_storyteller = simulator.step(msg_to_protagonist)\n", " print(f\"Protagonist ({protagonist_name}):\\n\\n{msg_to_storyteller.content}\\n\\n\")\n", " print(f\"Storyteller ({storyteller_name}):\\n\\n{msg_to_protagonist.content}\\n\\n\")\n", " n += 1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.16" } }, "nbformat": 4, "nbformat_minor": 2 }