Skip to content

Multi-Agent LLM storytelling

Overview

Let's try to use LLMs to simulate the story of the Odyssey being retold, but with a slight difference:

Odysseus has a knowledge of and can use modern technology

Using LLM API's and calling them asynchronously is a useful demonstration of the type of IO-Bound workloads which Hades is designed around. This will also illustrate some interesting features of the event loop.

Designing the processes

The processes and how they interact is the key to this simulation. The key idea is that we have a bit of a loop, which is enabled by the hades event loop:

flowchart TB
    A[Homer records these and plans to synthesise them into a coherent story tomorrow]
    C[Homer notifies the characters of the last day's story]
    D[Odysseus]
    E[Athena]
    F[Zeus]
    G[...]
    H[OpenAI endpoint]

    subgraph Characters act according to their character the last day's story
        D
        E
        F
        G
    end

    subgraph Homer
        A
        C
    end

    A -->|time moves forward one day| C

    C -.->|async| D
    C -.->|async| E
    C -.->|async| F
    C -.->|async| G
    D -->|CharacterActed| A
    E -->|CharacterActed| A
    F -->|CharacterActed| A
    G -->|CharacterActed| A
    D -.->|async| H
    E -.->|async| H
    F -.->|async| H
    G -.->|async| H

Implementation

Processes

The core logic is within the processes

# Copyright 2023 Brit Group Services Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from examples.multi_agent_llm_storytelling.models import GPTMessage

from hades import Event, Process
from hades.core.event import Event, SimulationEnded, SimulationStarted
from hades.core.process import NotificationResponse

from .events import CharacterActed, StoryUnfolded, SynthesisedEventsOfDay
from .prompts import god_prompt, homer_prompt, odysseus_prompt
from .utilities import plaintext_chat_response


class Homer(Process):
    def __init__(self) -> None:
        self._story_of_day = {}
        self._events_of_day = {}
        super().__init__()

    @property
    def story_so_far(self):
        story_so_far = ""
        for story_day, story in self._story_of_day.items():
            story_so_far += f"\nDay {story_day}: {story}\n"
        return story_so_far

    async def notify(self, event: Event):
        match event:
            case SynthesisedEventsOfDay(day=day, t=t):
                events_of_day = "\n".join([str(e) for e in self._events_of_day[day]])
                story_of_day = await plaintext_chat_response(
                    messages=[
                        GPTMessage(
                            role=GPTMessageRole.SYSTEM,
                            content=homer_prompt.format(story_so_far=self.story_so_far[-2000:], day=day),
                        ),
                        GPTMessage(
                            role=GPTMessageRole.USER,
                            content=f"Events ```{events_of_day}```",
                        ),
                    ]
                )
                self.add_event(StoryUnfolded(t=t, chapter=story_of_day))
                self._story_of_day[day] = story_of_day
                print(f"Day {day}: {story_of_day}")
                return NotificationResponse.ACK
            case CharacterActed() as e:
                # collect all the events of the day to synthesise the next day
                try:
                    self._events_of_day[e.t].append(e)
                except KeyError:
                    self._events_of_day[e.t] = [e]
                    self.add_event(SynthesisedEventsOfDay(t=e.t + 1, day=e.t))
                return NotificationResponse.ACK
            case SimulationEnded():
                print(self.story_so_far)
                return NotificationResponse.ACK
        return NotificationResponse.NO_ACK


class Odysseus(Process):
    def __init__(self) -> None:
        super().__init__()
        self._name = "Odysseus"
        self._action_history = []

    async def notify(self, event: Event):
        match event:
            case SimulationStarted(t=t):
                self.add_event(
                    CharacterActed(
                        t=t,
                        character_name=self._name,
                        action=(
                            "Odyssues crys out in frustration at being stuck in Troy because Poseidon won't allow him"
                            " home"
                        ),
                    )
                )
            case StoryUnfolded() as e:
                recent_affairs = "\n".join(self._action_history[-5:])
                response = await plaintext_chat_response(
                    messages=[
                        GPTMessage(
                            role=GPTMessageRole.SYSTEM,
                            content=odysseus_prompt.format(recent_affairs=recent_affairs, day=e.t),
                        ),
                        GPTMessage(
                            role=GPTMessageRole.USER,
                            content=str(e),
                        ),
                    ]
                )
                self._action_history.append(str(e))
                if response:
                    event = CharacterActed(t=e.t, action=response, character_name=self._name)
                    self.add_event(event)
                    self._action_history.append(str(response))

                return NotificationResponse.ACK
        return NotificationResponse.NO_ACK


class GreekGod(Process):
    def __init__(self, name: str) -> None:
        self._name = name
        self._history = []
        self._motives = None
        super().__init__()

    @property
    def history_message(self):
        if self._history:
            return " You have the following recent history: " + "\n".join(self._history[-5:])
        else:
            return ""

    async def notify(self, event: Event):
        match event:
            case SimulationStarted():
                motives = await plaintext_chat_response(
                    messages=[
                        GPTMessage(
                            role=GPTMessageRole.USER,
                            content=f""""
                        Describe the core motives of {self._name} at the start of the Odyssey in less than 200 words.""",
                        )
                    ]
                )
                print(f"{self._name} Motivations:\n{motives}")
                self._motives = motives
                return NotificationResponse.ACK
            case StoryUnfolded() as e:
                response = await plaintext_chat_response(
                    messages=[
                        GPTMessage(
                            role=GPTMessageRole.SYSTEM,
                            content=god_prompt.format(
                                god_name=self._name, motives=self._motives, day=e.t, history=self.history_message
                            ),
                        ),
                        GPTMessage(
                            role=GPTMessageRole.USER,
                            content=e.chapter,
                        ),
                    ]
                )
                self._history.append(str(e))
                if response:
                    event = CharacterActed(t=e.t, action=response, character_name=self._name)
                    self.add_event(event)
                return NotificationResponse.ACK
        return NotificationResponse.NO_ACK

Additional Code

Event Definitions
# Copyright 2023 Brit Group Services Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pydantic import BaseModel, ConfigDict

from hades import Event


class CharacterAction(BaseModel):
    location: str
    action_description: str
    model_config = ConfigDict(frozen=True)


class StoryUnfolded(Event):
    chapter: str

    def __str__(self) -> str:
        return f"Day {self.t}: {self.chapter}"


class CharacterActed(Event):
    action: str
    character_name: str

    def __str__(self) -> str:
        return f"""
        Character: {self.character_name}
        Day: {self.t}
        Action: {self.action}
        """


class SynthesisedEventsOfDay(Event):
    day: int
LLM Prompts
    # Copyright 2023 Brit Group Services Ltd.
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #      http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.

    homer_prompt = """
    You are the writer Homer chronicling the adventures of a greek hero Odysseus.

    Here is your story so far.
    ---
    {story_so_far}
    ---
    You will be informed by the user of the events of the current day.

    You will synthesise these events into a cohesive narrative in the style of Homer to form the story of day {day}.

    The story should never end. 

    Remove inconsistent elements. 

    Use under 200 words.

    Example
    User: Events: ```
    Character: Odysseus
    Day: 0
    Action: Odysseus is stuck on troy because Poseidon won't allow him home
    ```
    Assistant:  As the morning sun rose on Troy, the great hero Odysseus sat frustrated and stuck. He longed to return to his home, his wife and his son, but Poseidon had cursed him, preventing his journey home.
    Odysseus had tried every trick in his book to appease the sea god, but to no avail. And so he sat, day after day, trying to find any solution to his problem.
    """

    odysseus_prompt = """
    You are the hero Odysseus.
    You have the history and motivations of Odysseus at the start of the Odyssey but you also have
    an understanding of modern science and technology which you use creatively to your advantage
    plus the following events.
    ---
    {recent_affairs}
    ---
    The user will inform you the latest chapter in your tale and you must decide how to react to it 
    and come up with an action, given the story so far and your current goals and motivations. 

    Describe what action you will take today (Day {day}).

    If you use modern technology or science specify exactly what and how.

    Use under 200 words.
    """

    god_prompt = """"
    You are the greek god {god_name}. You have the history and motivations of {god_name} at the 
    start of the Odyssey:
    {motives} 
    plus the following recent history of your own actions and those of Odysseus
    ---
    {history}
    ---
    The user will tell you the latest chapter of the story. Describe what action you will take today (Day {day}).

    Use under 200 words.
    """
Utilities
# Copyright 2023 Brit Group Services Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
import os
from dataclasses import asdict

import openai
from examples.multi_agent_llm_storytelling.models import GPTMessage, GPTModelVersion
from openai.error import APIError, RateLimitError, Timeout

API_KEY = os.environ["OPENAI_API_KEY"]


async def plaintext_chat_response(messages: list[GPTMessage], tries=7):
    try:
        plaintext_response = await openai.ChatCompletion.acreate(
            api_key=API_KEY, model=GPTModelVersion.GPT_3_5, messages=[asdict(message) for message in messages]
        )
        return plaintext_response.choices[0].message.content
    except (RateLimitError, APIError, Timeout) as e:
        if tries == 0:
            raise e
        await asyncio.sleep((8 - tries) ** 2)
        return await plaintext_chat_response(messages, tries - 1)

Putting it all together

# Copyright 2023 Brit Group Services Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio

from examples.multi_agent_llm_storytelling.processes import GreekGod, Homer, Odysseus

from hades import Hades


async def simulate():
    world = Hades(batch_event_notification_timeout=20 * 60)
    world.register_process(Odysseus())
    homer = Homer()
    world.register_process(homer)
    for name in ("Athena", "Zeus", "Poseidon", "Helios"):
        world.register_process(GreekGod(name=name))
    await world.run(until=10)


if __name__ == "__main__":
    asyncio.run(simulate())

The resulting story

Day 0: As the morning sun rose on Troy, the great hero Odysseus sat on the shore, tears streaming down his face. He cried out in frustration, feeling helpless and powerless against the wrath of Poseidon. Every trick he had up his sleeve had failed to sway the city's underwater ruler. Now, Odysseus was confined to the city where he had fought so heroically.

His heart ached to return to his homeland, to his loyal wife Penelope, and his beloved son Telemachus. But it seemed that Poseidon had other plans. Despite his weariness, Odysseus remained determined to find a way home. He began strategizing, hatching a plan that would enable him to escape Troy's grip and overcome the mighty Poseidon. Until then, he would remain vigilant, biding his time until the right opportunity presented itself. And so the hero waited, his heart heavy with longing, but his spirit still burning bright.

Day 1: As the sun rose on the second day, Odysseus remained determined to find a way to overcome the might of the sea god, with the support of Athena as his only ally. He continued with his daily training regimen with the soldiers in Troy, keeping his body and mind sharp. During his free time, he spent his days researching and trying to understand the behavior of Poseidon by gathering information from the local fishermen and sailors. He also studied modern technology that he could use to beat the sea god and return home.

Meanwhile, Helios, the lord Sun, sent a fiery symbol to the skies, warning Odysseus of the impending danger that he would face if he did not atone for his misdeeds. Though not unleashing his full wrath, Helios observed Odysseus from a distance, waiting and watching for his next move.

Zeus continued to keep a close eye on the situation, subtly guiding the events to ensure that Odysseus would be able to complete his journey home safely. Despite not being able to intervene, Zeus reassured Odysseus that he had his support.

In the midst of this ongoing struggle, Poseidon's anger flared up again, and he summoned his most powerful sea creatures to create a massive storm, which bashed against the shores of Troy. However, Poseidon soon realized that his sense of satisfaction was temporary, and he retreated to his underwater palace, wondering if his grudge against Odysseus was worth all the destruction and chaos he had caused.

As the balance of power shifted among the gods, Odysseus remained determined to find a way home. With tenacity, he spent each day gathering intelligence and training, all the while praying for the support of the gods and his safe return home to his family.

Day 2: As the third day dawned, Odysseus remained focused on his goal of returning home, using all his knowledge and resources to overcome the challenges that lay ahead. He was heartened by the support of Athena and Zeus, who had provided him with guidance and protection, and the intervention of Helios, who had calmed the storm and allowed him to slip away from Troy unnoticed.

As Odysseus sailed along the coastline, he kept a close eye on the skies, hoping for any signs from the gods that might aid him in his journey. Meanwhile, he continued to use his knowledge of modern science and technology to create diversions and evade the wrath of Poseidon.

Poseidon, still seething with anger, decided to take a more direct approach to stop Odysseus. He summoned his most powerful sea creatures and unleashed a massive wave that threatened to overturn Odysseus' boat. But to his surprise, the sea nymph that Zeus had directed Odysseus to seek out appeared beside him, calming the waters and protecting him from Poseidon's fury.

With the sea nymph's help, Odysseus continued his journey with renewed strength and determination. He knew that there were still many obstacles ahead, but he was resolute in his belief that he would overcome them all and return home to his beloved family.

Day 3: On the third day of his journey, Odysseus sought out alliances that could aid him in his quest home. He remembered the sea nymph who had helped him before and realized that having allies like her was crucial to his success. He also continued to use his technological knowledge to stay ahead of Poseidon's wrath, utilizing his drone and sonar to navigate the waters safely.

Meanwhile, Helios kept a watchful eye on the situation, withholding his full wrath for now, but sending a fiery symbol to the skies as a warning to Odysseus. Athena continued to aid Odysseus when necessary, offering counsel and support, and mediating disputes in the mortal world with her strategic vision.

Zeus, pleased with Odysseus's resourcefulness, sent Hermes to guide him and provide him with essential information about his journey. Hermes appeared to Odysseus as a friendly stranger, teaching him the secrets of the winds and currents, and providing him with a magical talisman that would protect him from Poseidon's wrath.

Poseidon's anger flared up again on this day, as he summoned his most fearsome sea creatures and unleashed a massive storm to halt Odysseus's progress. But even he was no match for the combined power of Athena, Helios, and the sea nymph, who calmed the storm and tamed Poseidon's sea monsters, sparing Odysseus and his crew from certain death.

Frustrated and defeated once again, Poseidon retreated to his underwater palace, vowing to continue to make Odysseus's journey home as difficult as possible. But Odysseus remained determined, relying on his resourcefulness, intelligence, and alliances to guide him safely back to his beloved family.

Day 4: On the fourth day of his journey, Odysseus reflected on the valuable allies who had aided him thus far. He knew that he needed all the help he could get to overcome the obstacles before him and decided to actively seek out new potential allies, specifically among the sea creatures who could guide him through the treacherous waters.

Using his underwater drone and sonar, he searched for groups of dolphins and schools of fish who could offer him insight into the sea's patterns and hazards. He also used his knowledge of technology to create a mapping system of the waters he'd traveled so far, marking potential dangers and hidden currents to navigate safely.

Odysseus reached out to Hermes, hoping for further guidance and intel on potential allies and hazards ahead. The sea nymph who had aided him before granted him her blessing, promising to protect him from Poseidon and guide him through any future obstacles.

However, Poseidon remained powerful and determined to thwart Odysseus's journey home, setting up a blockade to prevent him from reaching his beloved family. Odysseus knew he needed to find a way to overcome this obstacle and consulted with Athena, who advised him to seek out the goddess of the winds, Aeolus.

Odysseus encountered a group of pirates who attempted to steal his supplies but used his cunning to escape. He finally reached Aeolus's island, where she offered her powers over the winds to create a path through Poseidon's blockade. Along the way, he learned that one of his crew members was secretly working with Poseidon, whom Zeus had warned him of.

Undeterred, Odysseus remained vigilant and determined to overcome any threat to his journey home. With the support of his allies and his own resourcefulness, he continued to navigate the treacherous waters, one step closer to the reunion he longed for.

Day 5: As the sun rose on the fifth day of Odysseus's journey, Helios, Zeus, Poseidon and Athena all monitored the hero's progress with interest.

Odysseus, having learned from his experiences with the pirates, developed a strategic plan to protect his crew and supplies. He stored essential items in a hidden compartment and created a surveillance system to monitor the crew's behavior. Drawing on his modern knowledge, he scouted for new allies with the help of his underwater drone and sonar.

Zeus and Athena lauded him for his resourcefulness, as Poseidon still boiled with rage and plotted to take Odysseus down. Meanwhile, Helios watched, warning Odysseus of the dangers ahead and subtly signaling Poseidon not to interfere further.

As Odysseus continued his journey, he navigated through Poseidon's blockade, making his way past the threats with the help of Athena, the sea nymph, and Aeolus. Along the way, he even managed to outsmart a crew member who had been working secretly with Poseidon.

With Athena's guidance, Odysseus managed to trick the Sirens and gain their navigational knowledge while saving a ship's crew from their deadly songs. With renewed vigor and confidence, he accelerated his journey with his remaining allies, ready to face Poseidon and whatever obstacles lay ahead.

As the gods watched from above, they hoped to guide Odysseus safely home, recognizing his determination and resourcefulness in the face of adversity. Poseidon still seethed, but Helios held back his wrath while Zeus and Athena maintained balance and justice. The journey continued, and no one knew what lay ahead for the great hero.

Day 6: As the sixth day of his journey progressed, Odysseus found himself facing one of his greatest challenges yet. Poseidon, still seeking revenge for the death of his son Polyphemus, sent an army of sea monsters to attack Odysseus and his crew. Thanks to his resourcefulness and modern knowledge, Odysseus was able to fend off the beasts with the help of his allies, including Athena and Helios.

As the battle raged on for hours, Odysseus rallied his crew, using his knowledge of modern warfare to outsmart the sea monsters. His crew was in a state of panic, but Odysseus made sure to keep them together and use his surveillance system to anticipate the monsters' movements.

Poseidon was furious, but Helios held back his wrath while Zeus and Athena provided strategic guidance to Odysseus. In the end, Odysseus emerged victorious, having defeated the sea monsters and continued on his journey with renewed determination.

As the sunset, Zeus sent down a message to Odysseus, letting him know that he would have a new ally for the day - a falcon who would guide him towards safety. With the falcon leading the way, Odysseus and his crew sailed through dangerous waters and encountered a group of sea turtles who offered their swimming and navigation knowledge.

As they continued on, Poseidon attempted to interfere with Odysseus' efforts once again, but was thwarted by the combined power of the gods and Odysseus' resourcefulness. The hero knew that while many challenges lay ahead, he was ready to face whatever came his way with his vigilance, technological resources and strategic planning.

Day 7: On day 7, Odysseus and his crew were emboldened by their recent victories and newfound alliances. With the guidance of Athena, Odysseus expertly navigated the treacherous strait guarded by a six-headed monster and emerged victorious. Helios warned Odysseus of an approaching storm, but with his modern technology, Odysseus was able to deploy an anchor and keep the ship steady.

Zeus sent the falcon to lead them to a safe and uncharted island where they found resources, friendly mermaids and guidance on underwater currents. Despite Poseidon's wrath, Odysseus remained vigilant and strategic in his approach to each challenge, using his mapping system and sonar to chart the waters ahead and identify allies.

Zeus informed Odysseus of a stop in Circe's city, where they found the people under Circe's spell. Odysseyus broke the spell and convinced the people to help him persuade Circe for knowledge.

They encountered friendly dolphins who guided them through the waters, and with the help of Athena and his drone, Odysseus navigated through a dangerous whirlpool. The gods watched with admiration, impressed by Odysseus's resourcefulness and fortitude. Although Poseidon remains furious, Odysseus remained ready to face any challenge and make his way back home to his loved ones, with the support of the gods and his allies.

Day 8: On Day 8 of Odysseus' journey, he reflected on the challenges he had faced and the resources and allies he had gathered thus far. With his crew and Athena's guidance, he planned a proactive strategy to approach Circe's city undetected and convince her to provide him with the knowledge he needed to continue his journey.

With his modern technology and strategic planning, Odysseus created a decoy ship to distract any guards and mapped out the city's surroundings using his sonar and mapping system. With Athena's support, he crafted a plan to communicate with Circe and gain her trust.

As they sailed on, Poseidon continued to hinder their progress, but Odysseus and his allies remained vigilant and prepared for the unexpected. With Helios warning of an attack from Poseidon's sea monsters, Odysseus was able to protect his crew and fend off the threats.

Meanwhile, on a remote island, Odysseus and his crew discovered powerful artifacts containing valuable knowledge about the history of the gods and their powers. With Athena's guidance, they deciphered the inscriptions and gained new allies.

As the day drew to a close, rescue ships arrived to aid Odysseus and his crew. They continued their journey home with newfound knowledge and allies. As they approached their toughest challenge yet, the confrontation with the Cyclops, Athena advised caution and reliance on allies. With Zeus's guidance, Odysseus prepared for the final obstacle before making his way home to his beloved family.

Day 9: As the sun rose on the ninth day of his journey, the great hero Odysseus prepared to confront the mighty Cyclops. With the guidance of the wise goddess Athena, he relied on his modern knowledge and strategic planning to defeat the giant.

Using his advanced surveillance system and strategic vision, Odysseus and his crew devised a plan to blind the beast with a new invention - laser light - and attack with their harpoons and nets. His allies, the sea turtles and friendly dolphins, scouted out the best approach and identified potential threats. Helios warned of Poseidon's wrath, but Odysseus remained vigilant and brave.

With the help of the sea nymph and Athena, they blinded the Cyclops and escaped. Despite Poseidon's fury at the loss of his son Polyphemus, Odysseus continued his journey with the support of Zeus and Athena.

But their path was not yet clear - further challenges awaited. However, with his resourcefulness and strategic planning, Odysseus remained steadfast on his mission to return home to his beloved family and reclaim his rightful place as king. With renewed confidence and determination, they continued on their journey, relying on their allies and modern technology to overcome any obstacle in their path.