Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: PrefectHQ/ControlFlow
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.9.2
Choose a base ref
...
head repository: PrefectHQ/ControlFlow
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Loading
Showing with 12,873 additions and 2,033 deletions.
  1. +17 −0 .github/ai-labeler.yml
  2. +8 −2 .github/labeler.yml
  3. +21 −0 .github/workflows/ai-labeler.yml
  4. +1 −0 .gitignore
  5. +1 −1 README.md
  6. +35 −5 docs/concepts/agents.mdx
  7. +7 −8 docs/concepts/flows.mdx
  8. +71 −18 docs/concepts/tasks.mdx
  9. +1 −2 docs/examples/call-routing.mdx
  10. +102 −0 docs/examples/features/early-termination.mdx
  11. +101 −0 docs/examples/features/memory.mdx
  12. +2 −7 docs/examples/features/multi-llm.mdx
  13. +1 −1 docs/examples/features/private-flows.mdx
  14. +1 −1 docs/examples/features/tools.mdx
  15. +2 −3 docs/examples/headline-categorization.mdx
  16. +0 −18 docs/examples/library.mdx
  17. +2 −3 docs/examples/named-entity-recognition.mdx
  18. +127 −0 docs/examples/seinfeld-conversation.mdx
  19. +2 −3 docs/examples/sentiment-classifier.mdx
  20. +50 −55 docs/guides/{llms.mdx → configure-llms.mdx}
  21. +3 −2 docs/guides/default-agent.mdx
  22. +62 −0 docs/guides/default-memory.mdx
  23. +1 −1 docs/guides/settings.mdx
  24. +1 −1 docs/installation.mdx
  25. 0 docs/{examples → llm-guides}/examples-guide.md
  26. +427 −0 docs/llm-guides/llm-guide.md
  27. 0 docs/{style_guide.mdx → llm-guides/style-guide.md}
  28. +11 −5 docs/mint.json
  29. +283 −0 docs/patterns/memory.mdx
  30. +123 −28 docs/patterns/running-tasks.mdx
  31. +252 −0 docs/patterns/streaming.mdx
  32. +82 −12 docs/patterns/task-results.mdx
  33. +2 −2 docs/patterns/tools.mdx
  34. +6 −10 docs/quickstart.mdx
  35. +9 −0 docs/snippets/version-badge.mdx
  36. +17 −0 docs/style.css
  37. +1 −1 docs/welcome.mdx
  38. +30 −0 examples/anonymization.py
  39. +55 −0 examples/asyncpg-memory.py
  40. +0 −38 examples/business_headline_sentiment.py
  41. +78 −0 examples/call_routing.py
  42. +0 −16 examples/choose_a_number.py
  43. +31 −0 examples/code_explanation.py
  44. +0 −96 examples/controlflow_docs.py
  45. +52 −0 examples/early_termination.py
  46. +0 −76 examples/engineer/engineer.py
  47. +0 −37 examples/engineer/instructions.md
  48. +26 −0 examples/generate_people.py
  49. +24 −0 examples/headline_categorization.py
  50. +75 −0 examples/language_tutor.py
  51. +37 −0 examples/memory.py
  52. +0 −20 examples/memory_between_flows.py
  53. +47 −0 examples/named_entity_recognition.py
  54. +47 −0 examples/pg-memory.py
  55. +33 −0 examples/pineapple_pizza.py
  56. +0 −43 examples/poem.py
  57. +30 −0 examples/private_flows.py
  58. +118 −0 examples/reasoning.py
  59. +0 −34 examples/restaurant_recs.py
  60. +35 −0 examples/rock_paper_scissors.py
  61. +11 −2 examples/{multi_agent_conversation.py → seinfeld.py}
  62. +29 −0 examples/sentiment_classifier.py
  63. +16 −0 examples/slackbot/Dockerfile
  64. 0 examples/slackbot/__init__.py
  65. +156 −0 examples/slackbot/agents.py
  66. +54 −0 examples/slackbot/custom_types.py
  67. BIN examples/slackbot/diagram.png
  68. +61 −0 examples/slackbot/main.py
  69. +70 −0 examples/slackbot/moderation.py
  70. +4 −0 examples/slackbot/requirements.txt
  71. +15 −0 examples/slackbot/settings.py
  72. +63 −0 examples/slackbot/tools.py
  73. +38 −0 examples/standardize_addresses.py
  74. +40 −0 examples/summarization.py
  75. +0 −34 examples/task_dag.py
  76. +0 −34 examples/teacher_student.py
  77. +25 −0 examples/translation.py
  78. +0 −30 examples/write_and_critique_paper.py
  79. +19 −8 pyproject.toml
  80. +5 −2 src/controlflow/__init__.py
  81. +0 −1 src/controlflow/agents/__init__.py
  82. +118 −60 src/controlflow/agents/agent.py
  83. +0 −99 src/controlflow/agents/memory.py
  84. +10 −3 src/controlflow/cli/dev.py
  85. +73 −53 src/controlflow/decorators.py
  86. +16 −7 src/controlflow/defaults.py
  87. +8 −4 src/controlflow/events/base.py
  88. +140 −27 src/controlflow/events/events.py
  89. +23 −18 src/controlflow/events/history.py
  90. +27 −17 src/controlflow/events/message_compiler.py
  91. +18 −9 src/controlflow/events/orchestrator_events.py
  92. +33 −0 src/controlflow/events/task_events.py
  93. +35 −27 src/controlflow/flows/flow.py
  94. +1 −1 src/controlflow/flows/graph.py
  95. 0 src/controlflow/handlers/__init__.py
  96. +24 −0 src/controlflow/handlers/callback_handler.py
  97. +402 −0 src/controlflow/handlers/print_handler.py
  98. +56 −0 src/controlflow/handlers/queue_handler.py
  99. +31 −12 src/controlflow/llm/models.py
  100. +32 −7 src/controlflow/llm/rules.py
  101. +2 −0 src/controlflow/memory/__init__.py
  102. +149 −0 src/controlflow/memory/async_memory.py
  103. +183 −0 src/controlflow/memory/memory.py
  104. 0 src/controlflow/memory/providers/__init__.py
  105. +77 −0 src/controlflow/memory/providers/chroma.py
  106. +74 −0 src/controlflow/memory/providers/lance.py
  107. +457 −0 src/controlflow/memory/providers/postgres.py
  108. +1 −0 src/controlflow/orchestration/__init__.py
  109. +166 −0 src/controlflow/orchestration/conditions.py
  110. +53 −10 src/controlflow/orchestration/handler.py
  111. +387 −203 src/controlflow/orchestration/orchestrator.py
  112. +0 −204 src/controlflow/orchestration/print_handler.py
  113. +19 −1 src/controlflow/orchestration/prompt_templates.py
  114. +1 −1 src/controlflow/orchestration/prompt_templates/flow.jinja
  115. +1 −1 src/controlflow/orchestration/prompt_templates/instructions.jinja
  116. +9 −0 src/controlflow/orchestration/prompt_templates/llm_instructions.jinja
  117. +12 −0 src/controlflow/orchestration/prompt_templates/memories.jinja
  118. +7 −1 src/controlflow/orchestration/prompt_templates/task.jinja
  119. +25 −10 src/controlflow/orchestration/prompt_templates/tasks.jinja
  120. +9 −9 src/controlflow/orchestration/turn_strategies.py
  121. +1 −1 src/controlflow/plan.py
  122. +0 −216 src/controlflow/planning/auto_tasks.py
  123. +91 −39 src/controlflow/run.py
  124. +67 −21 src/controlflow/settings.py
  125. +118 −0 src/controlflow/stream.py
  126. +248 −69 src/controlflow/tasks/task.py
  127. +39 −24 src/controlflow/tools/tools.py
  128. +30 −0 src/controlflow/utilities/general.py
  129. +23 −7 src/controlflow/utilities/testing.py
  130. +48 −8 tests/agents/test_agents.py
  131. +0 −20 tests/conftest.py
  132. +55 −22 tests/events/test_history.py
  133. +43 −2 tests/fixtures/controlflow.py
  134. 0 tests/llm/__init__.py
  135. +72 −0 tests/llm/test_models.py
  136. 0 tests/memory/__init__.py
  137. +46 −0 tests/memory/test_memory.py
  138. +161 −74 tests/orchestration/test_orchestrator.py
  139. +61 −0 tests/orchestration/test_rules.py
  140. +0 −45 tests/tasks/test_decorator.py
  141. +202 −21 tests/tasks/test_tasks.py
  142. +167 −0 tests/test_decorator.py
  143. +2 −1 tests/test_defaults.py
  144. +250 −4 tests/test_run.py
  145. +4 −1 tests/test_settings.py
  146. +3 −3 tests/tools/test_lc_tools.py
  147. +72 −5 tests/tools/test_tools.py
  148. +41 −0 tests/utilities/test_general.py
  149. +7 −6 tests/utilities/test_testing.py
  150. +5,187 −0 uv.lock
