From d13180f8359367e9da70dd331173d450adb0cd6a Mon Sep 17 00:00:00 2001 From: zachary62 Date: Tue, 27 May 2025 00:57:52 -0400 Subject: [PATCH] add local HITL example --- README.md | 1 + cookbook/pocketflow-cmd-hitl/README.md | 80 +++++++++++++++++++ cookbook/pocketflow-cmd-hitl/flow.py | 15 +--- cookbook/pocketflow-cmd-hitl/main.py | 6 +- cookbook/pocketflow-cmd-hitl/nodes.py | 22 ++--- cookbook/pocketflow-cmd-hitl/requirements.txt | 7 +- .../pocketflow-cmd-hitl/utils/call_llm.py | 27 ++++--- 7 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 cookbook/pocketflow-cmd-hitl/README.md diff --git a/README.md b/README.md index d8a5207..b02d40a 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ From there, it's easy to implement popular design patterns like ([Multi-](https: | [Chat Guardrail](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Dummy* | A travel advisor chatbot that only processes travel-related queries | | [Majority Vote](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ☆☆☆ *Dummy* | Improve reasoning accuracy by aggregating multiple solution attempts | | [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ☆☆☆ *Dummy* | Batch resume qualification using map-reduce pattern | +| [Cmd HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-cmd-hitl) | ☆☆☆ *Dummy* | A command-line joke generator with human-in-the-loop feedback | | [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Beginner* | A Taboo word game for async communication between 2 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 | diff --git a/cookbook/pocketflow-cmd-hitl/README.md b/cookbook/pocketflow-cmd-hitl/README.md new file mode 100644 index 0000000..e181c58 --- /dev/null +++ b/cookbook/pocketflow-cmd-hitl/README.md @@ -0,0 +1,80 @@ +# PocketFlow Command-Line Joke Generator (Human-in-the-Loop Example) + +A simple, interactive command-line application that generates jokes based on user-provided topics and direct human feedback. This serves as a clear example of a Human-in-the-Loop (HITL) workflow orchestrated by PocketFlow. + +## Features + +- **Interactive Joke Generation**: Ask for jokes on any topic. +- **Human-in-the-Loop Feedback**: Dislike a joke? Your feedback directly influences the next generation attempt. +- **Minimalist Design**: A straightforward example of using PocketFlow for HITL tasks. +- **Powered by LLMs**: (Uses Anthropic Claude via an API call for joke generation). + +## Getting Started + +This project is part of the PocketFlow cookbook examples. It's assumed you have already cloned the [PocketFlow repository](https://github.com/the-pocket/PocketFlow) and are in the `cookbook/pocketflow-cmd-hitl` directory. + +1. **Install required dependencies**: + ```bash + pip install -r requirements.txt + ``` + +2. **Set up your Anthropic API key**: + The application uses Anthropic Claude to generate jokes. You need to set your API key as an environment variable. + ```bash + export ANTHROPIC_API_KEY="your-anthropic-api-key-here" + ``` + You can test if your `call_llm.py` utility is working by running it directly: + ```bash + python utils/call_llm.py + ``` + +3. **Run the Joke Generator**: + ```bash + python main.py + ``` + +## How It Works + +The system uses a simple PocketFlow workflow: + +```mermaid +flowchart TD + GetTopic[GetTopicNode] --> GenerateJoke[GenerateJokeNode] + GenerateJoke --> GetFeedback[GetFeedbackNode] + GetFeedback -- "Approve" --> Z((End)) + GetFeedback -- "Disapprove" --> GenerateJoke +``` + +1. **GetTopicNode**: Prompts the user to enter a topic for the joke. +2. **GenerateJokeNode**: Sends the topic (and any previously disliked jokes as context) to an LLM to generate a new joke. +3. **GetFeedbackNode**: Shows the joke to the user and asks if they liked it. + * If **yes** (approved), the application ends. + * If **no** (disapproved), the disliked joke is recorded, and the flow loops back to `GenerateJokeNode` to try again. + +## Sample Output + +Here's an example of an interaction with the Joke Generator: + +``` +Welcome to the Command-Line Joke Generator! +What topic would you like a joke about? Pocket Flow: 100-line LLM framework + +Joke: Pocket Flow: Finally, an LLM framework that fits in your pocket! Too bad your model still needs a data center. +Did you like this joke? (yes/no): no +Okay, let me try another one. + +Joke: Pocket Flow: A 100-line LLM framework where 99 lines are imports and the last line is `print("TODO: implement intelligence")`. +Did you like this joke? (yes/no): yes +Great! Glad you liked it. + +Thanks for using the Joke Generator! +``` + +## Files + +- [`main.py`](./main.py): Entry point for the application. +- [`flow.py`](./flow.py): Defines the PocketFlow graph and node connections. +- [`nodes.py`](./nodes.py): Contains the definitions for `GetTopicNode`, `GenerateJokeNode`, and `GetFeedbackNode`. +- [`utils/call_llm.py`](./utils/call_llm.py): Utility function to interact with the LLM (Anthropic Claude). +- [`requirements.txt`](./requirements.txt): Lists project dependencies. +- [`docs/design.md`](./docs/design.md): The design document for this application. \ No newline at end of file diff --git a/cookbook/pocketflow-cmd-hitl/flow.py b/cookbook/pocketflow-cmd-hitl/flow.py index ea0a4f6..bf0df28 100644 --- a/cookbook/pocketflow-cmd-hitl/flow.py +++ b/cookbook/pocketflow-cmd-hitl/flow.py @@ -1,26 +1,15 @@ from pocketflow import Flow -from .nodes import GetTopicNode, GenerateJokeNode, GetFeedbackNode +from nodes import GetTopicNode, GenerateJokeNode, GetFeedbackNode def create_joke_flow() -> Flow: """Creates and returns the joke generation flow.""" - # Create nodes get_topic_node = GetTopicNode() generate_joke_node = GenerateJokeNode() get_feedback_node = GetFeedbackNode() - # Connect nodes - # GetTopicNode -> GenerateJokeNode (default action) get_topic_node >> generate_joke_node - - # GenerateJokeNode -> GetFeedbackNode (default action) generate_joke_node >> get_feedback_node + get_feedback_node - "Disapprove" >> generate_joke_node - # GetFeedbackNode actions: - # "Approve" -> Ends the flow (no further connection) - # "Disapprove" -> GenerateJokeNode - # get_feedback_node.connect_to(generate_joke_node, action="Disapprove") - get_feedback_node - "Disapprove" >> generate_joke_node # Alternative syntax - - # Create flow starting with the input node joke_flow = Flow(start=get_topic_node) return joke_flow \ No newline at end of file diff --git a/cookbook/pocketflow-cmd-hitl/main.py b/cookbook/pocketflow-cmd-hitl/main.py index 381c4aa..fe4987e 100644 --- a/cookbook/pocketflow-cmd-hitl/main.py +++ b/cookbook/pocketflow-cmd-hitl/main.py @@ -1,10 +1,9 @@ -from .flow import create_joke_flow +from flow import create_joke_flow def main(): """Main function to run the joke generator application.""" print("Welcome to the Command-Line Joke Generator!") - # Initialize the shared store as per the design shared = { "topic": None, "current_joke": None, @@ -12,10 +11,7 @@ def main(): "user_feedback": None } - # Create the flow joke_flow = create_joke_flow() - - # Run the flow joke_flow.run(shared) print("\nThanks for using the Joke Generator!") diff --git a/cookbook/pocketflow-cmd-hitl/nodes.py b/cookbook/pocketflow-cmd-hitl/nodes.py index 5c3ee9d..ea2bf81 100644 --- a/cookbook/pocketflow-cmd-hitl/nodes.py +++ b/cookbook/pocketflow-cmd-hitl/nodes.py @@ -1,5 +1,5 @@ from pocketflow import Node -from .utils.call_llm import call_llm +from utils.call_llm import call_llm class GetTopicNode(Node): """Prompts the user to enter the topic for the joke.""" @@ -8,36 +8,28 @@ class GetTopicNode(Node): def post(self, shared, _prep_res, exec_res): shared["topic"] = exec_res - # No specific action needed, default will move to next connected node - return "default" class GenerateJokeNode(Node): """Generates a joke based on the topic and any previous feedback.""" def prep(self, shared): - topic = shared.get("topic", "anything") # Default to "anything" if no topic + topic = shared.get("topic", "anything") disliked_jokes = shared.get("disliked_jokes", []) - prompt = f"Please generate a joke about {topic}." + prompt = f"Please generate an one-liner joke about: {topic}. Make it short and funny." if disliked_jokes: disliked_str = "; ".join(disliked_jokes) prompt = f"The user did not like the following jokes: [{disliked_str}]. Please generate a new, different joke about {topic}." return prompt def exec(self, prep_res): - return call_llm(prep_res) # prep_res is the prompt + return call_llm(prep_res) def post(self, shared, _prep_res, exec_res): shared["current_joke"] = exec_res print(f"\nJoke: {exec_res}") - return "default" class GetFeedbackNode(Node): """Presents the joke to the user and asks for approval.""" - # prep is not strictly needed as current_joke is printed by GenerateJokeNode - # but we can read it if we want to display it again here for example. - # def prep(self, shared): - # return shared.get("current_joke") - def exec(self, _prep_res): while True: feedback = input("Did you like this joke? (yes/no): ").strip().lower() @@ -49,8 +41,8 @@ class GetFeedbackNode(Node): if exec_res in ["yes", "y"]: shared["user_feedback"] = "approve" print("Great! Glad you liked it.") - return "Approve" # Action to end the flow - else: # "no" or "n" + return "Approve" + else: shared["user_feedback"] = "disapprove" current_joke = shared.get("current_joke") if current_joke: @@ -58,4 +50,4 @@ class GetFeedbackNode(Node): shared["disliked_jokes"] = [] shared["disliked_jokes"].append(current_joke) print("Okay, let me try another one.") - return "Disapprove" # Action to loop back to GenerateJokeNode \ No newline at end of file + return "Disapprove" \ No newline at end of file diff --git a/cookbook/pocketflow-cmd-hitl/requirements.txt b/cookbook/pocketflow-cmd-hitl/requirements.txt index 841db55..493f3f4 100644 --- a/cookbook/pocketflow-cmd-hitl/requirements.txt +++ b/cookbook/pocketflow-cmd-hitl/requirements.txt @@ -1,7 +1,2 @@ -# Add any project-specific dependencies here. -# For example: -# openai -# anthropic - pocketflow>=0.0.1 -openai>=1.0.0 \ No newline at end of file +anthropic>=0.20.0 # Or a recent version \ No newline at end of file diff --git a/cookbook/pocketflow-cmd-hitl/utils/call_llm.py b/cookbook/pocketflow-cmd-hitl/utils/call_llm.py index b256c95..5e3477c 100644 --- a/cookbook/pocketflow-cmd-hitl/utils/call_llm.py +++ b/cookbook/pocketflow-cmd-hitl/utils/call_llm.py @@ -1,17 +1,24 @@ +from anthropic import Anthropic import os -from openai import OpenAI def call_llm(prompt: str) -> str: - client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key")) - r = client.chat.completions.create( - model="gpt-4o", - messages=[{"role": "user", "content": prompt}] + client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-anthropic-api-key")) # Default if key not found + response = client.messages.create( + model="claude-3-haiku-20240307", # Using a smaller model for jokes + max_tokens=150, # Jokes don't need to be very long + messages=[ + {"role": "user", "content": prompt} + ] ) - return r.choices[0].message.content + return response.content[0].text if __name__ == "__main__": - print("Testing real LLM call:") - joke_prompt = "Tell me a short joke about a programmer." + print("Testing Anthropic LLM call for jokes:") + joke_prompt = "Tell me a one-liner joke about a cat." print(f"Prompt: {joke_prompt}") - response = call_llm(joke_prompt) - print(f"Response: {response}") \ No newline at end of file + try: + response = call_llm(joke_prompt) + print(f"Response: {response}") + except Exception as e: + print(f"Error calling LLM: {e}") + print("Please ensure your ANTHROPIC_API_KEY environment variable is set correctly.") \ No newline at end of file