Skip to content

Commit

Permalink
Add reasoning example
Browse files Browse the repository at this point in the history
  • Loading branch information
jlowin committed Oct 4, 2024
1 parent 7db9a32 commit cb5784a
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 11 deletions.
118 changes: 118 additions & 0 deletions examples/reasoning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
This example implements a reasoning loop that lets a relatively simple model
solve difficult problems.
Here, gpt-4o-mini is used to solve a problem that typically requires o1's
reasoning ability.
"""

import argparse

from pydantic import BaseModel, Field

import controlflow as cf
from controlflow.utilities.general import unwrap


class ReasoningStep(BaseModel):
explanation: str = Field(
description="""
A brief (<5 words) description of what you intend to
achieve in this step, to display to the user.
"""
)
reasoning: str = Field(
description="A single step of reasoning, not more than 1 or 2 sentences."
)
found_validated_solution: bool


@cf.flow
def solve_with_reasoning(goal: str, agent: cf.Agent) -> str:
while True:
response: ReasoningStep = cf.run(
objective="""
Carefully read the `goal` and analyze the problem.
Produce a single step of reasoning that advances you closer to a solution.
""",
instructions="""
You are working on solving a difficult problem (the `goal`). Based
on your previous thoughts and the overall goal, please perform **one
reasoning step** that advances you closer to a solution. Document
your thought process and any intermediate steps you take.
After marking this task complete for a single step, you will be
given a new reasoning task to continue working on the problem. The
loop will continue until you have a valid solution.
Complete the task as soon as you have a valid solution.
**Guidelines**
- You will not be able to brute force a solution exhaustively. You
must use your reasoning ability to make a plan that lets you make
progress.
- Each step should be focused on a specific aspect of the problem,
either advancing your understanding of the problem or validating a
solution.
- You should build on previous steps without repeating them.
- Since you will iterate your reasoning, you can explore multiple
approaches in different steps.
- Use logical and analytical thinking to reason through the problem.
- Ensure that your solution is valid and meets all requirements.
- If you find yourself spinning your wheels, take a step back and
re-evaluate your approach.
""",
result_type=ReasoningStep,
agents=[agent],
context=dict(goal=goal),
model_kwargs=dict(tool_choice="required"),
)

if response.found_validated_solution:
if cf.run(
"""
Check your solution to be absolutely sure that it is correct and meets
all requirements of the goal. If you return True, the loop will end. If you
return False, you will be able to continue reasoning.
""",
result_type=bool,
context=dict(goal=goal),
):
break

return cf.run(objective=goal, agents=[agent])


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Solve a reasoning problem.")
parser.add_argument("--goal", type=str, help="Custom goal to solve", default=None)
args = parser.parse_args()

agent = cf.Agent(name="Definitely not GPT-4o mini", model="openai/gpt-4o-mini")

# Default goal via https://www.reddit.com/r/singularity/comments/1fggo1e/comment/ln3ymsu/
default_goal = """
Using only four instances of the digit 9 and any combination of the following
mathematical operations: the decimal point, parentheses, addition (+),
subtraction (-), multiplication (*), division (/), factorial (!), and square
root (sqrt), create an equation that equals 24.
In order to validate your result, you should test that you have followed the rules:
1. You have used the correct number of variables
2. You have only used 9s and potentially a leading 0 for a decimal
3. You have used valid mathematical symbols
4. Your equation truly equates to 24.
"""

# Use the provided goal if available, otherwise use the default
goal = args.goal if args.goal is not None else default_goal
goal = unwrap(goal)
print(f"The goal is:\n\n{goal}")

result = solve_with_reasoning(goal=goal, agent=agent)

print(f"The solution is:\n\n{result}")
2 changes: 1 addition & 1 deletion src/controlflow/flows/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def add_task(self, task: Task):
)

# add the task's subtasks
for subtask in task._subtasks:
for subtask in task.subtasks:
self.add_edge(
Edge(
upstream=subtask,
Expand Down
10 changes: 6 additions & 4 deletions src/controlflow/orchestration/prompt_templates/task.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
{% if task.instructions %}- instructions: {{ task.instructions }}{% endif %}
{% if task.result_type %}- result type: {{ task.result_type }}{% endif %}
{% if task.context %}- context: {{ task.context }}{% endif %}
- can be completed before subtasks: {{ not task.wait_for_subtasks }}
{% if task.depends_on %}- depends on: {{ task.depends_on | map(attribute='id') | join(', ') }}{% endif %}
{% if task.parent %}- parent task: {{ task.parent.id }}{% endif %}
{% if task._subtasks%}- subtasks: {{ task._subtasks | map(attribute='id') | join(', ') }}{% endif %}
{% if task.parent %}- parent task ID: {{ task.parent.id }}{%endif %}
{% if task._subtasks%}- this task has the following subtask IDs: {{ task._subtasks | map(attribute='id') | join(', ') }}
{% if not task.wait_for_subtasks %}- complete this task as soon as you meet its objective, even if you haven't completed
its subtasks{% endif%}{% endif %}
{% if task.depends_on %}- this task depends on these upstream task IDs (includes subtasks): {{ task.depends_on |
map(attribute='id') | join(', ') }}{% endif %}
14 changes: 13 additions & 1 deletion src/controlflow/orchestration/prompt_templates/tasks.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,16 @@ it.
A task can only be marked complete one time. Do not attempt to mark a task
successful more than once. Even if the `result_type` does not appear to match
the objective, you must supply a single compatible result. Only mark a task
failed if there is a technical error or issue preventing completion.
failed if there is a technical error or issue preventing completion.

When a parent task must wait for subtasks, it means that all of its subtasks are
treated as upstream dependencies and must be completed before the parent task
can be marked as complete. However, if the parent task has
`wait_for_subtasks=False`, then it can and should be marked as complete as soon
as you can, regardless of the status of its subtasks.

## Subtask hierarchy

{% for task in task_hierarchy %}
{{ render_task_hierarchy(task) }}
{% endfor %}
15 changes: 10 additions & 5 deletions src/controlflow/tasks/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ class Task(ControlFlowModel):
)
context: dict = Field(
default_factory=dict,
description="Additional context for the task. If tasks are provided as "
"context, they are automatically added as `depends_on`",
description="Additional context for the task.",
)
parent: Optional["Task"] = Field(
NOTSET,
Expand Down Expand Up @@ -259,9 +258,16 @@ def __eq__(self, other):
if type(self) is type(other):
d1 = dict(self)
d2 = dict(other)

for attr in ["id", "created_at"]:
d1.pop(attr)
d2.pop(attr)

# conver sets to lists for comparison
d1["depends_on"] = list(d1["depends_on"])
d2["depends_on"] = list(d2["depends_on"])
d1["subtasks"] = list(self.subtasks)
d2["subtasks"] = list(other.subtasks)
return d1 == d2
return False

Expand Down Expand Up @@ -375,7 +381,6 @@ def add_subtask(self, task: "Task"):
elif task.parent is not self:
raise ValueError(f"{self.friendly_name()} already has a parent.")
self._subtasks.add(task)
self.depends_on.add(task)

def add_dependency(self, task: "Task"):
"""
Expand Down Expand Up @@ -489,8 +494,8 @@ def is_ready(self) -> bool:
incomplete, meaning it is ready to be worked on.
"""
depends_on = self.depends_on
if not self.wait_for_subtasks:
depends_on = depends_on.difference(self._subtasks)
if self.wait_for_subtasks:
depends_on = depends_on.union(self._subtasks)

return self.is_incomplete() and all(t.is_complete() for t in depends_on)

Expand Down

0 comments on commit cb5784a

Please sign in to comment.