17 changes: 17 additions & 0 deletions .github/ai-labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
labels:
- bug
- breaking change
- documentation
- feature
- enhancement
- question
- example
- good first issue
- tests
- ignore in release notes:
description: "Ignore this PR in release notes"
instructions: |
Administrative changes that don't need to be mentioned in release notes,
such as CI configuration changes. Small changes, including typos, SHOULD
be kept in the release notes; only changes that aren't relevant to
behavior should be ignored.
10 changes: 8 additions & 2 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
documentation:
- changed-files:
- any-glob-to-any-file: "docs/*"
- any-glob-to-any-file: "docs/**"

example:
- changed-files:
- any-glob-to-any-file:
- "examples/**"
- "docs/examples/**"

tests:
- changed-files:
- any-glob-to-any-file: "tests/*"
- any-glob-to-any-file: "tests/**"
21 changes: 21 additions & 0 deletions .github/workflows/ai-labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: AI Labeler

on:
issues:
types: [opened, reopened]
pull_request:
types: [opened, reopened]

jobs:
ai-labeler:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: jlowin/ai-labeler@v0.4.0
with:
include-repo-labels: true
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -174,3 +174,4 @@ cython_debug/
src/controlflow/_version.py
all_code.md
all_docs.md
llm_guides.md
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -54,7 +54,7 @@ Next, configure your LLM provider. ControlFlow's default provider is OpenAI, whi
export OPENAI_API_KEY=your-api-key
```

To use a different LLM provider, [see the LLM configuration docs](https://controlflow.ai/guides/llms).
To use a different LLM provider, [see the LLM configuration docs](https://controlflow.ai/guides/configure-llms).


## Workflow Example
40 changes: 35 additions & 5 deletions docs/concepts/agents.mdx
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ description: The intelligent workers in your AI workflows.
icon: robot
---

import { VersionBadge } from '/snippets/version-badge.mdx'

Agents are the intelligent, autonomous entities that power your AI workflows in ControlFlow. They represent AI models capable of understanding instructions, making decisions, and completing tasks.

```python
@@ -31,7 +33,6 @@ A more complex agent can be created by providing additional configuration. This

