diff --git a/cookbook/pocketflow-cmd-hitl/flow.py b/cookbook/pocketflow-cmd-hitl/flow.py new file mode 100644 index 0000000..ea0a4f6 --- /dev/null +++ b/cookbook/pocketflow-cmd-hitl/flow.py @@ -0,0 +1,26 @@ +from pocketflow import Flow +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 + + # 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 new file mode 100644 index 0000000..381c4aa --- /dev/null +++ b/cookbook/pocketflow-cmd-hitl/main.py @@ -0,0 +1,24 @@ +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, + "disliked_jokes": [], + "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!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/cookbook/pocketflow-cmd-hitl/nodes.py b/cookbook/pocketflow-cmd-hitl/nodes.py new file mode 100644 index 0000000..5c3ee9d --- /dev/null +++ b/cookbook/pocketflow-cmd-hitl/nodes.py @@ -0,0 +1,61 @@ +from pocketflow import Node +from .utils.call_llm import call_llm + +class GetTopicNode(Node): + """Prompts the user to enter the topic for the joke.""" + def exec(self, _shared): + return input("What topic would you like a joke about? ") + + 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 + disliked_jokes = shared.get("disliked_jokes", []) + + prompt = f"Please generate a joke about {topic}." + 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 + + 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() + if feedback in ["yes", "y", "no", "n"]: + return feedback + print("Invalid input. Please type 'yes' or 'no'.") + + def post(self, shared, _prep_res, exec_res): + 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" + shared["user_feedback"] = "disapprove" + current_joke = shared.get("current_joke") + if current_joke: + if "disliked_jokes" not in shared: + 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 diff --git a/cookbook/pocketflow-cmd-hitl/requirements.txt b/cookbook/pocketflow-cmd-hitl/requirements.txt new file mode 100644 index 0000000..841db55 --- /dev/null +++ b/cookbook/pocketflow-cmd-hitl/requirements.txt @@ -0,0 +1,7 @@ +# Add any project-specific dependencies here. +# For example: +# openai +# anthropic + +pocketflow>=0.0.1 +openai>=1.0.0 \ No newline at end of file diff --git a/cookbook/pocketflow-cmd-hitl/utils/__init__.py b/cookbook/pocketflow-cmd-hitl/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cookbook/pocketflow-cmd-hitl/utils/call_llm.py b/cookbook/pocketflow-cmd-hitl/utils/call_llm.py new file mode 100644 index 0000000..b256c95 --- /dev/null +++ b/cookbook/pocketflow-cmd-hitl/utils/call_llm.py @@ -0,0 +1,17 @@ +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}] + ) + return r.choices[0].message.content + +if __name__ == "__main__": + print("Testing real LLM call:") + joke_prompt = "Tell me a short joke about a programmer." + print(f"Prompt: {joke_prompt}") + response = call_llm(joke_prompt) + print(f"Response: {response}") \ No newline at end of file