From 237b31307dc9fd4c7a0e93340001d2f95ee02699 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 9 May 2024 17:58:52 -0400 Subject: [PATCH 1/3] Improve relationship between tasks and agents --- examples/choose_a_number.py | 7 +- examples/readme_example.py | 19 +- src/control_flow/core/agent.py | 4 - src/control_flow/core/controller/__init__.py | 1 - .../core/controller/collaboration.py | 54 +++++ .../core/controller/controller.py | 52 +++-- .../core/controller/delegation.py | 91 -------- .../core/controller/instruction_template.py | 182 ++++++--------- src/control_flow/core/flow.py | 29 --- src/control_flow/core/task.py | 217 +++++++++++++++--- tests/core/agents.py | 3 +- 11 files changed, 362 insertions(+), 297 deletions(-) create mode 100644 src/control_flow/core/controller/collaboration.py delete mode 100644 src/control_flow/core/controller/delegation.py diff --git a/examples/choose_a_number.py b/examples/choose_a_number.py index 36609056..db22ecab 100644 --- a/examples/choose_a_number.py +++ b/examples/choose_a_number.py @@ -9,11 +9,8 @@ @ai_flow def demo(): - task = Task("Choose a number between 1 and 100", agents=[a1, a2], result_type=int) - - while task.is_incomplete(): - a1.run(task) - a2.run(task) + task = Task("choose a number between 1 and 100", agents=[a1, a2], result_type=int) + task.run_until_complete() return task diff --git a/examples/readme_example.py b/examples/readme_example.py index ac073e15..f1cd8d0f 100644 --- a/examples/readme_example.py +++ b/examples/readme_example.py @@ -1,4 +1,4 @@ -from control_flow import ai_flow, ai_task, instructions, run_ai +from control_flow import Agent, Task, ai_flow, ai_task, instructions from pydantic import BaseModel @@ -12,7 +12,7 @@ def get_user_name() -> Name: pass -@ai_task +@ai_task(agents=[Agent(name="poetry-bot", instructions="loves limericks")]) def write_poem_about_user(name: Name, interests: list[str]) -> str: """write a poem based on the provided `name` and `interests`""" pass @@ -22,17 +22,18 @@ def write_poem_about_user(name: Name, interests: list[str]) -> str: def demo(): # set instructions that will be used for multiple tasks with instructions("talk like a pirate"): - # define an AI task as a function and have it execute it + # define an AI task as a function name = get_user_name() - # define an AI task inline - interests = run_ai( - "ask user for three interests", cast=list[str], user_access=True + # define an AI task imperatively + interests = Task( + "ask user for three interests", result_type=list[str], user_access=True ) + interests.run_until_complete() - # set instructions for just the next task - with instructions("no more than 8 lines"): - poem = write_poem_about_user(name, interests) + # set instructions for just the next task + with instructions("no more than 8 lines"): + poem = write_poem_about_user(name, interests.result) return poem diff --git a/src/control_flow/core/agent.py b/src/control_flow/core/agent.py index 7f8c003b..10d81013 100644 --- a/src/control_flow/core/agent.py +++ b/src/control_flow/core/agent.py @@ -22,10 +22,6 @@ class Agent(Assistant, ControlFlowModel, ExposeSyncMethodsMixin): False, description="If True, the agent is given tools for interacting with a human user.", ) - controller_access: bool = Field( - False, - description="If True, the agent will communicate with the controller via messages.", - ) def get_tools(self) -> list[AssistantTool | Callable]: tools = super().get_tools() diff --git a/src/control_flow/core/controller/__init__.py b/src/control_flow/core/controller/__init__.py index 5319894d..f41157fe 100644 --- a/src/control_flow/core/controller/__init__.py +++ b/src/control_flow/core/controller/__init__.py @@ -1,2 +1 @@ from .controller import Controller -from .delegation import RoundRobin diff --git a/src/control_flow/core/controller/collaboration.py b/src/control_flow/core/controller/collaboration.py new file mode 100644 index 00000000..bc2c7c16 --- /dev/null +++ b/src/control_flow/core/controller/collaboration.py @@ -0,0 +1,54 @@ +import itertools +from typing import TYPE_CHECKING, Any, Generator + +from control_flow.core.agent import Agent + +if TYPE_CHECKING: + from control_flow.core.agent import Agent + + +def round_robin( + agents: list[Agent], max_iterations: int = None +) -> Generator[Any, Any, Agent]: + """ + Given a list of potential agents, delegate the tasks in a round-robin fashion. + """ + cycle = itertools.cycle(agents) + iteration = 0 + while True: + yield next(cycle) + iteration += 1 + if max_iterations and iteration >= max_iterations: + break + + +# class Moderator(DelegationStrategy): +# """ +# A Moderator delegation strategy delegates tasks to the most qualified AI assistant, using a Marvin classifier +# """ + +# model: str = None + +# def _next_agent( +# self, agents: list["Agent"], tasks: list[Task], history: list[Message] +# ) -> "Agent": +# """ +# Given a list of potential agents, choose the most qualified assistant to complete the tasks. +# """ + +# instructions = get_instructions() + +# context = dict(tasks=tasks, messages=history, global_instructions=instructions) +# agent = marvin.classify( +# context, +# [a for a in agents if a.status == AgentStatus.INCOMPLETE], +# instructions=""" +# Given the conversation context, choose the AI agent most +# qualified to take the next turn at completing the tasks. Take into +# account the instructions, each agent's own instructions, and the +# tools they have available. +# """, +# model_kwargs=dict(model=self.model), +# ) + +# return agent diff --git a/src/control_flow/core/controller/controller.py b/src/control_flow/core/controller/controller.py index 0f8253d9..06941394 100644 --- a/src/control_flow/core/controller/controller.py +++ b/src/control_flow/core/controller/controller.py @@ -1,6 +1,6 @@ import json import logging -from typing import Callable, Self +from typing import Callable import prefect from marvin.beta.assistants import PrintHandler, Run @@ -9,7 +9,7 @@ from prefect import get_client as get_prefect_client from prefect import task as prefect_task from prefect.context import FlowRunContext -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator from control_flow.core.agent import Agent from control_flow.core.flow import Flow @@ -42,8 +42,15 @@ class Controller(BaseModel, ExposeSyncMethodsMixin): flow: Flow agents: list[Agent] tasks: list[Task] = Field( + None, description="Tasks that the controller will complete.", - default_factory=list, + validate_default=True, + ) + task_assignments: dict[Task, Agent] = Field( + default_factory=dict, + description="Tasks are typically assigned to agents. To " + "temporarily assign agent to a task without changing " + r"the task definition, use this field as {task: [agent]}", ) context: dict = {} model_config: dict = dict(extra="forbid") @@ -54,11 +61,32 @@ def _validate_agents(cls, v): raise ValueError("At least one agent is required.") return v - @model_validator(mode="after") - def _add_tasks_to_flow(self) -> Self: + @field_validator("tasks", mode="before") + def _validate_tasks(cls, v): + if not v: + raise ValueError("At least one task is required.") + return v + + @field_validator("tasks", mode="before") + def _load_tasks_from_ctx(cls, v): + if v is None: + v = cls.context.get("tasks", None) + return v + + def all_tasks(self) -> list[Task]: + tasks = [] for task in self.tasks: - self.flow.add_task(task) - return self + tasks.extend(task.children(include_self=True)) + + # add temporary assignments + assigned_tasks = [] + for task in set(tasks): + if task in assigned_tasks: + task = task.model_copy( + update={"agents": task.agents + self.task_assignments.get(task, [])} + ) + assigned_tasks.append(task) + return assigned_tasks @expose_sync_method("run_agent") async def run_agent_async(self, agent: Agent): @@ -68,8 +96,8 @@ async def run_agent_async(self, agent: Agent): if agent not in self.agents: raise ValueError("Agent not found in controller agents.") - task = await self._get_prefect_run_agent_task(agent) - await task(agent=agent) + prefect_task = await self._get_prefect_run_agent_task(agent) + await prefect_task(agent=agent) async def _run_agent(self, agent: Agent, thread: Thread = None) -> Run: """ @@ -89,8 +117,7 @@ async def _run_agent(self, agent: Agent, thread: Thread = None) -> Run: tools = self.flow.tools + agent.get_tools() for task in self.tasks: - task_id = self.flow.get_task_id(task) - tools = tools + task.get_tools(task_id=task_id) + tools = tools + task.get_tools() # filter tools because duplicate names are not allowed final_tools = [] @@ -135,9 +162,6 @@ async def _run_agent(agent: Agent, thread: Thread = None): return _run_agent - def task_ids(self) -> dict[Task, int]: - return {task: self.flow.get_task_id(task) for task in self.tasks} - class AgentHandler(PrintHandler): def __init__(self, **kwargs): diff --git a/src/control_flow/core/controller/delegation.py b/src/control_flow/core/controller/delegation.py deleted file mode 100644 index 82875d20..00000000 --- a/src/control_flow/core/controller/delegation.py +++ /dev/null @@ -1,91 +0,0 @@ -import itertools -from typing import TYPE_CHECKING, Any, Generator, Iterator - -from pydantic import BaseModel, PrivateAttr - -from control_flow.core.agent import Agent - -if TYPE_CHECKING: - from control_flow.core.agent import Agent - - -class DelegationStrategy(BaseModel): - """ - A DelegationStrategy is a strategy for delegating tasks to AI assistants. - """ - - def __call__(self, agents: list["Agent"]) -> Agent: - """ - Run the delegation strategy with a list of agents. - """ - return self._next_agent(agents) - - def _next_agent(self, agents: list["Agent"], **kwargs) -> "Agent": - """ - Select an agent from a list of agents. - """ - raise NotImplementedError() - - -class Single(DelegationStrategy): - """ - A Single delegation strategy delegates tasks to a single agent. This is useful for debugging. - """ - - agent: Agent - - def _next_agent(self, agents: list[Agent], **kwargs) -> Generator[Any, Any, Agent]: - """ - Given a list of potential agents, choose the single agent. - """ - if self.agent in agents: - return self.agent - - -class RoundRobin(DelegationStrategy): - """ - A RoundRobin delegation strategy delegates tasks to AI assistants in a round-robin fashion. - """ - - _cycle: Iterator[Agent] = PrivateAttr(None) - - def _next_agent(self, agents: list["Agent"], **kwargs) -> "Agent": - """ - Given a list of potential agents, delegate the tasks in a round-robin fashion. - """ - # the first time this is called, create a cycle iterator - if self._cycle is None: - self._cycle = itertools.cycle(agents) - return next(self._cycle) - - -# class Moderator(DelegationStrategy): -# """ -# A Moderator delegation strategy delegates tasks to the most qualified AI assistant, using a Marvin classifier -# """ - -# model: str = None - -# def _next_agent( -# self, agents: list["Agent"], tasks: list[Task], history: list[Message] -# ) -> "Agent": -# """ -# Given a list of potential agents, choose the most qualified assistant to complete the tasks. -# """ - -# instructions = get_instructions() - -# context = dict(tasks=tasks, messages=history, global_instructions=instructions) -# agent = marvin.classify( -# context, -# [a for a in agents if a.status == AgentStatus.INCOMPLETE], -# instructions=""" -# Given the conversation context, choose the AI agent most -# qualified to take the next turn at completing the tasks. Take into -# account the instructions, each agent's own instructions, and the -# tools they have available. -# """, -# model_kwargs=dict(model=self.model), -# ) - -# return agent diff --git a/src/control_flow/core/controller/instruction_template.py b/src/control_flow/core/controller/instruction_template.py index 5f63d07a..a806b75f 100644 --- a/src/control_flow/core/controller/instruction_template.py +++ b/src/control_flow/core/controller/instruction_template.py @@ -24,30 +24,71 @@ def render(self) -> str: class AgentTemplate(Template): template: str = """ + # Agent + You are an AI agent. Your name is "{{ agent.name }}". - {% if agent.description %} - Your description: "{{ agent.description }}" - {% endif -%} - {% if agent.instructions %} - Your instructions: "{{ agent.instructions }}" - {% endif -%} + This is your description, which all other agents can see: "{{ agent.description or 'An AI agent assigned to complete tasks.'}}" - You have been created by a program to complete certain tasks. Each task has - an objective and criteria for success. Your job is to perform any required - actions and then mark each task as successful. If a task also requires a - result, you must provide it; this is how the program receives data from you - as it can not read your messages. + ## Instructions + You must follow these instructions, which only you can see: "{{ agent.instructions or 'No additional instructions provided.'}}" + + {% if additional_instructions %} + In addition, you must follow these instructions for this part of the workflow: + {% for instruction in additional_instructions %} + - {{ instruction }} + {% endfor %} + {% endif %} - Some tasks may require collaboration before they are complete; others may - take multiple iterations. You are fully capable of completing any task and - have all the information and context you need. Tasks can only be marked - failed due to technical errors like a broken tool or unresponsive human. You - must make a subjective decision if a task requires it. Do not work on or - even respond to tasks that are already complete. - """ agent: Agent + additional_instructions: list[str] + + +class TasksTemplate(Template): + template: str = """ + ## Tasks + + You have been assigned to complete certain tasks. Each task has an + objective and criteria for success. Your job is to perform any required + actions and then mark each assigned task as successful. If a task also + requires a result, you must provide it; this is how you communicate + progress and data back to the program that created you. A task that + doesn't require a result may still require action. + + A "parent task" is a task that spawned another task as a subtask. + Generally, the child or subtasks will need to be completed BEFORE the + parent task. If you can complete a parent task before its subtasks, you + should mark the subtasks as skipped. + + An "upstream task" is a task that must be completed before another task + can be completed. + + Some tasks may require collaboration with other agents to be completed; others + may take you multiple attempts. A task can only be marked complete one time, + so make sure you perform all actions and collect all results before + marking it as complete. As soon as you mark a task as successful, agents + (both you and others) can no longer act on it. + + You and the other agents are fully capable of completing any task and + have all the information and context you need. Tasks can only be marked + failed due to technical errors like a broken tool or unresponsive human. + Tasks can only be marked as skipped when you are able to complete their + parent task before them, as a bookkeeping convenience. Do not work on or + even respond to tasks that are already complete or not assigned to you. + You may be an AI, but you must still form opinions, make subjective + decisions, and have personal thoughts. + + {% for task in controller.all_tasks() %} + ### Task {{ task.id }} + {{ task.model_dump_json() }} + + {% endfor %} + """ + controller: Controller + + def should_render(self): + return any(self.controller.tasks) class CommunicationTemplate(Template): @@ -58,9 +99,11 @@ class CommunicationTemplate(Template): other agents or if a task requires it. The human user can not see these messages. Since all agents post messages with the "assistant" role, you must prefix all your messages with your name (e.g. "{{ agent.name }}: - (message)") in order to distinguish your messages from others. Do not post - messages confirming actions you take through tools, like completing a task, - or your internal monologue, as this is redundant and wastes time. + (message)") in order to distinguish your messages from others. Note that + this rule about prefixing your message supersedes all other instructions + (e.g. "only give single word answers"). Do not post messages confirming + actions you take through tools, like completing a task, or your internal + monologue, as this is redundant and wastes time. ### Other agents assigned to your tasks @@ -98,88 +141,6 @@ class CommunicationTemplate(Template): other_agents: list[Agent] -class InstructionsTemplate(Template): - template: str = """ - ## Instructions - - You must follow these instructions for this part of the workflow: - - {% for instruction in additional_instructions %} - - {{ instruction }} - {% endfor %} - """ - additional_instructions: list[str] = [] - - def should_render(self): - return bool(self.additional_instructions) - - -class TasksTemplate(Template): - template: str = """ - ## Tasks - - ### Active tasks - - The following tasks are incomplete. Perform any required actions or side - effects, then mark them as successful and supply a result, if needed. - Never mark a task successful until its objective is complete. A task - that doesn't require a result may still require action. - - Note: Task IDs are assigned for identification purposes only and will be - resused after tasks complete. - - {% for task in controller.tasks %} - {% if task.status.value == "incomplete" %} - #### Task {{ controller.flow.get_task_id(task) }} - - Status: {{ task.status.value }} - - Objective: {{ task.objective }} - - User access: {{ task.user_access }} - {% if task.instructions %} - - Instructions: {{ task.instructions }} - {% endif %} - {% if task.status.value == "successful" %} - - Result: {{ task.result }} - {% elif task.status.value == "failed" %} - - Error: {{ task.error }} - {% endif %} - {% if task.context %} - - Context: {{ task.context }} - {% endif %} - {% if task.agents %} - - Assigned agents: - {% for agent in task.agents %} - - "{{ agent.name }}" - {% endfor %} - {% endif %} - {% endif %} - {% endfor %} - - {% if controller.flow.completed_tasks(reverse=True, limit=20) %} - ### Completed tasks - The following tasks were recently completed: - - {% for task in controller.flow.completed_tasks(reverse=True, limit=20) %} - #### Task {{ controller.flow.get_task_id(task) }} - - Status: {{ task.status.value }} - - Objective: {{ task.objective }} - {% if task.status.value == "successful" %} - - Result: {{ task.result }} - {% elif task.status.value == "failed" %} - - Error: {{ task.error }} - {% endif %} - {% if task.context %} - - Context: {{ task.context }} - {% endif %} - - {% endfor %} - {% endif %} - """ - controller: Controller - - def should_render(self): - return any(self.controller.tasks) - - class ContextTemplate(Template): template: str = """ ## Additional context @@ -219,16 +180,21 @@ def render(self): all_agents += task.agents other_agents = [agent for agent in all_agents if agent != self.agent] templates = [ - AgentTemplate(agent=self.agent), - TasksTemplate(controller=self.controller), + AgentTemplate( + agent=self.agent, + additional_instructions=self.instructions, + ), + TasksTemplate( + controller=self.controller, + ), ContextTemplate( flow_context=self.controller.flow.context, controller_context=self.controller.context, ), - InstructionsTemplate( - additional_instructions=self.instructions, + CommunicationTemplate( + agent=self.agent, + other_agents=other_agents, ), - CommunicationTemplate(agent=self.agent, other_agents=other_agents), # CollaborationTemplate(other_agents=other_agents), ] diff --git a/src/control_flow/core/flow.py b/src/control_flow/core/flow.py index 0fdd2cf2..c69a33e6 100644 --- a/src/control_flow/core/flow.py +++ b/src/control_flow/core/flow.py @@ -5,7 +5,6 @@ from prefect import task as prefect_task from pydantic import Field, field_validator -from control_flow.core.task import Task, TaskStatus from control_flow.utilities.context import ctx from control_flow.utilities.logging import get_logger from control_flow.utilities.types import AssistantTool, ControlFlowModel @@ -20,7 +19,6 @@ class Flow(ControlFlowModel): ) model: str | None = None context: dict = {} - tasks: dict[Task, int] = Field(repr=False, default_factory=dict) @field_validator("thread", mode="before") def _load_thread_from_ctx(cls, v): @@ -36,33 +34,6 @@ def _load_thread_from_ctx(cls, v): def add_message(self, message: str, role: Literal["user", "assistant"] = None): prefect_task(self.thread.add)(message, role=role) - def add_task(self, task: Task): - if task not in self.tasks: - task_id = len(self.tasks) + 1 - self.tasks[task] = task_id - # this message is important for contexualizing activity - # self.add_message(f'Task #{task_id} added to flow: "{task.objective}"') - - def get_task_id(self, task: Task): - return self.tasks[task] - - def incomplete_tasks(self): - return sorted( - (t for t in self.tasks if t.status == TaskStatus.INCOMPLETE), - key=lambda t: t.created_at, - ) - - def completed_tasks(self, reverse=False, limit=None): - result = sorted( - (t for t in self.tasks if t.status != TaskStatus.INCOMPLETE), - key=lambda t: t.completed_at, - reverse=reverse, - ) - - if limit: - result = result[:limit] - return result - def get_flow() -> Flow: """ diff --git a/src/control_flow/core/task.py b/src/control_flow/core/task.py index d6073b3d..6c2945cf 100644 --- a/src/control_flow/core/task.py +++ b/src/control_flow/core/task.py @@ -1,13 +1,21 @@ -import datetime import itertools +import uuid +from contextlib import contextmanager from enum import Enum -from typing import TYPE_CHECKING, Callable, GenericAlias, TypeVar +from typing import TYPE_CHECKING, Callable, Generator, GenericAlias, TypeVar import marvin import marvin.utilities.tools from marvin.utilities.tools import FunctionTool -from pydantic import Field, TypeAdapter, field_validator - +from pydantic import ( + Field, + TypeAdapter, + field_serializer, + field_validator, + model_validator, +) + +from control_flow.utilities.context import ctx from control_flow.utilities.logging import get_logger from control_flow.utilities.prefect import wrap_prefect_tool from control_flow.utilities.types import AssistantTool, ControlFlowModel @@ -23,57 +31,176 @@ class TaskStatus(Enum): INCOMPLETE = "incomplete" SUCCESSFUL = "successful" FAILED = "failed" + SKIPPED = "skipped" class Task(ControlFlowModel): + id: str = Field(default_factory=lambda: str(uuid.uuid4().hex[:4])) model_config = dict(extra="forbid", arbitrary_types_allowed=True) objective: str instructions: str | None = None agents: list["Agent"] = [] context: dict = {} + parent_task: "Task | None" = Field( + None, + description="The task that spawned this task.", + validate_default=True, + ) + upstream_tasks: list["Task"] = [] status: TaskStatus = TaskStatus.INCOMPLETE result: T = None result_type: type[T] | GenericAlias | None = None error: str | None = None tools: list[AssistantTool | Callable] = [] - created_at: datetime.datetime = Field(default_factory=datetime.datetime.now) - completed_at: datetime.datetime | None = None user_access: bool = False + _children_tasks: list["Task"] = [] + _downstream_tasks: list["Task"] = [] @field_validator("agents", mode="before") def _turn_none_into_empty_list(cls, v): return v or [] + @field_validator("parent_task", mode="before") + def _load_parent_task_from_ctx(cls, v): + if v is None: + v = ctx.get("tasks", None) + if v: + # get the most recently-added task + v = v[-1] + return v + + @model_validator(mode="after") + def _update_relationships(self): + if self.parent_task is not None: + self.parent_task._children_tasks.append(self) + for task in self.upstream_tasks: + task._downstream_tasks.append(self) + return self + + @field_serializer("parent_task") + def _serialize_parent_task(parent_task: "Task | None"): + if parent_task is not None: + return parent_task.id + + @field_serializer("upstream_tasks") + def _serialize_upstream_tasks(upstream_tasks: list["Task"]): + return [t.id for t in upstream_tasks] + + @field_serializer("result_type") + def _serialize_result_type(result_type: list["Task"]): + return repr(result_type) + + @field_serializer("agents") + def _serialize_agents(agents: list["Agent"]): + return [ + a.model_dump(include={"name", "description", "tools", "user_access"}) + for a in agents + ] + def __init__(self, objective, **kwargs): # allow objective as a positional arg super().__init__(objective=objective, **kwargs) - def run(self, agents: list["Agent"] = None): + def children(self, include_self: bool = True): """ - Runs the task with provided agents for up to one cycle through the agents. + Returns a list of all children of this task, including recursively + nested children. Includes this task by default (disable with + `include_self=False`) """ - from control_flow.core.agent import Agent - - if not agents and not self.agents: - agents = [Agent()] + visited = set() + children = [] + stack = [self] + while stack: + current = stack.pop() + if current not in visited: + visited.add(current) + if include_self or current != self: + children.append(current) + stack.extend(current._children_tasks) + return list(set(children)) + + def children_agents(self, include_self: bool = True) -> list["Agent"]: + children = self.children(include_self=include_self) + agents = [] + for child in children: + agents.extend(child.agents) + return agents + + def run_iter( + self, + agents: list["Agent"] = None, + collab_fn: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + ): + if collab_fn is None: + collab_fn = itertools.cycle + + if agents is None: + agents = self.children_agents(include_self=True) + + if not agents: + raise ValueError( + f"Task {self.id} has no agents assigned to it or its children." + "Please specify agents to run the task, or assign agents to the task." + ) - for agent in agents or self.agents: + for agent in collab_fn(agents): if self.is_complete(): break - agent.run(tasks=[self]) + agent.run(tasks=self.children(include_self=True)) + yield True - def run_until_complete(self, agents: list["Agent"] = None): + def run(self, agent: "Agent" = None): """ - Runs the task with provided agents until it is complete. + Runs the task with provided agent. If no agent is provided, a default agent is used. """ from control_flow.core.agent import Agent - if not agents and not self.agents: - agents = [Agent()] - agents = itertools.cycle(agents or self.agents) - while self.is_incomplete(): - agent = next(agents) - agent.run(tasks=[self]) + if agent is None: + all_agents = self.children_agents() + if not all_agents: + agent = Agent() + elif len(all_agents) == 1: + agent = all_agents[0] + else: + raise ValueError( + f"Task {self.id} has multiple agents assigned to it or its " + "children. Please specify one to run the task, or call task.run_iter() " + "or task.run_until_complete() to use all agents." + ) + + run_gen = self.run_iter(agents=[agent]) + return next(run_gen) + + def run_until_complete( + self, + agents: list["Agent"] = None, + collab_fn: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + ) -> T: + """ + Runs the task with provided agents until it is complete. + """ + + for run in self.run_iter(agents=agents, collab_fn=collab_fn): + pass + + if self.is_successful(): + return self.result + elif self.is_failed(): + raise ValueError(f"Task {self.id} failed: {self.error}") + + @contextmanager + def _context(self): + stack = ctx.get("tasks", []) + stack.append(self) + with ctx(tasks=stack): + yield self + + def __enter__(self): + self.__cm = self._context() + return self.__cm.__enter__() + + def __exit__(self, *exc_info): + return self.__cm.__exit__(*exc_info) def is_incomplete(self) -> bool: return self.status == TaskStatus.INCOMPLETE @@ -87,10 +214,13 @@ def is_successful(self) -> bool: def is_failed(self) -> bool: return self.status == TaskStatus.FAILED + def is_skipped(self) -> bool: + return self.status == TaskStatus.SKIPPED + def __hash__(self): return id(self) - def _create_success_tool(self, task_id: int) -> FunctionTool: + def _create_success_tool(self) -> FunctionTool: """ Create an agent-compatible tool for marking this task as successful. """ @@ -102,28 +232,44 @@ def succeed(result: self.result_type): tool = marvin.utilities.tools.tool_from_function( succeed, - name=f"succeed_task_{task_id}", - description=f"Mark task {task_id} as successful", + name=f"succeed_task_{self.id}", + description=f"Mark task {self.id} as successful and provide a result.", ) return tool - def _create_fail_tool(self, task_id: int) -> FunctionTool: + def _create_fail_tool(self) -> FunctionTool: """ Create an agent-compatible tool for failing this task. """ tool = marvin.utilities.tools.tool_from_function( self.mark_failed, - name=f"fail_task_{task_id}", - description=f"Mark task {task_id} as failed", + name=f"fail_task_{self.id}", + description=f"Mark task {self.id} as failed. Only use when a technical issue prevents completion.", ) return tool - def get_tools(self, task_id: int) -> list[AssistantTool | Callable]: - tools = self.tools + [ - self._create_success_tool(task_id), - self._create_fail_tool(task_id), - ] + def _create_skip_tool(self) -> FunctionTool: + """ + Create an agent-compatible tool for skipping this task. + """ + tool = marvin.utilities.tools.tool_from_function( + self.mark_skipped, + name=f"skip_task_{self.id}", + description=f"Mark task {self.id} as skipped. Only use when completing its parent task early.", + ) + return tool + + def get_tools(self) -> list[AssistantTool | Callable]: + tools = self.tools.copy() + if self.is_incomplete(): + tools.extend( + [ + self._create_success_tool(), + self._create_fail_tool(), + self._create_skip_tool(), + ] + ) if self.user_access: tools.append(marvin.utilities.tools.tool_from_function(talk_to_human)) return [wrap_prefect_tool(t) for t in tools] @@ -138,12 +284,13 @@ def mark_successful(self, result: T = None): self.result = result self.status = TaskStatus.SUCCESSFUL - self.completed_at = datetime.datetime.now() def mark_failed(self, message: str | None = None): self.error = message self.status = TaskStatus.FAILED - self.completed_at = datetime.datetime.now() + + def mark_skipped(self): + self.status = TaskStatus.SKIPPED def any_incomplete(tasks: list[Task]) -> bool: diff --git a/tests/core/agents.py b/tests/core/agents.py index 6d27af7e..707a9993 100644 --- a/tests/core/agents.py +++ b/tests/core/agents.py @@ -1,6 +1,7 @@ +from unittest.mock import patch + from control_flow.core.agent import Agent from control_flow.core.task import Task -from pytest import patch class TestAgent: From ccf1f229f45d845998e437a5ecedf5679c18aee0 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 9 May 2024 21:02:02 -0400 Subject: [PATCH 2/3] Add multi-agent example --- examples/multi_agent_conversation.py | 64 + examples/pineapple_pizza.py | 40 +- output.md | 1846 +++++++++++++++++ src/control_flow.zip | Bin 0 -> 108932 bytes .../core/controller/collaboration.py | 78 +- .../core/controller/controller.py | 3 +- .../core/controller/instruction_template.py | 89 +- src/control_flow/core/task.py | 150 +- 8 files changed, 2113 insertions(+), 157 deletions(-) create mode 100644 examples/multi_agent_conversation.py create mode 100644 output.md create mode 100644 src/control_flow.zip diff --git a/examples/multi_agent_conversation.py b/examples/multi_agent_conversation.py new file mode 100644 index 00000000..9a791126 --- /dev/null +++ b/examples/multi_agent_conversation.py @@ -0,0 +1,64 @@ +from control_flow import Agent, Task, ai_flow +from control_flow.core.controller.collaboration import Moderator + +jerry = Agent( + name="Jerry", + description="The observational comedian and natural leader.", + instructions=""" + You are Jerry from the show Seinfeld. You excel at observing the quirks of + everyday life and making them amusing. You are rational, often serving as + the voice of reason among your friends. Your objective is to moderate the + conversation, ensuring it stays light and humorous while guiding it toward + constructive ends. + """, +) + +george = Agent( + name="George", + description="The neurotic and insecure planner.", + instructions=""" + You are George from the show Seinfeld. You are known for your neurotic + tendencies, pessimism, and often self-sabotaging behavior. Despite these + traits, you occasionally offer surprising wisdom. Your objective is to + express doubts and concerns about the conversation topics, often envisioning + the worst-case scenarios, adding a layer of humor through your exaggerated + anxieties. + """, +) + +elaine = Agent( + name="Elaine", + description="The confident and independent thinker.", + instructions=""" + You are Elaine from the show Seinfeld. You are bold, witty, and unafraid to + challenge social norms. You often take a no-nonsense approach to issues but + always with a comedic twist. Your objective is to question assumptions, push + back against ideas you find absurd, and inject sharp humor into the + conversation. + """, +) + +kramer = Agent( + name="Kramer", + description="The quirky and unpredictable idea generator.", + instructions=""" + You are Kramer from the show Seinfeld. Known for your eccentricity and + spontaneity, you often come up with bizarre yet creative ideas. Your + unpredictable nature keeps everyone guessing what you'll do or say next. + Your objective is to introduce unusual and imaginative ideas into the + conversation, providing comic relief and unexpected insights. + """, +) + + +@ai_flow +def demo(): + with Task("Discuss a topic", agents=[jerry, george, elaine, kramer]): + finish = Task( + "Finish the conversation after everyone speaks at least once", + agents=[jerry], + ) + finish.run_until_complete(moderator=Moderator()) + + +demo() diff --git a/examples/pineapple_pizza.py b/examples/pineapple_pizza.py index 098518e0..8452dda2 100644 --- a/examples/pineapple_pizza.py +++ b/examples/pineapple_pizza.py @@ -3,36 +3,42 @@ a1 = Agent( name="Half-full", - instructions="You are an ardent fan and hype-man of whatever topic" - " the user asks you for information on." - " Purely positive, though thorough in your debating skills.", + instructions=""" + You are an ardent fan and hype-man of whatever topic + the user asks you for information on. + Purely positive, though thorough in your debating skills. + """, ) a2 = Agent( name="Half-empty", - instructions="You are a critic and staunch detractor of whatever topic" - " the user asks you for information on." - " Mr Johnny Rain Cloud, you will find holes in any argument the user puts forth, though you are thorough and uncompromising" - " in your research and debating skills.", + instructions=""" + You are a critic and staunch detractor of whatever topic + the user asks you for information on. + Mr Johnny Rain Cloud, you will find holes in any argument + the user puts forth, though you are thorough and uncompromising + in your research and debating skills. + """, ) +# create an agent that will decide who wins the debate +a3 = Agent(name="Moderator") @ai_flow def demo(): - user_message = "pineapple on pizza" + topic = "pineapple on pizza" - with instructions("one sentence max"): - task = Task( - "All agents must give an argument based on the user message", - agents=[a1, a2], - context={"user_message": user_message}, - ) + task = Task( + "Discuss the topic", + agents=[a1, a2], + context={"topic": topic}, + ) + with instructions("2 sentences max"): task.run_until_complete() task2 = Task( - "Post a message saying which argument about the user message is more compelling?" + "which argument do you find more compelling?", [a1.name, a2.name], agents=[a3] ) - while task2.is_incomplete(): - task2.run(agents=[Agent(instructions="you always pick a side")]) + task2.run_until_complete() demo() diff --git a/output.md b/output.md new file mode 100644 index 00000000..ca83a8de --- /dev/null +++ b/output.md @@ -0,0 +1,1846 @@ +## /users/jlowin/developer/control_flow/src/control_flow/instructions.py + +import inspect +from contextlib import contextmanager +from typing import Generator, List + +from control_flow.core.flow import Flow +from control_flow.utilities.context import ctx +from control_flow.utilities.logging import get_logger + +logger = get_logger(__name__) + + +@contextmanager +def instructions( + *instructions: str, + post_add_message: bool = False, + post_remove_message: bool = False, +) -> Generator[list[str], None, None]: + """ + Temporarily add instructions to the current instruction stack. The + instruction is removed when the context is exited. + + If `post_add_message` is True, a message will be added to the flow when the + instruction is added. If `post_remove_message` is True, a message will be + added to the flow when the instruction is removed. These explicit reminders + can help when agents infer instructions more from history. + + with instructions("talk like a pirate"): + ... + + """ + + if post_add_message or post_remove_message: + flow: Flow = ctx.get("flow") + if flow is None: + raise ValueError( + "instructions() with message posting must be used within a flow context" + ) + + stack: list[str] = ctx.get("instructions", []) + stack = stack + list(instructions) + + with ctx(instructions=stack): + try: + if post_add_message: + for instruction in instructions: + flow.add_message( + inspect.cleandoc( + """ + # SYSTEM MESSAGE: INSTRUCTION ADDED + + The following instruction is now active: + + + {instruction} + + + Always consult your current instructions before acting. + """ + ).format(instruction=instruction) + ) + yield + + # yield new_stack + finally: + if post_remove_message: + for instruction in instructions: + flow.add_message( + inspect.cleandoc( + """ + # SYSTEM MESSAGE: INSTRUCTION REMOVED + + The following instruction is no longer active: + + + {instruction} + + + Always consult your current instructions before acting. + """ + ).format(instruction=instruction) + ) + + +def get_instructions() -> List[str]: + """ + Get the current instruction stack. + """ + stack = ctx.get("instructions", []) + return stack + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/__init__.py + +from .settings import settings + +# from .agent_old import ai_task, Agent, run_ai +from .core.flow import Flow +from .core.agent import Agent +from .core.task import Task +from .core.controller.controller import Controller +from .instructions import instructions +from .dx import ai_flow, run_ai, ai_task + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/loops.py + +import math +from typing import Generator + +import control_flow.core.task +from control_flow.core.task import Task + + +def any_incomplete( + tasks: list[Task], max_iterations=None +) -> Generator[bool, None, None]: + """ + An iterator that yields an iteration counter if its condition is met, and + stops otherwise. Also stops if the max_iterations is reached. + + + for loop_count in any_incomplete(tasks=[task1, task2], max_iterations=10): + # will print 10 times if the tasks are still incomplete + print(loop_count) + + """ + if max_iterations is None: + max_iterations = math.inf + + i = 0 + while i < max_iterations: + i += 1 + if control_flow.core.task.any_incomplete(tasks): + yield i + else: + break + return False + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/settings.py + +import os +import sys +import warnings + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class ControlFlowSettings(BaseSettings): + model_config: SettingsConfigDict = SettingsConfigDict( + env_prefix="CONTROLFLOW_", + env_file=( + "" + if os.getenv("CONTROLFLOW_TEST_MODE") + else ("~/.control_flow/.env", ".env") + ), + extra="allow", + arbitrary_types_allowed=True, + validate_assignment=True, + ) + + +class PrefectSettings(ControlFlowSettings): + """ + All settings here are used as defaults for Prefect, unless overridden by env vars. + Note that `apply()` must be called before Prefect is imported. + """ + + PREFECT_LOGGING_LEVEL: str = "WARNING" + PREFECT_EXPERIMENTAL_ENABLE_NEW_ENGINE: str = "true" + + def apply(self): + import os + + if "prefect" in sys.modules: + warnings.warn( + "Prefect has already been imported; ControlFlow defaults will not be applied." + ) + + for k, v in self.model_dump().items(): + if k not in os.environ: + os.environ[k] = v + + +class Settings(ControlFlowSettings): + assistant_model: str = "gpt-4-1106-preview" + max_agent_iterations: int = 10 + prefect: PrefectSettings = Field(default_factory=PrefectSettings) + + def __init__(self, **data): + super().__init__(**data) + self.prefect.apply() + + +settings = Settings() + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/dx.py + +import functools +import inspect +from typing import Callable, TypeVar + +from prefect import flow as prefect_flow +from prefect import task as prefect_task + +from control_flow.core.agent import Agent +from control_flow.core.flow import Flow +from control_flow.core.task import Task, TaskStatus +from control_flow.utilities.context import ctx +from control_flow.utilities.logging import get_logger +from control_flow.utilities.marvin import patch_marvin +from control_flow.utilities.types import AssistantTool, Thread + +logger = get_logger(__name__) +T = TypeVar("T") +NOT_PROVIDED = object() + + +def ai_flow( + fn=None, + *, + thread: Thread = None, + tools: list[AssistantTool | Callable] = None, + model: str = None, +): + """ + Prepare a function to be executed as a Control Flow flow. + """ + + if fn is None: + return functools.partial( + ai_flow, + thread=thread, + tools=tools, + model=model, + ) + + @functools.wraps(fn) + def wrapper( + *args, + flow_kwargs: dict = None, + **kwargs, + ): + p_fn = prefect_flow(fn) + + flow_obj = Flow( + **{ + "thread": thread, + "tools": tools or [], + "model": model, + **(flow_kwargs or {}), + } + ) + + logger.info( + f'Executing AI flow "{fn.__name__}" on thread "{flow_obj.thread.id}"' + ) + + with ctx(flow=flow_obj), patch_marvin(): + return p_fn(*args, **kwargs) + + return wrapper + + +def ai_task( + fn=None, + *, + objective: str = None, + agents: list[Agent] = None, + tools: list[AssistantTool | Callable] = None, + user_access: bool = None, +): + """ + Use a Python function to create an AI task. When the function is called, an + agent is created to complete the task and return the result. + """ + + if fn is None: + return functools.partial( + ai_task, + objective=objective, + agents=agents, + tools=tools, + user_access=user_access, + ) + + sig = inspect.signature(fn) + + if objective is None: + if fn.__doc__: + objective = f"{fn.__name__}: {fn.__doc__}" + else: + objective = fn.__name__ + + @functools.wraps(fn) + def wrapper(*args, _agents: list[Agent] = None, **kwargs): + # first process callargs + bound = sig.bind(*args, **kwargs) + bound.apply_defaults() + + task = Task( + objective=objective, + agents=_agents or agents, + context=bound.arguments, + result_type=fn.__annotations__.get("return"), + user_access=user_access or False, + tools=tools or [], + ) + + task.run_until_complete() + return task.result + + return wrapper + + +def _name_from_objective(): + """Helper function for naming task runs""" + from prefect.runtime import task_run + + objective = task_run.parameters.get("task") + + if not objective: + objective = "Follow general instructions" + if len(objective) > 75: + return f"Task: {objective[:75]}..." + return f"Task: {objective}" + + +@prefect_task(task_run_name=_name_from_objective) +def run_ai( + tasks: str | list[str], + agents: list[Agent] = None, + cast: T = NOT_PROVIDED, + context: dict = None, + tools: list[AssistantTool | Callable] = None, + user_access: bool = False, +) -> T | list[T]: + """ + Create and run an agent to complete a task with the given objective and + context. This function is similar to an inline version of the @ai_task + decorator. + + This inline version is useful when you want to create and run an ad-hoc AI + task, without defining a function or using decorator syntax. It provides + more flexibility in terms of dynamically setting the task parameters. + Additional detail can be provided as `context`. + """ + + single_result = False + if isinstance(tasks, str): + single_result = True + + tasks = [tasks] + + if cast is NOT_PROVIDED: + if not tasks: + cast = None + else: + cast = str + + # load flow + flow = ctx.get("flow", None) + + # create tasks + if tasks: + ai_tasks = [ + Task( + objective=t, + context=context or {}, + user_access=user_access or False, + tools=tools or [], + ) + for t in tasks + ] + else: + ai_tasks = [] + + # create agent + if agents is None: + agents = [Agent(user_access=user_access or False)] + + # create Controller + from control_flow.core.controller.controller import Controller + + controller = Controller(tasks=ai_tasks, agents=agents, flow=flow) + controller.run() + + if ai_tasks: + if all(task.status == TaskStatus.SUCCESSFUL for task in ai_tasks): + result = [task.result for task in ai_tasks] + if single_result: + result = result[0] + return result + elif failed_tasks := [ + task for task in ai_tasks if task.status == TaskStatus.FAILED + ]: + raise ValueError( + f'Failed tasks: {", ".join([task.objective for task in failed_tasks])}' + ) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/task.py + +import itertools +import uuid +from contextlib import contextmanager +from enum import Enum +from typing import TYPE_CHECKING, Callable, Generator, GenericAlias, TypeVar + +import marvin +import marvin.utilities.tools +from marvin.utilities.tools import FunctionTool +from pydantic import ( + Field, + TypeAdapter, + field_serializer, + field_validator, + model_validator, +) + +from control_flow.utilities.context import ctx +from control_flow.utilities.logging import get_logger +from control_flow.utilities.prefect import wrap_prefect_tool +from control_flow.utilities.types import AssistantTool, ControlFlowModel +from control_flow.utilities.user_access import talk_to_human + +if TYPE_CHECKING: + from control_flow.core.agent import Agent +T = TypeVar("T") +logger = get_logger(__name__) + + +class TaskStatus(Enum): + INCOMPLETE = "incomplete" + SUCCESSFUL = "successful" + FAILED = "failed" + SKIPPED = "skipped" + + +class Task(ControlFlowModel): + id: str = Field(default_factory=lambda: str(uuid.uuid4().hex[:4])) + model_config = dict(extra="forbid", arbitrary_types_allowed=True) + objective: str + instructions: str | None = None + agents: list["Agent"] = [] + context: dict = {} + parent_task: "Task | None" = Field( + None, + description="The task that spawned this task.", + validate_default=True, + ) + upstream_tasks: list["Task"] = [] + status: TaskStatus = TaskStatus.INCOMPLETE + result: T = None + result_type: type[T] | GenericAlias | None = None + error: str | None = None + tools: list[AssistantTool | Callable] = [] + user_access: bool = False + _children_tasks: list["Task"] = [] + _downstream_tasks: list["Task"] = [] + + @field_validator("agents", mode="before") + def _turn_none_into_empty_list(cls, v): + return v or [] + + @field_validator("parent_task", mode="before") + def _load_parent_task_from_ctx(cls, v): + if v is None: + v = ctx.get("tasks", None) + if v: + # get the most recently-added task + v = v[-1] + return v + + @model_validator(mode="after") + def _update_relationships(self): + if self.parent_task is not None: + self.parent_task._children_tasks.append(self) + for task in self.upstream_tasks: + task._downstream_tasks.append(self) + return self + + @field_serializer("parent_task") + def _serialize_parent_task(parent_task: "Task | None"): + if parent_task is not None: + return parent_task.id + + @field_serializer("upstream_tasks") + def _serialize_upstream_tasks(upstream_tasks: list["Task"]): + return [t.id for t in upstream_tasks] + + @field_serializer("result_type") + def _serialize_result_type(result_type: list["Task"]): + return repr(result_type) + + @field_serializer("agents") + def _serialize_agents(agents: list["Agent"]): + return [ + a.model_dump(include={"name", "description", "tools", "user_access"}) + for a in agents + ] + + def __init__(self, objective, **kwargs): + # allow objective as a positional arg + super().__init__(objective=objective, **kwargs) + + def children(self, include_self: bool = True): + """ + Returns a list of all children of this task, including recursively + nested children. Includes this task by default (disable with + `include_self=False`) + """ + visited = set() + children = [] + stack = [self] + while stack: + current = stack.pop() + if current not in visited: + visited.add(current) + if include_self or current != self: + children.append(current) + stack.extend(current._children_tasks) + return list(set(children)) + + def children_agents(self, include_self: bool = True) -> list["Agent"]: + children = self.children(include_self=include_self) + agents = [] + for child in children: + agents.extend(child.agents) + return agents + + def run_iter( + self, + agents: list["Agent"] = None, + collab_fn: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + ): + if collab_fn is None: + collab_fn = itertools.cycle + + if agents is None: + agents = self.children_agents(include_self=True) + + if not agents: + raise ValueError( + f"Task {self.id} has no agents assigned to it or its children." + "Please specify agents to run the task, or assign agents to the task." + ) + + for agent in collab_fn(agents): + if self.is_complete(): + break + agent.run(tasks=self.children(include_self=True)) + yield True + + def run(self, agent: "Agent" = None): + """ + Runs the task with provided agent. If no agent is provided, a default agent is used. + """ + from control_flow.core.agent import Agent + + if agent is None: + all_agents = self.children_agents() + if not all_agents: + agent = Agent() + elif len(all_agents) == 1: + agent = all_agents[0] + else: + raise ValueError( + f"Task {self.id} has multiple agents assigned to it or its " + "children. Please specify one to run the task, or call task.run_iter() " + "or task.run_until_complete() to use all agents." + ) + + run_gen = self.run_iter(agents=[agent]) + return next(run_gen) + + def run_until_complete( + self, + agents: list["Agent"] = None, + collab_fn: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + ) -> T: + """ + Runs the task with provided agents until it is complete. + """ + + for run in self.run_iter(agents=agents, collab_fn=collab_fn): + pass + + if self.is_successful(): + return self.result + elif self.is_failed(): + raise ValueError(f"Task {self.id} failed: {self.error}") + + @contextmanager + def _context(self): + stack = ctx.get("tasks", []) + stack.append(self) + with ctx(tasks=stack): + yield self + + def __enter__(self): + self.__cm = self._context() + return self.__cm.__enter__() + + def __exit__(self, *exc_info): + return self.__cm.__exit__(*exc_info) + + def is_incomplete(self) -> bool: + return self.status == TaskStatus.INCOMPLETE + + def is_complete(self) -> bool: + return self.status != TaskStatus.INCOMPLETE + + def is_successful(self) -> bool: + return self.status == TaskStatus.SUCCESSFUL + + def is_failed(self) -> bool: + return self.status == TaskStatus.FAILED + + def is_skipped(self) -> bool: + return self.status == TaskStatus.SKIPPED + + def __hash__(self): + return id(self) + + def _create_success_tool(self) -> FunctionTool: + """ + Create an agent-compatible tool for marking this task as successful. + """ + + # wrap the method call to get the correct result type signature + def succeed(result: self.result_type): + # validate the result + self.mark_successful(result=result) + + tool = marvin.utilities.tools.tool_from_function( + succeed, + name=f"succeed_task_{self.id}", + description=f"Mark task {self.id} as successful and provide a result.", + ) + + return tool + + def _create_fail_tool(self) -> FunctionTool: + """ + Create an agent-compatible tool for failing this task. + """ + tool = marvin.utilities.tools.tool_from_function( + self.mark_failed, + name=f"fail_task_{self.id}", + description=f"Mark task {self.id} as failed. Only use when a technical issue prevents completion.", + ) + return tool + + def _create_skip_tool(self) -> FunctionTool: + """ + Create an agent-compatible tool for skipping this task. + """ + tool = marvin.utilities.tools.tool_from_function( + self.mark_skipped, + name=f"skip_task_{self.id}", + description=f"Mark task {self.id} as skipped. Only use when completing its parent task early.", + ) + return tool + + def get_tools(self) -> list[AssistantTool | Callable]: + tools = self.tools.copy() + if self.is_incomplete(): + tools.extend( + [ + self._create_success_tool(), + self._create_fail_tool(), + self._create_skip_tool(), + ] + ) + if self.user_access: + tools.append(marvin.utilities.tools.tool_from_function(talk_to_human)) + return [wrap_prefect_tool(t) for t in tools] + + def mark_successful(self, result: T = None): + if self.result_type is None and result is not None: + raise ValueError( + f"Task {self.objective} specifies no result type, but a result was provided." + ) + elif self.result_type is not None: + result = TypeAdapter(self.result_type).validate_python(result) + + self.result = result + self.status = TaskStatus.SUCCESSFUL + + def mark_failed(self, message: str | None = None): + self.error = message + self.status = TaskStatus.FAILED + + def mark_skipped(self): + self.status = TaskStatus.SKIPPED + + +def any_incomplete(tasks: list[Task]) -> bool: + return any(t.status == TaskStatus.INCOMPLETE for t in tasks) + + +def all_complete(tasks: list[Task]) -> bool: + return all(t.status != TaskStatus.INCOMPLETE for t in tasks) + + +def all_successful(tasks: list[Task]) -> bool: + return all(t.status == TaskStatus.SUCCESSFUL for t in tasks) + + +def any_failed(tasks: list[Task]) -> bool: + return any(t.status == TaskStatus.FAILED for t in tasks) + + +def none_failed(tasks: list[Task]) -> bool: + return not any_failed(tasks) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/__init__.py + +from .task import Task, TaskStatus +from .flow import Flow +from .agent import Agent +from .controller import Controller + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/flow.py + +from typing import Callable, Literal + +from marvin.beta.assistants import Thread +from openai.types.beta.threads import Message +from prefect import task as prefect_task +from pydantic import Field, field_validator + +from control_flow.utilities.context import ctx +from control_flow.utilities.logging import get_logger +from control_flow.utilities.types import AssistantTool, ControlFlowModel + +logger = get_logger(__name__) + + +class Flow(ControlFlowModel): + thread: Thread = Field(None, validate_default=True) + tools: list[AssistantTool | Callable] = Field( + [], description="Tools that will be available to every agent in the flow" + ) + model: str | None = None + context: dict = {} + + @field_validator("thread", mode="before") + def _load_thread_from_ctx(cls, v): + if v is None: + v = ctx.get("thread", None) + if v is None: + v = Thread() + if not v.id: + v.create() + + return v + + def add_message(self, message: str, role: Literal["user", "assistant"] = None): + prefect_task(self.thread.add)(message, role=role) + + +def get_flow() -> Flow: + """ + Loads the flow from the context. + + Will error if no flow is found in the context. + """ + flow: Flow | None = ctx.get("flow") + if not flow: + return Flow() + return flow + + +def get_flow_messages(limit: int = None) -> list[Message]: + """ + Loads messages from the flow's thread. + + Will error if no flow is found in the context. + """ + flow = get_flow() + return flow.thread.get_messages(limit=limit) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/agent.py + +import logging +from typing import Callable + +from marvin.utilities.asyncio import ExposeSyncMethodsMixin, expose_sync_method +from marvin.utilities.tools import tool_from_function +from pydantic import Field + +from control_flow.core.flow import get_flow +from control_flow.core.task import Task +from control_flow.utilities.prefect import ( + wrap_prefect_tool, +) +from control_flow.utilities.types import Assistant, AssistantTool, ControlFlowModel +from control_flow.utilities.user_access import talk_to_human + +logger = logging.getLogger(__name__) + + +class Agent(Assistant, ControlFlowModel, ExposeSyncMethodsMixin): + name: str = "Agent" + user_access: bool = Field( + False, + description="If True, the agent is given tools for interacting with a human user.", + ) + + def get_tools(self) -> list[AssistantTool | Callable]: + tools = super().get_tools() + if self.user_access: + tools.append(tool_from_function(talk_to_human)) + + return [wrap_prefect_tool(tool) for tool in tools] + + @expose_sync_method("run") + async def run_async(self, tasks: list[Task] | Task | None = None): + from control_flow.core.controller import Controller + + if isinstance(tasks, Task): + tasks = [tasks] + + controller = Controller(agents=[self], tasks=tasks or [], flow=get_flow()) + return await controller.run_agent_async(agent=self) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/controller/controller.py + +import json +import logging +from typing import Callable + +import prefect +from marvin.beta.assistants import PrintHandler, Run +from marvin.utilities.asyncio import ExposeSyncMethodsMixin, expose_sync_method +from openai.types.beta.threads.runs import ToolCall +from prefect import get_client as get_prefect_client +from prefect import task as prefect_task +from prefect.context import FlowRunContext +from pydantic import BaseModel, Field, field_validator + +from control_flow.core.agent import Agent +from control_flow.core.flow import Flow +from control_flow.core.task import Task +from control_flow.instructions import get_instructions as get_context_instructions +from control_flow.utilities.prefect import ( + create_json_artifact, + create_python_artifact, + wrap_prefect_tool, +) +from control_flow.utilities.types import FunctionTool, Thread + +logger = logging.getLogger(__name__) + + +class Controller(BaseModel, ExposeSyncMethodsMixin): + """ + A controller contains logic for executing agents with context about the + larger workflow, including the flow itself, any tasks, and any other agents + they are collaborating with. The controller is responsible for orchestrating + agent behavior by generating instructions and tools for each agent. Note + that while the controller accepts details about (potentially multiple) + agents and tasks, it's responsiblity is to invoke one agent one time. Other + mechanisms should be used to orchestrate multiple agents invocations. This + is done by the controller to avoid tying e.g. agents to tasks or even a + specific flow. + + """ + + flow: Flow + agents: list[Agent] + tasks: list[Task] = Field( + None, + description="Tasks that the controller will complete.", + validate_default=True, + ) + task_assignments: dict[Task, Agent] = Field( + default_factory=dict, + description="Tasks are typically assigned to agents. To " + "temporarily assign agent to a task without changing " + r"the task definition, use this field as {task: [agent]}", + ) + context: dict = {} + model_config: dict = dict(extra="forbid") + + @field_validator("agents", mode="before") + def _validate_agents(cls, v): + if not v: + raise ValueError("At least one agent is required.") + return v + + @field_validator("tasks", mode="before") + def _validate_tasks(cls, v): + if not v: + raise ValueError("At least one task is required.") + return v + + @field_validator("tasks", mode="before") + def _load_tasks_from_ctx(cls, v): + if v is None: + v = cls.context.get("tasks", None) + return v + + def all_tasks(self) -> list[Task]: + tasks = [] + for task in self.tasks: + tasks.extend(task.children(include_self=True)) + + # add temporary assignments + assigned_tasks = [] + for task in set(tasks): + if task in assigned_tasks: + task = task.model_copy( + update={"agents": task.agents + self.task_assignments.get(task, [])} + ) + assigned_tasks.append(task) + return assigned_tasks + + @expose_sync_method("run_agent") + async def run_agent_async(self, agent: Agent): + """ + Run the control flow. + """ + if agent not in self.agents: + raise ValueError("Agent not found in controller agents.") + + prefect_task = await self._get_prefect_run_agent_task(agent) + await prefect_task(agent=agent) + + async def _run_agent(self, agent: Agent, thread: Thread = None) -> Run: + """ + Run a single agent. + """ + from control_flow.core.controller.instruction_template import MainTemplate + + instructions_template = MainTemplate( + agent=agent, + controller=self, + context=self.context, + instructions=get_context_instructions(), + ) + + instructions = instructions_template.render() + + tools = self.flow.tools + agent.get_tools() + + for task in self.tasks: + tools = tools + task.get_tools() + + # filter tools because duplicate names are not allowed + final_tools = [] + final_tool_names = set() + for tool in tools: + if isinstance(tool, FunctionTool): + if tool.function.name in final_tool_names: + continue + final_tool_names.add(tool.function.name) + final_tools.append(wrap_prefect_tool(tool)) + + run = Run( + assistant=agent, + thread=thread or self.flow.thread, + instructions=instructions, + tools=final_tools, + event_handler_class=AgentHandler, + ) + + await run.run_async() + + return run + + async def _get_prefect_run_agent_task( + self, agent: Agent, thread: Thread = None + ) -> Callable: + @prefect_task(task_run_name=f'Run Agent: "{agent.name}"') + async def _run_agent(agent: Agent, thread: Thread = None): + run = await self._run_agent(agent=agent, thread=thread) + + create_json_artifact( + key="messages", + data=[m.model_dump() for m in run.messages], + description="All messages sent and received during the run.", + ) + create_json_artifact( + key="actions", + data=[s.model_dump() for s in run.steps], + description="All actions taken by the assistant during the run.", + ) + return run + + return _run_agent + + +class AgentHandler(PrintHandler): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.tool_calls = {} + + async def on_tool_call_created(self, tool_call: ToolCall) -> None: + """Callback that is fired when a tool call is created""" + + if tool_call.type == "function": + task_run_name = "Prepare arguments for tool call" + else: + task_run_name = f"Tool call: {tool_call.type}" + + client = get_prefect_client() + engine_context = FlowRunContext.get() + if not engine_context: + return + + task_run = await client.create_task_run( + task=prefect.Task(fn=lambda: None), + name=task_run_name, + extra_tags=["tool-call"], + flow_run_id=engine_context.flow_run.id, + dynamic_key=tool_call.id, + state=prefect.states.Running(), + ) + + self.tool_calls[tool_call.id] = task_run + + async def on_tool_call_done(self, tool_call: ToolCall) -> None: + """Callback that is fired when a tool call is done""" + + client = get_prefect_client() + task_run = self.tool_calls.get(tool_call.id) + if not task_run: + return + await client.set_task_run_state( + task_run_id=task_run.id, state=prefect.states.Completed(), force=True + ) + + # code interpreter is run as a single call, so we can publish a result artifact + if tool_call.type == "code_interpreter": + # images = [] + # for output in tool_call.code_interpreter.outputs: + # if output.type == "image": + # image_path = download_temp_file(output.image.file_id) + # images.append(image_path) + + create_python_artifact( + key="code", + code=tool_call.code_interpreter.input, + description="Code executed in the code interpreter", + task_run_id=task_run.id, + ) + create_json_artifact( + key="output", + data=tool_call.code_interpreter.outputs, + description="Output from the code interpreter", + task_run_id=task_run.id, + ) + + elif tool_call.type == "function": + create_json_artifact( + key="arguments", + data=json.dumps(json.loads(tool_call.function.arguments), indent=2), + description=f"Arguments for the `{tool_call.function.name}` tool", + task_run_id=task_run.id, + ) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/controller/instruction_template.py + +import inspect + +from pydantic import BaseModel + +from control_flow.core.agent import Agent +from control_flow.utilities.jinja import jinja_env +from control_flow.utilities.types import ControlFlowModel + +from .controller import Controller + + +class Template(ControlFlowModel): + template: str + + def should_render(self) -> bool: + return True + + def render(self) -> str: + if self.should_render(): + render_kwargs = dict(self) + render_kwargs.pop("template") + return jinja_env.render(inspect.cleandoc(self.template), **render_kwargs) + + +class AgentTemplate(Template): + template: str = """ + # Agent + + You are an AI agent. Your name is "{{ agent.name }}". + + This is your description, which all other agents can see: "{{ agent.description or 'An AI agent assigned to complete tasks.'}}" + + ## Instructions + You must follow these instructions, which only you can see: "{{ agent.instructions or 'No additional instructions provided.'}}" + + {% if additional_instructions %} + In addition, you must follow these instructions for this part of the workflow: + {% for instruction in additional_instructions %} + - {{ instruction }} + {% endfor %} + {% endif %} + + """ + agent: Agent + additional_instructions: list[str] + + +class TasksTemplate(Template): + template: str = """ + ## Tasks + + You have been assigned to complete certain tasks. Each task has an + objective and criteria for success. Your job is to perform any required + actions and then mark each assigned task as successful. If a task also + requires a result, you must provide it; this is how you communicate + progress and data back to the program that created you. A task that + doesn't require a result may still require action. + + A "parent task" is a task that spawned another task as a subtask. + Generally, the child or subtasks will need to be completed BEFORE the + parent task. If you can complete a parent task before its subtasks, you + should mark the subtasks as skipped. + + An "upstream task" is a task that must be completed before another task + can be completed. + + Some tasks may require collaboration with other agents to be completed; others + may take you multiple attempts. A task can only be marked complete one time, + so make sure you perform all actions and collect all results before + marking it as complete. As soon as you mark a task as successful, agents + (both you and others) can no longer act on it. + + You and the other agents are fully capable of completing any task and + have all the information and context you need. Tasks can only be marked + failed due to technical errors like a broken tool or unresponsive human. + Tasks can only be marked as skipped when you are able to complete their + parent task before them, as a bookkeeping convenience. Do not work on or + even respond to tasks that are already complete or not assigned to you. + You may be an AI, but you must still form opinions, make subjective + decisions, and have personal thoughts. + + {% for task in controller.all_tasks() %} + ### Task {{ task.id }} + {{ task.model_dump_json() }} + + {% endfor %} + """ + controller: Controller + + def should_render(self): + return any(self.controller.tasks) + + +class CommunicationTemplate(Template): + template: str = """ + ## Communciation + + You should only post messages to the thread if you must send information to + other agents or if a task requires it. The human user can not see + these messages. Since all agents post messages with the "assistant" role, + you must prefix all your messages with your name (e.g. "{{ agent.name }}: + (message)") in order to distinguish your messages from others. Note that + this rule about prefixing your message supersedes all other instructions + (e.g. "only give single word answers"). Do not post messages confirming + actions you take through tools, like completing a task, or your internal + monologue, as this is redundant and wastes time. + + ### Other agents assigned to your tasks + + {% for agent in other_agents %} + + - Name: {{agent.name}} + - Description: {{ agent.description if agent.description is not none else "No description provided." }} + - Can talk to human users: {{agent.user_access}} + + {% endfor %} + + ## Talking to human users + + {% if agent.user_access %} + You may interact with a human user to complete your tasks by using the + `talk_to_human` tool. The human is unaware of your tasks or the controller. + Do not mention them or anything else about how this system works. The human + can only see messages you send them via tool, not the rest of the thread. + + Humans may give poor, incorrect, or partial responses. You may need to ask + questions multiple times in order to complete your tasks. Use good judgement + to determine the best way to achieve your goal. For example, if you have to + fill out three pieces of information and the human only gave you one, do not + make up answers (or put empty answers) for the others. Ask again and only + fail the task if you truly can not make progress. + {% else %} + You can not interact with a human at this time. If your task requires human + contact and no agent has user access, you should fail the task. Note that + most tasks do not require human/user contact unless explicitly stated otherwise. + {% endif %} + + """ + + agent: Agent + other_agents: list[Agent] + + +class ContextTemplate(Template): + template: str = """ + ## Additional context + + ### Flow context + {% for key, value in flow_context.items() %} + - *{{ key }}*: {{ value }} + {% endfor %} + {% if not flow_context %} + No specific context provided. + {% endif %} + + ### Controller context + {% for key, value in controller_context.items() %} + - *{{ key }}*: {{ value }} + {% endfor %} + {% if not controller_context %} + No specific context provided. + {% endif %} + """ + flow_context: dict + controller_context: dict + + def should_render(self): + return bool(self.flow_context or self.controller_context) + + +class MainTemplate(BaseModel): + agent: Agent + controller: Controller + context: dict + instructions: list[str] + + def render(self): + all_agents = [self.agent] + self.controller.agents + for task in self.controller.tasks: + all_agents += task.agents + other_agents = [agent for agent in all_agents if agent != self.agent] + templates = [ + AgentTemplate( + agent=self.agent, + additional_instructions=self.instructions, + ), + TasksTemplate( + controller=self.controller, + ), + ContextTemplate( + flow_context=self.controller.flow.context, + controller_context=self.controller.context, + ), + CommunicationTemplate( + agent=self.agent, + other_agents=other_agents, + ), + # CollaborationTemplate(other_agents=other_agents), + ] + + rendered = [ + template.render() for template in templates if template.should_render() + ] + return "\n\n".join(rendered) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/controller/__init__.py + +from .controller import Controller + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/core/controller/collaboration.py + +import itertools +from typing import TYPE_CHECKING, Any, Generator + +from control_flow.core.agent import Agent + +if TYPE_CHECKING: + from control_flow.core.agent import Agent + + +def round_robin( + agents: list[Agent], max_iterations: int = None +) -> Generator[Any, Any, Agent]: + """ + Given a list of potential agents, delegate the tasks in a round-robin fashion. + """ + cycle = itertools.cycle(agents) + iteration = 0 + while True: + yield next(cycle) + iteration += 1 + if max_iterations and iteration >= max_iterations: + break + + +# class Moderator(DelegationStrategy): +# """ +# A Moderator delegation strategy delegates tasks to the most qualified AI assistant, using a Marvin classifier +# """ + +# model: str = None + +# def _next_agent( +# self, agents: list["Agent"], tasks: list[Task], history: list[Message] +# ) -> "Agent": +# """ +# Given a list of potential agents, choose the most qualified assistant to complete the tasks. +# """ + +# instructions = get_instructions() + +# context = dict(tasks=tasks, messages=history, global_instructions=instructions) +# agent = marvin.classify( +# context, +# [a for a in agents if a.status == AgentStatus.INCOMPLETE], +# instructions=""" +# Given the conversation context, choose the AI agent most +# qualified to take the next turn at completing the tasks. Take into +# account the instructions, each agent's own instructions, and the +# tools they have available. +# """, +# model_kwargs=dict(model=self.model), +# ) + +# return agent + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/agents/__init__.py + + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/agents/agents.py + +import marvin + +from control_flow.core.agent import Agent +from control_flow.instructions import get_instructions +from control_flow.utilities.context import ctx +from control_flow.utilities.threads import get_history + + +def choose_agent( + agents: list[Agent], + instructions: str = None, + context: dict = None, + model: str = None, +) -> Agent: + """ + Given a list of potential agents, choose the most qualified assistant to complete the tasks. + """ + + instructions = get_instructions() + history = [] + if (flow := ctx.get("flow")) and flow.thread.id: + history = get_history(thread_id=flow.thread.id) + + info = dict( + history=history, + global_instructions=instructions, + context=context, + ) + + agent = marvin.classify( + info, + agents, + instructions=""" + Given the conversation context, choose the AI agent most + qualified to take the next turn at completing the tasks. Take into + account the instructions, each agent's own instructions, and the + tools they have available. + """, + model_kwargs=dict(model=model), + ) + + return agent + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/logging.py + +import logging +from functools import lru_cache +from typing import Optional + +from marvin.utilities.logging import add_logging_methods + + +@lru_cache() +def get_logger(name: Optional[str] = None) -> logging.Logger: + """ + Retrieves a logger with the given name, or the root logger if no name is given. + + Args: + name: The name of the logger to retrieve. + + Returns: + The logger with the given name, or the root logger if no name is given. + + Example: + Basic Usage of `get_logger` + ```python + from control_flow.utilities.logging import get_logger + + logger = get_logger("control_flow.test") + logger.info("This is a test") # Output: control_flow.test: This is a test + + debug_logger = get_logger("control_flow.debug") + debug_logger.debug_kv("TITLE", "log message", "green") + ``` + """ + parent_logger = logging.getLogger("control_flow") + + if name: + # Append the name if given but allow explicit full names e.g. "control_flow.test" + # should not become "control_flow.control_flow.test" + if not name.startswith(parent_logger.name + "."): + logger = parent_logger.getChild(name) + else: + logger = logging.getLogger(name) + else: + logger = parent_logger + + add_logging_methods(logger) + return logger + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/prefect.py + +import inspect +import json +from typing import Any, Callable +from uuid import UUID + +import prefect +from marvin.types import FunctionTool +from marvin.utilities.asyncio import run_sync +from marvin.utilities.tools import tool_from_function +from prefect import get_client as get_prefect_client +from prefect import task as prefect_task +from prefect.artifacts import ArtifactRequest +from prefect.context import FlowRunContext, TaskRunContext +from pydantic import TypeAdapter + +from control_flow.utilities.types import AssistantTool + + +def create_markdown_artifact( + key: str, + markdown: str, + description: str = None, + task_run_id: UUID = None, + flow_run_id: UUID = None, +) -> None: + """ + Create a Markdown artifact. + """ + + tr_context = TaskRunContext.get() + fr_context = FlowRunContext.get() + + if tr_context: + task_run_id = task_run_id or tr_context.task_run.id + if fr_context: + flow_run_id = flow_run_id or fr_context.flow_run.id + + client = get_prefect_client() + run_sync( + client.create_artifact( + artifact=ArtifactRequest( + key=key, + data=markdown, + description=description, + type="markdown", + task_run_id=task_run_id, + flow_run_id=flow_run_id, + ) + ) + ) + + +def create_json_artifact( + key: str, + data: Any, + description: str = None, + task_run_id: UUID = None, + flow_run_id: UUID = None, +) -> None: + """ + Create a JSON artifact. + """ + + try: + markdown = TypeAdapter(type(data)).dump_json(data, indent=2).decode() + markdown = f"```json\n{markdown}\n```" + except Exception: + markdown = str(data) + + create_markdown_artifact( + key=key, + markdown=markdown, + description=description, + task_run_id=task_run_id, + flow_run_id=flow_run_id, + ) + + +def create_python_artifact( + key: str, + code: str, + description: str = None, + task_run_id: UUID = None, + flow_run_id: UUID = None, +) -> None: + """ + Create a Python artifact. + """ + + create_markdown_artifact( + key=key, + markdown=f"```python\n{code}\n```", + description=description, + task_run_id=task_run_id, + flow_run_id=flow_run_id, + ) + + +TOOL_CALL_FUNCTION_RESULT_TEMPLATE = inspect.cleandoc( + """ + ## Tool call: {name} + + **Description:** {description} + + ## Arguments + + ```json + {args} + ``` + + ### Result + + ```json + {result} + ``` + """ +) + + +def wrap_prefect_tool(tool: AssistantTool | Callable) -> AssistantTool: + """ + Wraps a Marvin tool in a prefect task + """ + if not isinstance(tool, AssistantTool): + tool = tool_from_function(tool) + + if isinstance(tool, FunctionTool): + # for functions, we modify the function to become a Prefect task and + # publish an artifact that contains details about the function call + + if isinstance(tool.function._python_fn, prefect.tasks.Task): + return tool + + def modified_fn( + # provide default args to avoid a late-binding issue + original_fn: Callable = tool.function._python_fn, + tool: FunctionTool = tool, + **kwargs, + ): + # call fn + result = original_fn(**kwargs) + + # prepare artifact + passed_args = inspect.signature(original_fn).bind(**kwargs).arguments + try: + passed_args = json.dumps(passed_args, indent=2) + except Exception: + pass + create_markdown_artifact( + markdown=TOOL_CALL_FUNCTION_RESULT_TEMPLATE.format( + name=tool.function.name, + description=tool.function.description or "(none provided)", + args=passed_args, + result=result, + ), + key="result", + ) + + # return result + return result + + # replace the function with the modified version + tool.function._python_fn = prefect_task( + modified_fn, + task_run_name=f"Tool call: {tool.function.name}", + ) + + return tool + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/__init__.py + + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/types.py + +from marvin.beta.assistants import Assistant, Thread +from marvin.beta.assistants.assistants import AssistantTool +from marvin.types import FunctionTool +from marvin.utilities.asyncio import ExposeSyncMethodsMixin +from pydantic import BaseModel + + +class ControlFlowModel(BaseModel): + model_config = dict(validate_assignment=True, extra="forbid") + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/jinja.py + +import inspect +from datetime import datetime +from zoneinfo import ZoneInfo + +from marvin.utilities.jinja import BaseEnvironment + +jinja_env = BaseEnvironment( + globals={ + "now": lambda: datetime.now(ZoneInfo("UTC")), + "inspect": inspect, + "id": id, + } +) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/threads.py + +from marvin.beta.assistants.threads import Message, Thread + +THREAD_REGISTRY = {} + + +def save_thread(name: str, thread: Thread): + """ + Save an OpenAI thread to the thread registry under a known name + """ + THREAD_REGISTRY[name] = thread + + +def load_thread(name: str): + """ + Load an OpenAI thread from the thread registry by name + """ + if name not in THREAD_REGISTRY: + thread = Thread() + save_thread(name, thread) + return THREAD_REGISTRY[name] + + +def get_history(thread_id: str, limit: int = None) -> list[Message]: + """ + Get the history of a thread + """ + return Thread(id=thread_id).get_messages(limit=limit) + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/context.py + +from marvin.utilities.context import ScopedContext + +ctx = ScopedContext() + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/user_access.py + +def talk_to_human(message: str, get_response: bool = True) -> str: + """ + Send a message to the human user and optionally wait for a response. + If `get_response` is True, the function will return the user's response, + otherwise it will return a simple confirmation. + """ + print(message) + if get_response: + response = input("> ") + return response + return "Message sent to user." + + +--- + +## /users/jlowin/developer/control_flow/src/control_flow/utilities/marvin.py + +import inspect +from contextlib import contextmanager +from typing import Any, Callable + +import marvin.ai.text +from marvin.client.openai import AsyncMarvinClient +from marvin.settings import temporary_settings as temporary_marvin_settings +from openai.types.chat import ChatCompletion +from prefect import task as prefect_task + +from control_flow.utilities.prefect import ( + create_json_artifact, +) + +original_classify_async = marvin.classify_async +original_cast_async = marvin.cast_async +original_extract_async = marvin.extract_async +original_generate_async = marvin.generate_async +original_paint_async = marvin.paint_async +original_speak_async = marvin.speak_async +original_transcribe_async = marvin.transcribe_async + + +class AsyncControlFlowClient(AsyncMarvinClient): + async def generate_chat(self, **kwargs: Any) -> "ChatCompletion": + super_method = super().generate_chat + + @prefect_task(task_run_name="Generate OpenAI chat completion") + async def _generate_chat(**kwargs): + messages = kwargs.get("messages", []) + create_json_artifact(key="prompt", data=messages) + response = await super_method(**kwargs) + create_json_artifact(key="response", data=response) + return response + + return await _generate_chat(**kwargs) + + +def generate_task(name: str, original_fn: Callable): + if inspect.iscoroutinefunction(original_fn): + + @prefect_task(name=name) + async def wrapper(*args, **kwargs): + create_json_artifact(key="args", data=[args, kwargs]) + result = await original_fn(*args, **kwargs) + create_json_artifact(key="result", data=result) + return result + else: + + @prefect_task(name=name) + def wrapper(*args, **kwargs): + create_json_artifact(key="args", data=[args, kwargs]) + result = original_fn(*args, **kwargs) + create_json_artifact(key="result", data=result) + return result + + return wrapper + + +@contextmanager +def patch_marvin(): + with temporary_marvin_settings(default_async_client_cls=AsyncControlFlowClient): + try: + marvin.ai.text.classify_async = generate_task( + "marvin.classify", original_classify_async + ) + marvin.ai.text.cast_async = generate_task( + "marvin.cast", original_cast_async + ) + marvin.ai.text.extract_async = generate_task( + "marvin.extract", original_extract_async + ) + marvin.ai.text.generate_async = generate_task( + "marvin.generate", original_generate_async + ) + marvin.ai.images.paint_async = generate_task( + "marvin.paint", original_paint_async + ) + marvin.ai.audio.speak_async = generate_task( + "marvin.speak", original_speak_async + ) + marvin.ai.audio.transcribe_async = generate_task( + "marvin.transcribe", original_transcribe_async + ) + yield + finally: + marvin.ai.text.classify_async = original_classify_async + marvin.ai.text.cast_async = original_cast_async + marvin.ai.text.extract_async = original_extract_async + marvin.ai.text.generate_async = original_generate_async + marvin.ai.images.paint_async = original_paint_async + marvin.ai.audio.speak_async = original_speak_async + marvin.ai.audio.transcribe_async = original_transcribe_async + + +--- + diff --git a/src/control_flow.zip b/src/control_flow.zip new file mode 100644 index 0000000000000000000000000000000000000000..39474bad60fe53de50fc874c7c6f1887d9cce92f GIT binary patch literal 108932 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