```python
import controlflow as cf
from langchain_openai import ChatOpenAI

agent = cf.Agent(
name="Data Analyst",
@@ -41,7 +42,7 @@ agent = cf.Agent(
"Browse the web for data and use Python to analyze it."
),
tools=[cf.tools.web.get_url, cf.tools.code.python],
model=ChatpOpenAI('gpt-4o-mini'),
model="openai/gpt-4o",
interactive=True,
)
```
@@ -68,7 +69,36 @@ Tools are Python functions that the agent can call to perform specific actions o

Each agent has a model, which is the LLM that powers the agent responses. This allows you to choose the most suitable model for your needs, based on factors such as performance, latency, and cost.

ControlFlow supports any LangChain LLM that supports chat and function calling. For more details on how to configure models, see the [LLMs guide](/guides/llms).
ControlFlow supports any LangChain LLM that supports chat and function calling. For more details on how to configure models, see the [LLMs guide](/guides/configure-llms).

```python
import controlflow as cf


agent1 = cf.Agent(model="openai/gpt-4o")
agent2 = cf.Agent(model="anthropic/claude-3-5-sonnet-20240620")
```

### LLM rules
<VersionBadge version="0.11.0" />

Each LLM provider may have different requirements for how messages are formatted or presented. For example, OpenAI permits system messages to be interspersed between user messages, but Anthropic requires them to be at the beginning of the conversation. ControlFlow uses provider-specific rules to properly compile messages for each agent's API.

For common providers like OpenAI and Anthropic, LLM rules can be automatically inferred from the agent's model. However, you can use a custom `LLMRules` object to override these rules or provide rules for non-standard providers.

Here is an example of how to tell the agent to use the Anthropic compilation rules with a custom model that can't be automatically inferred:

```python
import controlflow as cf

# note: this is just an example
llm_model = CustomAnthropicModel()

agent = cf.Agent(
model=model,
llm_rules=cf.llm.rules.AnthropicRules(model=model)
)
```

### Interactivity

