{ "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 an `DialogueSimulator` 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, Dict\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": [ "## `DialogueAgent` class\n", "The `DialogueAgent` class is a simple wrapper around the `ChatOpenAI` model that stores the message history from the `dialogue_agent`'s point of view by simply concatenating the messages as strings.\n", "\n", "It exposes two methods: \n", "- `send()`: applies the chatmodel to the message history and returns the message string\n", "- `receive(name, message)`: adds the `message` spoken by `name` to message history" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class DialogueAgent():\n", "\n", " def __init__(\n", " self,\n", " name,\n", " system_message: SystemMessage,\n", " model: ChatOpenAI,\n", " ) -> None:\n", " self.name = name\n", " self.system_message = system_message\n", " self.model = model\n", " self.message_history = f\"\"\"Here is the conversation so far.\n", " \"\"\"\n", " self.prefix = f'\\n{self.name}:'\n", " \n", " def send(self) -> str:\n", " \"\"\"\n", " Applies the chatmodel to the message history\n", " and returns the message string\n", " \"\"\"\n", " message = self.model(\n", " [self.system_message, \n", " HumanMessage(content=self.message_history+self.prefix)])\n", " return message.content\n", " \n", " def receive(self, name: str, message: str) -> None:\n", " \"\"\"\n", " Concatenates {message} spoken by {name} into message history\n", " \"\"\"\n", " self.message_history += f'\\n{name}: {message}'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `DialogueSimulator` class\n", "The `DialogueSimulator` class takes a list of agents. At each step, it performs the following:\n", "1. Select the next speaker\n", "2. Calls the next speaker to send a message \n", "3. Broadcasts the message to all other agents\n", "4. Update the step counter.\n", "The selection of the next speaker can be implemented as any function, but in this case we simply loop through the agents." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class DialogueSimulator():\n", " \n", " def __init__(self, agents: List[DialogueAgent]):\n", " self.agents = agents\n", " self._step = 0\n", " \n", " def reset(self, name: str, message: str):\n", " \"\"\"\n", " Initiates the conversation with a {message} from {name}\n", " \"\"\"\n", " for agent in self.agents:\n", " agent.receive(name, message)\n", " \n", " def select_next_speaker(self, step: int) -> int:\n", " idx = (step + 1) % len(self.agents)\n", " return idx\n", " \n", " def step(self) -> tuple[str, str]:\n", " # 1. choose the next speaker\n", " speaker = self.agents[self.select_next_speaker(self._step)]\n", " \n", " # 2. next speaker sends message\n", " message = speaker.send()\n", " \n", " # 3. everyone receives message\n", " for receiver in self.agents:\n", " receiver.receive(speaker.name, message)\n", " \n", " # 4. increment time\n", " self._step += 1\n", " \n", " return speaker.name, message" ] }, { "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 a brave and resourceful wizard. Your lightning scar and famous name precede you, but it is your heart that truly sets you apart. Your love and loyalty for your friends has been tested time and time again, and you have never faltered in your determination to vanquish evil.\n", "Storyteller Description:\n", "Dear Dungeon Master, you are a master of imagination, weaving enticing tales of adventure with a flick of your wrist. A patient guide, you lead Harry Potter through the perilous journey of finding Lord Voldemort's horcruxes, instilling excitement and wonder at every turn. Your storytelling prowess enchants all who dare to listen.\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": 11, "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", "For describing your own body movements, wrap your description in '*'.\n", "Do not change roles!\n", "Do not speak from the perspective of {storyteller_name}.\n", "Do not forget to finish speaking by saying, 'It is your turn, {storyteller_name}.'\n", "Do not add anything else.\n", "Remember you are the protagonist, {protagonist_name}.\n", "Stop speaking the moment you finish speaking from your perspective.\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", "For describing your own body movements, wrap your description in '*'.\n", "Do not change roles!\n", "Do not speak from the perspective of {protagonist_name}.\n", "Do not forget to finish speaking by saying, 'It is your turn, {protagonist_name}.'\n", "Do not add anything else.\n", "Remember you are the storyteller, {storyteller_name}.\n", "Stop speaking the moment you finish speaking from your perspective.\n", "\"\"\"\n", "))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Use an LLM to create an elaborate quest description" ] }, { "cell_type": "code", "execution_count": 12, "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 must journey to the hidden cave where one of Voldemort's horcruxes resides. The cave is guarded by enchanted creatures and curses that can only be lifted by a unique magical potion. Use your wit and skill to obtain the ingredients, brew the potion, and retrieve the horcrux before time runs out.\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\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Main Loop" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "protagonist = DialogueAgent(name=protagonist_name,\n", " system_message=protagonist_system_message, \n", " model=ChatOpenAI(temperature=0.2))\n", "storyteller = DialogueAgent(name=storyteller_name,\n", " system_message=storyteller_system_message, \n", " model=ChatOpenAI(temperature=0.2))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(Dungeon Master): Harry Potter, you must journey to the hidden cave where one of Voldemort's horcruxes resides. The cave is guarded by enchanted creatures and curses that can only be lifted by a unique magical potion. Use your wit and skill to obtain the ingredients, brew the potion, and retrieve the horcrux before time runs out.\n", "\n", "\n", "(Harry Potter): I take a deep breath and focus on the task at hand. I search my bag for any potions or ingredients that may be useful in brewing the unique magical potion. If I don't have any, I will search the surrounding area for any plants or herbs that may be useful. Once I have all the necessary ingredients, I will brew the potion and use it to lift the curses and defeat any enchanted creatures guarding the horcrux. It won't be easy, but I am determined to succeed.\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "(Dungeon Master): As you search your bag, you find a few useful potions and ingredients that will aid you in your quest. You also spot some herbs growing nearby that you recognize as key ingredients for the unique magical potion. You gather everything you need and begin brewing the potion. It takes some time, but you manage to create the perfect mixture. As you approach the cave, you drink the potion and feel a surge of power coursing through your veins. The curses and creatures guarding the horcrux are no match for you now. You retrieve the horcrux and add it to your collection. Well done, Harry Potter. But beware, the next horcrux will be even more challenging to obtain.\n", "It is your turn, Harry Potter.\n", "\n", "\n", "(Harry Potter): I take a moment to catch my breath and assess my next move. I know that the next horcrux will be even more difficult to obtain, but I am ready for the challenge. I consult my map and try to determine the location of the next horcrux. Once I have a general idea, I set off on foot, keeping my wand at the ready in case of any unexpected obstacles. I am determined to find and destroy all of Voldemort's horcruxes, no matter what it takes.\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "(Dungeon Master): As you consult your map, you notice that the next horcrux is located in a heavily guarded fortress. The fortress is surrounded by a moat filled with dangerous creatures and the entrance is protected by powerful spells. You will need to come up with a plan to get past the guards and break through the spells. As you approach the fortress, you notice a group of Death Eaters patrolling the perimeter. What do you do, Harry Potter?\n", "It is your turn, Harry Potter.\n", "\n", "\n", "(Harry Potter): I take cover behind a nearby tree and observe the Death Eaters' movements. I try to determine their patrol patterns and identify any weaknesses in their defenses. Once I have a plan, I use my invisibility cloak to sneak past them and make my way to the fortress entrance. I use my knowledge of spells to try and break through the protective enchantments. If that doesn't work, I will try to find another way in, perhaps through a secret passage or hidden entrance. I won't let anything stop me from finding and destroying the next horcrux.\n", "It is your turn, Dungeon Master.\n", "\n", "\n", "(Dungeon Master): As you observe the Death Eaters, you notice that they have a predictable patrol pattern. You wait for the right moment and use your invisibility cloak to sneak past them undetected. You make your way to the fortress entrance and try to break through the protective enchantments, but they prove to be too strong. You search for another way in and eventually find a hidden entrance that leads you to the horcrux. However, as you reach for it, you trigger a trap that sets off an alarm and alerts the Death Eaters to your presence. You must act quickly to escape before they catch you. What do you do, Harry Potter?\n", "It is your turn, Harry Potter.\n", "\n", "\n" ] } ], "source": [ "max_iters = 6\n", "n = 0\n", "\n", "simulator = DialogueSimulator(agents=[storyteller, protagonist])\n", "simulator.reset(storyteller_name, specified_quest)\n", "print(f\"({storyteller_name}): {specified_quest}\")\n", "print('\\n')\n", "\n", "while n < max_iters:\n", " name, message = simulator.step()\n", " print(f\"({name}): {message}\")\n", " print('\\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.1" } }, "nbformat": 4, "nbformat_minor": 2 }