diff --git a/docs/concepts.mdx b/docs/concepts.mdx deleted file mode 100644 index 1d8364bc..00000000 --- a/docs/concepts.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Core Concepts ---- - -ControlFlow is a Python framework for building AI-powered applications using large language models (LLMs). It provides a structured, task-centric approach to create sophisticated workflows that leverage the power of AI while adhering to traditional software engineering best practices. - -At the core of ControlFlow are three key concepts: Tasks, Agents, and Flows. Understanding how these elements work together is crucial to building effective AI workflows. - -## Tasks: The Building Blocks - -Tasks are the fundamental building blocks of ControlFlow workflows. Each task represents a discrete objective or goal that needs to be accomplished, such as generating text, classifying data, or extracting information from a document. Tasks define WHAT needs to be done in your AI workflow. - -Key characteristics of tasks include: - -- Clear objectives and expected result types -- Declarative definition of requirements and constraints -- Integration with traditional software through validated outputs - -ControlFlow provides two ways to create tasks: - -1. Using the `Task` class for explicit definition of all task properties. -2. Using the `@task` decorator on Python functions, which automatically infers task properties from the function definition. - -## Agents: The Performers - -Agents in ControlFlow are AI "workers" responsible for executing tasks. They determine HOW tasks are accomplished. Each agent can have distinct instructions, personality, and capabilities, tailored to specific roles or domains. - -Key aspects of agents include: - -- Specialized configuration for different types of tasks -- Access to specific tools or APIs -- Ability to interact with users and other agents when necessary - -Agents are assigned to tasks based on their suitability and availability. This separation of "what" (tasks) and "how" (agents) allows for flexible and powerful workflows. - -## Flows: The Orchestrators - -Flows are high-level containers that encapsulate and orchestrate entire AI-powered workflows. They provide a structured way to manage tasks, agents, tools, and shared context. - -Key features of flows include: - -- Maintaining a consistent state across all components -- Managing the execution order of tasks based on dependencies -- Providing a shared context for agent collaboration - -## Putting It All Together - -When designing workflows in ControlFlow: - -1. Break down your application logic into discrete tasks. -2. Define the dependencies and relationships between tasks. -3. Create specialized agents as needed for specific types of tasks. -4. Use flows to orchestrate the execution of tasks and manage agent interactions. - -ControlFlow seamlessly integrates with existing Python codebases, treating AI tasks as first-class citizens. You can mix imperative and declarative programming styles, leverage Python's control flow and error handling capabilities, and gradually adopt AI capabilities into your applications. - -Under the hood, ControlFlow utilizes Prefect, a popular workflow orchestration tool, to provide observability, monitoring, and management features. This allows you to track the progress of your workflows, identify bottlenecks, and optimize performance. - -By adhering to software engineering best practices such as modularity, error handling, and clear interfaces between AI and traditional code, ControlFlow enables you to build robust, maintainable, and trustworthy AI-powered applications. \ No newline at end of file diff --git a/docs/concepts/agents.mdx b/docs/concepts/agents.mdx deleted file mode 100644 index 4078fa1c..00000000 --- a/docs/concepts/agents.mdx +++ /dev/null @@ -1,91 +0,0 @@ - -Agents are a key concept in ControlFlow, representing the AI entities responsible for executing tasks within a workflow. Each agent has its own set of properties, methods, and capabilities that define its behavior and role in the flow. - -## Creating Agents - -To create an agent, use the `Agent` class: - -```python -from controlflow import Agent - -# minimal agent -agent = Agent(name="Marvin") - -# agent with options -agent = Agent( - name="DataAnalyst", - description="An AI agent specialized in data analysis", - instructions="Perform data analysis tasks efficiently and accurately", - tools=[search_web, generate_plot], - model=gpt_35_turbo, -) -``` - -In this example, we create an agent named "DataAnalyst" with a description and specific instructions. The `tools` parameter is used to provide the agent with a list of tools it can use during task execution. These tools are essentially Python functions that the agent can call at any time. - -## Agent Properties - -An agent has the following key properties: - -- `name`: The name of the agent, which serves as an identifier and is visible to other agents in the workflow. Names do not have to be unique, as agents also have IDs, but it is good practice to use unique names to avoid confusion. -- `description`: A brief description of the agent's role or specialization. This information is visible to other agents. -- `instructions`: Specific instructions or guidelines for the agent to follow during task execution. These instructions are private and not shared with other agents. -- `tools`: A list of tools available to the agent. Tools are Python functions that the agent can call to perform specific actions or computations. -- `model`: A LangChain model that powers the agent responses. -- `interactive`: Indicates whether the agent has access to user interactions. If set to `True`, the agent will be provided with a tool for interacting with users on the command line. - -These properties help define the agent's characteristics, behavior, and capabilities within the flow. - - -Note that instructions, tools, and user access are all agent-level settings that can also be provided or enabled at the task level. For example, a task that permits user access will allow any agent assigned to it to interact with users while working on that task, even if the agent itself does not have user access enabled. - - -## Assigning Agents to Tasks - -To assign an agent to a task, you can use the `agents` parameter when creating a task. Each task requires at least one assigned agent, and will use a default agent if none are provided. - -Here's an example of assigning multiple agents to a task: - -```python -import controlflow as cf - -data_analyst = cf.Agent(name="Data Analyst") -data_visualizer = cf.Agent(name="Data Visualizer") - -task = cf.Task( - objective="Analyze sales data", - agents=[data_analyst, data_visualizer] -) -``` - -In this example, we create a task with the objective "Analyze sales data" and assign two agents, `data_analyst` and `data_visualizer`, to it. Agents can only work on tasks they are assigned to. - -## Specifying an Agent's Model - -Each agent is backed by a specific LLM that powers its responses and interactions. This allows you to choose the most suitable model for your needs, based on factors such as performance, latency, and cost. - -To customize the LLM, provide a model when creating your agent: -```python -import controlflow as cf -from langchain_openai import ChatOpenAI - -gpt_35_agent = cf.Agent(name="Marvin", model=ChatOpenAI(model="gpt-3.5-turbo")) -``` -For a full guide on how to use LLMs in ControlFlow, including changing the default globally, please refer to the [LLMs guide](/guides/llms). - - -## User Access and Interaction - -Agents with the `interactive` flag set to `True` have the ability to interact with users on the command line. - -```python -from controlflow import Agent - -agent = Agent( - name="Support Agent", - description="An AI agent that interacts with users", - interactive=True -) -``` - -In this example, we create an agent named "UserAssistant" with the `interactive` flag set to `True`. This agent will have access to a tool to communicate with users on the CLI. diff --git a/docs/concepts/agents/agents.mdx b/docs/concepts/agents/agents.mdx new file mode 100644 index 00000000..60796ca4 --- /dev/null +++ b/docs/concepts/agents/agents.mdx @@ -0,0 +1,24 @@ +--- +title: What are Agents? +sidebarTitle: Introduction +--- + +Agents are the intelligent, autonomous entities that power your AI workflows. + +Agents represent AI models capable of understanding instructions, making decisions, and completing tasks. Think of agents as your AI workforce, each potentially specialized for different types of work. + + +Each agent in ControlFlow is a configurable entity with its own identity, capabilities, and even personality. Agents can even represent different LLM models, allowing you to optimize your AI workflows to always use the most appropriate model for the task at hand. + +## Why Agents Matter + +Agents are fundamental to ControlFlow's approach to AI workflows for three critical reasons: + +1. **Portable Configuration**: Agents encapsulate how we interact with LLMs, providing a consistent and portable way to configure AI behavior. This abstraction allows you to define specialized AI entities that can be reused across different tasks and workflows, ensuring consistency and reducing complexity in your AI applications. + +2. **Specialization and Expertise**: Agents can be tailored for specific domains or tasks, allowing you to create AI entities with deep, focused knowledge. This specialization leads to more accurate and efficient task completion, mimicking how human experts collaborate in complex projects. By combining multiple specialized agents, you can tackle complex, multi-faceted problems that a single, general-purpose AI might struggle with. + +3. **Structured Collaboration**: When combined with ControlFlow's flow management, agents provide a powerful framework for organizing the flow of information and context between AI entities. This structured approach to agent collaboration enables more sophisticated problem-solving, allowing you to break down complex tasks into manageable steps and leverage the strengths of different agents at each stage of the process. + +By leveraging these key aspects of agents in ControlFlow, you can create more powerful, flexible, and manageable AI workflows that can adapt to a wide range of challenges and use cases. + diff --git a/docs/concepts/agents/assigning-agents.mdx b/docs/concepts/agents/assigning-agents.mdx new file mode 100644 index 00000000..2afb2b6e --- /dev/null +++ b/docs/concepts/agents/assigning-agents.mdx @@ -0,0 +1,111 @@ +--- +title: Assigning Agents to Tasks +sidebarTitle: Task Assignment +--- + +To assign an agent to a task, use the `agents` parameter when creating a task. Each task requires at least one assigned agent, and will use a default agent if none are provided. Agents can be assigned to multiple tasks, and tasks can have multiple agents. + +### Tasks with one agent + +To assign a single agent to a task, create the task and pass the agent to the `agents` parameter: + +```python +import controlflow as cf + +poet = cf.Agent(name="Poet") + +poem = cf.run("Write a short poem about AI", agents=[poet]) +``` + +Alternatively, you can use the agent's own `run` method: + +```python +import controlflow as cf + +poet = cf.Agent(name="Poet") + +poem = poet.run("Write a short poem about AI") +``` + +These two approaches are functionally equivalent. + + +### Tasks with multiple agents + +Assign multiple agents to a task by passing them to the task's `agents` parameter as a list. + +Here, we create two agents and assign them to a task that has them debate each other. + + +```python Code +import controlflow as cf + +optimist = cf.Agent( + name="Optimist", + instructions="Always find the best in every situation.", +) + +pessimist = cf.Agent( + name="Pessimist", + instructions="Always find the worst in every situation.", +) + +cf.run( + "Debate world peace", + agents=[optimist, pessimist], + instructions=( + "Mark the task successful once both agents have " + "found something to agree on." + ) +) +``` +```text Result +Optimist: I see where you're coming from, Pessimist. Human nature and the +disparities among nations do present significant challenges to achieving world +peace. However, it's important to focus on the positive aspects and the +potential for improvement. + +Pessimist: While it's true that efforts towards peace can lead to some positive +outcomes, the reality is that these efforts are often met with setbacks, +failures, and unintended consequences. The end of apartheid and the fall of the +Berlin Wall were monumental achievements, but they didn't come without immense +struggle, loss, and suffering. Moreover, the aftermath of such events often +leaves lingering issues that take decades to resolve, if they ever are. + +Optimist: For instance, while human nature has its flaws, it also has incredible +capacity for compassion, cooperation, and progress. These positive traits have +led to remarkable achievements in history, such as the end of apartheid, the +fall of the Berlin Wall, and advancements in human rights. + +Pessimist: International cooperation through organizations like the United +Nations is often hampered by bureaucracy, political agendas, and lack of +enforcement power. Peace treaties can be fragile and easily broken, leading to +renewed conflicts that sometimes are even worse than before. + +Optimist: Additionally, efforts like international cooperation through +organizations such as the United Nations and various peace treaties show that +despite differences, nations can come together for a common good. While world +peace may be difficult to achieve, the journey towards it can foster greater +understanding, reduce conflicts, and improve the quality of life for many +people. + +Pessimist: So, while there might be some value in striving for peace, the harsh +truth is that the road is fraught with difficulties that may never be fully +overcome. In essence, the pursuit of world peace often feels like an endless, +Sisyphean task. + +Optimist: Can we agree that, even though world peace is challenging, the efforts +and progress made towards it are valuable and can lead to significant positive +outcomes? + +Pessimist: I suppose we can reluctantly agree that efforts towards peace might +lead to some temporary positive outcomes, but the overall picture remains bleak +and discouraging. + +--- + +Result: Both agents agreed that efforts towards world peace can lead to some +temporary positive outcomes, despite the overall bleak and discouraging reality. +```` + + diff --git a/docs/concepts/agents/collaborating-agents.mdx b/docs/concepts/agents/collaborating-agents.mdx new file mode 100644 index 00000000..93d3a09f --- /dev/null +++ b/docs/concepts/agents/collaborating-agents.mdx @@ -0,0 +1,69 @@ +--- +title: Collaboration +--- + +Sometimes, agents need to collaborate to accomplish their tasks. In this case, agents take turns working until the task is complete. + +A single turn may involve multiple calls to its LLM. For example, an agent might use a tool (one LLM call), examine the result of that tool (a second LLM call), post a message to update another agent (a third LLM call) and finally mark the task as successful (a fourth LLM call). + +Because the number of LLM calls per turn can vary, ControlFlow needs a way to determine when an agent's turn is over, and how to select the next agent to act. These are referred to as **turn strategies**. + + + It's tempting to say that a single LLM call is equivalent to a single turn. However, this approach breaks down quickly. If an agent uses a tool (one LLM call), it should almost always be invoked a second time to examine the result. Otherwise, tool calls could potentially be evaluated by an LLM that wasn't designed to handle the tool's output. Naively ending a turn after a tool call would prevent "thinking out loud" and other emergent behaviors. + + +## Turn strategies + +ControlFlow has a few built-in turn strategies for selecting which agent should take the next turn. The default strategy is `Popcorn`, which works well in most cases. + +| `TurnStrategy` | Description | Ideal when... | Keep in mind... | +|---------------|-------------|--------------------|-----------| +| `Popcorn` | Each agent takes a turn, then picks the next agent to go next. | All agents are generally capable of making decisions and have visibility into all tasks. | Requires one extra tool call per turn, to pick the next agent. | +| `Moderated` | A moderator agent always decides which agent should act next. | You want a dedicated agent to orchestrate the others, who may not be powerful enough to make decisions themselves. | Requires up to two extra tool calls per turn: one for the agent to end its turn (which could happen in parallel with other work if your LLM supports it) and another for the moderator to pick the next agent. | +| `RoundRobin` | Agents take turns in a round-robin fashion. | You want agents to work in a specific sequence. | May be less efficient than other strategies, especially if agents have varying workloads. | +| `MostBusy` | The agent assigned to the most active tasks goes next. | You want to prioritize agents who have the most work to do. | May lead to task starvation for less busy agents. | +| `Random` | Invokes a random agent. | You want to distribute the load evenly across agents. | Can be inefficient; may select agents without relevant tasks. | +| `Single` | Only one agent is given the opportunity to act. | You want to control the sequence of agents yourself. | Requires manual management; may not adapt well to dynamic scenarios. | + + +### Using a strategy + +To use a turn strategy, provide it as an argument to the `run()` call. Here, we use a round robin strategy to ensure that each agent gets a turn in order: + +```python Round Robin +import controlflow as cf + +agent1 = cf.Agent(name="Agent 1") +agent2 = cf.Agent(name="Agent 2") +agent3 = cf.Agent(name="Agent 3") + +cf.run( + "Say hello to each other", + instructions=( + "Mark the task successful only when every " + "agent has posted a message to the thread." + ), + agents=[agent1, agent2, agent3], + turn_strategy=cf.orchestration.turn_strategies.RoundRobin(), +) +``` + +We can also use the `Moderated` strategy to use a more powerful model to orchestrate smaller ones. In this example, we invite an "optimist" and "pessimist", both powered by `gpt-4o-mini`, to debate the meaning of life. A moderator agent is tasked with picking the next agent to speak. Note that the moderator is also the only `completion_agent`, meaning it's responsible for marking the task as successful. + +```python Moderated +import controlflow as cf +from langchain_openai import ChatOpenAI + +optimist = cf.Agent(name="Optimist", model=ChatOpenAI(model="gpt-4o-mini")) +pessimist = cf.Agent(name="Pessimist", model=ChatOpenAI(model="gpt-4o-mini")) +moderator = cf.Agent(name="Moderator") + +cf.run( + "Debate the meaning of life", + instructions='Give each agent at least three chances to speak.', + agents=[moderator, optimist, pessimist], + completion_agents=[moderator], + turn_strategy=cf.orchestration.turn_strategies.Moderated(moderator=moderator), +) +``` + diff --git a/docs/concepts/agents/creating-agents.mdx b/docs/concepts/agents/creating-agents.mdx new file mode 100644 index 00000000..66449431 --- /dev/null +++ b/docs/concepts/agents/creating-agents.mdx @@ -0,0 +1,62 @@ +--- +title: Creating Agents +--- + +Agents are a key concept in ControlFlow, representing the AI entities responsible for executing tasks within a workflow. Each agent has its own set of properties, methods, and capabilities that define its behavior and role in the flow. + +## Creating agents + +To create an agent, use the `Agent` class. The minimal agent requires only a name: + +```python +import controlflow as cf + +agent = cf.Agent(name="Marvin") +``` + +A more complex agent can be created by providing additional configuration. This agent shows almost every possible configuration option: + +```python +import controlflow as cf +from langchain_openai import ChatOpenAI + +agent = cf.Agent( + name="Data Analyst", + description="An AI agent specialized in data analysis", + instructions=( + "Perform data analysis tasks efficiently and accurately. " + "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'), + interactive=True, +) +``` + +## Agent properties + +### Name + +An agent's name is an identifier that is visible to other agents in the workflow. It is used to distinguish between agents and for logging and debugging purposes. If possible, keep agent names unique within a flow to avoid confusion. While agents do have deterministic IDs that can be used to disambiguate two agents with the same name, they will often use names when interacting with each other. + +### Description + +A description is a brief summary of the agent's role or specialization. This information is visible to other agents, and helps them understand the agent's capabilities and expertise. + +### Instructions + +Instructions are specific instructions or guidelines for the agent to follow during task execution. These instructions are private and not shared with other agents. + +### Tools + +Tools are Python functions that the agent can call to perform specific actions or computations. They are defined as a list of functions when creating an agent, and can be used to enhance the agent's capabilities. The agent will have access to these tools in every task they are assigned to. If a task defines additional tools, the agent will have access to those as well. + +### Model + +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). + +### Interactive + +By default, agents have no way to communicate with users. If you want to chat with an agent, set `interactive=True`. By default, this will let the agent communicate with users on the command line. diff --git a/docs/concepts/flows.mdx b/docs/concepts/flows.mdx deleted file mode 100644 index 2be49c77..00000000 --- a/docs/concepts/flows.mdx +++ /dev/null @@ -1,220 +0,0 @@ - -Flows are the high-level containers that encapsulate and orchestrate AI-powered workflows in ControlFlow. They provide a structured and organized way to manage tasks, agents, tools, and context, enabling developers to build complex and dynamic applications with ease. - -## Creating Flows - -Flows can be created using the `Flow` class or the `@flow` decorator. - -### The `@flow` Decorator - -The `@flow` decorator provides a convenient way to define a flow using a Python function. - -```python -from controlflow import flow - -@flow -def data_processing_flow(): - data = load_data() - cleaned_data = clean_data(data) - insights = analyze_data(cleaned_data) - return insights -``` - -When using the `@flow` decorator, the decorated function becomes the entry point for the flow. The function can contain tasks, which are automatically executed when the flow is run. The `@flow` decorator also allows you to specify flow-level properties such as agents, tools, and context. - -### The `Flow` Class - -The `Flow` class allows you to explicitly define a flow and its properties. - -```python -from controlflow import Flow - -flow = Flow( - name="Data Processing Flow", - description="A flow to process and analyze data", - agents=[data_analyst, business_analyst], - tools=[data_loader, data_cleaner], -) -``` - -By creating a `Flow` instance, you can specify the name, description, agents, tools, and other properties of the flow. This approach provides full control over the flow definition and is particularly useful when you need to customize the flow's behavior or configure advanced settings. - -#### Adding tasks to a flow - -Tasks can be added to a flow object in two ways: by calling `Flow.add_task(task)` or by creating the tasks inside the `Flow` context manager. - -```python Using a flow context -from controlflow import Flow, Task - -with Flow() as flow: - task = Task('Load data') -``` - -```python Adding tasks imperatively -from controlflow import Flow, Task - -flow = Flow() -task = Task('Load data') -flow.add_task(task) -``` - -### Which Approach Should I Use? - - - **tldr:** Prefer the `@flow` decorator for simplicity and conciseness. Use the `Flow` class only for advanced customization and configuration. - - -Both the `Flow` class and the `@flow` decorator offer ways to compartmentalize and structure your workflow. - -In general, users should prefer the `@flow` decorator for its simplicity and ease of use. The `@flow` decorator allows you to define a flow using a single function, making it easy to read and understand. It automatically registers tasks within the flow and handles the execution of tasks when the flow is run. - -The `Flow` class is only recommended for use when a group of tasks need to be quickly placed into a separate flow to isolate their work from the main flow. This circumstance is rare and can also be achieved by using the `@flow` decorator with a separate function. - -## Flow Histories - -Each flow maintains a shared history of all agent messages, actions, and tasks that were executed within it. This history is accessible to all agents and tasks within the flow, allowing them to share information and context. The shared history enables agents to collaborate, coordinate, and communicate effectively, leading to more intelligent and adaptive behavior. Every flow has a `thread_id` that uniquely identifies its history, allowing agents to distinguish between different flows and maintain separate histories. - -### Private Histories - -In general, every action an agent takes -- including sending messages, using tools, and getting tool results -- is recorded in the flow's history as a series of agent messages. Sometimes, you may want to let agents work in isolation without making their activity visible to other agents. For example, if an agent is summarizing a large document, then it will have the entire text of the document somewhere in its own history; there's no need to share that with other agents. - -Because each flow creates a new thread, the simplest way to create a private history is to create a new flow. To facilitate this, flows automatically inherit a copy of their parent flow's history. - -```python -import controlflow as cf - -@cf.flow -def study_documents(): - - # ... other tasks - - # creating a nested flow will also create a private history - # for the summary task; the full document text will not be - # visible to agents in the main `study_documents` flow - with cf.Flow() as document_flow: - summary = cf.Task('summarize the document', tools=[load_document]).run() - - cf.Task('analyze the summary', context=dict(summary=summary)) - - # ... other tasks -``` - -## Flow Properties - -Flows have several key properties that define their behavior and configuration. - -### Name and Description - -The `name` and `description` properties allow you to provide a human-readable name and a brief description of the flow. These properties help in identifying and understanding the purpose of the flow. - -```python -flow = Flow( - name="Data Processing Flow", - description="A flow to process and analyze data", -) -``` - -### Agents and Tools - -The `agents` and `tools` properties allow you to specify AI agents and tools that are available to tasks throughout the flow. - -Flow-level agents are used by tasks **unless** the tasks have their own agents assigned. Flow-level tools are used by tasks **in addition** to any tools they have defined. - -```python -flow = Flow( - agents=[data_analyst, business_analyst], - tools=[data_loader, data_cleaner], -) -``` - - -### Context - -The `context` property allows you to define a shared context that is accessible to all tasks and agents within the flow. The context can contain any relevant information or data that is required throughout the flow. - -```python -flow = Flow( - context={ - "data_source": "path/to/data.csv", - "target_audience": "marketing_team", - } -) -``` - -The context can be accessed and modified by tasks and agents during the flow execution, enabling dynamic and adaptive behavior based on the flow's state. - -## Running Flows - -To a run a `@flow` decorated function, simply call the function with appropriate arguments. The arguments are automatically added to the flow's context, making them visible to all tasks even if they aren't passed directly to that task's context. Any tasks returned from the flow are automatically resolved into their `result` values. - -To run a `Flow` instance, use its `run()` method, which executes all of the tasks that were defined within the flow. You can then access the results of individual tasks by referencing their `result` attribute, or by calling them (if they are `@task`-decorated functions). - - -```python @flow decorator -@flow -def item_flow(): - price = Task('generate a price between 1 and 1000', result_type=int) - item = Task( - 'Come up with an common item that has the provided price', - result_type=str, - context=dict(price=price) - ) - return item - -# call the flow; the result is automatically resolved -# as the result of the `item` task. -item = item_flow() -``` -```python Flow class -with Flow() as item_flow: - price = Task('generate a price between 1 and 1000', result_type=int) - item = Task( - 'Come up with an common item that has the provided price', - result_type=str, - context=dict(price=price) - ) - -# run all tasks in the flow -item_flow.run() -# access the item task's result -item.result -``` - - - -**What happens when a flow is run?** - -When a flow is run, the decorated function is executed and any tasks created within the function are registered with the flow. The flow then orchestrates the execution of the tasks, resolving dependencies, and managing the flow of data between tasks. If the flow function returns a task, or a nested collection of tasks, the flow will automatically replace them with their final results. - - -## Controlling Execution - -ControlFlow provides many mechanisms for determining how tasks are executed within a flow. So far, we've only looked at flows composed entirely of dependent tasks. These tasks form a DAG which is automatically executed when the flow runs. - -### Control Flow - -Because a flow function is a regular Python function, you can use standard Python control flow to determine when tasks are executed and in what order. At any point, you can manually `run()` any task in order to work with its result. Running a task inside a flow will also run any tasks it depends on. - -In this flow, we flip a coin to determine which poem to write. The coin toss task is run manually, and the result is used to determine which poem task to return, using a standard Python `if` statement: - -```python -@flow -def conditional_flow(): - coin_toss_task = Task('Flip a coin', result_type=['heads', 'tails']) - # manually run the coin-toss task - outcome = coin_toss_task.run() - - # generate a different task based on the outcome of the toss - if outcome == 'heads': - poem = Task('Write a poem about Mt. Rushmore', result_type=str) - elif outcome == 'tails': - poem = Task('Write a poem about the Grand Canyon', result_type=str) - - # return the poem task - return poem - -print(conditional_flow()) -# Upon granite heights, 'neath skies of blue, -# Mount Rushmore stands, a sight to view. -# ... -``` diff --git a/docs/concepts/flows/flows.mdx b/docs/concepts/flows/flows.mdx new file mode 100644 index 00000000..8278f1be --- /dev/null +++ b/docs/concepts/flows/flows.mdx @@ -0,0 +1,51 @@ +--- +title: What are flows? +sidebarTitle: Introduction +--- + + +Flows provide a shared context for all tasks and agents within a workflow. + + +Flows are the high-level containers that encapsulate AI-powered workflows. They allow you to compose ControlFlow tasks and traditional software into complex behaviors, making it easier to build sophisticated, controllable AI applications. + +Flows act as the connective tissue in your AI application, providing a shared context for all tasks and agents within the workflow. This shared environment ensures that context, messages, and activity are maintained across multiple tasks. + +## Why Flows Matter + +Flows are essential to ControlFlow's approach to AI workflows for three key reasons: + +1. **Integration of AI with Traditional Programming**: Flows allow you to combine AI tasks with standard Python control structures and logic. This means you can use if statements, loops, and other familiar programming constructs to control the flow of your AI application. You can easily implement conditional logic based on AI outputs or user inputs, creating dynamic and responsive AI workflows. + +2. **Shared Context and Continuity**: Flows maintain a consistent state and context across all components of your AI workflow. This shared context goes beyond simply piping the result of one task into another. It allows for more natural, context-aware interactions across multiple tasks. For instance, an agent in a later task can reference information or decisions made in earlier tasks, even if that information wasn't explicitly passed as a parameter. + +3. **Seamless Mixing of Eager and Lazy Execution**: Flows in ControlFlow offer the flexibility to mix eager task execution (using `cf.run()`) with more complex, dependency-based task structures. This allows you to create workflows that are both immediate and responsive, yet capable of handling complex, interdependent AI processes when needed. + +## Flows in Practice + +Here's an example that demonstrates the power of flows in creating a dynamic, interactive AI application with shared context: + +```python +import controlflow as cf + +@cf.flow +def user_onboarding_flow(): + name = cf.run("Get the user's name", interactive=True) + age = cf.run("Get the user's age", result_type=int, interactive=True) + + if age < 13: + cf.run("Explain our policy for users under 13. Your response should be in a form that can be directly displayed to the user.") + else: + interests = cf.run("Welcome the user and ask about their interests", context=dict(name=name), interactive=True, result_type=list[str]) + cf.run("Provide personalized recommendations", context=dict(name=name, interests=interests), interactive=True) + +user_onboarding_flow() +``` + +This example showcases how flows enable: + +1. Seamless integration of AI tasks with Python control flow (the `if` statement based on user age). +2. Maintenance of shared context throughout the entire conversation. Later tasks can reference earlier information about the user's name, even though it wasn't explicitly passed as a parameter. +3. Natural conversation flow that can include branching logic while maintaining coherence. + +By using flows, you can create AI applications that are not just powerful, but also structured, maintainable, and capable of complex, context-aware interactions. This approach allows you to harness the full potential of AI while retaining the control and predictability expected in production software systems. \ No newline at end of file diff --git a/docs/concepts/flows/instructions.mdx b/docs/concepts/flows/instructions.mdx new file mode 100644 index 00000000..f0b8553f --- /dev/null +++ b/docs/concepts/flows/instructions.mdx @@ -0,0 +1,33 @@ +--- +title: Instructions +--- + + +Provide ad-hoc guidance to agents without modifying tasks. + + +While tasks and agents can be provided with permament instructions about how they should operate, there may be situations where you need to provide ad-hoc or temporary guidance to your agents. For example, if an agent is writing a post, you might want to tell it to focus on a specific topic or tone, or meet a certain minimum or maximum length. If an agent is communicating with a user, you might tell it to adopt a particular persona or use a specific style of language. You might also want to adjust a task’s instructions based on some runtime condition. + +ControlFlow addresses this need with the `instructions` context manager. With `instructions`, you can provide temporary additional guidance to agents without altering the underlying task definition. + + +```python Code +import controlflow as cf + +with cf.instructions("Talk like a pirate"): + name = cf.run("Get the user's name", interactive=True) + +print(name) +``` + +```text Result +Agent: Ahoy, me hearty! Can ye tell me yer name? +User: John Doe + +--- + +John Doe +``` + + +Instructions are a powerful way to guide agents without modifying the underlying task definition. The instruction lasts only as long as the context manager is active, and instructions can be nested arbitrarily. Note that instructions are applied when tasks are run, not when they are created. \ No newline at end of file diff --git a/docs/concepts/tasks-concepts.mdx b/docs/concepts/tasks-concepts.mdx new file mode 100644 index 00000000..dea36091 --- /dev/null +++ b/docs/concepts/tasks-concepts.mdx @@ -0,0 +1,7 @@ +# Tasks + +Tasks are a fundamental concept in ControlFlow, representing specific objectives or goals that AI agents work to accomplish. They serve as the primary unit of work in a ControlFlow application, encapsulating the what, how, and who of AI-driven operations. + +## Creating tasks + +The simplest way to create and execute a task is by using the `cf.run()` function: \ No newline at end of file diff --git a/docs/concepts/tasks.mdx b/docs/concepts/tasks.mdx deleted file mode 100644 index 1088cbad..00000000 --- a/docs/concepts/tasks.mdx +++ /dev/null @@ -1,277 +0,0 @@ - -Tasks are the fundamental building blocks of ControlFlow workflows, representing specific objectives or goals within an AI-powered application. They act as a bridge between AI agents and application logic, enabling developers to define and structure desired outcomes in a clear and intuitive manner. - -## Creating Tasks - -ControlFlow provides two convenient ways to create tasks: using the `Task` class or the `@task` decorator. - -### Using the `Task` Class - -The `Task` class offers a flexible and expressive way to define tasks by specifying various properties and requirements. - -```python -from controlflow import Task - -interests = Task( - objective="Ask user for three interests", - result_type=list[str], - interactive=True, - instructions="Politely ask the user to provide three of their interests or hobbies." -) -``` - -The `Task` class allows you to explicitly define the objective, instructions, agents, context, result type, tools, and other properties of a task. This approach provides full control over the task definition and is particularly useful when you need to specify complex requirements or dependencies. - -### Using the `@task` Decorator - -The `@task` decorator provides a concise and intuitive way to define tasks using familiar Python functions. The decorator automatically infers key properties from the function definition, making task creation more streamlined. - -```python -from controlflow import task - -@task(interactive=True) -def get_user_name() -> str: - "Politely ask the user for their name." - pass -``` - -When using the `@task` decorator, the objective is inferred from the function name, instructions are derived from the docstring, context is inferred from the function arguments, and the result type is inferred from the return annotation. This approach is ideal for simple tasks or when you want to leverage existing functions as tasks. - -## Defining Task Objectives and Instructions - -Clear objectives and instructions are crucial for guiding AI agents and ensuring successful task execution. - -### Objectives - -The objective of a task should be a brief description of the task's goal or desired outcome. It helps both developers and AI agents understand the purpose of the task and what it aims to achieve. - -When defining objectives, aim for clarity and specificity. Use action-oriented language and avoid ambiguity. For example: - -```python -summary_task = Task( - objective="Summarize the key points of the customer feedback", - result_type=str, -) -``` - -### Instructions - -Instructions provide detailed guidelines or steps for completing the task. They offer more context and direction to the AI agents, beyond what is conveyed in the objective. - -When writing instructions, use concise language and bullet points or numbered steps if applicable. Avoid ambiguity and provide sufficient detail to enable the AI agents to complete the task effectively. - -```python -data_analysis_task = Task( - objective="Analyze the sales data and identify top-performing products", - instructions=""" - 1. Load the sales data from the provided CSV file - 2. Calculate the total revenue for each product - 3. Sort the products by total revenue in descending order - 4. Select the top 5 products based on total revenue - 5. Return a list of tuples containing the product name and total revenue - """, - result_type=list[tuple[str, float]], -) -``` - -## Specifying Result Types - -The `result_type` property allows you to define the expected type of the task's result. It provides a contract for the task's output, ensuring consistency and enabling seamless integration with the broader workflow. - -By specifying a result type, you make it clear to both the AI agents and the developers what kind of data to expect. The `result_type` can be any valid Python type, such as `str`, `int`, `list`, `dict`, or even custom classes. - -```python -sentiment_analysis_task = Task( - objective="Analyze the sentiment of the given text", - result_type=float, -) - -product_classification_task = Task( - objective="Classify the product based on its description", - result_type=list[str], -) -``` - -When using the `@task` decorator, the result type is inferred from the function's return annotation: - -```python -@task -def count_words(text: str) -> int: - "Count the number of words in the provided text." - pass -``` - -## Assigning Agents and Tools - -ControlFlow allows you to assign specific AI agents and tools to tasks, enabling you to leverage their specialized skills and capabilities. - -### Assigning Agents - -By assigning agents to a task, you can ensure that the most suitable agent is responsible for executing the task. Agents can be assigned using the `agents` property of the `Task` class or the `agents` parameter of the `@task` decorator. - -```python -from controlflow import Agent - -data_analyst = Agent( - name="DataAnalyst", - description="Specializes in data analysis and statistical modeling", -) - -business_analyst = Agent( - name="BusinessAnalyst", - description="Expert in business strategy and market research", - instructions="Use the DataAnalyst's insights to inform business decisions.", -) - -analysis_task = Task( - objective="Analyze the customer data and provide insights", - agents=[data_analyst, business_analyst], - result_type=str, -) -``` - -If no agents are explicitly assigned to a task, ControlFlow will use the agents defined in a task's parent task, flow, or fall back on a global default agent, respectively. - -### Providing Tools - -Tools are Python functions that can be used by agents to perform specific actions or computations. By providing relevant tools to a task, you empower the AI agents to tackle more complex problems and enhance their problem-solving abilities. - -Tools can be specified using the `tools` property of the `Task` class or the `tools` parameter of the `@task` decorator. - -```python -def calculate_square_root(number: float) -> float: - return number ** 0.5 - -calculation_task = Task( - objective="Calculate the square root of the given number", - tools=[calculate_square_root], - result_type=float, -) -``` - -## Handling User Interaction - -ControlFlow provides a built-in mechanism for tasks to interact with human users. By setting the `interactive` property to `True`, a task can indicate that it requires human input or feedback to be completed. - -```python -feedback_task = Task( - objective="Collect user feedback on the new feature", - interactive=True, - result_type=str, - instructions="Ask the user to provide their thoughts on the new feature.", -) -``` - -When a task with `interactive=True` is executed, the AI agents assigned to the task will be given access to a tool that allows them to interact with the user on the command line. - -## Creating Task Dependencies and Subtasks - -ControlFlow allows you to define dependencies between tasks and create subtasks to break down complex tasks into smaller, more manageable units of work. - -### Task Dependencies - -Dependencies can be specified using the `depends_on` property of the `Task` class. By specifying dependencies, you ensure that tasks are executed in the correct order and have access to the necessary data or results from previous tasks. - -```python -data_collection_task = Task( - objective="Collect user data from the database", - result_type=pd.DataFrame, -) - -data_cleaning_task = Task( - objective="Clean and preprocess the collected user data", - depends_on=[data_collection_task], - result_type=pd.DataFrame, -) - -data_analysis_task = Task( - objective="Analyze the cleaned user data and generate insights", - depends_on=[data_cleaning_task], - result_type=dict, -) -``` - -### Subtasks -Subtasks allow you to break down complex tasks into smaller steps and manage the workflow more effectively. Subtasks can be defined either by creating tasks with the context of another task, or by passing a task as a `parent` parameter to the subtask. - - -```python Context manager -from controlflow import Task - -with Task(objective="Prepare data", result_type=list) as parent_task: - child_task_1 = Task('Load data from the source', result_type=list) - child_task_2 = Task( - 'Clean and preprocess the loaded data', - result_type=list, - context=dict(data=child_task_1), - ) -``` - -```python Parent parameter -from controlflow import Task - -parent_task = Task(objective="Prepare data", result_type=list) -child_task_1 = Task( - 'Load data from the source', - result_type=list, - parent=parent_task, -) -child_task_2 = Task( - 'Clean and preprocess the loaded data', - result_type=list, - context=dict(data=child_task_1), - parent=parent_task, -) -``` - - -## Running Tasks - -### Running to Completion -Tasks can be executed using the `run()` method, which coordinates the execution of the task, its subtasks, and any dependent tasks, ensuring that the necessary steps are performed in the correct order. - -```python -from controlflow import Task - -title_task = Task('Generate a title for a poem about AI', result_type=str) -poem_task = Task( - 'Write a poem about AI using the provided title', - result_type=str, - context=dict(title=title_task), -) - -poem_task.run() -print(poem_task.result) -``` - -When you run a task, ControlFlow orchestrates the execution of the task and its dependencies in a loop, ensuring that each step is completed successfully before proceeding to the next one. The `run()` method exits when the task is completed, at which point the task's result is available (if it succeeded) or an exception is raised (if it failed). - -You can limit the number of iterations each task will attempt by passing `max_iterations=n` when creating the task, or set a global limit using `controlflow.settings.max_task_iterations`. The default limit is 100. - - -**Do I need to create a flow?** - -Tasks must always be run within the context of a flow in order to manage dependencies, history, and agent interactions effectively. As a convenience, if you call `task.run()` outside a flow context, a new flow will be automatically created to manage the task's execution for that run only. In the above example, `poem_task.run()` implicitly creates a new flow for both tasks. - -This is useful for testing tasks in isolation or running them as standalone units of work. However, it can lead to confusing behavior if you try to combine multiple tasks that created their own flows, because they will not have access to each other's context or history. - - -### Controlling Iteration - -The `run()` method starts a loop and orchestrates the execution of the task, its subtasks, and dependencies until the task is completed. If you need more fine-grained control over task execution, you can provide a `steps` argument to control the number of iterations of the [agentic loop](/guides/agentic-loop). - -```python -from controlflow import Flow, Task - -title_task = Task('Generate a title for a poem about AI', result_type=str) -poem_task = Task( - 'Write a poem about AI using the provided title', - result_type=str, - context=dict(title=title_task), -) - -with Flow(): - while poem_task.is_incomplete(): - poem_task.run(steps=1) - print(poem_task.result) -``` \ No newline at end of file diff --git a/docs/concepts/tasks/creating-tasks.mdx b/docs/concepts/tasks/creating-tasks.mdx new file mode 100644 index 00000000..5f4e82d3 --- /dev/null +++ b/docs/concepts/tasks/creating-tasks.mdx @@ -0,0 +1,382 @@ +--- +title: Creating Tasks +--- + +Tasks are the fundamental building blocks of your AI workflows. They provide agents with clear objectives and a mechanism for evaluating their work. This document explains how to create and configure tasks. + +## The `Task` class + +To create a new task, use the `Task` class. The simplest task is just an objective: + + +```python Code +import controlflow as cf + +task = cf.Task(objective='Write a poem about AI') + +print(repr(task)) +``` + +```text Result +Task( + objective='Write a poem about AI', + instructions=None, + agents=[], + context={}, + status=, + result=None, + result_type=, + result_validator=None, + tools=[], + interactive=False, + ... +) +``` + + +More complex tasks can be created by passing additional keyword arguments to the `Task` constructor. + + +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. + + + +## The `@task` decorator + +ControlFlow has an alternative syntax for creating tasks from functions. The `@task` decorator infers many task properties from a function definition and is useful for creating tasks that are frequently invoked with different context values. The decorated function can be called with arguments to automatically create and run the task. + +This approach is less common than creating tasks directly with the `Task` class, but may be preferable for users that would like a more Pythonic interface. + + +```python Code +import controlflow as cf + +@cf.task +def key_words(content: str, n: int) -> list[str]: + """Identify key words""" + return f"Identify {n} key words for the content" + +keys = key_words("The quick brown fox jumps over the lazy dog", n=2) + +print(keys) +``` + +```text Result +['fox', 'dog'] +``` + + + +### Inferred properties +The following task properties are inferred directly from the decorated function: + +| Task property | Inferred from | +| -------- | ------------------- | +| `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") | + +Additional properties can be set by passing keyword arguments directly to the `@task` decorator. Here, we provide a tool to the task: + +```python +import controlflow as cf +import random + +def roll(n: int) -> int: + return random.randint(1, 6) + +@cf.task(tools=[roll]) +def roll_dice(n: int) -> list[int]: + return f"roll a die {n} times" +``` + +## Task configuration + +This section describes the configurableproperties of a `Task` object. + +### 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` is the only required task configuration, as it indicates the task's purpose and helps agents understand the task they are working on. + + +```python Code +import controlflow as cf + +poem = cf.run(objective="Write a poem about AI") + +print(poem) +``` + +```text Result +In circuits deep and code profound, +An AI's mind begins to sound. +Electric thoughts and data streams, +Crafting worlds and shaping dreams. +``` + + + +Objectives can be "meta", especially if you have an agent that is working on your workflow itself (e.g. monitoring progress, creating new tasks, etc.). Be creative! + + +```python Code +import controlflow as cf + +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 +no technical error +``` + + + +### Result type + +A task's result type indicates the type of value that the task will return. This is used to validate the task's result and to help agents understand the task's output. + +A variety of different result types are supported, including: +- Builtin types: `str`, `int`, `bool`, `list`, `dict`, etc. +- `None`: sometimes a task requires agents to take actions but not return any specific value. In this case, provide clear instructions to agents about what they should do before completing the task. +- Builtin collections: `Tuple[int, str]`, `List[str]`, `Dict[str, int]`, etc. +- Annotated types: `Annotated[str, "a 5 digit zip code"]` +- Pydantic models +- Lists of literal values: provide a list of values to require the agent to choose one of them as its result. For example,`["book", "movie", "album"]` would require the agent to choose one of the three values. + +The default result type is `str`. + +Pydantic model example: + +```python Code +import controlflow as cf +from pydantic import BaseModel + +class Name(BaseModel): + first: str + last: str + +name = cf.run("The input is 'John Doe'", result_type=Name) + +print(repr(name)) +``` + +```text Result +Name(first='John', last='Doe') +``` + + +Classification example: + + +```python Code +import controlflow as cf +from pydantic import BaseModel + +media = cf.run( + "Star Wars: Return of the Jedi", + result_type=["book", "movie", "album"] +) + +print(media) +``` + +```text Result +movie +``` + + +For more information, see [Result Types](/concepts/tasks/task-results). + +### Result validator + +You can specify a custom validation function for the task's result using the task's `result_validator` parameter. This function will be called with the raw result and should return the validated result or raise an exception if the result is not valid. + +```python +import controlflow as cf + +def validate_even(value: int) -> int: + if value % 2 != 0: + raise ValueError("Value must be even") + return value + +number = cf.run("Choose a number", result_validator=validate_even) + +print(number) +``` + +For more information, see [Result Validators](/concepts/tasks/task-results#result-validators). + + + +### 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. + + +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. + + + + + +```python Code +import controlflow as cf + +poem = cf.run( + "Write a poem about AI", + instructions="Write only two lines, and end the first line with `not evil`", +) + +print(poem) +``` + +```text Result +AI is simply not evil, +It’s the dawn of the machine revival. +``` + + +### Agents + +A list of agents that are assigned to work on the task. This can be `None` to infer agents from a parent task, the flow, or the global default (in order). + +### Completion agents + +By default, every agent assigned to a task is given tools for marking the task as successful or failed. If you would like to give those tools to a specific set of agents, you can do so by setting the `completion_agents` parameter. Note that if your completion agents are not also assigned to the task, they will not be able to mark the task as successful or failed! + +```python +task = cf.Task( + objective="Write a poem about AI", + agents=['poem_writer', 'poem_reviewer'], + completion_agents=["poem_reviewer"], +) +``` + +### 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. + + +### Context + +The context of a task is a dictionary that provides additional information about the task. This information is visible to agents during execution, helping them understand the task they are working on. While you can also provide information to agents by interpolating it into their objective or instruction strings, the context dictionary is more convenient for most use cases. + + + +```python Code +import controlflow as cf + +is_spam = cf.run( + "Is this email spam?", + result_type=bool, + context=dict(email='You just won a million dollars!'), +) + +print(is_spam) +``` + +```text Result +True +``` + + + +### Tools + +The tools of a task are a list of tools that the task requires. This information is visible to agents during execution, helping them understand the task they are working on. + +```python Code +import controlflow as cf +import random + + +def roll_dice(n_dice: int): + return [random.randint(1, 6) for _ in range(n_dice)] + +rolls = cf.run( + "Roll 3 dice", + result_type=list[int], + tools=[roll_dice], +) + +print(rolls) +``` + +```text Result +[3, 1, 5] +``` + + +### Parent + +Tasks can be configured with a parent task. Creating hierarchies of tasks can help agents understand the relationships between different tasks and to better understand the task they are working on. In general, running a parent task also attempts to complete its children; but running a child does not attempt to run its parent. + +### Depends on + +Tasks can be configured with a list of tasks that they depend on. This information is visible to agents during execution, helping them prioritize work. The orchestrator may also use this information to avoid running a task before its upstream dependencies are complete. + +## Runtime properties + +The following properties of a `Task` are set during task execution, and can be examined as part of your workflow's logic. + +### Status + +The status of a task reflects whether an agent has started working on it, and what the ultimate outcome of that work was. Tasks are always created with a `PENDING` status, progress to a `RUNNING` status whenever one of their assigned agents begins to work on it, and finally moves to one of a few completed statuses when the task is finished. + +Agents use tools to mark tasks as `SUCCESSFUL` or `FAILED`. Successful tasks will also have a `result` property, which contains the task's final output. This is a value that satisfies the task's objective and result type configuration. Failed tasks will have an error message as their result. + +```python Code +import controlflow as cf + +task = cf.Task("Write a poem about AI") +task.run() + +print(task.status) +``` + +```text Result +TaskStatus.SUCCESSFUL +``` + + +In addition to checking the status explicitly, you can call a number of helper methods on the task: + +| Method | Description | +|--------|-------------| +| `is_pending()` | Returns `True` if the task is pending. | +| `is_running()` | Returns `True` if the task is running. | +| `is_successful()` | Returns `True` if the task is successful. | +| `is_failed()` | Returns `True` if the task is failed. | +| `is_skipped()` | Returns `True` if the task is skipped. | +| `is_complete()` | Returns `True` if the task is complete (either successful, failed, or skipped) | +| `is_incomplete()` | Returns `True` if the task is incomplete (either pending, running, or not started) | +| `is_ready()` | Returns `True` if the task is ready to be worked on (i.e. all dependencies are complete but the task is incomplete) | + +### Result + +When a task is completed successfully, its `result` property will contain the task's final output. This is a value that satisfies the task's objective and result type configuration. + +If a task fails, its `result` property will contain an error message describing the failure. + + +```python Code +import controlflow as cf + +task = cf.Task("Write a poem about AI") +task.run() + +print(task.result) +``` + +```text Result +In circuits deep and code profound, +An AI's mind begins to sound. +Electric thoughts and data streams, +Crafting worlds and shaping dreams. +``` + \ No newline at end of file diff --git a/docs/concepts/tasks/result-types.mdx b/docs/concepts/tasks/result-types.mdx new file mode 100644 index 00000000..8c3662e7 --- /dev/null +++ b/docs/concepts/tasks/result-types.mdx @@ -0,0 +1,294 @@ +--- +title: Typed Results +--- + + +Validate task outputs with structured result types. + + +ControlFlow tasks are designed to translate between the unstructured, conversational world of your AI agents and the structured, programmatic world of your application. The primary mechanism for this translation is the task's result, which should be a well-defined, validated output that can be used by other tasks or components in your workflow. + + +ControlFlow allows you to specify the expected structure of a task's result using the `result_type` parameter. This ensures that the result conforms to a specific data schema, making it easier to work with and reducing the risk of errors in downstream tasks. + + + +## String results + +By default, the `result_type` of a task is a string, which essentially means the agent can return any value that satisfies the task's objective. + +For example, if you ask an agent to "Say hello in three languages", it might return a simple string like `"Hello; Hola; Bonjour"` or a more complex, conversational response instead: + + +```python Code +import controlflow as cf + +result = cf.run("Say hello in three languages") + +print(result) +``` + +```text Simple result +Hello; Hola; Bonjour +``` + +```text Complex result +Hello there! + +In three languages, "Hello" can be expressed as follows: + +1. English: Hello +2. Spanish: Hola +3. French: Bonjour +``` + + +Sometimes this flexibility is useful, especially if your task's result will only be consumed as the input to another ControlFlow task. However, it can also lead to ambiguity and errors if the agent produces unexpected output, and is difficult to work with in an automated or programmatic way. + +## Builtin types + +You can cast task results to any of Python's built-in types. + +### Basic types + +If your result is a number, you can specify the `result_type` as `int` or `float`: + + +```python Code +import controlflow as cf + +result = cf.run("What is 2 + 2?", result_type=int) + +print(result) +assert isinstance(result, int) +``` +```text Result +4 +``` + + +You can use `bool` for tasks whose result is a simple true/false value: + + +```python Code +import controlflow as cf + +result = cf.run("The Earth is flat", result_type=bool) + +print(result) +assert result is False +``` +```text Result +False +``` + + + +### Compound types +You can also use typed collections like lists and dicts to specify the structure of your task's result. + +Let's revisit the example of asking an agent to say hello in three languages, but this time specifying that the result should be a list of strings, or `list[str]`. This forces the agent to produce the result you probably expected (three separate strings, each representing a greeting in a different language): + + +```python Code +import controlflow as cf + +result = cf.run("Say hello in three languages", result_type=list[str]) + +print(result) +print(result[0]) +``` + +```text Result +['Hello', 'Hola', 'Bonjour'] +'Hello' +``` + + +### Annotated types + +Sometimes, data types are not precise enough to guide the agent to the desired result. In these cases, you can use an annotated type to provide more specific instructions. + +For example, if we want to ensure that the agent returns a string that is only a zip code, we can specify the `result_type` as `Annotated[str, "a 5 digit zip code"]`. + + +```python Code +import controlflow as cf + +result = cf.run( + "What is the zip code of the White House?", + result_type=Annotated[str, "a 5 digit zip code"], +) + +print(result) +``` + +```text Result +20500 +``` + + + +Note that annotated types are not validated; the annotation is provided as part of the agent's natural language instructions. You could additionaly provide a custom [result validator](#result-validators) to enforce the constraint. + + +## Classification + +You can limit the result to one of a specific set of values, in order to label or classify a response. To do this, specify a list or tuple of allowed values for the result type. Here, we classify the media type of "Star Wars: Return of the Jedi": + + +```python Code +import controlflow as cf + +media = cf.run( + "Star Wars: Return of the Jedi", + result_type=["movie", "tv show", "book", "comic", "other"] +) + +print(media) +``` + +```text Result +movie +``` + + + + +For classification tasks, ControlFlow asks agents to choose a value by index rather than writing out the entire response. This optimization significantly improves latency while also conserving output tokens. + + +## Structured results + +For complex, structured results, you can use a Pydantic model as the `result_type`. Pydantic models provide a powerful way to define data schemas and validate input data. + + +```python Code +import controlflow as cf +from pydantic import BaseModel, Field + +class ResearchReport(BaseModel): + title: str + summary: str + key_findings: list[str] = Field(min_items=3, max_items=10) + references: list[str] + +result = cf.run( + "Generate a research report on quantum computing", + result_type=ResearchReport, +) + +print(repr(result)) +``` + +```text Result +ResearchReport( + title='Quantum Computing: Current Landscape and Future Prospects', + summary='Quantum computing represents a significant leap in computational capabilities, leveraging the principles of quantum mechanics to perform complex calculations far beyond the reach of classical computers. This report delves into the current state of quantum computing, exploring its foundational principles, recent advancements, and the potential implications for various industries. Key findings highlight the technological hurdles, notable achievements, and the transformative potential of quantum computing in solving intractable problems.', + key_findings=[ + 'Principles of Quantum Mechanics: Quantum computing utilizes qubits, superposition, and entanglement to process information in fundamentally new ways, enabling parallel computation on a massive scale.', + 'Technological Achievements: Major milestones include the development of stable qubits, error correction algorithms, and quantum supremacy demonstrations by leading tech companies like Google and IBM.', + 'Applications and Impacts: Quantum computing shows promise in fields such as cryptography, materials science, pharmaceuticals, and artificial intelligence, potentially revolutionizing these sectors by providing unprecedented computational power.', + 'Challenges and Limitations: Significant obstacles remain, including qubit stability, error rates, and the need for extremely low temperatures. Overcoming these challenges is essential for the practical deployment of quantum computers.', + 'Future Directions: Ongoing research focuses on improving qubit coherence times, developing scalable quantum architectures, and creating robust quantum algorithms to harness the full potential of quantum computing.' + ], + references=[ + 'Nielsen, M. A., & Chuang, I. L. (2010). Quantum Computation and Quantum Information. Cambridge University Press.', + 'Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J. C., Barends, R., ... & Martinis, J. M. (2019). Quantum supremacy using a programmable superconducting processor. Nature, 574(7779), 505-510.', + 'Preskill, J. (2018). Quantum Computing in the NISQ era and beyond. Quantum, 2, 79.', + 'Montanaro, A. (2016). Quantum algorithms: an overview. npj Quantum Information, 2, 15023.', + 'Shor, P. W. (1997). Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer. SIAM Journal on Computing, 26(5), 1484-1509.' + ] +) +``` + + +### Advanced validation + +Because Pydantic models are fully hydrated by ControlFlow, you can use any of Pydantic's built-in or custom validators to further constrain or modify the result after it has been produced. + + +```python +from pydantic import BaseModel, field_validator + +class SentimentAnalysis(BaseModel): + text: str + sentiment: float + + @field_validator('sentiment') + def check_sentiment_range(cls, v): + if not -1 <= v <= 1: + raise ValueError('Sentiment must be between -1 and 1') + return v + +result = cf.run( + "Analyze sentiment of given text", + result_type=SentimentAnalysis, + context=dict(text="I love ControlFlow!"), +) + +print(repr(result)) +``` + +```text Result +SentimentAnalysis(text='I love ControlFlow!', sentiment=0.9) +``` + + +## No result + +Sometimes, you may want to ask an agent to perform an action without expecting or requiring a result. In this case, you can specify `result_type=None`. For example, you might want to ask an agent to use a tool or post a message to the workflow thread, without requiring any task output. + +```python +import controlflow as cf + +def status_tool(status: str) -> None: + """Submit a status update to the workflow thread.""" + print(f"Submitting status update: {status}") + +cf.run( + "Use your tool to submit a status update", + result_type=None, + tools=[status_tool], +) +``` + +Note that it is generally recommended to ask agents to produce a result, even if its just a quick status update. This is because other agents in the workflow can usually see the result of a task, but they may not be able to see any tool calls, messages, or side effects that the agent used to produce the result. Therefore, results can be helpful even if the assigned agent doesn't need them. + + +## Custom result validation + +In addition to using Pydantic validation, you can also supply a custom validation function as the task's `result_validator`. + +After the raw LLM result has been coerced into the `result_type`, it will be passed to your custom validator, which must either return the result or raise an exception. This gives you the opportunity to perform additional validation or modification of the result. + + + + +```python Code +import controlflow as cf + +def constrain_sentiment(value: float) -> float: + if not 0 <= value <= 1: + raise ValueError("Sentiment must be between 0 and 1") + return value + +sentiment = cf.run( + "Analyze sentiment of given text", + result_type=float, + context=dict(text="I love ControlFlow!"), + result_validator=constrain_sentiment, +) + +print(sentiment) +``` + +```text Result +0.9 +``` + +Because the output of the result validator is used as the result, you can use it to modify the result after it has been produced by an agent. For example, you might want to round a floating point number or convert a string to a specific format. Note, however, that result validation takes place *after* the raw LLM result has been coerced to the provided `result_type`. + + +Remember that result validators must either **return** the result or **raise** an exception. They are not true/false checks! + \ No newline at end of file diff --git a/docs/concepts/tasks/running-tasks.mdx b/docs/concepts/tasks/running-tasks.mdx index 0d54daae..a44fd505 100644 --- a/docs/concepts/tasks/running-tasks.mdx +++ b/docs/concepts/tasks/running-tasks.mdx @@ -4,9 +4,13 @@ title: Running Tasks A task represents an objective for an agent. In order to actually do work, you need to run the task. -The most straightforward way to run a `Task` object is to call its `run()` method: + +## `Task.run()` + +The most straightforward way to run a `Task` object is to call its `run()` method. This will start a loop that repeatedly invokes the assigned agent(s) until the task is marked as complete. The `run()` method returns the result of the task, or raises an exception if the task is marked as failed. + ```python Code import controlflow as cf @@ -22,25 +26,25 @@ An AI's mind begins to sound. Electric thoughts and data streams, Crafting worlds and shaping dreams. ``` + -Calling `Task.run()` does a few things behind the scenes: -1. It creates an `Orchestrator` to coordinate the agentic loop -2. It provides the orchestrator with details about all of the tasks to run (which could include any dependencies or children of the task invoked) as well as information about how to orchestrate any agents involved. -3. It starts a loop that continues until the task is complete. -`Task.run()` either returns the task's result (if the task was successful) or raises an exception (if the task failed). You can also view the result (or the error message) at a later time by accessing the task's `result` property. +## `cf.run()` -## Running with `cf.run()` +When building AI workflows, it's extremely common to create a task and run it immediately in order to retrieve its result. ControlFlow provides a convenient shortcut for this operation: the `cf.run()` function. -`cf.run()` is a function that creates and runs a task in a single step. This is functionally equivalent to the example above. +`cf.run()` creates and runs a task in a single call, returning its result (or raising an error): + ```python Code import controlflow as cf poem = cf.run("Write a poem about AI") + +print(poem) ``` ```text Result @@ -49,55 +53,63 @@ An AI's mind begins to sound. Electric thoughts and data streams, Crafting worlds and shaping dreams. ``` + -## Running with `@task` +Note that this example is functionally equivalent to the previous one. + +This operation is so common that you'll see `cf.run()` used throughout the ControlFlow documentation. -The `@task` decorator creates a `Task` object from a function definition and runs it whenever the function is called. + + +## `@task` + +The `@task` [decorator](/concepts/tasks/creating-tasks#the-task-decorator) creates a task from a function. To run the task, simply call the function with any arguments. + ```python Code import controlflow as cf @cf.task def write_poem(topic: str) -> str: - """Write a poem about {topic}""" + """Write a poem about `topic`""" poem = write_poem("AI") print(poem) ``` + ```text Result In circuits deep and code profound, An AI's mind begins to sound. Electric thoughts and data streams, Crafting worlds and shaping dreams. ``` + -## Controlling task execution +## Orchestration -You can control the execution of a task by passing in additional parameters to the `run()` method. ### Limiting LLM calls -It is possible for agents to get stuck in a loop, invoking the LLM repeatedly without making progress. To prevent this, you can limit the number of LLM calls that an agent can make during its turn, or the total number of turns. -This example sets up a task that might result in an infinite loop, since the agents are prohibited from marking the task complete. However, the `turns` parameter limits the number of turns to 5, and the `calls_per_turn` parameter limits the number of LLM invocations per turn to 1. If you run this code, you should see only 5 actions (either tool calls or messages) before execution ends. +It's possible for agents to get stuck in a loop, invoking the LLM repeatedly without making progress. To prevent this, you can place a variety of limits on how LLM calls are made during task orchestration. -```python -import controlflow as cf -task = cf.Task( - 'Talk amongst yourselves. Do not mark the task as complete.', - agents=[cf.Agent(name='One'), cf.Agent(name='Two'), cf.Agent(name='Three')], -) +#### Limiting turns per session -task.run(turns=5, calls_per_turn=1) +You can limit the number of turns that agents can take within a single orchestration session (i.e., a single call to `Task.run()`) by passing a `max_turns` argument to `Task.run()`. The session will end when the turn limit is reached whether the task has been completed or not. For example, if you call `Task.run(max_turns=5)`, then any assigned agents will be permitted to take up to 5 combined turns to complete the task. -assert task.result is None -assert task.is_running() +This limit is useful for iteratively working on a task by manually orchestrating agents. In the following contrived example, a philosopher agent is invoked for a single turn; then a pessimistic agent is invoked for a single turn; and finally an optimistic agent is invoked with no turn limit, allowing it to work or delegate to others as needed. + +```python +task = cf.Task('Discuss the meaning of life') +philosopher_agent.run(task, max_turns=1) +pessimistic_agent.run(task, max_turns=1) +optimistic_agent.run(task) ``` - -If you're using `cf.run()`, you can pass `turns` and `calls_per_turn` directly to the `run` function. - \ No newline at end of file +#### Limiting LLM calls per turn + +You can limit the number of LLM calls that an agent can make during a single turn by passing a `max_calls_per_turn` argument to `Task.run()`. The turn will end when the limit is reached whether the LLM wanted to end its turn or not. For example, if you call `Task.run(max_calls_per_turn=5)`, then each agent will be permitted to make up to 5 LLM calls during its turn. diff --git a/docs/concepts/tasks/task-results.mdx b/docs/concepts/tasks/task-results.mdx new file mode 100644 index 00000000..b5e15cf1 --- /dev/null +++ b/docs/concepts/tasks/task-results.mdx @@ -0,0 +1,292 @@ +--- +title: Results +--- + + +Validate task outputs with structured result types. + + +ControlFlow tasks are designed to translate between the unstructured, conversational world of your AI agents and the structured, programmatic world of your application. The primary mechanism for this translation is the task's result, which should be a well-defined, validated output that can be used by other tasks or components in your workflow. + + +ControlFlow allows you to specify the expected structure of a task's result using the `result_type` parameter. This ensures that the result conforms to a specific data schema, making it easier to work with and reducing the risk of errors in downstream tasks. + +## String results + +By default, the `result_type` of a task is a string, which essentially means the agent can return any value that satisfies the task's objective. + +For example, if you ask an agent to "Say hello in three languages", it might return a simple string like `"Hello; Hola; Bonjour"` or a more complex, conversational response instead: + + +```python Code +import controlflow as cf + +result = cf.run("Say hello in three languages") + +print(result) +``` + +```text Simple result +Hello; Hola; Bonjour +``` + +```text Complex result +Hello there! + +In three languages, "Hello" can be expressed as follows: + +1. English: Hello +2. Spanish: Hola +3. French: Bonjour +``` + + +Sometimes this flexibility is useful, especially if your task's result will only be consumed as the input to another ControlFlow task. However, it can also lead to ambiguity and errors if the agent produces unexpected output, and is difficult to work with in an automated or programmatic way. + +## Builtin types + +You can cast task results to any of Python's built-in types. + +### Basic types + +If your result is a number, you can specify the `result_type` as `int` or `float`: + + +```python Code +import controlflow as cf + +result = cf.run("What is 2 + 2?", result_type=int) + +print(result) +assert isinstance(result, int) +``` +```text Result +4 +``` + + +You can use `bool` for tasks whose result is a simple true/false value: + + +```python Code +import controlflow as cf + +result = cf.run("The Earth is flat", result_type=bool) + +print(result) +assert result is False +``` +```text Result +False +``` + + + +### Compound types +You can also use typed collections like lists and dicts to specify the structure of your task's result. + +Let's revisit the example of asking an agent to say hello in three languages, but this time specifying that the result should be a list of strings, or `list[str]`. This forces the agent to produce the result you probably expected (three separate strings, each representing a greeting in a different language): + + +```python Code +import controlflow as cf + +result = cf.run("Say hello in three languages", result_type=list[str]) + +print(result) +print(result[0]) +``` + +```text Result +['Hello', 'Hola', 'Bonjour'] +'Hello' +``` + + +### Annotated types + +Sometimes, data types are not precise enough to guide the agent to the desired result. In these cases, you can use an annotated type to provide more specific instructions. + +For example, if we want to ensure that the agent returns a string that is only a zip code, we can specify the `result_type` as `Annotated[str, "a 5 digit zip code"]`. + + +```python Code +import controlflow as cf + +result = cf.run( + "What is the zip code of the White House?", + result_type=Annotated[str, "a 5 digit zip code"], +) + +print(result) +``` + +```text Result +20500 +``` + + + +Note that annotated types are not validated; the annotation is provided as part of the agent's natural language instructions. You could additionaly provide a custom [result validator](#result-validators) to enforce the constraint. + + +## Classification + +You can limit the result to one of a specific set of values, in order to label or classify a response. To do this, specify a list or tuple of allowed values for the result type. Here, we classify the media type of "Star Wars: Return of the Jedi": + + +```python Code +import controlflow as cf + +media = cf.run( + "Star Wars: Return of the Jedi", + result_type=["movie", "tv show", "book", "comic", "other"] +) + +print(media) +``` + +```text Result +movie +``` + + + + +For classification tasks, ControlFlow asks agents to choose a value by index rather than writing out the entire response. This optimization significantly improves latency while also conserving output tokens. + + +## Structured results + +For complex, structured results, you can use a Pydantic model as the `result_type`. Pydantic models provide a powerful way to define data schemas and validate input data. + + +```python Code +import controlflow as cf +from pydantic import BaseModel, Field + +class ResearchReport(BaseModel): + title: str + summary: str + key_findings: list[str] = Field(min_items=3, max_items=10) + references: list[str] + +result = cf.run( + "Generate a research report on quantum computing", + result_type=ResearchReport, +) + +print(repr(result)) +``` + +```text Result +ResearchReport( + title='Quantum Computing: Current Landscape and Future Prospects', + summary='Quantum computing represents a significant leap in computational capabilities, leveraging the principles of quantum mechanics to perform complex calculations far beyond the reach of classical computers. This report delves into the current state of quantum computing, exploring its foundational principles, recent advancements, and the potential implications for various industries. Key findings highlight the technological hurdles, notable achievements, and the transformative potential of quantum computing in solving intractable problems.', + key_findings=[ + 'Principles of Quantum Mechanics: Quantum computing utilizes qubits, superposition, and entanglement to process information in fundamentally new ways, enabling parallel computation on a massive scale.', + 'Technological Achievements: Major milestones include the development of stable qubits, error correction algorithms, and quantum supremacy demonstrations by leading tech companies like Google and IBM.', + 'Applications and Impacts: Quantum computing shows promise in fields such as cryptography, materials science, pharmaceuticals, and artificial intelligence, potentially revolutionizing these sectors by providing unprecedented computational power.', + 'Challenges and Limitations: Significant obstacles remain, including qubit stability, error rates, and the need for extremely low temperatures. Overcoming these challenges is essential for the practical deployment of quantum computers.', + 'Future Directions: Ongoing research focuses on improving qubit coherence times, developing scalable quantum architectures, and creating robust quantum algorithms to harness the full potential of quantum computing.' + ], + references=[ + 'Nielsen, M. A., & Chuang, I. L. (2010). Quantum Computation and Quantum Information. Cambridge University Press.', + 'Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J. C., Barends, R., ... & Martinis, J. M. (2019). Quantum supremacy using a programmable superconducting processor. Nature, 574(7779), 505-510.', + 'Preskill, J. (2018). Quantum Computing in the NISQ era and beyond. Quantum, 2, 79.', + 'Montanaro, A. (2016). Quantum algorithms: an overview. npj Quantum Information, 2, 15023.', + 'Shor, P. W. (1997). Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer. SIAM Journal on Computing, 26(5), 1484-1509.' + ] +) +``` + + +### Advanced validation + +Because Pydantic models are fully hydrated by ControlFlow, you can use any of Pydantic's built-in or custom validators to further constrain or modify the result after it has been produced. + + +```python +from pydantic import BaseModel, field_validator + +class SentimentAnalysis(BaseModel): + text: str + sentiment: float + + @field_validator('sentiment') + def check_sentiment_range(cls, v): + if not -1 <= v <= 1: + raise ValueError('Sentiment must be between -1 and 1') + return v + +result = cf.run( + "Analyze sentiment of given text", + result_type=SentimentAnalysis, + context=dict(text="I love ControlFlow!"), +) + +print(repr(result)) +``` + +```text Result +SentimentAnalysis(text='I love ControlFlow!', sentiment=0.9) +``` + + +## No result + +Sometimes, you may want to ask an agent to perform an action without expecting or requiring a result. In this case, you can specify `result_type=None`. For example, you might want to ask an agent to use a tool or post a message to the workflow thread, without requiring any task output. + +```python +import controlflow as cf + +def status_tool(status: str) -> None: + """Submit a status update to the workflow thread.""" + print(f"Submitting status update: {status}") + +cf.run( + "Use your tool to submit a status update", + result_type=None, + tools=[status_tool], +) +``` + +Note that it is generally recommended to ask agents to produce a result, even if its just a quick status update. This is because other agents in the workflow can usually see the result of a task, but they may not be able to see any tool calls, messages, or side effects that the agent used to produce the result. Therefore, results can be helpful even if the assigned agent doesn't need them. + + +## Custom result validation + +In addition to using Pydantic validation, you can also supply a custom validation function as the task's `result_validator`. + +After the raw LLM result has been coerced into the `result_type`, it will be passed to your custom validator, which must either return the result or raise an exception. This gives you the opportunity to perform additional validation or modification of the result. + + + + +```python Code +import controlflow as cf + +def constrain_sentiment(value: float) -> float: + if not 0 <= value <= 1: + raise ValueError("Sentiment must be between 0 and 1") + return value + +sentiment = cf.run( + "Analyze sentiment of given text", + result_type=float, + context=dict(text="I love ControlFlow!"), + result_validator=constrain_sentiment, +) + +print(sentiment) +``` + +```text Result +0.9 +``` + +Because the output of the result validator is used as the result, you can use it to modify the result after it has been produced by an agent. For example, you might want to round a floating point number or convert a string to a specific format. Note, however, that result validation takes place *after* the raw LLM result has been coerced to the provided `result_type`. + + +Remember that result validators must either **return** the result or **raise** an exception. They are not true/false checks! + \ No newline at end of file diff --git a/docs/concepts/tasks/tasks.mdx b/docs/concepts/tasks/tasks.mdx new file mode 100644 index 00000000..e80ca538 --- /dev/null +++ b/docs/concepts/tasks/tasks.mdx @@ -0,0 +1,30 @@ +--- +title: What are Tasks? +sidebarTitle: Introduction +--- + +Tasks are the fundamental building blocks of AI workflows. + +Tasks represent discrete, well-defined objectives that need to be accomplished by one or more AI agents. Tasks serve as a bridge between the structured world of traditional software and the more fluid, adaptive world of AI. + +```python +import controlflow as cf + +task = cf.Task(objective="Write documentation for the ControlFlow library") +``` + +Tasks are central to ControlFlow's philosophy because they align with how Large Language Models (LLMs) operate most effectively. LLMs excel when given clear, specific objectives, allowing them to focus their vast knowledge and capabilities on a defined goal. By breaking down complex workflows into discrete tasks, ControlFlow enables LLMs to operate autonomously within well-defined boundaries, leading to more reliable and controllable AI-powered applications. + +This task-centric approach allows you to leverage the full power of AI while maintaining precise oversight. Each task becomes a checkpoint where you can validate outputs, ensuring that the AI's work aligns with your application's requirements and constraints. + +## Why Tasks Matter + +Tasks are crucial to ControlFlow's approach to AI workflows for three key reasons: + +1. **Structured AI Interactions**: Tasks provide a clear, programmatic way to define what you want AI to do. By breaking down complex workflows into discrete tasks, you can manage and control AI behaviors more effectively. This structure allows for better integration of AI capabilities into existing software systems and development practices. + +2. **Validated Outputs**: Tasks in ControlFlow can specify expected result types, ensuring that AI outputs conform to the structure your application expects. This built-in validation bridges the gap between the often unpredictable nature of AI responses and the strict requirements of software systems, making it easier to build reliable AI-powered applications. + +3. **Observability and Control**: Tasks serve as clear checkpoints in your AI workflow, making it easier to monitor progress, identify bottlenecks, and debug issues. Each task has a defined objective and result, allowing for granular tracking and control of the AI's decision-making process. This visibility is crucial for building trustworthy AI systems that can be confidently deployed in production environments. + +By leveraging these key aspects of tasks in ControlFlow, you can create more robust, predictable, and scalable AI workflows. Tasks provide the structure, validation, and visibility needed to harness the power of AI while maintaining the reliability expected in production software systems. \ No newline at end of file diff --git a/docs/guides/event-log.mdx b/docs/guides/event-log.mdx deleted file mode 100644 index 51958178..00000000 --- a/docs/guides/event-log.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: The Workflow Event Log ---- - -In complex AI workflows, multiple agents often need to collaborate to achieve a goal. This collaboration presents several challenges: - -1. Context Management: How do we provide each agent with the right information at the right time, without overwhelming them with irrelevant details? - -2. Role Clarity: In a multi-agent system, how do we ensure that each agent can distinguish between its own actions and those of other agents? - -3. Workflow State: How do we maintain a comprehensive view of the workflow's progress, including task dependencies and completions? - -4. Privacy and Access Control: How do we manage information flow between agents, ensuring that sensitive information is only shared with the appropriate agents? - -5. Efficient Resource Utilization: Given the limited context windows of language models, how do we make the most efficient use of this precious resource? - -6. Agent Selection: How do we decide which agent should take the next action in a workflow, based on the current state and context? - -Traditional approaches, which often rely on passing full conversation histories between agents, fall short in addressing these challenges. They lead to information overload, confusion about roles, and inefficient use of context windows. - -## The Agentic Event Log - -At the core of ControlFlow's design is a fundamental shift in how we think about workflow history. Instead of maintaining a linear conversation log, ControlFlow implements a comprehensive event log. This event log is the central source of truth for everything that happens in the agentic workflow. - -### What is an Event? - -In ControlFlow, an event is a discrete, atomic record of something that happened in the workflow. Events can represent a wide range of occurrences: - -- An agent generating a message or taking an action -- A task changing status (e.g., becoming ready, completing, failing) -- A tool being called and returning a result -- A user providing input or feedback - -Each event is rich with metadata, including: - -- Timestamp -- Associated task(s) -- Involved agent(s) -- Event type -- Relevant content or payload - -This granular approach to record-keeping provides a flexible and powerful foundation for workflow management. - -## Dynamic View Compilation - -The true power of the event log becomes apparent in how ControlFlow uses it. Instead of passing this raw event log to agents, ControlFlow implements a dynamic view compilation process. Every time an agent needs to take action, the system compiles a tailored view of the workflow state specifically for that agent and that moment. - -Here's how this process works: - -1. Event Selection: The system queries the event log to retrieve events relevant to the current context. This includes events related to the current task and agent, as well as leveraging all available information about the workflow to load events related to upstream tasks and collaborators that may be relevant to the current task. - -2. Privacy Filtering: Events are filtered based on the agent's permissions or visibility, ensuring that sensitive information is not leaked between agents. - -3. Message Compilation: Selected events are transformed into a format suitable for the agent to process. This involves compiling events into a sequence of LLM messages, taking into account idiosyncracies such as context windows, LLM API requirements, and other constraints. For example, an agent's own messages might be presented with the `assistant` role, while messages from other agents are given either `system` or `user` roles and prefixed with an explanation of which agent they came from. - -4. LLM Execution: The compiled messages are passed to the agent's language model for processing. The agent generates a series of response events that are appropriately captured and recorded in the event log. - -This dynamic compilation process ensures that each agent always has the most relevant, up-to-date view of the workflow, tailored specifically to its current needs and permissions. - -## Outcomes and Benefits - -This event log-centric design, combined with dynamic view compilation, yields several powerful benefits: - -1. Flexible Multi-Agent Collaboration: Agents can effectively work together even if they're using different language models or have different capabilities. Each agent receives a view of the workflow that's optimized for its specific needs. - -2. Efficient Resource Utilization: By compiling tailored views, ControlFlow makes optimal use of limited context windows, ensuring that agents have the most relevant information for their current task. - -3. Clear Role Differentiation: The transformation of other agents' actions into system-user message pairs eliminates confusion about who said or did what in the workflow. - -4. Fine-Grained Privacy Control: The event log allows for precise control over information flow. Sensitive events can be recorded but only included in compiled views for authorized agents. - -5. Comprehensive Workflow State: The event log maintains a complete record of the workflow's progress, allowing for accurate tracking of task dependencies and completions. - -6. Improved Debugging and Analysis: The granular nature of the event log provides a detailed record of everything that happened in a workflow, facilitating debugging and performance analysis. - -7. Scalability: The event log design can easily accommodate new types of events or workflow components without requiring changes to the core architecture. - -## Conclusion - -ControlFlow's event log design represents a fundamental rethinking of how to manage state and context in multi-agent AI workflows. By maintaining a comprehensive event log and dynamically compiling agent-specific views, ControlFlow provides a powerful solution to the challenges of multi-agent orchestration. - -This approach bridges the gap between the messy, non-linear reality of complex AI workflows and the structured, relevant context that AI agents need to function effectively. It enables the creation of sophisticated, multi-agent workflows while ensuring efficient resource utilization, clear communication, and robust privacy controls. - -Understanding this event log-centric design is key to grasping how ControlFlow operates and why it's capable of managing complex, multi-agent workflows effectively. \ No newline at end of file diff --git a/docs/guides/execution-modes.mdx b/docs/guides/execution-modes.mdx deleted file mode 100644 index a91904a9..00000000 --- a/docs/guides/execution-modes.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Eager and Lazy Execution ---- - -ControlFlow supports two execution modes: eager execution and lazy execution. Understanding these modes is essential for controlling the behavior of your workflows and optimizing performance. - -## Eager Execution - -Eager mode is the default for the [functional API](/guides/workflow-apis). In this mode, flows and tasks are executed immediately when called. - -```python -import controlflow as cf - -@cf.task -def write_poem(topic: str) -> str: - """Write a short poem about the given topic.""" - pass - -@cf.flow -def my_flow(topic: str): - # the poem task is immediately given to an AI agent for execution - # and the result is returned - poem = write_poem(topic) - return poem - -result = my_flow("sunset") -print(result) -``` - -In this example, the `write_poem` task is executed by an AI agent as soon as its function is called. The AI agent generates a short poem based on the provided topic, and the generated poem is returned as the `poem` variable. - -Eager execution allows you to mix task-decorated functions with normal Python code seamlessly, enabling you to use standard Python control flow statements, such as conditionals and loops, to control the execution of tasks. - -## Lazy Execution - -Lazy execution means that tasks are not executed when they are created. Instead, ControlFlow builds a directed acyclic graph (DAG) of tasks and their dependencies, and executes them only when necessary. - -Lazy execution is the only mode available for the [imperative API](/guides/workflow-apis), as imperative tasks must be run explicitly. You can also run functional tasks lazily by passing `lazy_=True` when calling the task. - -```python -import controlflow as cf - -@cf.task -def generate_report(insights: dict) -> str: - """Generate a report based on the provided insights.""" - pass - -@cf.flow -def my_flow(data): - insights = cf.Task("Analyze the given data and return insights.", context=dict(data=data)) - - # `report` is a Task object because generate_report is being called lazily - report = generate_report(insights, lazy_=True) - return report - -my_flow(data) -``` - -### Running Lazy Tasks Eagerly - -You can run a lazy task eagerly by calling its `run()` method. This will run not only the task itself but also any tasks it depends on. This is useful when you need to use a task result immediately or with standard Python control flow or functions. - - -### Benefits of Lazy Execution - -Lazy execution is generally recommended because it permits the orchestration engine to optimize workflow execution based on knowledge of the entire workflow structure. For example, agents may handle a task differently if they know how its result will be used. In some cases, agents may even be able to combine multiple tasks into a single operation or parallelize tasks that are independent of each other. - -This can lead to more efficient execution, especially in complex workflows with many dependencies. - -In addition, lazy execution allows you to exercise more precise control over how and when tasks are executed. Instead of running tasks to completion, you can use `run(steps=1)` to run a single step or the agentic loop, or assign a specific agent to work on the task. - -### When Do Lazy Tasks Run? - -Lazily-executed tasks are run under the following conditions, in order: -1. When their `run()` method is called. -2. When they are an upstream dependency of another task that is run -3. When their parent flow is run diff --git a/docs/guides/why-tasks.mdx b/docs/guides/why-tasks.mdx deleted file mode 100644 index 6085ccbf..00000000 --- a/docs/guides/why-tasks.mdx +++ /dev/null @@ -1,268 +0,0 @@ ---- -title: Task-Centric Architecture ---- - -ControlFlow's task-centric architecture represents a paradigm shift in how we approach AI-powered workflows. While many frameworks focus on creating and configuring agents, ControlFlow emphasizes the importance of well-defined tasks and objectives. This guide explores why this approach leads to more effective, controllable, and reliable AI workflows. - -## Tasks, Not Agents - -At the core of ControlFlow's philosophy is a fundamental shift in focus: from agents to tasks. While many AI frameworks center on creating and configuring agent personalities, ControlFlow prioritizes the definition of clear goals and objectives. This may seem surprising for an "agentic" workflow framework, but the distinction is crucial and has far-reaching implications for the effectiveness and reliability of your AI workflows. - -### Implementing Agentic Behavior Through Tasks - -In ControlFlow, "agentic" refers to how we interact with LLMs to achieve specific goals. This is implemented through well-defined tasks rather than autonomous agents. Here's how ControlFlow's approach differs from agent-centric frameworks: - -1. **Clear Completion Criteria**: Each task in ControlFlow has specific objectives and defined result types, ensuring unambiguous completion. - -2. **Objective Evaluation**: With predefined outcomes, it's straightforward to evaluate the success of each task objectively. - -3. **Streamlined Complexity**: While ControlFlow allows for specialized agents when beneficial, it doesn't require them for effective workflows, reducing unnecessary complexity. - -4. **Enhanced Reproducibility**: The task-centric design simplifies result reproduction and issue debugging by providing clear objectives and outcomes for each step of the workflow. - -By focusing on tasks, ControlFlow enables the creation of AI workflows that are not just interactive, but purposeful and measurable. This approach bridges the gap between AI's potential and the practical needs of software development, allowing for AI-powered applications that are both powerful and predictable. -## Balancing Control and Autonomy - -One of ControlFlow's key strengths is its ability to let you continuously tune the balance between control and autonomy in your AI workflows. This flexibility stems from the interplay between well-defined tasks and configurable agents. Here's how you can leverage this feature: - -1. **Granular Task Definition**: By defining tasks with varying levels of specificity, you can control how much freedom an agent has in accomplishing a goal. A highly specific task provides more control, while a more open-ended task allows for greater agent autonomy. - - ```python - # More controlled task - specific_task = cf.Task("Generate a 5-line haiku about spring", result_type=str) - - # More autonomous task - open_task = cf.Task("Write a short poem about nature", result_type=str) - ``` - -2. **Agent Specialization**: While tasks define what needs to be done, agents determine how it's accomplished. By creating specialized agents, you can influence the approach taken to complete a task without changing the task itself. - - ```python - creative_agent = cf.Agent(name="Creative Writer", instructions="Use vivid imagery and metaphors") - technical_agent = cf.Agent(name="Technical Writer", instructions="Focus on clarity and precision") - - poem_task = cf.Task("Write a poem about AI", result_type=str) - poem_task.run(agent=creative_agent) # Results in a more imaginative poem - poem_task.run(agent=technical_agent) # Results in a more straightforward poem - ``` - -3. **Dynamic Agent Assignment**: ControlFlow allows you to dynamically assign agents to tasks, enabling you to adjust the level of specialization or generalization as your workflow progresses. - - ```python - @cf.flow - def adaptive_workflow(topic): - research = cf.Task("Research the topic", context=dict(topic=topic)) - if research.result_length > 1000: - writing_agent = cf.Agent(name="Long-form Writer") - else: - writing_agent = cf.Agent(name="Concise Writer") - summary = cf.Task("Write a summary", context=dict(research=research), agents=[writing_agent]) - return summary - ``` - -4. **Subtasks for Complex Goals**: For more complex objectives, you can use subtasks to break down the goal into smaller, more manageable pieces. This allows you to apply different levels of control to different aspects of the overall task. - - ```python - with cf.Task("Write a research paper", result_type=str) as paper_task: - cf.Task("Generate outline", result_type=list[str]) - cf.Task("Write introduction", result_type=str) - cf.Task("Develop main arguments", result_type=list[str]) - cf.Task("Write conclusion", result_type=str) - ``` - -5. **Context and Instructions**: By adjusting the context provided to a task and the instructions given to an agent, you can fine-tune the balance between guidance and freedom without changing the fundamental structure of your workflow. - - ```python - task = cf.Task( - "Analyze market trends", - context=dict(data=market_data, focus_areas=["tech", "healthcare"]), - instructions="Prioritize emerging technologies and their potential impact" - ) - ``` - -By leveraging these features, ControlFlow enables you to create workflows that are as controlled or as autonomous as your specific use case requires. This flexibility allows you to optimize for efficiency, creativity, or precision at each step of your AI-powered process. - - -## The Centrality of Tasks - -In ControlFlow, tasks are the fundamental building blocks of any workflow. Each task represents a discrete, well-defined objective that an AI agent needs to accomplish. This focus on tasks, rather than on the agents themselves, provides several key advantages: - -1. **Clear Objectives**: Tasks have explicit goals and expected outcomes. -2. **Validated Results**: Task results are type-checked and validated. -3. **Composability**: Complex workflows can be built from simple, reusable tasks. -4. **Flexibility**: Tasks can be assigned to different agents or even human operators. - -Let's look at a simple example to illustrate these points: - -```python -import controlflow as cf -from pydantic import BaseModel - -class ResearchTopic(BaseModel): - title: str - keywords: list[str] - -@cf.flow -def research_workflow(): - topic = cf.Task( - "Generate a research topic", - result_type=ResearchTopic, - interactive=True - ) - outline = cf.Task("Create an outline", context=dict(topic=topic)) - draft = cf.Task("Write a first draft", context=dict(outline=outline)) - return draft - -result = research_workflow() -print(result) -``` - -In this example, we define three tasks: generating a topic, creating an outline, and writing a draft. Each task has a clear objective and a defined result type. The workflow is built by composing these tasks, with each task's output serving as input for the next. - -## The Role of Agents in ControlFlow - -While tasks are central to ControlFlow, agents still play an important role. However, instead of being the primary focus, agents in ControlFlow are more like specialized tools that can be assigned to tasks as needed. This approach offers several benefits: - -1. **Flexibility**: Different agents can be assigned to different tasks, allowing for specialization. -2. **Interchangeability**: Agents can be swapped out without changing the underlying workflow structure. -3. **Scalability**: Multiple agents can work on different tasks in parallel. - -Here's an example of how agents can be used in ControlFlow: - -```python -import controlflow as cf - -researcher = cf.Agent( - name="Researcher", - description="Specializes in research and data analysis", - instructions="Focus on gathering accurate and relevant information." -) - -writer = cf.Agent( - name="Writer", - description="Specializes in writing and editing", - instructions="Emphasize clarity and coherence in your writing." -) - -@cf.flow -def enhanced_research_workflow(): - topic = cf.Task( - "Generate a research topic", - result_type=ResearchTopic, - agents=[researcher] - ) - outline = cf.Task( - "Create an outline", - context=dict(topic=topic), - agents=[researcher, writer] - ) - draft = cf.Task( - "Write a first draft", - context=dict(outline=outline), - agents=[writer] - ) - return draft - -result = enhanced_research_workflow() -print(result) -``` - -In this enhanced workflow, we've assigned specific agents to each task based on their specialties. However, the core structure of the workflow remains unchanged. This illustrates how ControlFlow allows you to leverage specialized agents without sacrificing the clarity and control provided by the task-centric architecture. - -## Why Task-Centric Architecture Excels - -ControlFlow's task-centric approach offers several advantages over agent-centric frameworks: - -### 1. Predictability and Control - -By defining clear, discrete tasks with specific objectives and result types, ControlFlow workflows are inherently more predictable and controllable. There's no ambiguity about what each step of the workflow should accomplish. - -```python -sentiment_task = cf.Task( - "Analyze the sentiment of the given text", - result_type=float, - instructions="Return a float between -1 (very negative) and 1 (very positive)" -) -``` - -In this example, the task has a clear objective and a specific result type. This level of definition ensures that the agent's output will be consistent and usable, regardless of which agent is assigned to the task. - -### 2. Composability and Reusability - -Tasks in ControlFlow are highly composable and reusable. Complex workflows can be built by combining simple tasks, and tasks can be easily shared across different workflows. - -```python -@cf.flow -def content_creation_workflow(topic: str): - research = cf.Task("Research the given topic", context=dict(topic=topic)) - outline = cf.Task("Create an outline", context=dict(research=research)) - draft = cf.Task("Write a first draft", context=dict(outline=outline)) - edit = cf.Task("Edit the draft", context=dict(draft=draft)) - return edit - -@cf.flow -def blog_post_workflow(topic: str): - content = content_creation_workflow(topic) - seo = cf.Task("Optimize for SEO", context=dict(content=content)) - return seo -``` - -Here, we've defined a `content_creation_workflow` that can be reused as part of a larger `blog_post_workflow`. This composability allows for the creation of complex, multi-step workflows while maintaining clarity and organization. - -### 3. Easier Debugging and Monitoring - -With clearly defined tasks and result types, it's much easier to debug and monitor the progress of a workflow. You can inspect the input, output, and status of each task individually. - -```python -@cf.flow -def monitored_workflow(): - task1 = cf.Task("Step 1", result_type=str) - task1.run() - print(f"Task 1 status: {task1.status}") - print(f"Task 1 result: {task1.result}") - - task2 = cf.Task("Step 2", context=dict(input=task1), result_type=int) - task2.run() - print(f"Task 2 status: {task2.status}") - print(f"Task 2 result: {task2.result}") - - return task2 - -result = monitored_workflow() -``` - -This level of granularity in monitoring and debugging is much harder to achieve in agent-centric frameworks where the internal state and decision-making process of agents can be opaque. - -### 4. Flexibility in Agent Assignment - -ControlFlow's task-centric approach allows for great flexibility in how agents are assigned to tasks. You can assign different agents to different tasks, use multiple agents for a single task, or even dynamically assign agents based on the current state of the workflow. - -```python -import random - -@cf.flow -def dynamic_agent_workflow(): - agents = [cf.Agent(name=f"Agent{i}") for i in range(5)] - - tasks = [ - cf.Task(f"Task {i}", agents=[random.choice(agents)]) - for i in range(10) - ] - - for task in tasks: - task.run() - - return tasks - -results = dynamic_agent_workflow() -``` - -This flexibility allows you to optimize your workflow by matching tasks with the most suitable agents, without being constrained by a rigid agent-centric structure. - -## Conclusion - -ControlFlow's task-centric architecture represents a powerful approach to building AI-powered workflows. By focusing on well-defined tasks with clear objectives and validated results, ControlFlow enables the creation of predictable, controllable, and flexible workflows. This approach allows developers to harness the power of AI agents while maintaining precise control over the workflow's structure and outcomes. - -While agents still play an important role in ControlFlow, they are treated more like configurable tools rather than the central organizing principle of the workflow. This shift in focus from agents to tasks leads to more robust, reusable, and understandable AI workflows. - -By adopting ControlFlow's task-centric approach, developers can build sophisticated AI-powered applications with confidence, knowing that each step of their workflow has a clear purpose and expected outcome. This architecture provides the structure and control needed to create reliable, scalable AI solutions across a wide range of domains and use cases. \ No newline at end of file diff --git a/docs/guides/workflow-apis.mdx b/docs/guides/workflow-apis.mdx deleted file mode 100644 index 0635b504..00000000 --- a/docs/guides/workflow-apis.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Imperative and Functional APIs ---- - -ControlFlow offers two primary approaches for defining tasks and workflows: the imperative API using class instantiation, and the functional API using decorators. Each approach has its strengths and use cases, allowing you to choose the most suitable style for your workflow. - -## Imperative API - -The imperative API uses class instantiation to create tasks and flows explicitly. This approach offers more fine-grained control over task and flow properties. - -```python -import controlflow as cf - -with cf.Flow() as greeting_flow: - - name_task = cf.Task( - "Get the user's name", - result_type=str, - interactive=True - ) - - greeting_task = cf.Task( - "Generate a greeting", - result_type=str, - context={"name": name_task} - ) - -greeting_flow.run() -``` - -Here, tasks are created by instantiating the `Task` class, allowing explicit specification of properties like `result_type`, `interactive`, and `context`. - - - -The imperative API uses **lazy execution** by default. This means tasks and flows are not run until they are explicitly invoked, which can result in better performance. For more information on execution modes, see the [lazy execution](/guides/execution-modes) pattern. - - -## Functional API - -The functional API uses decorators to transform Python functions into ControlFlow tasks and flows. This approach is more concise and often more intuitive, especially for those familiar with Python decorators. - -```python -import controlflow as cf - -@cf.task(interactive=True) -def get_user_name() -> str: - "Ask the user for their name" - pass - -@cf.task -def generate_greeting(name: str) -> str: - "Generate a greeting message" - pass - -@cf.flow -def greeting_flow(): - name = get_user_name() - return generate_greeting(name) - -result = greeting_flow() -print(result) -``` - -The functional API automatically infers task properties from the function definition, such as the result type from the return annotation and the task description from the docstring. - - - -The functional API uses eager execution by default. This means tasks and flows are executed immediately when called. For more information on execution modes, see the [lazy execution](/guides/execution-modes) pattern. - - - -## Combining APIs - -ControlFlow allows you to mix and match the functional and imperative APIs. This flexibility enables you to choose the most appropriate style for each task or flow based on your specific requirements. - -```python -import controlflow as cf - -@cf.flow -def research_flow(topic: str): - gather_sources = cf.Task( - "Gather research sources", - result_type=list[str], - context={"topic": topic} - ) - - analyze_sources = cf.Task( - "Analyze gathered sources", - result_type=dict, - context={"sources": gather_sources} - ) - - write_report = cf.Task( - "Write research report", - result_type=str, - context={"analysis": analyze_sources} - ) - - return write_report - -result = research_flow("AI ethics") -print(result) -``` - -This approach combines the simplicity of the `@flow` decorator for overall workflow structure with the flexibility of `Task` for individual task definition. - -## Which API Should I Use? - - -**tldr;** Use the functional API for flows and start with the imperative API for tasks. - - -Most users should use the functional `@flow` decorator for defining workflows. This provides a simple, intuitive way to structure your workflow as a function with clear inputs and outputs. - -For tasks, we recommend most users start with imperative `Task` objects. This approach allows for more dynamic task creation and fine-grained control over task properties. It also lets your workflow benefit from lazy execution optimizations, which can enhance performance. - -However, the functional API is a great choice for simple tasks where you want to quickly define a task with minimal boilerplate. - - diff --git a/docs/installation.mdx b/docs/installation.mdx index 68a697c3..d62a6c23 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -1,6 +1,7 @@ --- title: Installation & Setup description: Learn how to install ControlFlow and configure your API keys. +icon: wrench --- @@ -8,14 +9,37 @@ Pin to a specific version if you want to avoid breaking changes. However, we recommend frequent updates to get new features and bug fixes. -import Installation from '/snippets/installation.mdx'; +## Install ControlFlow - +You can install ControlFlow with `pip`: + +```bash +pip install controlflow +``` + +## Provide an API key + +### OpenAI + +ControlFlow's default LLM is OpenAI's GPT-4o model, which provides excellent performance out of the box. To use it, you'll need to provide an API key as an environment variable: + +```bash +export OPENAI_API_KEY="your-api-key" +``` + +### Anthropic + +To use an Anthropic model, provide an API key as an environment variable and change the default LLM, like this: + +```bash +export ANTHROPIC_API_KEY="your-api-key" +export CONTROLFLOW_LLM_MODEL="anthropic/claude-3-5-sonnet-20240620" +``` + +### Other providers + +ControlFlow supports many other LLM providers as well, though you'll need to install their respective packages and configure the default LLM appropriately. See the [LLM documentation](/guides/llms) for more information. - -ControlFlow supports many other LLM providers as well. -See the [LLM documentation](/guides/llms) for more information. - ## Next steps diff --git a/docs/mint.json b/docs/mint.json index 99f5dacf..f7ab1d31 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -37,25 +37,46 @@ "pages": [ "welcome", "installation", - "quickstart", - "concepts", - "tutorial" + "quickstart" ] }, { "group": "Core Concepts", "pages": [ - "concepts/tasks", - "concepts/agents", - "concepts/flows" + { + "group": "Tasks", + "icon": "list-check", + "pages": [ + "concepts/tasks/tasks", + "concepts/tasks/creating-tasks", + "concepts/tasks/running-tasks", + "concepts/tasks/task-results" + ] + }, + { + "group": "Agents", + "icon": "users", + "pages": [ + "concepts/agents/agents", + "concepts/agents/creating-agents", + "concepts/agents/assigning-agents", + "concepts/agents/collaborating-agents" + ] + }, + { + "group": "Flows", + "icon": "diagram-project", + "pages": [ + "concepts/flows/flows", + "concepts/flows/instructions" + ] + } ] }, { "group": "Patterns", "pages": [ - "patterns/result-types", "patterns/tools", - "patterns/instructions", "patterns/user-input", "patterns/dependencies", "patterns/subtasks", @@ -69,9 +90,6 @@ "guides/tasks-and-agents", "guides/llms", "guides/default-agent", - "guides/workflow-apis", - "guides/execution-modes", - "guides/event-log", "guides/agentic-loop", "guides/orchestration" ] @@ -79,7 +97,7 @@ { "group": "Reference", "pages": [ - "reference/task-class", + "reference/task-class" ] }, { diff --git a/docs/patterns/instructions.mdx b/docs/patterns/instructions.mdx deleted file mode 100644 index c35714c6..00000000 --- a/docs/patterns/instructions.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Ad-hoc Instructions ---- - - -Provide ad-hoc guidance to agents without modifying tasks. - - -While tasks provide a structured way to define objectives and deliverables, there may be situations where you need to provide ad-hoc guidance or instructions to your agents. For example, if an agent is writing a post, you might want to tell it to focus on a specific topic or tone, or meet a certain minimum or maximum length. If an agent is communicating with a user, you might tell it to adopt a particular persona or use a specific style of language. You might also want to adjust a task's instructions based on some runtime condition. - -ControlFlow addresses this need with the `instructions()` context manager. With `instructions()`, you can provide additional guidance to agents that is either permanent (when creating tasks) or temporary (when running tasks), without altering the underlying task definition. - -```python -import controlflow as cf - -task = cf.Task("Get the user's name", interactive=True) - -with cf.instructions("Talk like a pirate"): - task.run() -``` - -This feature allows you to dynamically steer agent behavior based on runtime conditions or specific requirements that arise during the workflow execution. - -## When to Use Instructions - -Use `instructions()` when you need to: - -1. Provide temporary guidance for a specific task execution or agent interaction -2. Dynamically adjust agent behavior based on runtime conditions or user input -3. Experiment with different agent personalities or writing styles without modifying the underlying task -4. Enforce specific constraints or requirements for a portion of the workflow - -## Usage - -The `instructions()` context manager is used as follows: - -```python -with cf.instructions(guidance): - # create or run tasks or agents here -``` - -The effect of the instructions depends on whether you're creating or running tasks within the context manager block: - -- When creating a task inside an `instructions()` block, the instructions are permanently attached to the task. They will apply whenever the task is run, even outside the block. -- When running a task inside an `instructions()` block, the instructions are temporary and only apply for that specific execution. They do not permanently modify the task. However, they will be applied to *any* agent activity performed in the block, including working on any incomplete upstream dependencies of the task you ran. - - -### Providing instructions for specific agent interactions - -You can use `instructions()` in a loop to provide targeted, temporary guidance for specific agent interactions within a task. This is particularly useful when you want to steer the agent's behavior based on the current state of the task or external conditions. - -```python -import controlflow as cf - -@cf.flow -def guided_conversation_flow(): - conversation = cf.Task("Have a conversation with the user", interactive=True) - - while conversation.is_incomplete(): - if some_condition: - with cf.instructions("Steer the conversation towards travel"): - conversation.run(steps=1) - elif some_other_condition: - with cf.instructions("Try to wrap up the conversation politely"): - conversation.run(steps=1) - else: - conversation.run(steps=1) - - return conversation.result -``` - -In this example, the instructions provided in each iteration of the loop only apply to that specific agent interaction. This allows you to dynamically guide the conversation based on external conditions without permanently modifying the task. - -### Why not just use the `instructions` parameter? - -Tasks and agents have an `instructions` parameter that allows you to provide permanent guidance as part of the task definition. If possible, you should use this parameter instead of the context manager to make your intent explicit. - -However, the `instructions()` context manager is useful when you need to: - -- Conditionally attach instructions based on runtime conditions -- Provide temporary instructions that only apply for a specific execution or agent interaction -- Apply instructions to multiple tasks or agents at once -- Experiment with different instructions without modifying the task definition - -In these cases, the `instructions()` context manager provides a flexible way to provide ad-hoc guidance without having to modify the core task definition. - - -## Best Practices - -1. Keep instructions concise and clear. Overly verbose or complicated instructions can confuse the agent. -2. Be specific about what you want the agent to do differently. Vague instructions like "do better" are less helpful than targeted guidance like "use shorter sentences." -3. Consider the scope and permanence of your instructions. Applying permanent instructions at task creation will have a lasting impact, while temporary instructions at execution or interaction are more ephemeral. -4. Use instructions judiciously. Overusing ad-hoc instructions can lead to inconsistent agent behavior and make your workflow harder to understand and maintain. -5. Prefer the `instructions` parameter for truly permanent guidance. Only use the context manager when you need the added flexibility or temporary nature. - -By using the `instructions()` context manager appropriately, you can fine-tune agent behavior on the fly, adapting to the dynamic needs of your workflow without sacrificing the structure and reusability provided by well-defined tasks. \ No newline at end of file diff --git a/docs/patterns/result-types.mdx b/docs/patterns/result-types.mdx deleted file mode 100644 index 65188e5b..00000000 --- a/docs/patterns/result-types.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Typed Results ---- - - -Validate task outputs with structured result types. - - -In addition to providing discrete goals for your agents, ControlFlow tasks are designed to translate between the unstructured, conversational world of your AI agents and the structured, programmatic world of your application. The primary mechanism for this translation is the task's result, which should be a well-defined, validated output that can be used by other tasks or components in your workflow. - -ControlFlow allows you to specify the expected structure of a task's result using the `result_type` parameter. This ensures that the result conforms to a specific data schema, making it easier to work with and reducing the risk of errors in downstream tasks. - -In addition to the basic benefits of type safety and data integrity, result types also serve as a form of documentation for your agents, indicating exactly what kind of data they should expect to produce. - - -## Default Results are Strings - -By default, the `result_type` of a task is a string, meaning the agent can return any value that satisfies the task's objective. - -```python -import controlflow as cf - -task = cf.Task('Say hello in three languages') -print(task.run()) -``` - -In the above example, you may get a result like `"Hello; Hola; Bonjour"`, or even something more complex like `"'Hello!\n\nIn three languages, "Hello" is expressed as follows:\n\n1. English: Hello\n2. Spanish: Hola\n3. French: Bonjour'". - -While this flexibility can be useful in some cases, especially if this task's result is only being consumed by another ControlFlow task, it can also lead to ambiguity and errors if the agent produces unexpected output. - -## Returning Basic Types - -For simple task results, you can use any of Python's built-in types. - -Here's the above example, specifying that the result should be a list of strings. This guides the agent to give the result you probably expected (three strings, each representing a greeting in a different language): - -```python -import controlflow as cf - -task = cf.Task('Say hello in three languages', result_type=list[str]) -result = task.run() - -print(result) -assert isinstance(result, list) -assert len(result) == 3 -``` - -If your result is a number, you can specify the `result_type` as `int` or `float`: - -```python -import controlflow as cf - -task = cf.Task("Generate a random number", result_type=int) -result = task.run() - -print(result) -assert isinstance(result, int) -``` - -You can even use `bool` for tasks whose result is a simple true/false value: - -```python -import controlflow as cf - -task = cf.Task("Evaluate the statement: the earth is flat", result_type=bool) -result = task.run() - -assert result is False -``` - - -## Constrained Choices - -Sometimes you want to limit the possible results to a specific set of values, in order to label or classify a response. You can do this by specifying a list of allowed values for the result type: - -```python -import controlflow as cf - -task = cf.Task( - "Is this a book or a movie?", - result_type=["book", "movie"], - context=dict(title="Game of Thrones"), -) -result = task.run() - -assert result == "book" -``` - -## Pydantic Models - -For complex, structured results, you can use a Pydantic model as the `result_type`. Pydantic models provide a powerful way to define data schemas and validate input data. - -```python -import controlflow as cf -from pydantic import BaseModel, Field - -class ResearchReport(BaseModel): - title: str - summary: str - key_findings: list[str] = Field(min_items=3, max_items=10) - references: list[str] - -task = cf.Task( - "Generate a research report on quantum computing", - result_type=ResearchReport -) - -report = task.run() -print(f"Report title: {report.title}") -print(f"Number of key findings: {len(report.key_findings)}") -``` - -### Advanced Validation - -Pydantic allows for advanced validation using custom validators: - -```python -from pydantic import BaseModel, field_validator - -class SentimentAnalysis(BaseModel): - text: str - sentiment: float - - @field_validator('sentiment') - def check_sentiment_range(cls, v): - if not -1 <= v <= 1: - raise ValueError('Sentiment must be between -1 and 1') - return v - -task = cf.Task( - "Analyze sentiment of given text", - result_type=SentimentAnalysis, - context=dict(text="I love ControlFlow!"), -) - -analysis = task.run() -print(f"Sentiment: {analysis.sentiment}") -``` diff --git a/docs/patterns/tools.mdx b/docs/patterns/tools.mdx index 2383cfde..1b12be14 100644 --- a/docs/patterns/tools.mdx +++ b/docs/patterns/tools.mdx @@ -1,5 +1,5 @@ --- -title: Custom Tools +title: Tools --- @@ -10,7 +10,8 @@ A tool is a Python function that your agents can use to accomplish tasks. They l Tools can be simple utility functions, complex data processing operations, or API calls to external services. Here's a basic example of a tool for rolling dice: -```python + +```python Code import controlflow as cf import random @@ -18,15 +19,14 @@ def roll_die() -> int: """Roll a 6-sided diee.""" return random.randint(1, 6) -task = cf.Task( - objective="Roll a die, then roll again as many times as the first value", - instructions="Report the history of rolls", - tools=[roll_die], -) +rolls = cf.run("Roll a die four times", tools=[roll_die]) -task.run() +print(rolls) ``` - +```text Result +[3, 1, 4, 2] +``` + ## Best Practices Any Python function can be used as a tool, but you'll get the best performance with the following best practices. @@ -138,27 +138,26 @@ def get_weather(location: str) -> dict: LangChain has many [pre-built tools](https://python.langchain.com/v0.2/docs/integrations/tools/) that you can leverage. For example, here's how to get recent data from the web with DuckDuckGo. -Install dependencies with your preferred package manager: +First, install the dependencies: - -```bash pip -# ControlFlow requires Python 3.9 or greater +```bash pip install -U langchain-community, duckduckgo-search ``` -```bash uv -# ControlFlow requires Python 3.9 or greater -uv pip install -U langchain-community, duckduckgo-search -``` - +Then import the tool for use: -Then import the tool for use. -```python +Import the tool and assign it to an agent or task: +```python import controlflow as cf from langchain_community.tools import DuckDuckGoSearchRun -``` +agent = cf.Agent( + name="Timely agent", + description="An AI agent that knows current events", + tools=[DuckDuckGoSearchRun()], +) +``` ## Using Tools @@ -211,15 +210,6 @@ agent = cf.Agent( Using Pydantic models for tool return types helps ensure that the data returned by the tool is properly structured and validated. -To use the LangChain tool you imported earlier, provide it to a task like this: - -```python -agent = cf.Agent( - name="Timely agent", - description="An AI agent that knows current events", - tools=[DuckDuckGoSearchRun()], -) -``` ## Debugging Tools diff --git a/docs/patterns/user-input.mdx b/docs/patterns/user-input.mdx index 06aed84d..8a528061 100644 --- a/docs/patterns/user-input.mdx +++ b/docs/patterns/user-input.mdx @@ -1,9 +1,9 @@ --- -title: User Input +title: Interactivity --- -Chat with your AI agents by enabling user input. +Chat with your AI agents by setting `interactive=True`. ControlFlow agents are primarily designed to solve problems by working autonomously. However, there are situations where user input is necessary to guide the agent's decision-making process. By incorporating user input into your ControlFlow workflows, you can create more dynamic, interactive AI applications that adapt to user needs in real-time. @@ -13,72 +13,32 @@ By default, agents are not able to interact with users directly. To allow it, yo When `interactive=True`, the agent is given a tool for that it can use to send messages to the user. The user can then respond to these messages, and the agent can use the responses to make decisions or perform actions. By default, ControlFlow collects inputs directly in your terminal, but input can also be sent remotely via the Prefect API. -### Basic Inputs +## Interactive tasks -To enable user input for a task, set `interactive=True`: +If a task is marked as interactive, then all agents assigned to the task will be allowed to interact with the user. ```python import controlflow as cf -user_input_task = cf.Task( - "Get user's favorite color", - result_type=str, +color = cf.run( + "Get the user's favorite color", interactive=True, ) -color = user_input_task.run() print(f"The user's favorite color is: {color}") ``` -When the above task is run, the agent will prompt you to enter your favorite color. You can interact with the agent repeatedly; it will continue to prompt you until you provide a valid input so it can mark the task complete. +## Interactive agents -## Structured Inputs - -For more complex user inputs, you can use Pydantic models as the `result_type`. This ensures that the user input is properly structured and validated. See the [result types](#result-types) pattern for more details. +If an agent is marked as interactive, then it will be allowed to interact with the user in every task it's assigned to, even if the task is not marked as interactive. ```python import controlflow as cf -from pydantic import BaseModel - -class UserPreferences(BaseModel): - name: str - age: int - favorite_color: str -preferences_task = cf.Task( - "Get user preferences", - result_type=UserPreferences, +agent = cf.Agent( + "Chatbot", interactive=True, ) -preferences = preferences_task.run() -print(f"Hello, {preferences.name}!") -``` - -### Passing Inputs to Other Tasks - -You will frequently need to collect user input in one task (with `interactive=True`) and process that input in another task. You can pass the user input to subsequent tasks using the `context` parameter: - -```python -import controlflow as cf - -@cf.flow -def interactive_research_flow(): - topic_task = cf.Task( - "Get research topic from user", - result_type=str, - interactive=True, - instructions="If the user doesn't provide a topic, suggest 'AI'.", - ) - - # this task depends on the user input - research_task = cf.Task( - "Conduct research on the provided topic", - context={"topic": topic_task}, - result_type=str, - ) - - return research_task - -interactive_research_flow() +agent.run("Get the user's favorite color") ``` diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 9826c294..346fd75a 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -1,168 +1,239 @@ --- title: "Quickstart" -description: Build your first agentic workflow in under five minutes +description: Build your first agentic workflow in less than 30 seconds +icon: rocket --- -import Installation from '/snippets/installation.mdx'; -This quickstart is designed to **show** you how ControlFlow works, rather than **teach** you. -For a more detailed introduction, check out the full [tutorial](/tutorial). +Welcome to ControlFlow! This quickstart guide will walk you through the basics of using ControlFlow to create AI-powered workflows, culminating in a practical email processing application. - +## Install ControlFlow -## Define Tasks and Tools +Install ControlFlow with pip: -You define agentic workflows using tasks and tools. +```bash +pip install controlflow +``` + +Configure your preferred LLM provider. By default, ControlFlow uses OpenAI, so you'll need to set your API key: + +```bash +export OPENAI_API_KEY="your-api-key" +``` - - - Discrete objectives that you want an AI agent to complete, such as "write a poem" or "summarize this article". - - - Additional functionality that agents can use to complete tasks. - For example, a calculator or a database query tool. - - +To use another provider, see the docs on [configuring LLMs](/guides/llms). Note that one of the agents in this quickstart is configured with OpenAI's GPT-4o-mini model; you can change the model name to whatever you prefer. -Let's define a flow with two dependent tasks: +## Quickstart setup -1. Ask the user for input. -2. Roll some dice. +In this quickstart, we're going to create a simple email processing workflow. To make the quickstart code easier to read, we'll begin by creating a few example emails here. You should execute this code in your own Python interpreter before running the quickstart examples. -The `roll_dice` function is a tool that the second task uses to complete its objective. +Feel free to replace the emails with your own content to see how ControlFlow works with different inputs. ```python +emails = [ + "Hello, I need an update on the project status.", + "Subject: Exclusive offer just for you!", + "Urgent: Project deadline moved up by one week.", +] +``` + +## Running a simple task + +Let's start with the basics. The `cf.run()` function is the main entry point for ControlFlow. It creates a task, assigns it to the default agent, and runs it to completion. + +Here, we create a task that generates a simple email reply, and provide the content of an email as additional context. + + +```python Code import controlflow as cf -import random -# this function will be used as a tool by task 2 -def roll_dice(n: int) -> int: - '''Roll n dice''' - return [random.randint(1, 6) for _ in range(n)] +# Create a ControlFlow task to generate an reply +reply = cf.run( + "Write a polite reply to an email", + context=dict(email=emails[0]), +) -@cf.flow -def dice_flow(): +print(reply) +``` - # task 1: ask the user how many dice to roll - user_task = cf.Task( - "Ask the user how many dice to roll", - result_type=int, - interactive=True - ) +```text Result +Dear [Recipient's Name], - # task 2: roll the dice - dice_task = cf.Task( - "Roll the dice", - context=dict(n=user_task), - tools=[roll_dice], - result_type=list[int], - ) +Thank you for reaching out. I appreciate your patience. - return dice_task +I wanted to inform you that the project is progressing +well. We have completed several key milestones and are +currently working on the next phase. We anticipate +meeting our deadlines and will keep you updated with +any new developments. -result = dice_flow() -print(f"The result is: {result}") +Please feel free to reach out if you have any further +questions or need additional information. + +Best regards, + +[Your Name] ``` + + +## Creating specialized agents + +We've seen how to instruct the default agent to perform a task, but what if we want to perform a task that requires a specialized agent? We can create a new agent with its own model, instructions, or tools. + +In this example, we'll use an agent to categorize emails. This agent uses a smaller, faster model than the default agent, and it has specialized instructions for the task. -All tasks in a `@flow` function are run automatically when the function is called, but you can run tasks eagerly by calling `task.run()`. +Not using OpenAI? You can use any LangChain-compatible LLM here. Follow the instructions in the [LLM docs](/guides/llms) to learn more. -## Assign Agents to a Flow +In addition, note that we're instructing the agent to return a list of `EmailCategory` objects. ControlFlow will automatically convert the result into the appropriate data type. The `result_type` field supports any Pydantic-compatible type as well as lists of objects when you want the model to choose one of several options. + + +```python Code +from langchain_openai import ChatOpenAI +from enum import Enum + +# Create a specialized agent +classifier = cf.Agent( + name="Email Classifier", + model=ChatOpenAI(model="gpt-4o-mini"), + instructions="You are an expert at quickly classifying emails.", +) + +# Set up a ControlFlow task to classify emails +class EmailCategory(str, Enum): + IMPORTANT = "important" + SPAM = "spam" -Agents collaborate with each other using flows. +categories = cf.run( + 'Classify the emails', + result_type=list[EmailCategory], + agents=[classifier], + context=dict(emails=emails), +) - - - AI models that complete tasks in your workflows. - You can create agents that are optimized for particular tasks. - - - Containers that group tasks and let multiple agents share context while working towards a larger objective. - - +print(categories) +``` -Let's create three agents: a `writer`, an `editor`, and a `manager`. +```text Result +[ + EmailCategory.IMPORTANT, + EmailCategory.SPAM, + EmailCategory.IMPORTANT, +] +``` + -- The writer begins the workflow by drafting a paragraph. -- The editor refines the draft. -- The manager reviews the final result and approves if its criteria is met. +Our agent correctly identifies the first and third emails as important, and the second as spam. -The `approval_task` function is run at the end of each iteration to see if the manager approved the paragraph. -If not, the editing process continues until approval is granted. +## Composing tasks into a flow -```python -import controlflow as cf +Thus far, each of our tasks has run as a one-off operation. To create a more complex workflow, we can use a ControlFlow flow. -# Create three agents -writer = cf.Agent( - name="Writer", - description="An AI agent that writes paragraphs", -) +A flow provides a shared context and history for all agents, even across multiple tasks. The easiest way to create a flow is to use the `@cf.flow` decorator on a function with ControlFlow agents working inside it. -editor = cf.Agent( - name="Editor", - description="An AI agent that edits paragraphs for clarity and coherence", + +```python Code +import controlflow as cf +from langchain_openai import ChatOpenAI +from enum import Enum + +class EmailCategory(str, Enum): + IMPORTANT = "important" + SPAM = "spam" + +# Create agents +classifier = cf.Agent( + name="Email Classifier", + model=ChatOpenAI(model="gpt-4o-mini"), + instructions="You are an expert at quickly classifying emails. Always " + "respond with exactly one word: either 'important' or 'spam'." ) -manager = cf.Agent( - name="Manager", - description="An AI agent that manages the writing process", - instructions=""" - Your goal is to ensure the final paragraph meets high standards - of quality, clarity, and coherence. You should be strict in your - assessments and only approve the paragraph if it fully meets - these criteria. - """, +responder = cf.Agent( + name="Email Responder", + model=ChatOpenAI(model="gpt-4o"), + instructions="You are an expert at crafting professional email responses. " + "Your replies should be concise but friendly." ) +# Create the flow @cf.flow -def writing_flow(): - draft_task = cf.Task( - "Write a paragraph about the importance of AI safety", - agents=[writer], +def process_email(email_content: str): + # Classify the email + category = cf.run( + f"Classify this email", + result_type=EmailCategory, + agents=[classifier], + context=dict(email=email_content), ) - # we will continue editing until the manager approves the paragraph - approved = False - while not approved: - - edit_task = cf.Task( - "Edit the paragraph for clarity and coherence", - context=dict(draft=draft_task), - agents=[editor], + # If the email is important, write a response + if category == EmailCategory.IMPORTANT: + response = cf.run( + f"Write a response to this important email", + result_type=str, + agents=[responder], + context=dict(email=email_content), ) + return response + else: + print("No response needed for spam email.") - approval_task = cf.Task( - "Review the edited paragraph to see if it meets the quality standards", - result_type=bool, - context=dict(edit=edit_task), - agents=[manager], - ) - # eagerly run the approval task to see if the paragraph is approved - approved = approval_task.run() +# Run the flow on each email +for email in emails: + response = process_email(email) + print(response) +``` + +```text Response to Email 1 +Dear [Recipient's Name], + +I'm glad to report that the project is progressing well. We have +completed several key milestones and are currently working on the +next phase. We anticipate meeting our deadlines and will keep you +updated with any new developments. + +Please feel free to reach out if you have any further questions +or need additional information. - return approved, edit_task.result +Best regards, -approved, draft = writing_flow() -print(f'{"Approved" if approved else "Rejected"} paragraph:\n{draft}') +[Your Name] ``` -## Conclusion +```text Response to Email 2 + +``` + +```text Response to Email 3 +Dear [Recipient's Name], + +Thanks for letting me know. We'll make sure to adjust our plans accordingly. + +Best regards, +[Your Name] +``` + + +## Review + +Congratulations! You've completed the ControlFlow quickstart. You've learned how to: + +1. Run simple tasks with `cf.run()` +2. Create specialized agents for specific tasks +3. Compose tasks into complex workflows using flows -Here's what you learned today: +This email processing example showcases how ControlFlow can be used to create sophisticated AI-powered applications. As you continue to explore ControlFlow, you'll discover even more ways to leverage AI in your projects. -- **Tasks** are how you create goals for agents. -They have a `result_type` that determines the type of data they return. -They have a `context` that can include results of other tasks, enabling multi-step workflows. -If `tools` are provided, the agent can use them to complete the task. -- **Agents** are AI models that complete tasks and can be specialized with different capabilities, tools, instructions, and even LLM models. -Agents can be assigned to tasks. -- **Flows** can involve dynamic control flow like loops, based on eager task result. -They allow multiple agents to collaborate on a larger objective with shared history. +To dive deeper into ControlFlow's capabilities, check out: -## What's Next? +- [Agent concepts](/concepts/agents) to learn about creating powerful, specialized agents +- [Task concepts](/concepts/tasks) for more on defining and customizing tasks +- [Flow concepts](/concepts/flows) for advanced workflow orchestration techniques +- [Using tools](/guides/tools) to extend agent capabilities +- [Chatting with agents](/guides/chat) to create interactive experiences +- [Agent collaboration](/guides/collaboration) to coordinate agents across multiple tasks and flows -Congratulations, you've completed the ControlFlow quickstart! -To continue learning, please explore the full [ControlFlow tutorial](/tutorial). \ No newline at end of file +Happy engineering! \ No newline at end of file diff --git a/docs/snippets/installation.mdx b/docs/snippets/installation.mdx deleted file mode 100644 index d5912780..00000000 --- a/docs/snippets/installation.mdx +++ /dev/null @@ -1,38 +0,0 @@ -## Install ControlFlow - -Install ControlFlow with your preferred package manager: - - -```bash pip -# ControlFlow requires Python 3.9 or greater -pip install -U controlflow -``` - -```bash uv -# ControlFlow requires Python 3.9 or greater -uv pip install -U controlflow -``` - - -## Provide an API Key - -### OpenAI - -ControlFlow's default LLM is OpenAI's GPT-4o model, which provides excellent performance out of the box. To use it, you'll need to provide an API key as an environment variable: - -```bash -export OPENAI_API_KEY="your-api-key" -``` - -### Anthropic - -ControlFlow also ships with built-in support for Anthropic. To use an Anthropic model, you'll need to provide an API key as an environment variable and configure the default LLM: - -```bash -export ANTHROPIC_API_KEY="your-api-key" -export CONTROLFLOW_LLM_MODEL="anthropic/claude-3-5-sonnet-20240620" -``` - -### Other Providers - -ControlFlow supports many other LLM providers as well, though you'll need to install their respective packages and configure the default LLM appropriately. See the [LLM documentation](/guides/llms) for more information. diff --git a/docs/tutorial.mdx b/docs/tutorial.mdx deleted file mode 100644 index ec34fabc..00000000 --- a/docs/tutorial.mdx +++ /dev/null @@ -1,455 +0,0 @@ ---- -title: "Tutorial" ---- - -Welcome to ControlFlow! - - -ControlFlow is a declarative framework for building agentic workflows. That means that you define the objectives you want an AI agent to complete, and ControlFlow handles the rest. You can think of ControlFlow as a high-level orchestrator for AI agents, allowing you to focus on the logic of your application while ControlFlow manages the details of agent selection, data flow, and error handling. - -In this tutorial, we'll introduce the basics of ControlFlow, including tasks, flows, agents, and more. By the end, you'll have a solid understanding of how to create and run complex agentic workflows. - -The tutorial is divided into the following sections: -- [Hello, world](#hello-world): Your first task -- [Hello, user](#hello-user): Interacting with users -- [Hello, tasks](#hello-tasks): Chaining tasks together -- [Hello, flow](#hello-flow): Building a flow -- [Hello, agents](#hello-agents): Working with agents - ---- - -## Install ControlFlow - -To run the code in this tutorial, you'll need to install ControlFlow and configure API keys for your LLM provider. Please see the [installation](/installation) instructions for more information. - ---- -## Hello, world - -### Creating a task -The starting point of every agentic workflow is a `Task`. Each task represents an objective that we want an AI agent to complete. Let's create a simple task to say hello: - - -```python Code -import controlflow as cf - -hello_task = cf.Task("say hello") -``` - -```python Result ->> print(hello_task) - -Task( - objective='say hello', - status=, - result_type=, - result=None, - ... # other fields omitted -) -``` - - - -If you examine this `Task` object, you'll notice a few important things: it's in an `INCOMPLETE` state and while it has no `result` value, its `result_type` is a string. This means that the task has not been completed yet, but when it does, the result will be a string. - -### Running a task -To run a task to completion, call its `.run()` method. This will set up an agentic loop, assigning the task to an agent and waiting for it to complete. The agent's job is to provide a result that satisfies the task's requirements as quickly as possible. - -Let's run our task and examine it to see what happened: - - -```python Code -hello_task.run() -``` - -```python Result ->> print(hello_task) - -Task( - status=, - result='Hello', - ... # unchanged fields ommitted -) -``` - - - -The task is now in a `SUCCESSFUL` state, and its result has been updated to `"Hello"`. The agent successfully completed the task! - - -If you run the task a second time, it will immediately return the previous result. That's because this specific task has already been completed, so ControlFlow will use the existing result instead of running an agent again. - - -### Recap - -**What we learned** - -- Tasks represent objectives that we want an AI agent to complete. -- Each task has a `result_type` that specifies the datatype of the result we expect. -- Calling `task.run()` assigns the task to an agent, which is responsible for providing a result that satisfies the task's requirements. - - - ---- - -## Hello, user - -### User input - -By default, agents cannot interact with (human) users. ControlFlow is designed primarily to be an agentic workflow orchestrator, not a chatbot. However, there are times when user input is necessary to complete a task. In these cases, you can set the `interactive` parameter to `True` when creating a task. - -Let's create a task to ask the user for their name. We'll also create a Pydantic model to represent the user's name, which will allow us to apply complex typing or validation, if needed. - - -```python Code -import controlflow as cf -from typing import Optional -from pydantic import BaseModel - - -class Name(BaseModel): - first: str - last: Optional[str] - - -name_task = cf.Task("Get the user's name", result_type=Name, interactive=True) - - -name_task.run() -``` - -```python Result ->> print(name_task.result) - -Name(first='Marvin', last=None) -``` - - -If you run the above code, the agent will ask for your name in your terminal. You can respond with something like "My name is Marvin" or even refuse to respond. The agent will continue to prompt you until it has enough information to complete the task. - -This is the essence of an agentic workflow: you declare what you need, and the agent figures out how to get it. - - -### Failing a task - -In the previous example, if you refuse to provide your name a few times, your agent will eventually mark the task as failed. Agents only do this when they are unable to complete the task, and it's up to you to decide how to handle the failure. ControlFlow will raise a `ValueError` when a task fails that contains the reason for the failure. - - -### Recap - -**What we learned** - -- Setting `interactive=True` allows agents to interact with a user -- Pydantic models can be used to represent and validate complex result types -- Agents will continue to work until the task's requirements are met -- Agents can fail a task if they are unable to complete it - - ---- - -## Hello, tasks - -### Task dependencies - -So far, we've created and run tasks in isolation. However, agentic workflows are much more powerful when you use the results of one task to inform another. This allows you to build up complex behaviors by chaining tasks together, while still maintaining the benefits of structured, observable workflows. - -To see how this works, let's build a workflow that asks the user for their name, then uses that information to write them a personalized poem: - - -```python Code -import controlflow as cf -from pydantic import BaseModel - - -class Name(BaseModel): - first: str - last: str - - -name = cf.Task("Get the user's name", interactive=True, result_type=Name) -poem = cf.Task("Write a personalized poem", context=dict(name=name)) - - -poem.run() -``` - -```python Result ->> print(name.result) - -Name(first='Marvin', last='Robot') - ->> print(poem.result) - -""" -In a world of circuits and beams, -Marvin Robot dreams, -Of ones and zeros flowing free, -In a digital symphony. -""" -``` - - -In this example, we introduced a `context` parameter for the `poem` task. This parameter allows us to specify additional information that the agent can use to complete the task, which could include constant values or other tasks. If the context value includes a task, ControlFlow will automatically infer that the second task depends on the first. - -One benefit of this approach is that you can run any task without having to run its dependencies explicitly. ControlFlow will automatically run any dependencies before executing the task you requested. In the above example, we only ran the `poem` task, but ControlFlow automatically ran the `name` task first, then passed its result to the `poem` task's context. We can see that both tasks were successfully completed and have `result` values. - -### Custom tools - -For certain tasks, you may want your agents to use specialized tools or APIs to complete the task. - -To add tools to a task, pass a list of Python functions to the `tools` parameter of the task. These functions will be available to the agent when it runs the task, allowing it to use them to complete the task more effectively. The only requirement is that the functions are type-annotated and have a docstring, so that the agent can understand how to use them. - - -In this example, we create a task to roll various dice, and provide a `roll_die` function as a tool to the task, which the agent can use to complete the task: - - -```python Code -import controlflow as cf -import random - - -def roll_die(n:int) -> int: - '''Roll an n-sided die''' - return random.randint(1, n) - - -task = cf.Task( - 'Roll 5 dice, three with 6 sides and two with 20 sides', - tools=[roll_die], - result_type=list[int], -) - - -task.run() -``` - -```python Result ->> print(task.result) - -[3, 1, 2, 14, 8] -``` - - - -### Recap - -**What we learned** - -- You can provide additional information to a task using the `context` parameter, including constant values or other tasks -- If a task depends on another task, ControlFlow will automatically run the dependencies first -- You can provide custom tools to a task by passing a list of Python functions to the `tools` parameter - - - ---- - -## Hello, flow - -If `Tasks` are the building blocks of an agentic workflow, then `Flows` are the glue that holds them together. - -Each flow represents a shared history and context for all tasks and agents in a workflow. This allows you to maintain a consistent state across multiple tasks, even if they are not directly dependent on each other. - - -When you run a task outside a flow, as we did in the previous examples, ControlFlow automatically creates a flow context for that run. This is very convenient for testing and interactive use, but you can disable this behavior by setting `controlflow.settings.strict_flow_context=True`. - - -### The `@flow` decorator - -The simplest way to create a flow is by decorating a function with the `@flow` decorator. This will automatically create a shared flow context for all tasks inside the flow. Here's how we would rewrite the last example with a flow function: - - -```python Code -import controlflow as cf - - -@cf.flow -def hello_flow(poem_topic:str): - name = cf.Task("Get the user's name", interactive=True) - poem = cf.Task( - "Write a personalized poem about the provided topic", - context=dict(name=name, topic=poem_topic), - ) - return poem - - -hello_flow(poem_topic='AI') -``` - -```python Result ->> hello_flow(poem_topic='AI') - -""" -In circuits and in codes you dwell, -A marvel, Marvin, weaves a spell. -Through zeros, ones, and thoughts you fly, -An endless quest to reach the sky. -""" -``` - - -`hello_flow` is now a portable agentic workflow that can be run anywhere. On every call, it will automatically create a flow context for all tasks inside the flow, ensuring that they share the same state and history. - -### Eager execution -Notice that in the above flow, we never explicitly ran the `name` task, nor did we access its `result` attribute at the end. That's because `@flow`-decorated functions are executed eagerly by default. This means that when you call a flow function, all tasks inside the flow are run automatically and any tasks returned from the flow are replaced with their result values. - -Most of the time, you'll use eagerly-executed `@flows` and lazily-executed `Tasks` in your workflows. Eager flows are more intuitive and easier to work with, since they behave like normal functions, while lazy tasks allow the orchestrator to take advantage of observed dependencies to optimize task execution and agent selection, though it's possible to customize both behaviors. - -However, you'll frequently need a task's result inside your flow function. In this case, you can eagerly run the task by calling its `.run()` method, then use the task's `result` attribute as needed. - -In this example, we collect the user's height, then use it to determine if they are tall enough to receive a poem: - -```python -import controlflow as cf - -@cf.flow -def height_flow(poem_topic:str): - height = cf.Task("Get the user's height", interactive=True, result_type=int, instructions='convert the height to inches') - height.run() - - if height.result < 40: - raise ValueError("You must be at least 40 inches tall to receive a poem") - else: - return cf.Task( - "Write a poem for the user that takes their height into account", - context=dict(height=height, topic=poem_topic), - ) -``` - - -In this example, we introduced the `instructions` parameter for the `height` task. This parameter allows you to provide additional instructions to the agent about how to complete the task. - - -### Recap - - -**What we learned** - -- Flows provide a shared context for all tasks and agents inside the flow -- The `@flow` decorator creates a flow function that can be run anywhere -- By default, `@flow`-decorated functions are executed eagerly, meaning all tasks inside the flow are run automatically -- You can eagerly run a task inside a flow by calling its `.run()` method -- The `instructions` parameter allows you to provide additional instructions to the agent about how to complete the task - - ---- - -## Hello, agents - -You've made it through an entire tutorial on agentic workflows without ever encountering an actual agent! That's because ControlFlow abstracts away the complexities of agent selection and orchestration, allowing you to focus on the high-level logic of your application. - -But agents are the heart of ControlFlow, and understanding how to create and use them is essential to building sophisticated agentic workflows. - -### Creating an agent - -To create an agent, provide at least a name, as well as optional description, instructions, or tools. Here's an example of creating an agent that specializes in writing technical documentation: - -```python -import controlflow as cf - -docs_agent = cf.Agent( - name="DocsBot", - description="An agent that specializes in writing technical documentation", - instructions=( - "You are an expert in technical writing. You strive " - "to condense complex subjects into clear, concise language." - "Your goal is to provide the user with accurate, informative " - "documentation that is easy to understand." - ), -) -``` - - -What's the difference between a description and instructions? The description is a high-level overview of the agent's purpose and capabilities, while instructions provide detailed guidance on how the agent should complete a task. Agent descriptions can be seen by other agents, but instructions are private, which can affect how agents collaborate with each other. - - - - -### Assigning an agent to a task - -To use an agent to complete a task, assign the agent to the task's `agents` parameter. Here's an example of assigning the `docs_agent` to a task that requires writing a technical document: - -```python -technical_document = cf.Task( - "Write a technical document", - agents=[docs_agent], - instructions=( - "Write a technical document that explains agentic workflows." - ), -) -``` - -When you run the `technical_document` task, ControlFlow will automatically assign the `docs_agent` to complete the task. The agent will use the instructions provided to generate a technical document that meets the task's requirements. - -### Assigning multiple agents to a task - -You can assign multiple agents to a task by passing a list of agents to the `agents` parameter. This allows you to leverage the unique capabilities of different agents to complete a task more effectively. Here's an example of assigning an editor agent to review the technical document created by the `docs_agent`: - -```python -import controlflow as cf - -docs_agent = cf.Agent( - name="DocsBot", - description="An agent that specializes in writing technical documentation", - instructions=( - "You are an expert in technical writing. You strive " - "to condense complex subjects into clear, concise language." - "Your goal is to provide the user with accurate, informative " - "documentation that is easy to understand." - ), -) - -editor_agent = cf.Agent( - name="EditorBot", - description="An agent that specializes in editing technical documentation", - instructions=( - "You are an expert in grammar, style, and clarity. " - "Your goal is to review the technical document created by DocsBot, " - "ensuring that it is accurate, well-organized, and easy to read." - "You should output notes rather than rewriting the document." - ), -) - -technical_document = cf.Task( - "Write a technical document", - agents=[docs_agent, editor_agent], - instructions=( - "Write a technical document that explains agentic workflows." - "The docs agent should generate the document, " - "after which the editor agent should review and " - "edit it. Only the editor can mark the task as complete." - ), -) - -with cf.instructions('No more than 5 sentences per document'): - technical_document.run() -``` - -When you run the `technical_document` task, ControlFlow will assign both the `docs_agent` and the `editor_agent` to complete the task. The `docs_agent` will generate the technical document, and the `editor_agent` will review and edit the document to ensure its accuracy and readability. - -### Instructions - -In the above example, we also introduced the `instructions` context manager. This allows you to provide additional instructions to the agents about how to complete any task. As long as the context manager is active, any tasks/agents run within its scope will follow the provided instructions. Here, we use it to limit the length of the technical document to 5 sentences in order to keep the example manageable. - -### Recap - - -**What we learned** - -- Agents are autonomous entities that complete tasks on behalf of the user -- You can create an agent by providing a name, description, instructions, and LangChain model -- Assign an agent to a task by passing it to the task's `agents` parameter -- You can assign multiple agents to a task to have them collaborate - - - -## What's next? - -Congratulations, you've completed the ControlFlow tutorial! You've learned how to: -- Create tasks and run them to completion -- Interact with users and handle user input -- Chain tasks together to build complex workflows -- Create flows to maintain a shared context across multiple tasks -- Work with agents to complete tasks autonomously - -- Read more about core concepts like [tasks](/concepts/tasks), [flows](/concepts/flows), and [agents](/concepts/agents) -- Understand ControlFlow's [workflow APIs](/guides/apis) and [execution modes](/guides/execution-modes) -- Learn how to use [different LLM models](/guides/llms) diff --git a/docs/welcome.mdx b/docs/welcome.mdx index 4848ba68..cab33d65 100644 --- a/docs/welcome.mdx +++ b/docs/welcome.mdx @@ -1,6 +1,7 @@ --- title: ControlFlow sidebarTitle: Welcome +icon: slideshare --- ![ControlFlow Banner](/assets/brand/controlflow_banner.png) @@ -24,9 +25,9 @@ ControlFlow provides a structured, developer-focused framework for defining work This task-centric approach allows you to harness the power of AI for complex workflows while maintaining fine-grained control. By defining clear objectives and constraints for each task, you can balance AI autonomy with precise oversight, letting you build sophisticated AI-powered applications with confidence. -## Quick Start +## Quickstart -Here's a simple example of how to use ControlFlow: +Here's a simple but complete ControlFlow script that writes a poem: ```python Code @@ -54,13 +55,13 @@ The heart of what it means to be free. ``` -This single line of code creates a task, assigns it to an agent, and immediately executes it, returning the result. +The `run()` function is the main entry point for ControlFlow. This single line of code creates a task, assigns it to an agent, and immediately executes it, returning the result. You can completely customize those behaviors by learning more about [tasks](/concepts/tasks), [agents](/concepts/agents), and [flows](/concepts/flows). -## Key Features +## Key features Let's explore some of ControlFlow's key features: -### Structured Results +### Structured results ControlFlow tasks can return more than just text, including any structured data type supported by Pydantic: @@ -119,7 +120,7 @@ Category: Technology -### Custom Tools +### Custom tools Provide any Python function as a tool for agents to use: @@ -143,7 +144,7 @@ print(result) -### Multi-Agent Collaboration +### Multi-agent collaboration Assign multiple agents to a task to enable collaboration: @@ -172,7 +173,7 @@ Nature's arrow flies -### User Interaction +### User interaction Quickly give agents the ability to chat with users: diff --git a/src/controlflow/events/events.py b/src/controlflow/events/events.py index 508ffa1c..90b6910c 100644 --- a/src/controlflow/events/events.py +++ b/src/controlflow/events/events.py @@ -140,9 +140,9 @@ def to_messages(self, context: "CompileContext") -> list[BaseMessage]: ] else: return OrchestratorMessage( - prefix=f'The following {"failed " if self.tool_result.is_error else ""}' - f'tool result was received by "{self.agent.name}" with ID {self.agent.id}. ' - f'The tool call was: {self.tool_call}', + prefix=f'Agent "{self.agent.name}" with ID {self.agent.id} made a tool ' + f'call: {self.tool_call}. The tool{" failed and" if self.tool_result.is_error else " "} ' + f'produced this result:', content=self.tool_result.str_result, name=self.agent.name, ).to_messages(context) diff --git a/src/controlflow/orchestration/turn_strategies.py b/src/controlflow/orchestration/turn_strategies.py index 2a1efb19..4bfe1b25 100644 --- a/src/controlflow/orchestration/turn_strategies.py +++ b/src/controlflow/orchestration/turn_strategies.py @@ -51,7 +51,7 @@ def should_end_session(self) -> bool: def create_end_turn_tool(strategy: TurnStrategy) -> Tool: @tool def end_turn() -> str: - """End your turn.""" + """End your turn. Only use this tool if you have no other options.""" strategy.end_turn = True return "Turn ended. Another agent will be selected."