From 4c7c4486700bce163bd4f29e99ae266cb6ff6c73 Mon Sep 17 00:00:00 2001 From: zachary62 Date: Wed, 19 Mar 2025 12:50:49 -0400 Subject: [PATCH] add tutorial --- cookbook/pocketflow-agent/README.md | 85 ++++++++++++++ cookbook/pocketflow-agent/flow.py | 33 ++++++ cookbook/pocketflow-agent/main.py | 27 +++++ cookbook/pocketflow-agent/nodes.py | 128 +++++++++++++++++++++ cookbook/pocketflow-agent/requirements.txt | 4 + cookbook/pocketflow-agent/utils.py | 30 +++++ 6 files changed, 307 insertions(+) create mode 100644 cookbook/pocketflow-agent/README.md create mode 100644 cookbook/pocketflow-agent/flow.py create mode 100644 cookbook/pocketflow-agent/main.py create mode 100644 cookbook/pocketflow-agent/nodes.py create mode 100644 cookbook/pocketflow-agent/requirements.txt create mode 100644 cookbook/pocketflow-agent/utils.py diff --git a/cookbook/pocketflow-agent/README.md b/cookbook/pocketflow-agent/README.md new file mode 100644 index 0000000..29a1b25 --- /dev/null +++ b/cookbook/pocketflow-agent/README.md @@ -0,0 +1,85 @@ +# PocketFlow Research Agent - Tutorial for Dummy + +This project demonstrates a simple LLM-powered research agent built with PocketFlow, a minimalist LLM framework in 100 lines. For more information on PocketFlow and how to build LLM agents, check out: + +- [LLM Agents are simply Graph — Tutorial For Dummies](https://zacharyhuang.substack.com/p/llm-agent-internal-as-a-graph-tutorial) +- [PocketFlow GitHub](https://github.com/the-pocket/PocketFlow) +- [PocketFlow Documentation](https://the-pocket.github.io/PocketFlow/) + +## What It Does + +This agent can: +1. Answer questions by searching for information when needed +2. Make decisions about when to search and when to answer +3. Generate helpful responses based on collected research + +## Setting Up + +### Prerequisites +- Python 3.8+ +- OpenAI API key + +### Installation + +1. Install the required packages: +```bash +pip install -r requirements.txt +``` + +## Structure + +- [`main.py`](./main.py): Entry point and user interface +- [`flow.py`](./flow.py): Creates and connects the agent flow +- [`nodes.py`](./nodes.py): Defines the decision and action nodes +- [`utils.py`](./utils.py): Contains utility functions for LLM calls and web searches + +## Quick Start Guide + +### Step 1: Set Up Your OpenAI API Key + +First, you must provide your OpenAI API key: + +```bash +export OPENAI_API_KEY="your-api-key-here" +``` + +### Step 2: Test Utilities + +Verify that your API key is working by testing the utilities: + +```bash +python utils.py +``` + +This will test both the LLM call functionality and the web search capability. + +### Step 3: Run the Agent + +Run the agent with the default question ("Who won the Nobel Prize in Physics 2024?"): + +```bash +python main.py +``` + +### Step 4: Ask Custom Questions + +To ask your own question, use the `--` prefix: + +```bash +python main.py --"What is quantum computing?" +``` + +## How It Works + +The agent is structured as a simple directed graph with three main nodes: + +```mermaid +graph TD + A[DecideAction] -->|"search"| B[SearchWeb] + A -->|"answer"| C[AnswerQuestion] + B -->|"decide"| A +``` + +1. **DecideAction**: Determines whether to search for information or provide an answer +2. **SearchWeb**: Searches the web for information +3. **AnswerQuestion**: Creates a final answer once enough information is gathered diff --git a/cookbook/pocketflow-agent/flow.py b/cookbook/pocketflow-agent/flow.py new file mode 100644 index 0000000..bc0cb80 --- /dev/null +++ b/cookbook/pocketflow-agent/flow.py @@ -0,0 +1,33 @@ +from pocketflow import Flow +from nodes import DecideAction, SearchWeb, AnswerQuestion + +def create_agent_flow(): + """ + Create and connect the nodes to form a complete agent flow. + + The flow works like this: + 1. DecideAction node decides whether to search or answer + 2. If search, go to SearchWeb node + 3. If answer, go to AnswerQuestion node + 4. After SearchWeb completes, go back to DecideAction + + Returns: + Flow: A complete research agent flow + """ + # Create instances of each node + decide = DecideAction() + search = SearchWeb() + answer = AnswerQuestion() + + # Connect the nodes + # If DecideAction returns "search", go to SearchWeb + decide - "search" >> search + + # If DecideAction returns "answer", go to AnswerQuestion + decide - "answer" >> answer + + # After SearchWeb completes and returns "decide", go back to DecideAction + search - "decide" >> decide + + # Create and return the flow, starting with the DecideAction node + return Flow(start=decide) \ No newline at end of file diff --git a/cookbook/pocketflow-agent/main.py b/cookbook/pocketflow-agent/main.py new file mode 100644 index 0000000..4d2cbc4 --- /dev/null +++ b/cookbook/pocketflow-agent/main.py @@ -0,0 +1,27 @@ +import sys +from flow import create_agent_flow + +def main(): + """Simple function to process a question.""" + # Default question + default_question = "Who won the Nobel Prize in Physics 2024?" + + # Get question from command line if provided with -- + question = default_question + for arg in sys.argv[1:]: + if arg.startswith("--"): + question = arg[2:] + break + + # Create the agent flow + agent_flow = create_agent_flow() + + # Process the question + shared = {"question": question} + print(f"šŸ¤” Processing question: {question}") + agent_flow.run(shared) + print("\nšŸŽÆ Final Answer:") + print(shared.get("answer", "No answer found")) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/cookbook/pocketflow-agent/nodes.py b/cookbook/pocketflow-agent/nodes.py new file mode 100644 index 0000000..d709a9c --- /dev/null +++ b/cookbook/pocketflow-agent/nodes.py @@ -0,0 +1,128 @@ +from pocketflow import Node +from utils import call_llm, search_web +import yaml + +class DecideAction(Node): + def prep(self, shared): + """Prepare the context and question for the decision-making process.""" + # Get the current context (default to "No previous search" if none exists) + context = shared.get("context", "No previous search") + # Get the question from the shared store + question = shared["question"] + # Return both for the exec step + return question, context + + def exec(self, inputs): + """Call the LLM to decide whether to search or answer.""" + question, context = inputs + + print(f"šŸ¤” Agent deciding what to do next...") + + # Create a prompt to help the LLM decide what to do next + prompt = f""" +### CONTEXT +You are a research assistant that can search the web. +Question: {question} +Previous Research: {context} + +### ACTION SPACE +[1] search + Description: Look up more information on the web + Parameters: + - query (str): What to search for + +[2] answer + Description: Answer the question with current knowledge + Parameters: + - answer (str): Final answer to the question + +## NEXT ACTION +Decide the next action based on the context and available actions. +Return your response in this format: + +```yaml +thinking: | + +action: search OR answer +reason: +search_query: +```""" + + # Call the LLM to make a decision + response = call_llm(prompt) + + # Parse the response to get the decision + yaml_str = response.split("```yaml")[1].split("```")[0].strip() + decision = yaml.safe_load(yaml_str) + + return decision + + def post(self, shared, prep_res, exec_res): + """Save the decision and determine the next step in the flow.""" + # If LLM decided to search, save the search query + if exec_res["action"] == "search": + shared["search_query"] = exec_res["search_query"] + print(f"šŸ” Agent decided to search for: {exec_res['search_query']}") + else: + print(f"šŸ’” Agent decided to answer the question") + + # Return the action to determine the next node in the flow + return exec_res["action"] + +class SearchWeb(Node): + def prep(self, shared): + """Get the search query from the shared store.""" + return shared["search_query"] + + def exec(self, search_query): + """Search the web for the given query.""" + # Call the search utility function + print(f"🌐 Searching the web for: {search_query}") + results = search_web(search_query) + return results + + def post(self, shared, prep_res, exec_res): + """Save the search results and go back to the decision node.""" + # Add the search results to the context in the shared store + previous = shared.get("context", "") + shared["context"] = previous + "\n\nSEARCH: " + shared["search_query"] + "\nRESULTS: " + exec_res + + print(f"šŸ“š Found information, analyzing results...") + + # Always go back to the decision node after searching + return "decide" + +class AnswerQuestion(Node): + def prep(self, shared): + """Get the question and context for answering.""" + return shared["question"], shared.get("context", "") + + def exec(self, inputs): + """Call the LLM to generate a final answer.""" + question, context = inputs + + print(f"āœļø Crafting final answer...") + + # Create a prompt for the LLM to answer the question + prompt = f""" +### CONTEXT +Based on the following information, answer the question. +Question: {question} +Research: {context} + +## YOUR ANSWER: +Provide a comprehensive answer using the research results. +""" + # Call the LLM to generate an answer + answer = call_llm(prompt) + return answer + + def post(self, shared, prep_res, exec_res): + """Save the final answer and complete the flow.""" + # Save the answer in the shared store + shared["answer"] = exec_res + + print(f"āœ… Answer generated successfully") + + # We're done - no need to continue the flow + return "done" \ No newline at end of file diff --git a/cookbook/pocketflow-agent/requirements.txt b/cookbook/pocketflow-agent/requirements.txt new file mode 100644 index 0000000..38d5c4c --- /dev/null +++ b/cookbook/pocketflow-agent/requirements.txt @@ -0,0 +1,4 @@ +pocketflow>=0.0.1 +aiohttp>=3.8.0 # For async HTTP requests +openai>=1.0.0 # For async LLM calls +duckduckgo-search>=7.5.2 # For web search \ No newline at end of file diff --git a/cookbook/pocketflow-agent/utils.py b/cookbook/pocketflow-agent/utils.py new file mode 100644 index 0000000..c56a175 --- /dev/null +++ b/cookbook/pocketflow-agent/utils.py @@ -0,0 +1,30 @@ +from openai import OpenAI +import os +from duckduckgo_search import DDGS + +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", + messages=[{"role": "user", "content": prompt}] + ) + return r.choices[0].message.content + +def search_web(query): + results = DDGS().text(query, max_results=5) + # Convert results to a string + results_str = "\n\n".join([f"Title: {r['title']}\nURL: {r['href']}\nSnippet: {r['body']}" for r in results]) + return results_str + +if __name__ == "__main__": + print("## Testing call_llm") + prompt = "In a few words, what is the meaning of life?" + print(f"## Prompt: {prompt}") + response = call_llm(prompt) + print(f"## Response: {response}") + + print("## Testing search_web") + query = "Who won the Nobel Prize in Physics 2024?" + print(f"## Query: {query}") + results = search_web(query) + print(f"## Results: {results}") \ No newline at end of file