@@ -196,7 +226,7 @@ temporary positive outcomes, despite the overall bleak and discouraging reality.
</CodeGroup>
When tasks have multiple agents, it's important to understand how they collaborate (and to provide them with clear instructions to guide that behavior). To learn more, see the [collaboration](/patterns/collaboration) doc.
When tasks have multiple agents, it's important to understand how they collaborate (and to provide them with clear instructions to guide that behavior). To learn more, see the [multi-agent collaboration docs](/patterns/running-tasks#multi-agent-collaboration).
#### Assigning completion agents
@@ -221,4 +251,4 @@ task = cf.Task(
# only a1 and a2 can mark the task as successful
completion_agents=[a1, a2],
)
```
```
15 changes: 7 additions & 8 deletions docs/concepts/flows.mdx
Original file line number Diff line number Diff line change
@@ -119,19 +119,18 @@ The following flow properties are inferred from the decorated function:
| ------------- | ------------- |
| `name` | The function's name |
| `description` | The function's docstring |
| `context` | The function's arguments (keyed by argument name) |
| `context` | The function's arguments, if specified as `context_kwargs` (keyed by argument name) |

Additional properties can be set by passing keyword arguments directly to the `@flow` decorator or to the `flow_kwargs` parameter when calling the decorated function.

<Tip>
You may not want the arguments to your flow function to be used as context. In that case, you can set `args_as_context=False` when decorating or calling the function:
To automatically put some of your flow's arguments into the global context that all agents can see, specify `context_kwargs` when decorating your flow:

```python
@cf.flow(args_as_context=False)
def my_flow(secret_var: str):
@cf.flow(context_kwargs=["x"])
def my_flow(x: int, y: int):
# x will be automatically added to a global, agent-visible context
...
```
</Tip>

Additional properties can be set by passing keyword arguments directly to the `@flow` decorator or to the `flow_kwargs` parameter when calling the decorated function.

### The `Flow` object and context manager

89 changes: 71 additions & 18 deletions docs/concepts/tasks.mdx
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ A task in ControlFlow typically consists of:
There are two primary ways to create tasks in ControlFlow: using the `Task` class directly, or using the `@task` decorator.

<Tip>
In practice, you will often use the `cf.run` function to create and run a task in a single step. This is a common operation and accepts all the same arguments as creating a `Task` directly. See [Running Tasks](/concepts/tasks/running-tasks) for more information.
In practice, you will often use the `cf.run` function to create and run a task in a single step. This is a common operation and accepts all the same arguments as creating a `Task` directly. See [Running Tasks](/patterns/running-tasks) for more information.
</Tip>

### Using the `Task` class
@@ -45,7 +45,7 @@ import controlflow as cf
task = cf.Task(
objective="Write a poem about the provided topic",
instructions="Write four lines that rhyme",
context={"topic": "AI"}
context={"topic": "AI"}
)

result = task.run()
@@ -90,11 +90,11 @@ AI, the future, as it softly glows.
The following task properties are inferred directly from the decorated function:

| Task property | Inferred from |
| -------- | ------------------- |
| `name` | The function's name |
| -------- | ------------------- |
| `name` | The function's name |
| `objective` | The function's docstring and return value (if any) |
| `result_type` | The function's return annotation |
| `context` | The function's arguments (keyed by argument name) and return value (keyed as "Additional context") |
| `result_type` | The function's return annotation |
| `context` | The function's arguments (keyed by argument name) and return value (keyed as "Additional context") |

Additional properties can be set by passing keyword arguments directly to the `@task` decorator.

@@ -104,7 +104,7 @@ When creating a task, you can configure various properties to define its behavio

### Objective

The objective of a task is the main goal that the task is working towards. It is used to guide the task's execution and to help agents understand the task's purpose.
The objective of a task is the main goal that the task is working towards. It is used to guide the task's execution and to help agents understand the task's purpose.

The `objective` is the only required task configuration, as it indicates the task's purpose and helps agents understand the task they are working on.

@@ -136,8 +136,8 @@ cf.run("Write a poem... then fail this task.")
```

```text Result
ValueError: Task 04561cda ("Write a poem... then fail this task.")
failed: Task instructed to be marked as failed despite
ValueError: Task 04561cda ("Write a poem... then fail this task.")
failed: Task instructed to be marked as failed despite
no technical error
```
</CodeGroup>
@@ -185,7 +185,7 @@ import controlflow as cf
from pydantic import BaseModel

media = cf.run(
"Star Wars: Return of the Jedi",
"Star Wars: Return of the Jedi",
result_type=["book", "movie", "album"]
)

@@ -197,7 +197,7 @@ movie
```
</CodeGroup>

