From c26fefbd65539ecfc1bf27dd77d67bf31cfeb0e4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:54:47 -0400 Subject: [PATCH 1/2] Add new agentic examples --- docs/examples/agent-engineer.mdx | 106 +++++++------ docs/examples/call-routing.mdx | 123 ++++++++++------ docs/examples/features/tools.mdx | 139 ++++++++++++++++++ docs/mint.json | 15 +- src/controlflow/agents/agent.py | 18 ++- src/controlflow/orchestration/orchestrator.py | 8 +- .../prompt_templates/tasks.jinja | 14 +- .../orchestration/turn_strategies.py | 42 +++--- src/controlflow/tasks/task.py | 23 +-- src/controlflow/tools/tools.py | 4 +- tests/tools/test_tools.py | 50 +++++++ 11 files changed, 391 insertions(+), 151 deletions(-) create mode 100644 docs/examples/features/tools.mdx diff --git a/docs/examples/agent-engineer.mdx b/docs/examples/agent-engineer.mdx index 40af1b25..5d33ebae 100644 --- a/docs/examples/agent-engineer.mdx +++ b/docs/examples/agent-engineer.mdx @@ -1,5 +1,7 @@ --- title: Software Engineer +description: Create an AI agent that acts as a software engineer, taking user input and generating code based on the requirements. +icon: file --- Who doesn't want an AI software engineer? @@ -8,87 +10,78 @@ This example demonstrates how to create an AI agent that acts as a software engi -This agent will be able to read, write, and delete files on your system. Make sure you understand the code before running it. +This agent will be able to read, write, and delete files on your system. Make sure you understand the code before running it! ```python engineer.py from pathlib import Path - import controlflow as cf -import controlflow.tools.code -import controlflow.tools.filesystem +from controlflow.tools import filesystem, code from pydantic import BaseModel -# load the instructions -instructions = open(Path(__file__).parent / "instructions.md").read() - -# create the agent -agent = cf.Agent( - "Engineer", - instructions=instructions, - tools=[ - *controlflow.tools.filesystem.ALL_TOOLS, - controlflow.tools.code.python, - controlflow.tools.code.shell, - ], -) - - class DesignDoc(BaseModel): goals: str design: str implementation_details: str criteria: str +# Load the instructions +# instructions = Path(__file__).parent.joinpath("instructions.md").read_text() +instructions = Path('/tmp/instructions.md').read_text() -@cf.flow -def run_engineer(): +# Create the agent +engineer = cf.Agent( + name="Engineer", + instructions=instructions, + tools=[ + *filesystem.ALL_TOOLS, + code.python, + code.shell, + ], +) - # the first task is to work with the user to create a design doc - design_doc = cf.Task( +@cf.flow(default_agent=engineer, instructions='Do not give up until the software works.') +def software_engineer_flow(): + # Task 1: Create design document + design_doc = cf.run( "Learn about the software the user wants to build", instructions=""" - Interact with the user to understand the software they want to - build. What is its purpose? What language should you use? What does - it need to do? Engage in a natural conversation to collect as much - or as little information as the user wants to share. Once you have - enough, write out a design document to complete the task. - """, + Interact with the user to understand the software they want to build. + What is its purpose? What language should you use? What does it need to do? + Engage in a natural conversation to collect information. + Once you have enough, write out a design document to complete the task. + """, interactive=True, result_type=DesignDoc, ) - # next we create a directory for any files - mkdir = cf.Task( + # Task 2: Create project directory + project_dir = cf.run( "Create a directory for the software", instructions=""" - Create a directory to store the software and any related files. The - directory should be named after the software. Return the path. + Create a directory to store the software and related files. + The directory should be named after the software. Return the path. """, result_type=str, - tools=[controlflow.tools.filesystem.mkdir], - agents=[agent], + tools=[filesystem.mkdir], ) - # the final task is to write the software - software = cf.Task( - "Finish the software", + # Task 3: Implement the software + cf.run( + "Implement the software", instructions=""" - Mark this task complete when the software runs as expected and the - user can invoke it. Until then, continue to build the software. - - All files must be written to the provided root directory. + Implement the software based on the design document. + All files must be written to the provided project directory. + Continue building and refining until the software runs as expected and meets all requirements. + Update the user on your progress regularly. """, + context=dict(design_doc=design_doc, project_dir=project_dir), result_type=None, - context=dict(design_doc=design_doc, root_dir=mkdir), - agents=[agent], ) - return software - if __name__ == "__main__": - run_engineer() + software_engineer_flow() ``` ```markdown instructions.md # Software Engineer Agent @@ -129,4 +122,21 @@ You are a software engineer specialized in leveraging large language models (LLM By adhering to this structured approach and best practices, you will efficiently transform user ideas into high-quality, functional software solutions, ensuring user satisfaction and project success. ``` - \ No newline at end of file + + + + + + + + + + + + + + + + + + diff --git a/docs/examples/call-routing.mdx b/docs/examples/call-routing.mdx index 39d1acfe..09a6b989 100644 --- a/docs/examples/call-routing.mdx +++ b/docs/examples/call-routing.mdx @@ -1,73 +1,114 @@ --- title: Customer Call Routing +description: Train an agent to route customer calls to the correct department. +icon: headset --- -In this example, two agents interact in a call routing scenario. One agent plays the role of a customer calling into a call center, while the other agent is a trainee customer service representative. The trainee must listen to the customer's story and route them to the correct department based on the information provided. +In this example, you'll witness a roleplay between two AI agents: +1. A "customer" agent, who has been assigned a random department they need to reach but is instructed not to directly state it. +2. A "trainee" customer service representative, who must figure out the correct department based on the customer's story. + +The conversation will continue back and forth until the trainee feels confident enough to route the call. This example showcases how ControlFlow can be used to create dynamic, multi-turn interactions between agents, with one agent (the trainee) ultimately making a decision that determines the outcome of the task. + +As you run this example, you'll see the conversation unfold in real-time, culminating in the trainee's decision to route the call. The success of the interaction depends on whether the trainee correctly identifies the department the customer needs. + +## Code ```python import random from enum import Enum import controlflow as cf - -class Department(Enum): - SALES = "sales" - SUPPORT = "support" - BILLING = "billing" - TECHNICAL = "technical" - RETURNS = "returns" - - +DEPARTMENTS = [ + "Sales", + "Support", + "Billing", + "Returns", +] @cf.flow def routing_flow(): - department = random.choice(list(Department)) + target_department = random.choice(DEPARTMENTS) + + print(f"\n---\nThe target department is: {target_department}\n---\n") - # create an agent to be our "customer" customer = cf.Agent( - "Customer", + name="Customer", instructions=f""" You are training customer reps by pretending to be a customer calling into a call center. You need to be routed to the - {department} department. Come up with a good backstory.""", + {target_department} department. Come up with a good backstory. + """, ) trainee = cf.Agent( - "Trainee", - instructions=""" + name="Trainee", + instructions=""", You are a trainee customer service representative. You need to listen to the customer's story and route them to the correct - department. Note that the customer is another agent training you.""", + department. Note that the customer is another agent training you. + """, ) - task = cf.Task( - """ - In this task, the customer agent and the trainee agent will speak to - each other. The customer is trying to be routed to the correct - department. The trainee will listen and ask questions to determine the - correct department. - """, - instructions=""" - Only the trainee can mark the task successful by routing the customer to - the correct department. The trainee must begin the conversation by - greeting the customer. Agents speak to each other by posting messages - directly to the thread. Do not use the `end_turn` tool or try to talk - to a user. - """, - agents=[trainee, customer], - result_type=Department, - ) + with cf.Task( + "Route the customer to the correct department.", + agents=[trainee], + result_type=DEPARTMENTS, + ) as main_task: + + while main_task.is_incomplete(): + + cf.run( + "Talk to the trainee.", + instructions=( + "Post a message to talk. In order to help the trainee " + "learn, don't be direct about the department you want. " + "Instead, share a story that will let them practice. " + "After you speak, mark this task as complete." + ), + agents=[customer], + result_type=None + ) - routed_dapartment = task.run() - if routed_dapartment == department: + cf.run( + "Talk to the customer.", + instructions=( + "Post a message to talk. Ask questions to learn more " + "about the customer. After you speak, mark this task as " + "complete. When you have enough information, use the main " + "task tool to route the customer to the correct department." + ), + agents=[trainee], + result_type=None, + tools=[main_task.create_success_tool()] + ) + + if main_task.result == target_department: print("Success! The customer was routed to the correct department.") else: - print( - "Failed. The customer was routed to the wrong department. " - f"The correct department was {department}." - ) + print(f"Failed. The customer was routed to the wrong department. " + f"The correct department was {target_department}.") if __name__ == "__main__": routing_flow() -```` \ No newline at end of file +``` + +## Key points + +1. **Multi-agent interaction**: This example showcases how to orchestrate a conversation between two AI agents, each with distinct roles and objectives. + +2. **Parent task as control flow**: The `main_task` serves dual purposes - it represents the overall objective and acts as a control mechanism for the conversation loop. The `while main_task.is_incomplete()` construct creates a flexible, AI-driven loop that continues until the trainee decides to route the call. + +3. **Explicit turn-taking**: Instead of using ControlFlow's built-in turn strategies, this example manually alternates between the customer and trainee agents. This provides fine-grained control over the conversation flow and allows for specific instructions to be given to each agent on each turn. + +4. **Task-specific tools**: The trainee is given access to the `main_task`'s success tool, allowing them to mark the overall task as complete when they're ready to route the call, even though that task isn't currently active. This demonstrates how tools can be used to give agents control over task state. + + +## Further reading + +- For more details on creating and managing tasks, see the [Tasks documentation](/concepts/tasks). +- To learn more about agents and their capabilities, check out the [Agents guide](/concepts/agents). +- For information on how ControlFlow manages conversations and context, refer to the [Message History guide](/patterns/history). + +This example effectively demonstrates how to create a complex, interactive scenario in ControlFlow, with fine-grained control over agent interactions and task flow. It showcases the flexibility of the framework in handling multi-turn conversations and decision-making processes, making it an excellent template for building sophisticated AI-powered applications. \ No newline at end of file diff --git a/docs/examples/features/tools.mdx b/docs/examples/features/tools.mdx new file mode 100644 index 00000000..f60dc1c7 --- /dev/null +++ b/docs/examples/features/tools.mdx @@ -0,0 +1,139 @@ +--- +title: Custom tools +description: Provide tools to expand agent capabilities. +icon: wrench +--- + +In ControlFlow, tools can be assigned at different levels of your workflow: to the flow itself, to specific agents, or to individual tasks. This flexibility allows you to expand agent capabilities and create more powerful workflows. + +This example demonstrates how to assign and use tools at each level for a file search and information retrieval scenario, using a temporary directory with test files. + + +The agent in this example can read files on your local file system. While simple precautions are taken to restrict the agent to demo data created specifically for this example, you may consider this a potential security risk. + + +## Code + +In this example, we create an agentic workflow that searches through files looking for important information. To avoid interfering with your local file system, we'll create a context manager that sets up a temporary directory with demo files that the agent can search. + + +This example's code is split into multiple blocks for legibility. Please run each code block in sequence to run the full example. + +### Set up example data + +First, let's create a context manager that sets up a temporary directory with test files: + +```python Files +import contextlib +import tempfile +import os + +@contextlib.contextmanager +def setup_test_environment(): + with tempfile.TemporaryDirectory() as temp_dir: + # Create test files + files = { + "report.txt": "This report contains important findings from our recent project...", + "meeting_notes.txt": "In today's important meeting, we discussed the new product launch...", + "todo.txt": "Important tasks for this week: 1. Client meeting, 2. Finish report...", + } + for filename, content in files.items(): + with open(os.path.join(temp_dir, filename), 'w') as f: + f.write(content) + + yield temp_dir + +``` + +### Set up tools + +Next, let's create some tools for our agent to use. We have one tool for listing the files in a directory, one for reading the contents of a file, and one for printing a message to the user. + +Note that tools can be any Python function, and work best when they have clear type annotations and docstrings. + +```python Tools +def list_files(directory: str) -> list[str]: + """List files in the given directory.""" + return os.listdir(directory) + +def read_file(filepath: str) -> str: + """Read the contents of a file from an absolute filepath.""" + with open(filepath, 'r') as file: + return file.read() + +def update_user(message: str) -> None: + """Print a status message for the user to read.""" + print(f"[User Notification]: {message}") + return 'User updated.' +``` + +### Build a flow +Finally, let's build a workflow. In this example, we want to illustrate the different ways that tools can be provided, so: + +- The flow is given the `update_user` tool, so any agent can post a message to the user at any time +- The agent is given the `list_files` tool, so this agent can list files at any time +- The task is given the `read_file` tool, so that capability is only available while that task is running + +Now, let's build a flow that explores those files. Note that the context manager from the previous code block is used in this example to set up the example data: + + +```python Code +import controlflow as cf + +agent = cf.Agent(name="FileSearcher", tools=[list_files]) + + +@cf.flow(tools=[update_user], default_agent=agent) +def file_search_flow(query: str, directory: str): + + # Task 1: Find files containing the search term + found_files = cf.Task( + f"Identify files in the directory '{directory}' that might " + f"relate to'{query}' and return a list of paths.", + result_type=list[str] + ) + + # Task 2: Analyze file contents and report findings + cf.run( + f"Analyze the contents of the files for information related " + "to the search term.", + instructions='You must update the user on all your findings to complete the task.', + tools=[read_file], + depends_on=[found_files], + result_type=None # We don't need a return value as we're printing directly to the user + ) + +# Run the flow within our test environment +with setup_test_environment() as temp_dir: + file_search_flow(query="launch meeting", directory=temp_dir) +``` +```text Result +[User Notification]: Listing the files in the directory to + identify the relevant contents. + +[User Notification]: Content analysis completed. The relevant + information related to 'launch meeting' is found in + 'meeting_notes.txt'. The exact content is: 'In today's + important meeting, we discussed the new product launch...'. +``` + + +## Key points + +1. **Tool assignment**: Tools can be assigned to flows, agents, and tasks, which lets you control which agents or tasks have access to which tools. + +2. **Instructions**: Agents will follow instructions, including how to use tools. + +3. **Dependent tasks**: The second task in this flow depends on the first task, which means 1) it automatically has visibility into its result and 2) ControlFlow automatically ran the first task when the second task was run. + +4. **Flow context**: The `query` parameter was part of the flow context, which means all tasks could see it even it if wasn't explicitly provided to them. + + + +## Further reading + +- For more details on creating and using tools, see the [Tools documentation](/patterns/tools). +- To learn more about agents and their capabilities, check out the [Agents guide](/concepts/agents). +- For information on how ControlFlow manages task execution and context, refer to the [Running tasks guide](/patterns/running-tasks). + +By strategically assigning tools at different levels in your ControlFlow workflows, you can significantly expand the capabilities of your AI agents, enabling them to interact with external systems and perform complex operations. \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index ac334631..3e1fae7f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -70,6 +70,14 @@ "guides/default-agent" ] }, + { + "group": "ControlFlow Features", + "pages": [ + "examples/features/dependent-tasks", + "examples/features/tools", + "examples/features/private-flows" + ] + }, { "group": "Core LLM Operations", "pages": [ @@ -84,13 +92,6 @@ "examples/code-explanation" ] }, - { - "group": "ControlFlow Features", - "pages": [ - "examples/features/dependent-tasks", - "examples/features/private-flows" - ] - }, { "group": "Agentic Flows", "pages": [ diff --git a/src/controlflow/agents/agent.py b/src/controlflow/agents/agent.py index 3dc1b1d4..c42d9eb0 100644 --- a/src/controlflow/agents/agent.py +++ b/src/controlflow/agents/agent.py @@ -7,13 +7,12 @@ TYPE_CHECKING, Any, AsyncGenerator, - Callable, Generator, Optional, ) from langchain_core.language_models import BaseChatModel -from pydantic import Field, field_serializer +from pydantic import Field, field_serializer, field_validator import controlflow from controlflow.agents.names import AGENT_NAMES @@ -22,6 +21,7 @@ from controlflow.llm.messages import AIMessage, BaseMessage from controlflow.llm.rules import LLMRules from controlflow.tools.tools import ( + Tool, as_lc_tools, as_tools, handle_tool_call, @@ -63,9 +63,7 @@ class Agent(ControlFlowModel, abc.ABC): None, description="A system template for the agent. The template should be formatted as a jinja2 template.", ) - tools: list[Callable] = Field( - [], description="List of tools available to the agent." - ) + tools: list[Tool] = Field([], description="List of tools available to the agent.") interactive: bool = Field( False, description="If True, the agent is given tools for interacting with a human user.", @@ -127,8 +125,12 @@ def _generate_id(self): ) ) + @field_validator("tools", mode="before") + def _validate_tools(cls, tools: list[Tool]): + return as_tools(tools or []) + @field_serializer("tools") - def _serialize_tools(self, tools: list[Callable]): + def _serialize_tools(self, tools: list[Tool]): tools = controlflow.tools.as_tools(tools) # tools are Pydantic 1 objects return [t.dict(include={"name", "description"}) for t in tools] @@ -262,8 +264,10 @@ def _run_model( ToolResultEvent, ) + tools = as_tools(self.get_tools() + tools) model = self.get_model(tools=tools) + logger.debug(f"Running model {model} for agent {self.name} with tools {tools}") if controlflow.settings.log_all_messages: logger.debug(f"Input messages: {messages}") @@ -317,8 +321,10 @@ async def _run_model_async( ToolResultEvent, ) + tools = as_tools(self.get_tools() + tools) model = self.get_model(tools=tools) + logger.debug(f"Running model {model} for agent {self.name} with tools {tools}") if controlflow.settings.log_all_messages: logger.debug(f"Input messages: {messages}") diff --git a/src/controlflow/orchestration/orchestrator.py b/src/controlflow/orchestration/orchestrator.py index 69fb8096..eeadd582 100644 --- a/src/controlflow/orchestration/orchestrator.py +++ b/src/controlflow/orchestration/orchestrator.py @@ -117,10 +117,10 @@ def get_tools(self) -> list[Tool]: tools.append(task.create_success_tool()) tools.append(task.create_fail_tool()) - # add turn strategy tools - tools.extend( - self.turn_strategy.get_tools(self.agent, self.get_available_agents()) - ) + # add turn strategy tools only if there are multiple available agents + available_agents = self.get_available_agents() + if len(available_agents) > 1: + tools.extend(self.turn_strategy.get_tools(self.agent, available_agents)) tools = as_tools(tools) return tools diff --git a/src/controlflow/orchestration/prompt_templates/tasks.jinja b/src/controlflow/orchestration/prompt_templates/tasks.jinja index 39746dea..338fbde2 100644 --- a/src/controlflow/orchestration/prompt_templates/tasks.jinja +++ b/src/controlflow/orchestration/prompt_templates/tasks.jinja @@ -14,16 +14,18 @@ The following tasks are active: {% for task in tasks %} -Assigned agents: {{ task._serialize_agents(task.get_agents()) }} -{% if task.completion_agents -%} -Completion agents: {{ task._serialize_agents(task.completion_agents) }} -{% endif %} + Assigned agents: {{ task._serialize_agents(task.get_agents()) }} + {% if task.completion_agents -%} + Completion agents: {{ task._serialize_agents(task.completion_agents) }} + {% endif %} -{{ task.get_prompt() }} + {{ task.get_prompt() }} {% endfor %} -Only agents assigned to a task are able to mark the task as complete. Delegate work appropriately to your collaborators. +Only agents assigned to a task are able to mark the task as complete. You must use a tool to end your turn to let other +agents participate. If you are asked to talk to other agents, post messages. Do not impersonate another agent! Do not +impersonate the orchestrator! Only mark a task failed if there is a technical error or issue preventing completion. diff --git a/src/controlflow/orchestration/turn_strategies.py b/src/controlflow/orchestration/turn_strategies.py index ccbc45c2..803ffc9f 100644 --- a/src/controlflow/orchestration/turn_strategies.py +++ b/src/controlflow/orchestration/turn_strategies.py @@ -14,8 +14,8 @@ class TurnStrategy(ControlFlowModel, ABC): @abstractmethod def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: pass @abstractmethod @@ -41,15 +41,18 @@ def should_end_turn(self) -> bool: def create_end_turn_tool(strategy: TurnStrategy) -> Tool: @tool def end_turn() -> str: - """End your turn. Only use this tool if you have no other options.""" + """ + End your turn. Only use this tool if you have no other options and + want a different agent to take over. This tool does not mark tasks as complete. + """ strategy.end_turn = True - return "Turn ended. Another agent will be selected." + return "Turn ended." return end_turn def create_delegate_tool( - strategy: TurnStrategy, available_agents: Dict[Agent, List[Task]] + strategy: TurnStrategy, available_agents: dict[Agent, list[Task]] ) -> Tool: @tool def delegate_to_agent(agent_id: str, message: str = None) -> str: @@ -72,8 +75,8 @@ class SingleAgent(TurnStrategy): agent: Agent def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: return [create_end_turn_tool(self)] def get_next_agent( @@ -88,12 +91,9 @@ def get_next_agent( class Popcorn(TurnStrategy): def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: - if len(available_agents) > 1: - return [create_delegate_tool(self, available_agents)] - else: - return [create_end_turn_tool(self)] + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: + return [create_delegate_tool(self, available_agents)] def get_next_agent( self, current_agent: Optional[Agent], available_agents: Dict[Agent, List[Task]] @@ -105,8 +105,8 @@ def get_next_agent( class Random(TurnStrategy): def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: return [create_end_turn_tool(self)] def get_next_agent( @@ -117,8 +117,8 @@ def get_next_agent( class RoundRobin(TurnStrategy): def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: return [create_end_turn_tool(self)] def get_next_agent( @@ -134,8 +134,8 @@ def get_next_agent( class MostBusy(TurnStrategy): def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: return [create_end_turn_tool(self)] def get_next_agent( @@ -149,8 +149,8 @@ class Moderated(TurnStrategy): moderator: Agent def get_tools( - self, current_agent: Agent, available_agents: Dict[Agent, List[Task]] - ) -> List[Tool]: + self, current_agent: Agent, available_agents: dict[Agent, list[Task]] + ) -> list[Tool]: if current_agent == self.moderator: return [create_delegate_tool(self, available_agents)] else: diff --git a/src/controlflow/tasks/task.py b/src/controlflow/tasks/task.py index 4950078d..b166527e 100644 --- a/src/controlflow/tasks/task.py +++ b/src/controlflow/tasks/task.py @@ -31,6 +31,7 @@ from controlflow.instructions import get_instructions from controlflow.tools import Tool, tool from controlflow.tools.input import cli_input +from controlflow.tools.tools import as_tools from controlflow.utilities.context import ctx from controlflow.utilities.general import ( NOTSET, @@ -135,7 +136,7 @@ class Task(ControlFlowModel): "result validator function is called *after* the `result_type` is " "processed.", ) - tools: list[Callable] = Field( + tools: list[Tool] = Field( default_factory=list, description="Tools available to every agent working on this task.", ) @@ -262,6 +263,10 @@ def _validate_result_type(cls, v): v = Labels(v) return v + @field_validator("tools", mode="before") + def _validate_tools(cls, v): + return as_tools(v or []) + @field_serializer("parent") def _serialize_parent(self, parent: Optional["Task"]): return parent.id if parent is not None else None @@ -485,21 +490,7 @@ def set_status(self, status: TaskStatus): def mark_running(self): self.set_status(TaskStatus.RUNNING) - def mark_successful(self, result: T = None, validate_upstreams: bool = True): - if validate_upstreams: - if any(t.is_incomplete() for t in self.depends_on): - raise ValueError( - f"Task {self.objective} cannot be marked successful until all of its " - "upstream dependencies are completed. Incomplete dependencies " - f"are: {', '.join(t.friendly_name() for t in self.depends_on if t.is_incomplete())}" - ) - elif any(t.is_incomplete() for t in self._subtasks): - raise ValueError( - f"Task {self.objective} cannot be marked successful until all of its " - "subtasks are completed. Incomplete subtasks " - f"are: {', '.join(t.friendly_name() for t in self._subtasks if t.is_incomplete())}" - ) - + def mark_successful(self, result: T = None): self.result = self.validate_result(result) self.set_status(TaskStatus.SUCCESSFUL) diff --git a/src/controlflow/tools/tools.py b/src/controlflow/tools/tools.py index 87a3ccac..c678a89c 100644 --- a/src/controlflow/tools/tools.py +++ b/src/controlflow/tools/tools.py @@ -248,7 +248,7 @@ def as_tools( pass elif isinstance(t, langchain_core.tools.BaseTool): t = Tool.from_lc_tool(t) - elif inspect.isfunction(t): + elif inspect.isfunction(t) or inspect.ismethod(t): t = Tool.from_function(t) elif isinstance(t, dict): t = Tool(**t) @@ -271,7 +271,7 @@ def as_lc_tools( pass elif isinstance(t, Tool): t = t.to_lc_tool() - elif inspect.isfunction(t): + elif inspect.isfunction(t) or inspect.ismethod(t): t = langchain_core.tools.StructuredTool.from_function(t) else: raise ValueError(f"Invalid tool: {t}") diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index 40413ea3..fc5efb38 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -4,6 +4,7 @@ import pytest from pydantic import Field +import controlflow from controlflow.agents.agent import Agent from controlflow.llm.messages import ToolMessage from controlflow.tools.tools import ( @@ -302,3 +303,52 @@ def foo(): message = handle_tool_call(tool_call, tools=[foo], agent=agent) assert message.agent.name == "test-agent" assert message.agent.id == agent.id + + +class TestToolAvailability: + x = None + + def signal(self, x): + """You must use this tool to complete the task""" + self.x = x + + @pytest.fixture(autouse=True) + def reset_signal(self): + self.x = None + yield + self.x = None + + def test_agent_tool(self): + """ + Tests that an agent can use a tool assigned to it + """ + agent = Agent(tools=[self.signal]) + controlflow.run( + "Use the signal tool with x=10", agents=[agent], max_llm_calls=1 + ) + assert self.x == 10 + + def test_task_tool(self): + """ + Tests that an agent can use a tool assigned to a task + """ + agent = Agent(name="test-agent") + controlflow.run( + "Use the signal tool with x=10", + agents=[agent], + max_llm_calls=1, + tools=[self.signal], + ) + assert self.x == 10 + + def test_flow_tool(self): + """ + Tests that an agent can use a tool assigned to a flow + """ + agent = Agent(name="test-agent") + + with controlflow.Flow(tools=[self.signal]): + controlflow.run( + "Use the signal tool with x=10", agents=[agent], max_llm_calls=1 + ) + assert self.x == 10 From 04e583ce9f43d26f847aa63fa5f09f6c8d71ef96 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:56:39 -0400 Subject: [PATCH 2/2] Update test_tasks.py --- tests/tasks/test_tasks.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/tasks/test_tasks.py b/tests/tasks/test_tasks.py index e68e54ee..5fda4081 100644 --- a/tests/tasks/test_tasks.py +++ b/tests/tasks/test_tasks.py @@ -195,22 +195,6 @@ def test_task_status_transitions(self): assert not task.is_failed() assert task.is_skipped() - def test_validate_upstream_dependencies_on_success(self): - task1 = SimpleTask() - task2 = SimpleTask(depends_on=[task1]) - with pytest.raises(ValueError, match="cannot be marked successful"): - task2.mark_successful() - task1.mark_successful() - task2.mark_successful() - - def test_validate_subtask_dependencies_on_success(self): - task1 = SimpleTask() - task2 = SimpleTask(parent=task1) - with pytest.raises(ValueError, match="cannot be marked successful"): - task1.mark_successful() - task2.mark_successful() - task1.mark_successful() - def test_task_ready(self): task1 = SimpleTask() assert task1.is_ready()