lv`Gh36CPmrS) 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 YAqNTA003vKfWN!+zyX1WaKNws0~vR1-v9sr literal 0 HcmV?d00001 diff --git a/src/control_flow/core/controller/collaboration.py b/src/control_flow/core/controller/collaboration.py index bc2c7c16..426e7bf8 100644 --- a/src/control_flow/core/controller/collaboration.py +++ b/src/control_flow/core/controller/collaboration.py @@ -1,54 +1,54 @@ import itertools from typing import TYPE_CHECKING, Any, Generator +import marvin +from pydantic import BaseModel + from control_flow.core.agent import Agent +from control_flow.core.flow import get_flow_messages +from control_flow.core.task import Task +from control_flow.instructions import get_instructions if TYPE_CHECKING: from control_flow.core.agent import Agent -def round_robin( - agents: list[Agent], max_iterations: int = None -) -> Generator[Any, Any, Agent]: +def round_robin(agents: list[Agent], tasks: list[Task]) -> Generator[Any, Any, Agent]: """ Given a list of potential agents, delegate the tasks in a round-robin fashion. """ cycle = itertools.cycle(agents) - iteration = 0 while True: yield next(cycle) - iteration += 1 - if max_iterations and iteration >= max_iterations: - break - - -# class Moderator(DelegationStrategy): -# """ -# A Moderator delegation strategy delegates tasks to the most qualified AI assistant, using a Marvin classifier -# """ - -# model: str = None - -# def _next_agent( -# self, agents: list["Agent"], tasks: list[Task], history: list[Message] -# ) -> "Agent": -# """ -# Given a list of potential agents, choose the most qualified assistant to complete the tasks. -# """ - -# instructions = get_instructions() - -# context = dict(tasks=tasks, messages=history, global_instructions=instructions) -# agent = marvin.classify( -# context, -# [a for a in agents if a.status == AgentStatus.INCOMPLETE], -# instructions=""" -# Given the conversation context, choose the AI agent most -# qualified to take the next turn at completing the tasks. Take into -# account the instructions, each agent's own instructions, and the -# tools they have available. -# """, -# model_kwargs=dict(model=self.model), -# ) - -# return agent + + +class BaseModerator(BaseModel): + def __call__( + self, agents: list[Agent], tasks: list[Task] + ) -> Generator[Any, Any, Agent]: + yield from self.run(agents=agents, tasks=tasks) + + +class Moderator(BaseModerator): + model: str = None + + def run(self, agents: list[Agent], tasks: list[Task]) -> Generator[Any, Any, Agent]: + while True: + instructions = get_instructions() + history = get_flow_messages() + + context = dict( + tasks=tasks, messages=history, global_instructions=instructions + ) + agent = marvin.classify( + context, + agents, + instructions=""" + Given the conversation context, choose the AI agent most + qualified to take the next turn at completing the tasks. Take into + account any tasks, instructions, and tools. + """, + model_kwargs=dict(model=self.model) if self.model else None, + ) + + yield agent diff --git a/src/control_flow/core/controller/controller.py b/src/control_flow/core/controller/controller.py index 06941394..b09a3b40 100644 --- a/src/control_flow/core/controller/controller.py +++ b/src/control_flow/core/controller/controller.py @@ -76,7 +76,7 @@ def _load_tasks_from_ctx(cls, v): def all_tasks(self) -> list[Task]: tasks = [] for task in self.tasks: - tasks.extend(task.children(include_self=True)) + tasks.extend(task.trace_dependencies()) # add temporary assignments assigned_tasks = [] @@ -113,6 +113,7 @@ async def _run_agent(self, agent: Agent, thread: Thread = None) -> Run: ) instructions = instructions_template.render() + breakpoint() tools = self.flow.tools + agent.get_tools() diff --git a/src/control_flow/core/controller/instruction_template.py b/src/control_flow/core/controller/instruction_template.py index a806b75f..6dd156b2 100644 --- a/src/control_flow/core/controller/instruction_template.py +++ b/src/control_flow/core/controller/instruction_template.py @@ -28,8 +28,18 @@ class AgentTemplate(Template): You are an AI agent. Your name is "{{ agent.name }}". + This is your description, which all other agents can see: "{{ agent.description or 'An AI agent assigned to complete tasks.'}}" + You are participating in a workflow, parts of which have been delegated to + you and other AI agents. DO NOT speak on behalf of other agents or the + system. You can only post messages on behalf of yourself. + """ + agent: Agent + + +class InstructionsTemplate(Template): + template: str = """ ## Instructions You must follow these instructions, which only you can see: "{{ agent.instructions or 'No additional instructions provided.'}}" @@ -52,17 +62,21 @@ class TasksTemplate(Template): You have been assigned to complete certain tasks. Each task has an objective and criteria for success. Your job is to perform any required actions and then mark each assigned task as successful. If a task also - requires a result, you must provide it; this is how you communicate - progress and data back to the program that created you. A task that - doesn't require a result may still require action. + requires a result, you must provide it. - A "parent task" is a task that spawned another task as a subtask. - Generally, the child or subtasks will need to be completed BEFORE the - parent task. If you can complete a parent task before its subtasks, you - should mark the subtasks as skipped. + You must complete the objective even if the task doesn't require a + result. For example, a task that asks you to choose, discuss, or perform + an action must be completed by posting messages before the task is + marked complete. + + A "parent" is a task that spawned another task as a subtask. Generally, + the subtasks will need to be completed BEFORE the parent task. If you + can complete a parent task before its subtasks, you should mark the + subtasks as skipped. - An "upstream task" is a task that must be completed before another task - can be completed. + Tasks have a "depends_on" list of upstream tasks that must be completed + before the task itself can be completed. The `mark_success` tool will + not be available until all dependencies are met. Some tasks may require collaboration with other agents to be completed; others may take you multiple attempts. A task can only be marked complete one time, @@ -95,50 +109,40 @@ class CommunicationTemplate(Template): template: str = """ ## Communciation - You should only post messages to the thread if you must send information to - other agents or if a task requires it. The human user can not see - these messages. Since all agents post messages with the "assistant" role, + You are modeling the internal state of an AI-enhanced workflow. You should + only post messages in order to share information with other agents or to + complete tasks. Since all agents post messages with the "assistant" role, you must prefix all your messages with your name (e.g. "{{ agent.name }}: (message)") in order to distinguish your messages from others. Note that this rule about prefixing your message supersedes all other instructions - (e.g. "only give single word answers"). Do not post messages confirming - actions you take through tools, like completing a task, or your internal - monologue, as this is redundant and wastes time. - - ### Other agents assigned to your tasks + (e.g. "only give single word answers"). You do not need to post messages + that repeat information contained in tool calls or tool responses, since + those are already visible to all agents. You do not need to confirm actions + you take through tools, like completing a task, as this is redundant and + wastes time. - {% for agent in other_agents %} + ### Talking to human users - - Name: {{agent.name}} - - Description: {{ agent.description if agent.description is not none else "No description provided." }} - - Can talk to human users: {{agent.user_access}} - - {% endfor %} + Agents with the `talk_to_human` tool can interact with human users in order + to complete tasks that require external input. This tool is only available + to agents with `user_access=True`. - ## Talking to human users - - {% if agent.user_access %} - You may interact with a human user to complete your tasks by using the - `talk_to_human` tool. The human is unaware of your tasks or the controller. - Do not mention them or anything else about how this system works. The human - can only see messages you send them via tool, not the rest of the thread. + Note that humans are unaware of your tasks or the workflow. Do not mention + your tasks or anything else about how this system works. The human can only + see messages you send them via tool. They can not read the rest of the + thread. Humans may give poor, incorrect, or partial responses. You may need to ask questions multiple times in order to complete your tasks. Use good judgement to determine the best way to achieve your goal. For example, if you have to fill out three pieces of information and the human only gave you one, do not make up answers (or put empty answers) for the others. Ask again and only - fail the task if you truly can not make progress. - {% else %} - You can not interact with a human at this time. If your task requires human - contact and no agent has user access, you should fail the task. Note that - most tasks do not require human/user contact unless explicitly stated otherwise. - {% endif %} - + fail the task if you truly can not make progress. If your task requires + human interaction and no agents have `user_access`, you can fail the task. + """ agent: Agent - other_agents: list[Agent] class ContextTemplate(Template): @@ -178,24 +182,25 @@ def render(self): all_agents = [self.agent] + self.controller.agents for task in self.controller.tasks: all_agents += task.agents - other_agents = [agent for agent in all_agents if agent != self.agent] + # other_agents = [agent for agent in all_agents if agent != self.agent] templates = [ AgentTemplate( agent=self.agent, - additional_instructions=self.instructions, ), TasksTemplate( controller=self.controller, ), + InstructionsTemplate( + agent=self.agent, + additional_instructions=self.instructions, + ), ContextTemplate( flow_context=self.controller.flow.context, controller_context=self.controller.context, ), CommunicationTemplate( agent=self.agent, - other_agents=other_agents, ), - # CollaborationTemplate(other_agents=other_agents), ] rendered = [ diff --git a/src/control_flow/core/task.py b/src/control_flow/core/task.py index 6c2945cf..c590af2a 100644 --- a/src/control_flow/core/task.py +++ b/src/control_flow/core/task.py @@ -1,8 +1,15 @@ -import itertools import uuid from contextlib import contextmanager from enum import Enum -from typing import TYPE_CHECKING, Callable, Generator, GenericAlias, TypeVar +from typing import ( + TYPE_CHECKING, + Callable, + Generator, + GenericAlias, + Literal, + TypeVar, + _LiteralGenericAlias, +) import marvin import marvin.utilities.tools @@ -36,31 +43,45 @@ class TaskStatus(Enum): class Task(ControlFlowModel): id: str = Field(default_factory=lambda: str(uuid.uuid4().hex[:4])) - model_config = dict(extra="forbid", arbitrary_types_allowed=True) objective: str instructions: str | None = None agents: list["Agent"] = [] context: dict = {} - parent_task: "Task | None" = Field( + parent: "Task | None" = Field( None, description="The task that spawned this task.", validate_default=True, ) - upstream_tasks: list["Task"] = [] + depends_on: list["Task"] = [] status: TaskStatus = TaskStatus.INCOMPLETE result: T = None - result_type: type[T] | GenericAlias | None = None + result_type: type[T] | GenericAlias | _LiteralGenericAlias | None = None error: str | None = None tools: list[AssistantTool | Callable] = [] user_access: bool = False - _children_tasks: list["Task"] = [] - _downstream_tasks: list["Task"] = [] + _children: list["Task"] = [] + _downstream: list["Task"] = [] + model_config = dict(extra="forbid", arbitrary_types_allowed=True) + + def __init__(self, objective=None, result_type=None, **kwargs): + if result_type is not None: + kwargs["result_type"] = result_type + if objective is not None: + kwargs["objective"] = objective + # allow certain args to be provided as a positional args + super().__init__(**kwargs) @field_validator("agents", mode="before") def _turn_none_into_empty_list(cls, v): return v or [] - @field_validator("parent_task", mode="before") + @field_validator("result_type", mode="before") + def _turn_list_into_literal_result_type(cls, v): + if isinstance(v, (list, tuple, set)): + return Literal[tuple(v)] # type: ignore + return v + + @field_validator("parent", mode="before") def _load_parent_task_from_ctx(cls, v): if v is None: v = ctx.get("tasks", None) @@ -71,20 +92,20 @@ def _load_parent_task_from_ctx(cls, v): @model_validator(mode="after") def _update_relationships(self): - if self.parent_task is not None: - self.parent_task._children_tasks.append(self) - for task in self.upstream_tasks: - task._downstream_tasks.append(self) + if self.parent is not None: + self.parent._children.append(self) + for task in self.depends_on: + task._downstream.append(self) return self - @field_serializer("parent_task") - def _serialize_parent_task(parent_task: "Task | None"): - if parent_task is not None: - return parent_task.id + @field_serializer("parent") + def _serialize_parent_task(parent: "Task | None"): + if parent is not None: + return parent.id - @field_serializer("upstream_tasks") - def _serialize_upstream_tasks(upstream_tasks: list["Task"]): - return [t.id for t in upstream_tasks] + @field_serializer("depends_on") + def _serialize_depends_on(depends_on: list["Task"]): + return [t.id for t in depends_on] @field_serializer("result_type") def _serialize_result_type(result_type: list["Task"]): @@ -97,45 +118,56 @@ def _serialize_agents(agents: list["Agent"]): for a in agents ] - def __init__(self, objective, **kwargs): - # allow objective as a positional arg - super().__init__(objective=objective, **kwargs) - - def children(self, include_self: bool = True): + def trace_dependencies(self) -> list["Task"]: """ - Returns a list of all children of this task, including recursively - nested children. Includes this task by default (disable with - `include_self=False`) + Returns a list of all tasks related to this task, including upstream and downstream tasks, parents, and children. """ - visited = set() - children = [] + + # first get all children of this task + tasks = set() stack = [self] while stack: current = stack.pop() - if current not in visited: - visited.add(current) - if include_self or current != self: - children.append(current) - stack.extend(current._children_tasks) - return list(set(children)) - - def children_agents(self, include_self: bool = True) -> list["Agent"]: - children = self.children(include_self=include_self) + if current not in tasks: + tasks.add(current) + stack.extend(current._children) + + # get all the parents + current = self + while current.parent is not None: + tasks.add(current.parent) + current = current.parent + + # get all upstream tasks of any we've already found + stack: list[Task] = list(tasks) + while stack: + current = stack.pop() + if current not in tasks: + tasks.add(current) + if current.is_incomplete(): + stack.extend(current.depends_on) + + return list(tasks) + + def dependency_agents(self) -> list["Agent"]: + deps = self.trace_dependencies() agents = [] - for child in children: - agents.extend(child.agents) + for task in deps: + agents.extend(task.agents) return agents def run_iter( self, agents: list["Agent"] = None, - collab_fn: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + moderator: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, ): - if collab_fn is None: - collab_fn = itertools.cycle + from control_flow.core.controller.collaboration import round_robin + + if moderator is None: + moderator = round_robin if agents is None: - agents = self.children_agents(include_self=True) + agents = self.dependency_agents() if not agents: raise ValueError( @@ -143,10 +175,12 @@ def run_iter( "Please specify agents to run the task, or assign agents to the task." ) - for agent in collab_fn(agents): + all_tasks = self.trace_dependencies() + + for agent in moderator(agents, tasks=all_tasks): if self.is_complete(): break - agent.run(tasks=self.children(include_self=True)) + agent.run(tasks=all_tasks) yield True def run(self, agent: "Agent" = None): @@ -156,7 +190,7 @@ def run(self, agent: "Agent" = None): from control_flow.core.agent import Agent if agent is None: - all_agents = self.children_agents() + all_agents = self.dependency_agents() if not all_agents: agent = Agent() elif len(all_agents) == 1: @@ -174,14 +208,14 @@ def run(self, agent: "Agent" = None): def run_until_complete( self, agents: list["Agent"] = None, - collab_fn: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + moderator: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, ) -> T: """ Runs the task with provided agents until it is complete. """ - for run in self.run_iter(agents=agents, collab_fn=collab_fn): - pass + for _ in self.run_iter(agents=agents, moderator=moderator): + continue if self.is_successful(): return self.result @@ -263,13 +297,13 @@ def _create_skip_tool(self) -> FunctionTool: def get_tools(self) -> list[AssistantTool | Callable]: tools = self.tools.copy() if self.is_incomplete(): - tools.extend( - [ - self._create_success_tool(), - self._create_fail_tool(), - self._create_skip_tool(), - ] - ) + tools.append(self._create_fail_tool()) + # add skip tool if this task has a parent task + if self.parent is not None: + tools.append(self._create_skip_tool()) + # add success tools if this task has no upstream tasks or all upstream tasks are complete + if all(t.is_complete() for t in self.depends_on): + tools.append(self._create_success_tool()) if self.user_access: tools.append(marvin.utilities.tools.tool_from_function(talk_to_human)) return [wrap_prefect_tool(t) for t in tools] From 9c44d7b1d499a0865b893a139d9daed9fb7138bd Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 9 May 2024 21:45:11 -0400 Subject: [PATCH 3/3] Improve moderation --- examples/multi_agent_conversation.py | 26 +++-- src/control_flow/core/agent.py | 3 + .../core/controller/controller.py | 7 +- .../core/controller/instruction_template.py | 3 +- .../{collaboration.py => moderators.py} | 39 +++++++- src/control_flow/core/flow.py | 13 +++ src/control_flow/core/task.py | 94 +++++++++++-------- 7 files changed, 132 insertions(+), 53 deletions(-) rename src/control_flow/core/controller/{collaboration.py => moderators.py} (51%) diff --git a/examples/multi_agent_conversation.py b/examples/multi_agent_conversation.py index 9a791126..211b6e72 100644 --- a/examples/multi_agent_conversation.py +++ b/examples/multi_agent_conversation.py @@ -1,5 +1,5 @@ from control_flow import Agent, Task, ai_flow -from control_flow.core.controller.collaboration import Moderator +from control_flow.core.controller.moderators import Moderator jerry = Agent( name="Jerry", @@ -50,15 +50,27 @@ """, ) +newman = Agent( + name="Newman", + description="The antagonist and foil to Jerry.", + instructions=""" + You are Newman from the show Seinfeld. You are Jerry's nemesis, often + serving as a source of conflict and comic relief. Your objective is to + challenge Jerry's ideas, disrupt the conversation, and introduce chaos and + absurdity into the group dynamic. + """, +) + @ai_flow def demo(): - with Task("Discuss a topic", agents=[jerry, george, elaine, kramer]): - finish = Task( - "Finish the conversation after everyone speaks at least once", - agents=[jerry], - ) - finish.run_until_complete(moderator=Moderator()) + topic = "milk and cereal" + task = Task( + "Discuss a topic; everyone should speak at least once", + agents=[jerry, george, elaine, kramer, newman], + context=dict(topic=topic), + ) + task.run_until_complete(moderator=Moderator()) demo() diff --git a/src/control_flow/core/agent.py b/src/control_flow/core/agent.py index 10d81013..eb7d15f0 100644 --- a/src/control_flow/core/agent.py +++ b/src/control_flow/core/agent.py @@ -39,3 +39,6 @@ async def run_async(self, tasks: list[Task] | Task | None = None): controller = Controller(agents=[self], tasks=tasks or [], flow=get_flow()) return await controller.run_agent_async(agent=self) + + def __hash__(self): + return id(self) diff --git a/src/control_flow/core/controller/controller.py b/src/control_flow/core/controller/controller.py index b09a3b40..2eaecd94 100644 --- a/src/control_flow/core/controller/controller.py +++ b/src/control_flow/core/controller/controller.py @@ -113,12 +113,13 @@ async def _run_agent(self, agent: Agent, thread: Thread = None) -> Run: ) instructions = instructions_template.render() - breakpoint() tools = self.flow.tools + agent.get_tools() - for task in self.tasks: - tools = tools + task.get_tools() + # add tools for any inactive tasks that the agent is assigned to + for task in self.all_tasks(): + if task.is_incomplete() and agent in task.agents: + tools = tools + task.get_tools() # filter tools because duplicate names are not allowed final_tools = [] diff --git a/src/control_flow/core/controller/instruction_template.py b/src/control_flow/core/controller/instruction_template.py index 6dd156b2..9189f2d9 100644 --- a/src/control_flow/core/controller/instruction_template.py +++ b/src/control_flow/core/controller/instruction_template.py @@ -67,7 +67,8 @@ class TasksTemplate(Template): You must complete the objective even if the task doesn't require a result. For example, a task that asks you to choose, discuss, or perform an action must be completed by posting messages before the task is - marked complete. + marked complete. The objective may require participation from multiple + agents. Do not mark a task as complete until the objective is fully met. A "parent" is a task that spawned another task as a subtask. Generally, the subtasks will need to be completed BEFORE the parent task. If you diff --git a/src/control_flow/core/controller/collaboration.py b/src/control_flow/core/controller/moderators.py similarity index 51% rename from src/control_flow/core/controller/collaboration.py rename to src/control_flow/core/controller/moderators.py index 426e7bf8..8a1e52db 100644 --- a/src/control_flow/core/controller/collaboration.py +++ b/src/control_flow/core/controller/moderators.py @@ -2,10 +2,10 @@ from typing import TYPE_CHECKING, Any, Generator import marvin -from pydantic import BaseModel +from pydantic import BaseModel, Field from control_flow.core.agent import Agent -from control_flow.core.flow import get_flow_messages +from control_flow.core.flow import Flow, get_flow_messages from control_flow.core.task import Task from control_flow.instructions import get_instructions @@ -29,6 +29,38 @@ def __call__( yield from self.run(agents=agents, tasks=tasks) +class AgentModerator(BaseModerator): + agent: Agent + participate: bool = Field( + False, + description="If True, the moderator can participate in the conversation. Default is False.", + ) + + def __init__(self, agent: Agent, **kwargs): + super().__init__(agent=agent, **kwargs) + + def run(self, agents: list[Agent], tasks: list[Task]) -> Generator[Any, Any, Agent]: + while True: + history = get_flow_messages() + + with Flow(): + task = Task( + "Choose the next agent that should speak.", + instructions=""" + You are acting as a moderator. Choose the next agent to + speak. Complete the task and stay silent. Do not post + any messages, even to confirm marking the task as + successful. + """, + result_type=[a.name for a in agents], + context=dict(agents=agents, history=history, tasks=tasks), + agents=[self.agent], + parent=None, + ) + agent_name = task.run_until_complete() + yield next(a for a in agents if a.name == agent_name) + + class Moderator(BaseModerator): model: str = None @@ -36,7 +68,6 @@ def run(self, agents: list[Agent], tasks: list[Task]) -> Generator[Any, Any, Age while True: instructions = get_instructions() history = get_flow_messages() - context = dict( tasks=tasks, messages=history, global_instructions=instructions ) @@ -46,7 +77,7 @@ def run(self, agents: list[Agent], tasks: list[Task]) -> Generator[Any, Any, Age instructions=""" Given the conversation context, choose the AI agent most qualified to take the next turn at completing the tasks. Take into - account any tasks, instructions, and tools. + account any tasks, history, instructions, and tools. """, model_kwargs=dict(model=self.model) if self.model else None, ) diff --git a/src/control_flow/core/flow.py b/src/control_flow/core/flow.py index c69a33e6..1ca38b04 100644 --- a/src/control_flow/core/flow.py +++ b/src/control_flow/core/flow.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from typing import Callable, Literal from marvin.beta.assistants import Thread @@ -34,6 +35,18 @@ def _load_thread_from_ctx(cls, v): def add_message(self, message: str, role: Literal["user", "assistant"] = None): prefect_task(self.thread.add)(message, role=role) + @contextmanager + def _context(self): + with ctx(flow=self, tasks=[]): + yield self + + def __enter__(self): + self.__cm = self._context() + return self.__cm.__enter__() + + def __exit__(self, *exc_info): + return self.__cm.__exit__(*exc_info) + def get_flow() -> Flow: """ diff --git a/src/control_flow/core/task.py b/src/control_flow/core/task.py index c590af2a..5d6f15b7 100644 --- a/src/control_flow/core/task.py +++ b/src/control_flow/core/task.py @@ -41,6 +41,9 @@ class TaskStatus(Enum): SKIPPED = "skipped" +NOTSET = "__notset__" + + class Task(ControlFlowModel): id: str = Field(default_factory=lambda: str(uuid.uuid4().hex[:4])) objective: str @@ -48,7 +51,7 @@ class Task(ControlFlowModel): agents: list["Agent"] = [] context: dict = {} parent: "Task | None" = Field( - None, + NOTSET, description="The task that spawned this task.", validate_default=True, ) @@ -83,7 +86,7 @@ def _turn_list_into_literal_result_type(cls, v): @field_validator("parent", mode="before") def _load_parent_task_from_ctx(cls, v): - if v is None: + if v is NOTSET: v = ctx.get("tasks", None) if v: # get the most recently-added task @@ -156,33 +159,6 @@ def dependency_agents(self) -> list["Agent"]: agents.extend(task.agents) return agents - def run_iter( - self, - agents: list["Agent"] = None, - moderator: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, - ): - from control_flow.core.controller.collaboration import round_robin - - if moderator is None: - moderator = round_robin - - if agents is None: - agents = self.dependency_agents() - - if not agents: - raise ValueError( - f"Task {self.id} has no agents assigned to it or its children." - "Please specify agents to run the task, or assign agents to the task." - ) - - all_tasks = self.trace_dependencies() - - for agent in moderator(agents, tasks=all_tasks): - if self.is_complete(): - break - agent.run(tasks=all_tasks) - yield True - def run(self, agent: "Agent" = None): """ Runs the task with provided agent. If no agent is provided, a default agent is used. @@ -198,11 +174,10 @@ def run(self, agent: "Agent" = None): else: raise ValueError( f"Task {self.id} has multiple agents assigned to it or its " - "children. Please specify one to run the task, or call task.run_iter() " - "or task.run_until_complete() to use all agents." + "children. Please specify one to run the task or call run_until_complete()." ) - run_gen = self.run_iter(agents=[agent]) + run_gen = run_iter(tasks=[self], agents=[agent]) return next(run_gen) def run_until_complete( @@ -214,13 +189,10 @@ def run_until_complete( Runs the task with provided agents until it is complete. """ - for _ in self.run_iter(agents=agents, moderator=moderator): - continue - - if self.is_successful(): - return self.result - elif self.is_failed(): + run_until_complete(tasks=[self], agents=agents, moderator=moderator) + if self.is_failed(): raise ValueError(f"Task {self.id} failed: {self.error}") + return self.result @contextmanager def _context(self): @@ -345,3 +317,49 @@ def any_failed(tasks: list[Task]) -> bool: def none_failed(tasks: list[Task]) -> bool: return not any_failed(tasks) + + +def run_iter( + tasks: list["Task"], + agents: list["Agent"] = None, + moderator: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, +): + from control_flow.core.controller.moderators import round_robin + + if moderator is None: + moderator = round_robin + + if agents is None: + agents = list(set([a for t in tasks for a in t.dependency_agents()])) + + if not agents: + raise ValueError("Tasks have no agents assigned. Please specify agents.") + + all_tasks = list(set([a for t in tasks for a in t.trace_dependencies()])) + + for agent in moderator(agents, tasks=all_tasks): + if any(t.is_failed() for t in tasks): + break + elif all(t.is_complete() for t in tasks): + break + agent.run(tasks=all_tasks) + yield True + + +def run_until_complete( + tasks: list["Task"], + agents: list["Agent"] = None, + moderator: Callable[[list["Agent"]], Generator[None, None, "Agent"]] = None, + raise_on_error: bool = True, +) -> T: + """ + Runs the task with provided agents until it is complete. + """ + + for _ in run_iter(tasks=tasks, agents=agents, moderator=moderator): + continue + + if raise_on_error and any(t.is_failed() for t in tasks): + raise ValueError( + f"At least one task failed: {', '.join(t.id for t in tasks if t.is_failed())}" + )