For more information, see [Result Types](/concepts/tasks/task-results).
For more information, see [Task Results](/patterns/task-results).

### Result validator

@@ -216,13 +216,13 @@ number = cf.run("Choose a number", result_validator=validate_even)
print(number)
```

For more information, see [Result Validators](/concepts/tasks/task-results#result-validators).
For more information, see [Validation](/patterns/task-results#validation).



### Instructions

The instructions of a task are a string that provides detailed instructions for the task. This information is visible to agents during execution, helping them understand the task they are working on.
The instructions of a task are a string that provides detailed instructions for the task. This information is visible to agents during execution, helping them understand the task they are working on.

<Tip>
As a general rule, use the task's `objective` to describe what the task's result should be, and use the `instructions` to provide more detailed instructions on how to achieve the objective.
@@ -235,7 +235,7 @@ As a general rule, use the task's `objective` to describe what the task's result
import controlflow as cf

poem = cf.run(
"Write a poem about AI",
"Write a poem about AI",
instructions="Write only two lines, and end the first line with `not evil`",
)

@@ -264,6 +264,59 @@ task = cf.Task(
)
```

Note that this setting reflects the configuration of the `completion_tools` parameter.

### Completion tools

import { VersionBadge } from '/snippets/version-badge.mdx'

<VersionBadge version="0.10" />

In addition to specifying which agents are automatically given completion tools, you can control which completion tools are generated for a task using the `completion_tools` parameter. This allows you to specify whether you want to provide success and/or failure tools, or even provide custom completion tools.

The `completion_tools` parameter accepts a list of strings, where each string represents a tool to be generated. The available options are:

- `"SUCCEED"`: Generates a tool for marking the task as successful.
- `"FAIL"`: Generates a tool for marking the task as failed.

If `completion_tools` is not specified, both `"SUCCEED"` and `"FAIL"` tools will be generated by default.

You can manually create completion tools and provide them to your agents by calling `task.get_success_tool()` and `task.get_fail_tool()`.

<Warning>
If you exclude `completion_tools`, agents may be unable to complete the task or become stuck in a failure state. Without caps on LLM turns or calls, this could lead to runaway LLM usage. Make sure to manually manage how agents complete tasks if you are using a custom set of completion tools.
</Warning>

Here are some examples:

```python
# Generate both success and failure tools (default behavior, equivalent to `completion_tools=None`)
task = cf.Task(
objective="Write a poem about AI",
completion_tools=["SUCCEED", "FAIL"],
)

# Only generate a success tool
task = cf.Task(
objective="Write a poem about AI",
completion_tools=["SUCCEED"],
)

# Only generate a failure tool
task = cf.Task(
objective="Write a poem about AI",
completion_tools=["FAIL"],
)

# Don't generate any completion tools
task = cf.Task(
objective="Write a poem about AI",
completion_tools=[],
)
```

By controlling which completion tools are generated, you can customize the task completion process to better suit your workflow needs. For example, you might want to prevent agents from marking a task as failed, or you might want to provide your own custom completion tools instead of using the default ones.

### Name

The name of a task is a string that identifies the task within the workflow. It is used primarily for logging and debugging purposes, though it is also shown to agents during execution to help identify the task they are working on.
@@ -279,8 +332,8 @@ The context of a task is a dictionary that provides additional information about
import controlflow as cf

is_spam = cf.run(
"Is this email spam?",
result_type=bool,
"Is this email spam?",
result_type=bool,
context=dict(email='You just won a million dollars!'),
)

@@ -307,7 +360,7 @@ def roll_dice(n_dice: int):

rolls = cf.run(
"Roll 3 dice",
result_type=list[int],
result_type=list[int],
tools=[roll_dice],
)

@@ -386,4 +439,4 @@ An AI's mind begins to sound.
Electric thoughts and data streams,
Crafting worlds and shaping dreams.
```
</CodeGroup>
</CodeGroup>
3 changes: 1 addition & 2 deletions docs/examples/call-routing.mdx
Original file line number Diff line number Diff line change
@@ -17,7 +17,6 @@ As you run this example, you'll see the conversation unfold in real-time, culmin

```python
import random
from enum import Enum
import controlflow as cf

DEPARTMENTS = [
@@ -81,7 +80,7 @@ def routing_flow():
),
agents=[trainee],
result_type=None,
tools=[main_task.create_success_tool()]
tools=[main_task.get_success_tool()]
)

if main_task.result == target_department:
Loading