From 7473d4a0178cd278c3f5bdbf41034c81d8d00986 Mon Sep 17 00:00:00 2001 From: zachary62 Date: Sat, 22 Mar 2025 13:49:50 -0400 Subject: [PATCH] add multi-agent tutorial --- README.md | 4 +- cookbook/pocketflow-multi-agent/README.md | 83 ++++++++++++++++++ cookbook/pocketflow-multi-agent/main.py | 100 ++++++++++++++++++++++ cookbook/pocketflow-multi-agent/utils.py | 14 +++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 cookbook/pocketflow-multi-agent/README.md create mode 100644 cookbook/pocketflow-multi-agent/main.py create mode 100644 cookbook/pocketflow-multi-agent/utils.py diff --git a/README.md b/README.md index a621f90..1cca3b2 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,13 @@ From there, it's easy to implement popular design patterns like ([Multi-](https: | [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ☆☆☆
*Dummy* | A resume qualification processor using map-reduce pattern for batch evaluation | | [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆
*Dummy* | A research agent that can search the web and answer questions | | [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆
*Dummy* | A real-time LLM streaming demo with user interrupt capability | -| [Parallel](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆
*Beginner* | A parallel execution demo that shows 3x speedup | +| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆
*Beginner* | A Taboo word game for asynchronous communication between two agents | | [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆
*Beginner* | Research agent is getting unreliable... Let's build a supervision process| +| [Parallel](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆
*Beginner* | A parallel execution demo that shows 3x speedup | | [Thinking](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆
*Beginner* | Solve complex reasoning problems through Chain-of-Thought | | [Memory](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆
*Beginner* | A chat bot with short-term and long-term memory | + 👀 Want to see other tutorials for dummies? [Create an issue!](https://github.com/The-Pocket/PocketFlow/issues/new) diff --git a/cookbook/pocketflow-multi-agent/README.md b/cookbook/pocketflow-multi-agent/README.md new file mode 100644 index 0000000..68e8b3f --- /dev/null +++ b/cookbook/pocketflow-multi-agent/README.md @@ -0,0 +1,83 @@ +# Multi-Agent Taboo Game + +A PocketFlow example that demonstrates how to implement asynchronous multi-agent communication using the Taboo word guessing game. + +## Features + +- Implement asynchronous communication between two AI agents (Hinter and Guesser) +- Use AsyncNode for non-blocking agent interactions +- Create dynamic conversation flow through asyncio message queues +- Demonstrate complex turn-based game mechanics with LLMs +- Automatically terminate the game when the correct word is guessed + +## Getting Started + +1. Install the required dependencies: + +```bash +pip install -r requirements.txt +``` + +2. Set your OpenAI API key as an environment variable: + +```bash +export OPENAI_API_KEY=your_api_key_here +``` + +3. Run the application: + +```bash +python main.py +``` + +## How It Works + +The workflow follows an asynchronous multi-agent communication pattern: + +```mermaid +flowchart LR + AsyncHinter[AsyncHinter Node] <--> MessageQueue{Message Queue} + MessageQueue <--> AsyncGuesser[AsyncGuesser Node] +``` + +Here's what each component does: + +1. **AsyncHinter Node**: Generates hints about the target word while avoiding forbidden words +2. **AsyncGuesser Node**: Makes guesses based on the hints received from the Hinter +3. **Message Queue**: Facilitates asynchronous communication between the agents + +## Files + +- [`main.py`](./main.py): Main entry point implementing the AsyncHinter and AsyncGuesser nodes and game flow +- [`utils.py`](./utils.py): Utility functions including LLM wrappers for generating hints and guesses +- [`requirements.txt`](./requirements.txt): Lists the required dependencies + +## Example Output + +``` +=========== Taboo Game Starting! =========== +Target word: nostalgic +Forbidden words: ['memory', 'past', 'remember', 'feeling', 'longing'] +============================================ + +Hinter: Here's your hint - Sentiment for earlier times. +Guesser: I guess it's - Nostalgia + +Hinter: Here's your hint - Sentiment for earlier times. +Guesser: I guess it's - Reminiscence + +Hinter: Here's your hint - Yearning for days gone by. +Guesser: I guess it's - Sentimentality + +Hinter: Here's your hint - Reliving cherished moments or experiences. +Guesser: I guess it's - Memories + +Hinter: Here's your hint - Recollection of cherished experiences. +Guesser: I guess it's - Reflection + +Hinter: Here's your hint - Yearning for earlier times. +Guesser: I guess it's - Longing + +Hinter: Here's your hint - Sentiment for earlier times. +Guesser: I guess it's - Nostalgic +Game Over - Correct guess! \ No newline at end of file diff --git a/cookbook/pocketflow-multi-agent/main.py b/cookbook/pocketflow-multi-agent/main.py new file mode 100644 index 0000000..4045252 --- /dev/null +++ b/cookbook/pocketflow-multi-agent/main.py @@ -0,0 +1,100 @@ +import asyncio +from pocketflow import AsyncNode, AsyncFlow +from utils import call_llm + +class AsyncHinter(AsyncNode): + async def prep_async(self, shared): + # Wait for message from guesser (or empty string at start) + guess = await shared["hinter_queue"].get() + if guess == "GAME_OVER": + return None + return shared["target_word"], shared["forbidden_words"], shared.get("past_guesses", []) + + async def exec_async(self, inputs): + if inputs is None: + return None + target, forbidden, past_guesses = inputs + prompt = f"Generate hint for '{target}'\nForbidden words: {forbidden}" + if past_guesses: + prompt += f"\nPrevious wrong guesses: {past_guesses}\nMake hint more specific." + prompt += "\nUse at most 5 words." + + hint = call_llm(prompt) + print(f"\nHinter: Here's your hint - {hint}") + return hint + + async def post_async(self, shared, prep_res, exec_res): + if exec_res is None: + return "end" + # Send hint to guesser + await shared["guesser_queue"].put(exec_res) + return "continue" + +class AsyncGuesser(AsyncNode): + async def prep_async(self, shared): + # Wait for hint from hinter + hint = await shared["guesser_queue"].get() + return hint, shared.get("past_guesses", []) + + async def exec_async(self, inputs): + hint, past_guesses = inputs + prompt = f"Given hint: {hint}, past wrong guesses: {past_guesses}, make a new guess. Directly reply a single word:" + guess = call_llm(prompt) + print(f"Guesser: I guess it's - {guess}") + return guess + + async def post_async(self, shared, prep_res, exec_res): + # Check if guess is correct + if exec_res.lower() == shared["target_word"].lower(): + print("Game Over - Correct guess!") + await shared["hinter_queue"].put("GAME_OVER") + return "end" + + # Store the guess in shared state + if "past_guesses" not in shared: + shared["past_guesses"] = [] + shared["past_guesses"].append(exec_res) + + # Send guess to hinter + await shared["hinter_queue"].put(exec_res) + return "continue" + +async def main(): + # Set up game + shared = { + "target_word": "nostalgic", + "forbidden_words": ["memory", "past", "remember", "feeling", "longing"], + "hinter_queue": asyncio.Queue(), + "guesser_queue": asyncio.Queue() + } + + print("=========== Taboo Game Starting! ===========") + print(f"Target word: {shared['target_word']}") + print(f"Forbidden words: {shared['forbidden_words']}") + print("============================================") + + # Initialize by sending empty guess to hinter + await shared["hinter_queue"].put("") + + # Create nodes and flows + hinter = AsyncHinter() + guesser = AsyncGuesser() + + # Set up flows + hinter_flow = AsyncFlow(start=hinter) + guesser_flow = AsyncFlow(start=guesser) + + # Connect nodes to themselves for looping + hinter - "continue" >> hinter + guesser - "continue" >> guesser + + # Run both agents concurrently + await asyncio.gather( + hinter_flow.run_async(shared), + guesser_flow.run_async(shared) + ) + + print("=========== Game Complete! ===========") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/pocketflow-multi-agent/utils.py b/cookbook/pocketflow-multi-agent/utils.py new file mode 100644 index 0000000..5104b84 --- /dev/null +++ b/cookbook/pocketflow-multi-agent/utils.py @@ -0,0 +1,14 @@ +import os +from openai import OpenAI + +def call_llm(prompt): + client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key")) + r = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}] + ) + return r.choices[0].message.content + +# Example usage +if __name__ == "__main__": + print(call_llm("Tell me a short joke")) \ No newline at end of file