From ff0bca2d6719486efb064fa7f0f44c97dc679f10 Mon Sep 17 00:00:00 2001 From: zachary62 Date: Thu, 20 Mar 2025 20:50:31 -0400 Subject: [PATCH] add thinking --- .../pocketflow-chain-of-thought/README.md | 348 +++++++++++++++++- .../pocketflow-chain-of-thought/design.md | 53 +++ cookbook/pocketflow-chain-of-thought/flow.py | 13 + cookbook/pocketflow-chain-of-thought/main.py | 33 ++ cookbook/pocketflow-chain-of-thought/nodes.py | 116 ++++++ .../requirements.txt | 2 + cookbook/pocketflow-chain-of-thought/utils.py | 20 + 7 files changed, 581 insertions(+), 4 deletions(-) create mode 100644 cookbook/pocketflow-chain-of-thought/design.md create mode 100644 cookbook/pocketflow-chain-of-thought/flow.py create mode 100644 cookbook/pocketflow-chain-of-thought/main.py create mode 100644 cookbook/pocketflow-chain-of-thought/nodes.py create mode 100644 cookbook/pocketflow-chain-of-thought/requirements.txt create mode 100644 cookbook/pocketflow-chain-of-thought/utils.py diff --git a/cookbook/pocketflow-chain-of-thought/README.md b/cookbook/pocketflow-chain-of-thought/README.md index 46bc250..fda01de 100644 --- a/cookbook/pocketflow-chain-of-thought/README.md +++ b/cookbook/pocketflow-chain-of-thought/README.md @@ -1,7 +1,347 @@ -# Chain-of-Thought +# Extended Thinking -The simplest implementation is to tell the AI to "think step by step" or to provide examples of step-by-step reasoning in the prompt. This guides the AI to break down its thinking. +This project demonstrates an extended thinking mode implementation that enables LLMs to solve complex reasoning problems by thinking step-by-step. It's designed to improve problem-solving accuracy through deliberate reasoning. -Further more thinking models like sonnet 3.7, O1 natively support. +## Features -However, we can also simulate without thinking model. \ No newline at end of file +- Improves model reasoning on complex problems +- Works with models like Claude 3.7 Sonnet that support extended thinking +- Solves problems that direct prompting often fails on +- Provides detailed reasoning traces for verification + +## Getting Started + +1. Install the required packages: +```bash +pip install -r requirements.txt +``` + +2. Set up your API key: +```bash +export ANTHROPIC_API_KEY="your-api-key-here" +``` + +3. Run a test problem to see thinking mode in action: +```bash +python main.py +``` + +4. Try your own reasoning problem: +```bash +python main.py --"Your complex reasoning problem here" +``` + +## How It Works + +The implementation uses a self-looping Chain of Thought node that allows an LLM to think through complex problems step by step: + +```mermaid +flowchart LR + cot[ChainOfThoughtNode] -->|"continue"| cot +``` + +Each time the node loops, it: +1. Reads the problem and previous thoughts +2. Generates the next thought or final solution +3. Decides whether more thinking is needed + +This approach helps LLMs solve problems that would be difficult with a single-pass approach. + +## Comparison with Different Approaches + +- **Standard prompting**: Telling the AI to "think step by step" or providing examples helps, but the thinking is usually not significant enough +- **Extended thinking models**: Models like Claude 3.7 Sonnet, GPT-4o, and Deepseek R1 natively support extended thinking with much better results +- **This implementation**: Explores how to achieve extended thinking with non-thinking models + +## Example Thinking Process + +> **Problem**: Break a stick, then break the longer piece again. What's the probability of forming a triangle? + +This problem demonstrates why extended thinking is valuable: + +- **Standard models without thinking**: Get the wrong answer +- **Models with extended thinking**: Find the correct answer (0.386) + +For comparison: +- [Claude 3.7 Sonnet (without thinking)](https://claude.ai/share/31bf938c-94dd-42f6-bfac-e82ba3616dbc): Wrong answer +- [GPT-4o with thinking](https://chatgpt.com/share/67dcb1bf-ceb0-8000-823a-8ce894032e37): Correct answer after 1.5 min +- [Claude 3.7 Sonnet with thinking](https://claude.ai/share/0863f9fd-ae75-4a0c-84ee-f7443d2fcf4a): Correct answer after 4.5 min + +Below is an example of how Claude 3.7 Sonnet uses thinking mode to solve this complex problem, and get the correct result: + +``` +🤔 Processing question: Break a stick, then break the longer piece again. What's the probability of forming a triangle? + +Let me understand the problem. We have a stick of some length, which we can call 1 for simplicity. We break it at a random point, creating two pieces. Then we break the longer piece again at a random point. This gives us three pieces. We want to find the probability that these three pieces can form a triangle. + +For three segments to form a triangle, the sum of the lengths of any two sides must be greater than the length of the third side. So we need to check: +a + b > c +a + c > b +b + c > a + +Where a, b, and c are the lengths of the three pieces. + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +Thought 2: Let me set up the problem mathematically. Let's say the original stick has length 1. + +First break: We break the stick at a point X, where X is chosen uniformly from [0,1]. This gives us two pieces with lengths X and 1-X. + +Second break: We break the longer piece at a random point. If X ≤ 0.5, then 1-X is the longer piece. If X > 0.5, then X is the longer piece. + +Case 1: If X ≤ 0.5, the longer piece is 1-X. We break it at point Y(1-X), where Y is chosen uniformly from [0,1]. This gives pieces of lengths: +- X (from first break) +- Y(1-X) (from second break) +- (1-Y)(1-X) (remaining from second break) + +Case 2: If X > 0.5, the longer piece is X. We break it at point YX, where Y is chosen uniformly from [0,1]. This gives pieces of lengths: +- 1-X (from first break) +- YX (from second break) +- (1-Y)X (remaining from second break) + +For each case, I need to determine when these three pieces can form a triangle, then calculate the probability. + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +Thought 3: Let's analyze when the three pieces can form a triangle for both cases. + +For a triangle to form, each side must be less than the sum of the other two sides. So we need: + +Case 1 (X ≤ 0.5): We have pieces of lengths X, Y(1-X), and (1-Y)(1-X) + +The triangle inequality conditions are: +- X + Y(1-X) > (1-Y)(1-X) +- X + (1-Y)(1-X) > Y(1-X) +- Y(1-X) + (1-Y)(1-X) > X + +Simplifying: +- X + Y(1-X) > (1-Y)(1-X) +- X + (1-Y)(1-X) > Y(1-X) +- (1-X) > X (since Y(1-X) + (1-Y)(1-X) = 1-X) + +The third condition is always true when X ≤ 0.5 (which is our assumption for Case 1). + +Let's work on the first condition: +X + Y(1-X) > (1-Y)(1-X) +X + Y(1-X) > (1-X) - Y(1-X) +X + 2Y(1-X) > (1-X) +X + 2Y(1-X) > 1-X +2X + 2Y(1-X) > 1 +2X + 2Y - 2YX > 1 + +Similarly for the second condition: +X + (1-Y)(1-X) > Y(1-X) +X + (1-X) - Y(1-X) > Y(1-X) +X + (1-X) > 2Y(1-X) +1 > 2Y(1-X) +1/(2(1-X)) > Y + +So for Case 1, we need: +- 2X + 2Y - 2YX > 1 +- Y < 1/(2(1-X)) + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +Thought 4: I need to continue analyzing Case 1 and then move on to Case 2. + +For Case 1 (X ≤ 0.5), we have the constraints: +- 2X + 2Y - 2YX > 1 +- Y < 1/(2(1-X)) + +Let's rewrite the first constraint: +2X + 2Y - 2YX > 1 +2Y(1-X) > 1 - 2X +Y > (1 - 2X)/(2(1-X)) + +So for Case 1, we need: +- Y > (1 - 2X)/(2(1-X)) +- Y < 1/(2(1-X)) + +This means Y must be in the range ((1 - 2X)/(2(1-X)), 1/(2(1-X))). For this range to be valid, we need: +(1 - 2X)/(2(1-X)) < 1/(2(1-X)) +1 - 2X < 1 +-2X < 0 +X > 0 + +Which is true for our domain. So we have a valid range for Y when X ≤ 0.5. + +Now let's analyze Case 2 (X > 0.5): We have pieces of lengths (1-X), YX, and (1-Y)X + +The triangle inequality conditions are: +- (1-X) + YX > (1-Y)X +- (1-X) + (1-Y)X > YX +- YX + (1-Y)X > (1-X) + +Simplifying: +- (1-X) + YX > (1-Y)X +- (1-X) + (1-Y)X > YX +- X > (1-X) (since YX + (1-Y)X = X) + +The third condition is always true when X > 0.5 (which is our assumption for Case 2). + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +Thought 5: Continuing with Case 2 (X > 0.5), let's simplify the remaining triangle inequality conditions: + +First condition: +(1-X) + YX > (1-Y)X +(1-X) + YX > X - YX +(1-X) + 2YX > X +1 - X + 2YX > X +1 + 2YX > 2X +2YX > 2X - 1 +Y > (2X - 1)/(2X) + +Second condition: +(1-X) + (1-Y)X > YX +(1-X) + X - YX > YX +(1-X) + X > 2YX +1 > 2YX +1/(2X) > Y + +So for Case 2, we need: +- Y > (2X - 1)/(2X) +- Y < 1/(2X) + +This means Y must be in the range ((2X - 1)/(2X), 1/(2X)). For this range to be valid, we need: +(2X - 1)/(2X) < 1/(2X) +2X - 1 < 1 +2X < 2 +X < 1 + +Which is true for our domain. So we have a valid range for Y when X > 0.5. + +Now I need to calculate the probability. For both cases, I need to find the area of the region in the X-Y plane where the triangle can be formed, divided by the total area. + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +Thought 6: Now I need to calculate the probability by finding the area in the X-Y plane where the triangle can be formed. + +For Case 1 (X ≤ 0.5): +- Y must be between (1 - 2X)/(2(1-X)) and 1/(2(1-X)) + +The probability for this case is the area of the region divided by the total area of the X-Y plane for X ≤ 0.5, which is 0.5 × 1 = 0.5. + +Area = ∫₀^(0.5) [1/(2(1-X)) - (1 - 2X)/(2(1-X))] dX + = ∫₀^(0.5) [(1 - (1 - 2X))/(2(1-X))] dX + = ∫₀^(0.5) [2X/(2(1-X))] dX + = ∫₀^(0.5) [X/(1-X)] dX + +This integral equals -X - ln(1-X) evaluated from 0 to 0.5, which is: +[-0.5 - ln(0.5)] - [0 - ln(1)] = -0.5 - (-0.693) = -0.5 + 0.693 = 0.193 + +So the probability for Case 1 is 0.193/0.5 = 0.386. + +For Case 2 (X > 0.5): +- Y must be between (2X - 1)/(2X) and 1/(2X) + +The probability for this case is the area of the region divided by the total area of the X-Y plane for X > 0.5, which is 0.5 × 1 = 0.5. + +Area = ∫₀.₅^1 [1/(2X) - (2X - 1)/(2X)] dX + = ∫₀.₅^1 [(1 - (2X - 1))/(2X)] dX + = ∫₀.₅^1 [(2 - 2X)/(2X)] dX + = ∫₀.₅^1 [(1 - X)/X] dX + = ∫₀.₅^1 [1/X - 1] dX + = [ln|X| - X]₀.₅^1 + = [ln(1) - 1] - [ln(0.5) - 0.5] = -1 - (-0.693 - 0.5) = -1 + 0.693 + 0.5 = 0.193 + +So the probability for Case 2 is also 0.193/0.5 = 0.386. + +The total probability is the sum of the probabilities for both cases: 0.386 + 0.386 = 0.772. + +Wait, I should double-check my calculations. Let me reconsider the integrals. + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +Thought 7: Let me double-check the calculations for both cases more carefully. + +For Case 1 (X ≤ 0.5), the constraints are: +- Y > (1 - 2X)/(2(1-X)) +- Y < 1/(2(1-X)) + +The area for which the triangle can be formed is: +∫₀^(0.5) [1/(2(1-X)) - (1 - 2X)/(2(1-X))] dX + +Simplifying the integrand: +[1 - (1 - 2X)]/(2(1-X)) = 2X/(2(1-X)) = X/(1-X) + +So we have: +∫₀^(0.5) X/(1-X) dX + +This can be rewritten as: +∫₀^(0.5) [1/(1-X) - 1] dX = ∫₀^(0.5) 1/(1-X) dX - ∫₀^(0.5) 1 dX += -ln|1-X|₀^(0.5) - X₀^(0.5) += -ln(0.5) - 0.5 - (-ln(1) - 0) += -ln(0.5) - 0.5 += 0.693 - 0.5 += 0.193 + +So the probability for Case 1 is 0.193/0.5 = 0.386. + +For Case 2 (X > 0.5), the constraints are: +- Y > (2X - 1)/(2X) +- Y < 1/(2X) + +The area for which the triangle can be formed is: +∫₀.₅^1 [1/(2X) - (2X - 1)/(2X)] dX + +Simplifying the integrand: +[1 - (2X - 1)]/(2X) = (2 - 2X)/(2X) = (1 - X)/X + +So we have: +∫₀.₅^1 (1 - X)/X dX = ∫₀.₅^1 (1/X - 1) dX += ln|X|₀.₅^1 - X₀.₅^1 += ln(1) - ln(0.5) - (1 - 0.5) += 0 - (-0.693) - 0.5 += 0.693 - 0.5 += 0.193 + +So the probability for Case 2 is also 0.193/0.5 = 0.386. + +The total probability is the sum of both: 0.386 + 0.386 = 0.772. + +This confirms our previous calculation. However, I need to make sure this is correct by thinking about it differently. The total area in the X-Y plane is 1 (as both X and Y range from 0 to 1). The area where triangles can form is 0.193 + 0.193 = 0.386. So the probability should be 0.386/1 = 0.386, not 0.772. + +Wait, that's not right. I need to be more careful about the sample space... + +Next thought needed: True +Total thoughts estimate: 8 +-------------------------------------------------- + +=== FINAL SOLUTION === +Thought 8: I need to clarify my understanding of the problem and the probability calculation. + +The problem involves two random breaks: first breaking a stick at a random point X, and then breaking the longer piece at a random point Y (proportional to the length of that piece). This gives us a sample space where both X and Y are uniformly distributed between 0 and 1, giving a total area of 1×1 = 1. + +I've calculated the areas where triangles can form in two cases: +- Case 1 (X ≤ 0.5): Area = 0.193 +- Case 2 (X > 0.5): Area = 0.193 + +The total area where triangles can form is 0.193 + 0.193 = 0.386. + +Since the total sample space has area 1, the probability is 0.386/1 = 0.386. + +Wait - I see my mistake in Thought 6 and 7. I incorrectly divided by 0.5 (the range of X in each case), but I should divide by the total area of the sample space, which is 1. + +So the final probability is 0.386, or approximately 25/65 ≈ 0.385. + +After further reflection, let me represent this as ln(2) - 1/2, which equals approximately 0.693 - 0.5 = 0.193 for each case, giving a total probability of 2(ln(2) - 1/2) = 2ln(2) - 1 ≈ 0.386. + +Therefore, the probability of forming a triangle is 2ln(2) - 1, which is approximately 0.386 or about 39%. + +====================== +``` + +> Note: Even with thinking mode, models don't always get the right answer, but their accuracy significantly improves on complex reasoning tasks. \ No newline at end of file diff --git a/cookbook/pocketflow-chain-of-thought/design.md b/cookbook/pocketflow-chain-of-thought/design.md new file mode 100644 index 0000000..727e776 --- /dev/null +++ b/cookbook/pocketflow-chain-of-thought/design.md @@ -0,0 +1,53 @@ +# Chain of Thought Node + +## 1. Requirements +Create a self-looping Chain of Thought node that can: +- Generate thoughts to solve a problem step by step +- Revise previous thoughts when necessary +- Branch to explore alternative approaches +- Track thought numbers and adjust total thoughts dynamically +- Generate and verify hypotheses +- Provide a final solution when reasoning is complete + +## 2. Flow Design +This will be a simple flow with a single node that can call itself repeatedly: + +```mermaid +flowchart LR + cot[ChainOfThoughtNode] -->|"continue"| cot +``` + +## 3. Utilities +We'll need one primary utility function: +- `call_llm`: Call LLM to generate the next thought based on the problem and previous thoughts + +## 4. Node Design +### Shared Store Design +```python +shared = { + "problem": "The problem statement goes here", + "thoughts": [], # List of thought objects + "current_thought_number": 0, + "total_thoughts_estimate": 5, # Initial estimate, can change + "solution": None # Final solution when complete +} +``` + +Each thought in the "thoughts" list will be a dictionary with: +```python +{ + "content": "The actual thought text", + "thought_number": 1, + "is_revision": False, + "revises_thought": None, + "branch_from_thought": None, + "branch_id": None, + "next_thought_needed": True +} +``` + +### Chain of Thought Node +- `type`: Regular (self-looping) +- `prep`: Read the problem and all thoughts so far from shared store +- `exec`: Call LLM to generate next thought or solution +- `post`: Update shared store with the new thought and decide whether to continue or finish \ No newline at end of file diff --git a/cookbook/pocketflow-chain-of-thought/flow.py b/cookbook/pocketflow-chain-of-thought/flow.py new file mode 100644 index 0000000..af2404a --- /dev/null +++ b/cookbook/pocketflow-chain-of-thought/flow.py @@ -0,0 +1,13 @@ +from pocketflow import Flow +from nodes import ChainOfThoughtNode + +def create_chain_of_thought_flow(): + # Create a ChainOfThoughtNode + cot_node = ChainOfThoughtNode() + + # Connect the node to itself for the "continue" action + cot_node - "continue" >> cot_node + + # Create the flow + cot_flow = Flow(start=cot_node) + return cot_flow \ No newline at end of file diff --git a/cookbook/pocketflow-chain-of-thought/main.py b/cookbook/pocketflow-chain-of-thought/main.py new file mode 100644 index 0000000..9b604d6 --- /dev/null +++ b/cookbook/pocketflow-chain-of-thought/main.py @@ -0,0 +1,33 @@ +import sys +from flow import create_chain_of_thought_flow + +def main(): + # Default question + default_question = "Break a stick, then break the longer piece again. What's the probability of forming a triangle?" + + # Get question from command line if provided with -- + question = default_question + for arg in sys.argv[1:]: + if arg.startswith("--"): + question = arg[2:] + break + + print(f"🤔 Processing question: {question}") + + # Create the flow + cot_flow = create_chain_of_thought_flow() + + # Set up shared state + shared = { + "problem": question, + "thoughts": [], + "current_thought_number": 0, + "total_thoughts_estimate": 10, + "solution": None + } + + # Run the flow + cot_flow.run(shared) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/cookbook/pocketflow-chain-of-thought/nodes.py b/cookbook/pocketflow-chain-of-thought/nodes.py new file mode 100644 index 0000000..6f07394 --- /dev/null +++ b/cookbook/pocketflow-chain-of-thought/nodes.py @@ -0,0 +1,116 @@ +from pocketflow import Node +import yaml +from utils import call_llm + +class ChainOfThoughtNode(Node): + def prep(self, shared): + # Gather problem and previous thoughts + problem = shared.get("problem", "") + thoughts = shared.get("thoughts", []) + current_thought_number = shared.get("current_thought_number", 0) + # Increment the current thought number in the shared store + shared["current_thought_number"] = current_thought_number + 1 + total_thoughts_estimate = shared.get("total_thoughts_estimate", 5) + + # Format previous thoughts + thoughts_text = "\n".join([ + f"Thought {t['thought_number']}: {t['content']}" + + (f" (Revision of Thought {t['revises_thought']})" if t.get('is_revision') and t.get('revises_thought') else "") + + (f" (Branch from Thought {t['branch_from_thought']}, Branch ID: {t['branch_id']})" + if t.get('branch_from_thought') else "") + for t in thoughts + ]) + + return { + "problem": problem, + "thoughts_text": thoughts_text, + "thoughts": thoughts, + "current_thought_number": current_thought_number + 1, + "total_thoughts_estimate": total_thoughts_estimate + } + + def exec(self, prep_res): + problem = prep_res["problem"] + thoughts_text = prep_res["thoughts_text"] + current_thought_number = prep_res["current_thought_number"] + total_thoughts_estimate = prep_res["total_thoughts_estimate"] + + # Create the prompt for the LLM + prompt = f""" +You are solving a hard problem using Chain of Thought reasoning. Think step-by-step. + +Problem: {problem} + +Previous thoughts: +{thoughts_text if thoughts_text else "No previous thoughts yet."} + +Please generate the next thought (Thought {current_thought_number}). You can: +1. Continue with the next logical step +2. Revise a previous thought if needed +3. Branch into a new line of thinking +4. Generate a hypothesis if you have enough information +5. Verify a hypothesis against your reasoning +6. Provide a final solution if you've reached a conclusion + +Current thought number: {current_thought_number} +Current estimate of total thoughts needed: {total_thoughts_estimate} + +Format your response as a YAML structure with these fields: +- content: Your thought content +- next_thought_needed: true/false (true if more thinking is needed) +- is_revision: true/false (true if revising a previous thought) +- revises_thought: null or number (if is_revision is true) +- branch_from_thought: null or number (if branching from previous thought) +- branch_id: null or string (a short identifier for this branch) +- total_thoughts: number (your updated estimate if changed) + +Only set next_thought_needed to false when you have a complete solution and the content explains the solution. +Output in YAML format: +```yaml +content: | + # If you have a complete solution, explain the solution here. + # If it's a revision, provide the updated thought here. + # If it's a branch, provide the new thought here. +next_thought_needed: true/false +is_revision: true/false +revises_thought: null or number +branch_from_thought: null or number +branch_id: null or string +total_thoughts: number +```""" + + response = call_llm(prompt) + yaml_str = response.split("```yaml")[1].split("```")[0].strip() + thought_data = yaml.safe_load(yaml_str) + + # Add thought number + thought_data["thought_number"] = current_thought_number + return thought_data + + + def post(self, shared, prep_res, exec_res): + # Add the new thought to the list + if "thoughts" not in shared: + shared["thoughts"] = [] + + shared["thoughts"].append(exec_res) + + # Update total_thoughts_estimate if changed + if "total_thoughts" in exec_res and exec_res["total_thoughts"] != shared.get("total_thoughts_estimate", 5): + shared["total_thoughts_estimate"] = exec_res["total_thoughts"] + + # If we're done, extract the solution from the last thought + if exec_res.get("next_thought_needed", True) == False: + shared["solution"] = exec_res["content"] + print("\n=== FINAL SOLUTION ===") + print(exec_res["content"]) + print("======================\n") + return "end" + + # Otherwise, continue the chain + print(f"\n{exec_res['content']}") + print(f"Next thought needed: {exec_res.get('next_thought_needed', True)}") + print(f"Total thoughts estimate: {shared.get('total_thoughts_estimate', 5)}") + print("-" * 50) + + return "continue" # Continue the chain \ No newline at end of file diff --git a/cookbook/pocketflow-chain-of-thought/requirements.txt b/cookbook/pocketflow-chain-of-thought/requirements.txt new file mode 100644 index 0000000..bf3b8c2 --- /dev/null +++ b/cookbook/pocketflow-chain-of-thought/requirements.txt @@ -0,0 +1,2 @@ +pocketflow>=0.0.1 +anthropic>=0.15.0 # For Claude API access \ No newline at end of file diff --git a/cookbook/pocketflow-chain-of-thought/utils.py b/cookbook/pocketflow-chain-of-thought/utils.py new file mode 100644 index 0000000..96dd0ae --- /dev/null +++ b/cookbook/pocketflow-chain-of-thought/utils.py @@ -0,0 +1,20 @@ +from anthropic import Anthropic +import os + +def call_llm(prompt): + client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key")) + response = client.messages.create( + model="claude-3-7-sonnet-20250219", + max_tokens=1000, + messages=[ + {"role": "user", "content": prompt} + ] + ) + return response.content[0].text + +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}") \ No newline at end of file