From 6193eb13fa0b56a214d920c60f6b1e5225899e65 Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:37:21 -0400 Subject: [PATCH 1/5] Disable telemetry explicitly (#1536) * Disable telemetry explicitly * fix linting * revert parts to og --- src/crewai/telemetry/telemetry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/crewai/telemetry/telemetry.py b/src/crewai/telemetry/telemetry.py index a08ccd96f6..e191f8d4d4 100644 --- a/src/crewai/telemetry/telemetry.py +++ b/src/crewai/telemetry/telemetry.py @@ -21,7 +21,7 @@ def suppress_warnings(): from opentelemetry import trace # noqa: E402 -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # noqa: E402 +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # noqa: E402 from opentelemetry.sdk.resources import SERVICE_NAME, Resource # noqa: E402 from opentelemetry.sdk.trace import TracerProvider # noqa: E402 from opentelemetry.sdk.trace.export import BatchSpanProcessor # noqa: E402 @@ -48,6 +48,10 @@ class Telemetry: def __init__(self): self.ready = False self.trace_set = False + + if os.getenv("OTEL_SDK_DISABLED", "false").lower() == "true": + return + try: telemetry_endpoint = "https://telemetry.crewai.com:4319" self.resource = Resource( From 4ae07468f37db34e92df20a6aaae255ffdec7950 Mon Sep 17 00:00:00 2001 From: Robin Wang <6220861+MottoX@users.noreply.github.com> Date: Thu, 31 Oct 2024 04:45:19 +0800 Subject: [PATCH 2/5] Enhance log storage to support more data types (#1530) --- src/crewai/memory/storage/kickoff_task_outputs_storage.py | 2 +- src/crewai/utilities/crew_json_encoder.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/crewai/memory/storage/kickoff_task_outputs_storage.py b/src/crewai/memory/storage/kickoff_task_outputs_storage.py index 57623eef80..dbb5f124b1 100644 --- a/src/crewai/memory/storage/kickoff_task_outputs_storage.py +++ b/src/crewai/memory/storage/kickoff_task_outputs_storage.py @@ -70,7 +70,7 @@ def add( task.expected_output, json.dumps(output, cls=CrewJSONEncoder), task_index, - json.dumps(inputs), + json.dumps(inputs, cls=CrewJSONEncoder), was_replayed, ), ) diff --git a/src/crewai/utilities/crew_json_encoder.py b/src/crewai/utilities/crew_json_encoder.py index 3cab07ffc7..c3f95fcf65 100644 --- a/src/crewai/utilities/crew_json_encoder.py +++ b/src/crewai/utilities/crew_json_encoder.py @@ -2,13 +2,14 @@ import json from uuid import UUID from pydantic import BaseModel +from decimal import Decimal class CrewJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, BaseModel): return self._handle_pydantic_model(obj) - elif isinstance(obj, UUID): + elif isinstance(obj, UUID) or isinstance(obj, Decimal): return str(obj) elif isinstance(obj, datetime) or isinstance(obj, date): From ec2967c3624f355cb652541f4a64f21df1d9dc17 Mon Sep 17 00:00:00 2001 From: Tony Kipkemboi Date: Wed, 30 Oct 2024 21:56:13 -0400 Subject: [PATCH 3/5] Add llm providers accordion group (#1534) * add llm providers accordion group * fix numbering --- docs/concepts/llms.mdx | 267 ++++++++++++++++++++++++++++------------- 1 file changed, 182 insertions(+), 85 deletions(-) diff --git a/docs/concepts/llms.mdx b/docs/concepts/llms.mdx index 90e86adaff..835c2491fe 100644 --- a/docs/concepts/llms.mdx +++ b/docs/concepts/llms.mdx @@ -25,52 +25,55 @@ By default, CrewAI uses the `gpt-4o-mini` model. It uses environment variables i - `OPENAI_API_BASE` - `OPENAI_API_KEY` -### 2. String Identifier +### 2. Custom LLM Objects -```python Code -agent = Agent(llm="gpt-4o", ...) -``` +Pass a custom LLM implementation or object from another library. -### 3. LLM Instance +See below for examples. -List of [more providers](https://docs.litellm.ai/docs/providers). + + + ```python Code + agent = Agent(llm="gpt-4o", ...) + ``` + -```python Code -from crewai import LLM + + ```python Code + from crewai import LLM -llm = LLM(model="gpt-4", temperature=0.7) -agent = Agent(llm=llm, ...) -``` - -### 4. Custom LLM Objects - -Pass a custom LLM implementation or object from another library. + llm = LLM(model="gpt-4", temperature=0.7) + agent = Agent(llm=llm, ...) + ``` + + ## Connecting to OpenAI-Compatible LLMs You can connect to OpenAI-compatible LLMs using either environment variables or by setting specific attributes on the LLM class: -1. Using environment variables: - -```python Code -import os - -os.environ["OPENAI_API_KEY"] = "your-api-key" -os.environ["OPENAI_API_BASE"] = "https://api.your-provider.com/v1" -``` - -2. Using LLM class attributes: - -```python Code -from crewai import LLM - -llm = LLM( - model="custom-model-name", - api_key="your-api-key", - base_url="https://api.your-provider.com/v1" -) -agent = Agent(llm=llm, ...) -``` + + + ```python Code + import os + + os.environ["OPENAI_API_KEY"] = "your-api-key" + os.environ["OPENAI_API_BASE"] = "https://api.your-provider.com/v1" + ``` + + + ```python Code + from crewai import LLM + + llm = LLM( + model="custom-model-name", + api_key="your-api-key", + base_url="https://api.your-provider.com/v1" + ) + agent = Agent(llm=llm, ...) + ``` + + ## LLM Configuration Options @@ -97,55 +100,149 @@ When configuring an LLM for your agent, you have access to a wide range of param | **api_key** | `str` | Your API key for authentication. | -## OpenAI Example Configuration - -```python Code -from crewai import LLM - -llm = LLM( - model="gpt-4", - temperature=0.8, - max_tokens=150, - top_p=0.9, - frequency_penalty=0.1, - presence_penalty=0.1, - stop=["END"], - seed=42, - base_url="https://api.openai.com/v1", - api_key="your-api-key-here" -) -agent = Agent(llm=llm, ...) -``` - -## Cerebras Example Configuration - -```python Code -from crewai import LLM - -llm = LLM( - model="cerebras/llama-3.1-70b", - base_url="https://api.cerebras.ai/v1", - api_key="your-api-key-here" -) -agent = Agent(llm=llm, ...) -``` - -## Using Ollama (Local LLMs) - -CrewAI supports using Ollama for running open-source models locally: - -1. Install Ollama: [ollama.ai](https://ollama.ai/) -2. Run a model: `ollama run llama2` -3. Configure agent: - -```python Code -from crewai import LLM - -agent = Agent( - llm=LLM(model="ollama/llama3.1", base_url="http://localhost:11434"), - ... -) -``` +These are examples of how to configure LLMs for your agent. + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="gpt-4", + temperature=0.8, + max_tokens=150, + top_p=0.9, + frequency_penalty=0.1, + presence_penalty=0.1, + stop=["END"], + seed=42, + base_url="https://api.openai.com/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="cerebras/llama-3.1-70b", + base_url="https://api.cerebras.ai/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + CrewAI supports using Ollama for running open-source models locally: + + 1. Install Ollama: [ollama.ai](https://ollama.ai/) + 2. Run a model: `ollama run llama2` + 3. Configure agent: + + ```python Code + from crewai import LLM + + agent = Agent( + llm=LLM( + model="ollama/llama3.1", + base_url="http://localhost:11434" + ), + ... + ) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="groq/llama3-8b-8192", + base_url="https://api.groq.com/openai/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="anthropic/claude-3-5-sonnet-20241022", + base_url="https://api.anthropic.com/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="fireworks/meta-llama-3.1-8b-instruct", + base_url="https://api.fireworks.ai/inference/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="gemini/gemini-1.5-flash", + base_url="https://api.gemini.google.com/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="perplexity/mistral-7b-instruct", + base_url="https://api.perplexity.ai/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + + + + ```python Code + from crewai import LLM + + llm = LLM( + model="watsonx/ibm/granite-13b-chat-v2", + base_url="https://api.watsonx.ai/v1", + api_key="your-api-key-here" + ) + agent = Agent(llm=llm, ...) + ``` + + ## Changing the Base API URL From 66698503b878459ed5d7d208c7ec37e995a54f28 Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Thu, 31 Oct 2024 15:00:58 -0300 Subject: [PATCH 4/5] Replace .netrc with uv environment variables (#1541) This commit replaces .netrc with uv environment variables for installing tools from private repositories. To store credentials, I created a new and reusable settings file for the CLI in `$HOME/.config/crewai/settings.json`. The issue with .netrc files is that they are applied system-wide and are scoped by hostname, meaning we can't differentiate tool repositories requests from regular requests to CrewAI's API. --- src/crewai/cli/config.py | 38 ++++++++++++ src/crewai/cli/tools/main.py | 40 +++++++------ tests/cli/config_test.py | 109 +++++++++++++++++++++++++++++++++++ tests/cli/tools/test_main.py | 1 + 4 files changed, 169 insertions(+), 19 deletions(-) create mode 100644 src/crewai/cli/config.py create mode 100644 tests/cli/config_test.py diff --git a/src/crewai/cli/config.py b/src/crewai/cli/config.py new file mode 100644 index 0000000000..000f1e6d02 --- /dev/null +++ b/src/crewai/cli/config.py @@ -0,0 +1,38 @@ +import json +from pathlib import Path +from pydantic import BaseModel, Field +from typing import Optional + +DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json" + +class Settings(BaseModel): + tool_repository_username: Optional[str] = Field(None, description="Username for interacting with the Tool Repository") + tool_repository_password: Optional[str] = Field(None, description="Password for interacting with the Tool Repository") + config_path: Path = Field(default=DEFAULT_CONFIG_PATH, exclude=True) + + def __init__(self, config_path: Path = DEFAULT_CONFIG_PATH, **data): + """Load Settings from config path""" + config_path.parent.mkdir(parents=True, exist_ok=True) + + file_data = {} + if config_path.is_file(): + try: + with config_path.open("r") as f: + file_data = json.load(f) + except json.JSONDecodeError: + file_data = {} + + merged_data = {**file_data, **data} + super().__init__(config_path=config_path, **merged_data) + + def dump(self) -> None: + """Save current settings to settings.json""" + if self.config_path.is_file(): + with self.config_path.open("r") as f: + existing_data = json.load(f) + else: + existing_data = {} + + updated_data = {**existing_data, **self.model_dump(exclude_unset=True)} + with self.config_path.open("w") as f: + json.dump(updated_data, f, indent=4) diff --git a/src/crewai/cli/tools/main.py b/src/crewai/cli/tools/main.py index c875229c3c..35652a1ec6 100644 --- a/src/crewai/cli/tools/main.py +++ b/src/crewai/cli/tools/main.py @@ -1,17 +1,15 @@ import base64 import os -import platform import subprocess import tempfile from pathlib import Path -from netrc import netrc -import stat import click from rich.console import Console from crewai.cli import git from crewai.cli.command import BaseCommand, PlusAPIMixin +from crewai.cli.config import Settings from crewai.cli.utils import ( get_project_description, get_project_name, @@ -153,26 +151,16 @@ def login(self): raise SystemExit login_response_json = login_response.json() - self._set_netrc_credentials(login_response_json["credential"]) + + settings = Settings() + settings.tool_repository_username = login_response_json["credential"]["username"] + settings.tool_repository_password = login_response_json["credential"]["password"] + settings.dump() console.print( "Successfully authenticated to the tool repository.", style="bold green" ) - def _set_netrc_credentials(self, credentials, netrc_path=None): - if not netrc_path: - netrc_filename = "_netrc" if platform.system() == "Windows" else ".netrc" - netrc_path = Path.home() / netrc_filename - netrc_path.touch(mode=stat.S_IRUSR | stat.S_IWUSR, exist_ok=True) - - netrc_instance = netrc(file=netrc_path) - netrc_instance.hosts["app.crewai.com"] = (credentials["username"], "", credentials["password"]) - - with open(netrc_path, 'w') as file: - file.write(str(netrc_instance)) - - console.print(f"Added credentials to {netrc_path}", style="bold green") - def _add_package(self, tool_details): tool_handle = tool_details["handle"] repository_handle = tool_details["repository"]["handle"] @@ -187,7 +175,11 @@ def _add_package(self, tool_details): tool_handle, ] add_package_result = subprocess.run( - add_package_command, capture_output=False, text=True, check=True + add_package_command, + capture_output=False, + env=self._build_env_with_credentials(repository_handle), + text=True, + check=True ) if add_package_result.stderr: @@ -206,3 +198,13 @@ def _ensure_not_in_project(self): "[bold yellow]Tip:[/bold yellow] Navigate to a different directory and try again." ) raise SystemExit + + def _build_env_with_credentials(self, repository_handle: str): + repository_handle = repository_handle.upper().replace("-", "_") + settings = Settings() + + env = os.environ.copy() + env[f"UV_INDEX_{repository_handle}_USERNAME"] = str(settings.tool_repository_username or "") + env[f"UV_INDEX_{repository_handle}_PASSWORD"] = str(settings.tool_repository_password or "") + + return env diff --git a/tests/cli/config_test.py b/tests/cli/config_test.py new file mode 100644 index 0000000000..1065d47300 --- /dev/null +++ b/tests/cli/config_test.py @@ -0,0 +1,109 @@ +import unittest +import json +import tempfile +import shutil +from pathlib import Path +from crewai.cli.config import Settings + +class TestSettings(unittest.TestCase): + def setUp(self): + self.test_dir = Path(tempfile.mkdtemp()) + self.config_path = self.test_dir / "settings.json" + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def test_empty_initialization(self): + settings = Settings(config_path=self.config_path) + self.assertIsNone(settings.tool_repository_username) + self.assertIsNone(settings.tool_repository_password) + + def test_initialization_with_data(self): + settings = Settings( + config_path=self.config_path, + tool_repository_username="user1" + ) + self.assertEqual(settings.tool_repository_username, "user1") + self.assertIsNone(settings.tool_repository_password) + + def test_initialization_with_existing_file(self): + self.config_path.parent.mkdir(parents=True, exist_ok=True) + with self.config_path.open("w") as f: + json.dump({"tool_repository_username": "file_user"}, f) + + settings = Settings(config_path=self.config_path) + self.assertEqual(settings.tool_repository_username, "file_user") + + def test_merge_file_and_input_data(self): + self.config_path.parent.mkdir(parents=True, exist_ok=True) + with self.config_path.open("w") as f: + json.dump({ + "tool_repository_username": "file_user", + "tool_repository_password": "file_pass" + }, f) + + settings = Settings( + config_path=self.config_path, + tool_repository_username="new_user" + ) + self.assertEqual(settings.tool_repository_username, "new_user") + self.assertEqual(settings.tool_repository_password, "file_pass") + + def test_dump_new_settings(self): + settings = Settings( + config_path=self.config_path, + tool_repository_username="user1" + ) + settings.dump() + + with self.config_path.open("r") as f: + saved_data = json.load(f) + + self.assertEqual(saved_data["tool_repository_username"], "user1") + + def test_update_existing_settings(self): + self.config_path.parent.mkdir(parents=True, exist_ok=True) + with self.config_path.open("w") as f: + json.dump({"existing_setting": "value"}, f) + + settings = Settings( + config_path=self.config_path, + tool_repository_username="user1" + ) + settings.dump() + + with self.config_path.open("r") as f: + saved_data = json.load(f) + + self.assertEqual(saved_data["existing_setting"], "value") + self.assertEqual(saved_data["tool_repository_username"], "user1") + + def test_none_values(self): + settings = Settings( + config_path=self.config_path, + tool_repository_username=None + ) + settings.dump() + + with self.config_path.open("r") as f: + saved_data = json.load(f) + + self.assertIsNone(saved_data.get("tool_repository_username")) + + def test_invalid_json_in_config(self): + self.config_path.parent.mkdir(parents=True, exist_ok=True) + with self.config_path.open("w") as f: + f.write("invalid json") + + try: + settings = Settings(config_path=self.config_path) + self.assertIsNone(settings.tool_repository_username) + except json.JSONDecodeError: + self.fail("Settings initialization should handle invalid JSON") + + def test_empty_config_file(self): + self.config_path.parent.mkdir(parents=True, exist_ok=True) + self.config_path.touch() + + settings = Settings(config_path=self.config_path) + self.assertIsNone(settings.tool_repository_username) diff --git a/tests/cli/tools/test_main.py b/tests/cli/tools/test_main.py index e4fc19be32..3804fb9b41 100644 --- a/tests/cli/tools/test_main.py +++ b/tests/cli/tools/test_main.py @@ -82,6 +82,7 @@ def test_install_success(mock_get, mock_subprocess_run): capture_output=False, text=True, check=True, + env=unittest.mock.ANY ) assert "Succesfully installed sample-tool" in output From e66a135d5d41a1dec37b32ddebd329d0f0dab670 Mon Sep 17 00:00:00 2001 From: C0deZ Date: Fri, 1 Nov 2024 12:30:48 -0400 Subject: [PATCH 5/5] refactor: Move BaseTool to main package and centralize tool description generation (#1514) * move base_tool to main package and consolidate tool desscription generation * update import path * update tests * update doc * add base_tool test * migrate agent delegation tools to use BaseTool * update tests * update import path for tool * fix lint * update param signature * add from_langchain to BaseTool for backwards support of langchain tools * fix the case where StructuredTool doesn't have func --------- Co-authored-by: c0dez Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com> --- docs/concepts/tools.mdx | 99 +++++----- docs/how-to/create-custom-tools.mdx | 18 +- src/crewai/agent.py | 25 +-- src/crewai/agents/agent_builder/base_agent.py | 13 +- src/crewai/agents/tools_handler.py | 2 +- .../cli/templates/crew/tools/custom_tool.py | 3 +- .../cli/templates/flow/tools/custom_tool.py | 2 +- .../templates/pipeline/tools/custom_tool.py | 3 +- .../pipeline_router/tools/custom_tool.py | 3 +- .../tool/src/{{folder_name}}/tool.py | 3 +- src/crewai/crew.py | 2 +- src/crewai/task.py | 7 +- src/crewai/tools/__init__.py | 1 + src/crewai/tools/agent_tools.py | 25 --- src/crewai/tools/agent_tools/agent_tools.py | 32 +++ .../tools/agent_tools/ask_question_tool.py | 26 +++ .../agent_tools/base_agent_tools.py} | 40 +--- .../tools/agent_tools/delegate_work_tool.py | 29 +++ src/crewai/tools/base_tool.py | 186 ++++++++++++++++++ .../tools/{ => cache_tools}/cache_tools.py | 0 src/crewai/tools/tool_usage.py | 20 +- tests/agent_test.py | 36 ++-- tests/agent_tools/lol.py | 0 tests/agents/agent_builder/base_agent_test.py | 5 +- tests/crew_test.py | 23 ++- tests/task_test.py | 6 +- tests/{ => tools}/agent_tools/__init__.py | 0 .../agent_tools/agent_tools_test.py | 30 +-- .../cassettes/test_ask_question.yaml | 0 ...t_ask_question_with_coworker_as_array.yaml | 0 ...uestion_with_wrong_co_worker_variable.yaml | 0 .../cassettes/test_delegate_work.yaml | 0 ...te_work_with_wrong_co_worker_variable.yaml | 0 ...egate_work_withwith_coworker_as_array.yaml | 0 tests/tools/test_base_tool.py | 109 ++++++++++ tests/tools/test_tool_usage.py | 16 +- 36 files changed, 547 insertions(+), 217 deletions(-) delete mode 100644 src/crewai/tools/agent_tools.py create mode 100644 src/crewai/tools/agent_tools/agent_tools.py create mode 100644 src/crewai/tools/agent_tools/ask_question_tool.py rename src/crewai/{agents/agent_builder/utilities/base_agent_tool.py => tools/agent_tools/base_agent_tools.py} (67%) create mode 100644 src/crewai/tools/agent_tools/delegate_work_tool.py create mode 100644 src/crewai/tools/base_tool.py rename src/crewai/tools/{ => cache_tools}/cache_tools.py (100%) delete mode 100644 tests/agent_tools/lol.py rename tests/{ => tools}/agent_tools/__init__.py (100%) rename tests/{ => tools}/agent_tools/agent_tools_test.py (95%) rename tests/{ => tools}/agent_tools/cassettes/test_ask_question.yaml (100%) rename tests/{ => tools}/agent_tools/cassettes/test_ask_question_with_coworker_as_array.yaml (100%) rename tests/{ => tools}/agent_tools/cassettes/test_ask_question_with_wrong_co_worker_variable.yaml (100%) rename tests/{ => tools}/agent_tools/cassettes/test_delegate_work.yaml (100%) rename tests/{ => tools}/agent_tools/cassettes/test_delegate_work_with_wrong_co_worker_variable.yaml (100%) rename tests/{ => tools}/agent_tools/cassettes/test_delegate_work_withwith_coworker_as_array.yaml (100%) create mode 100644 tests/tools/test_base_tool.py diff --git a/docs/concepts/tools.mdx b/docs/concepts/tools.mdx index 12ab96c6ea..9b9c6a32ee 100644 --- a/docs/concepts/tools.mdx +++ b/docs/concepts/tools.mdx @@ -5,13 +5,14 @@ icon: screwdriver-wrench --- ## Introduction -CrewAI tools empower agents with capabilities ranging from web searching and data analysis to collaboration and delegating tasks among coworkers. + +CrewAI tools empower agents with capabilities ranging from web searching and data analysis to collaboration and delegating tasks among coworkers. This documentation outlines how to create, integrate, and leverage these tools within the CrewAI framework, including a new focus on collaboration tools. ## What is a Tool? -A tool in CrewAI is a skill or function that agents can utilize to perform various actions. -This includes tools from the [CrewAI Toolkit](https://github.com/joaomdmoura/crewai-tools) and [LangChain Tools](https://python.langchain.com/docs/integrations/tools), +A tool in CrewAI is a skill or function that agents can utilize to perform various actions. +This includes tools from the [CrewAI Toolkit](https://github.com/joaomdmoura/crewai-tools) and [LangChain Tools](https://python.langchain.com/docs/integrations/tools), enabling everything from simple searches to complex interactions and effective teamwork among agents. ## Key Characteristics of Tools @@ -103,57 +104,53 @@ crew.kickoff() Here is a list of the available tools and their descriptions: -| Tool | Description | -| :-------------------------- | :-------------------------------------------------------------------------------------------- | -| **BrowserbaseLoadTool** | A tool for interacting with and extracting data from web browsers. | -| **CodeDocsSearchTool** | A RAG tool optimized for searching through code documentation and related technical documents. | -| **CodeInterpreterTool** | A tool for interpreting python code. | -| **ComposioTool** | Enables use of Composio tools. | -| **CSVSearchTool** | A RAG tool designed for searching within CSV files, tailored to handle structured data. | -| **DALL-E Tool** | A tool for generating images using the DALL-E API. | -| **DirectorySearchTool** | A RAG tool for searching within directories, useful for navigating through file systems. | -| **DOCXSearchTool** | A RAG tool aimed at searching within DOCX documents, ideal for processing Word files. | -| **DirectoryReadTool** | Facilitates reading and processing of directory structures and their contents. | -| **EXASearchTool** | A tool designed for performing exhaustive searches across various data sources. | -| **FileReadTool** | Enables reading and extracting data from files, supporting various file formats. | -| **FirecrawlSearchTool** | A tool to search webpages using Firecrawl and return the results. | -| **FirecrawlCrawlWebsiteTool** | A tool for crawling webpages using Firecrawl. | -| **FirecrawlScrapeWebsiteTool** | A tool for scraping webpages URL using Firecrawl and returning its contents. | -| **GithubSearchTool** | A RAG tool for searching within GitHub repositories, useful for code and documentation search.| -| **SerperDevTool** | A specialized tool for development purposes, with specific functionalities under development. | -| **TXTSearchTool** | A RAG tool focused on searching within text (.txt) files, suitable for unstructured data. | -| **JSONSearchTool** | A RAG tool designed for searching within JSON files, catering to structured data handling. | -| **LlamaIndexTool** | Enables the use of LlamaIndex tools. | -| **MDXSearchTool** | A RAG tool tailored for searching within Markdown (MDX) files, useful for documentation. | -| **PDFSearchTool** | A RAG tool aimed at searching within PDF documents, ideal for processing scanned documents. | -| **PGSearchTool** | A RAG tool optimized for searching within PostgreSQL databases, suitable for database queries. | -| **Vision Tool** | A tool for generating images using the DALL-E API. | -| **RagTool** | A general-purpose RAG tool capable of handling various data sources and types. | -| **ScrapeElementFromWebsiteTool** | Enables scraping specific elements from websites, useful for targeted data extraction. | -| **ScrapeWebsiteTool** | Facilitates scraping entire websites, ideal for comprehensive data collection. | -| **WebsiteSearchTool** | A RAG tool for searching website content, optimized for web data extraction. | -| **XMLSearchTool** | A RAG tool designed for searching within XML files, suitable for structured data formats. | -| **YoutubeChannelSearchTool**| A RAG tool for searching within YouTube channels, useful for video content analysis. | -| **YoutubeVideoSearchTool** | A RAG tool aimed at searching within YouTube videos, ideal for video data extraction. | +| Tool | Description | +| :------------------------------- | :--------------------------------------------------------------------------------------------- | +| **BrowserbaseLoadTool** | A tool for interacting with and extracting data from web browsers. | +| **CodeDocsSearchTool** | A RAG tool optimized for searching through code documentation and related technical documents. | +| **CodeInterpreterTool** | A tool for interpreting python code. | +| **ComposioTool** | Enables use of Composio tools. | +| **CSVSearchTool** | A RAG tool designed for searching within CSV files, tailored to handle structured data. | +| **DALL-E Tool** | A tool for generating images using the DALL-E API. | +| **DirectorySearchTool** | A RAG tool for searching within directories, useful for navigating through file systems. | +| **DOCXSearchTool** | A RAG tool aimed at searching within DOCX documents, ideal for processing Word files. | +| **DirectoryReadTool** | Facilitates reading and processing of directory structures and their contents. | +| **EXASearchTool** | A tool designed for performing exhaustive searches across various data sources. | +| **FileReadTool** | Enables reading and extracting data from files, supporting various file formats. | +| **FirecrawlSearchTool** | A tool to search webpages using Firecrawl and return the results. | +| **FirecrawlCrawlWebsiteTool** | A tool for crawling webpages using Firecrawl. | +| **FirecrawlScrapeWebsiteTool** | A tool for scraping webpages URL using Firecrawl and returning its contents. | +| **GithubSearchTool** | A RAG tool for searching within GitHub repositories, useful for code and documentation search. | +| **SerperDevTool** | A specialized tool for development purposes, with specific functionalities under development. | +| **TXTSearchTool** | A RAG tool focused on searching within text (.txt) files, suitable for unstructured data. | +| **JSONSearchTool** | A RAG tool designed for searching within JSON files, catering to structured data handling. | +| **LlamaIndexTool** | Enables the use of LlamaIndex tools. | +| **MDXSearchTool** | A RAG tool tailored for searching within Markdown (MDX) files, useful for documentation. | +| **PDFSearchTool** | A RAG tool aimed at searching within PDF documents, ideal for processing scanned documents. | +| **PGSearchTool** | A RAG tool optimized for searching within PostgreSQL databases, suitable for database queries. | +| **Vision Tool** | A tool for generating images using the DALL-E API. | +| **RagTool** | A general-purpose RAG tool capable of handling various data sources and types. | +| **ScrapeElementFromWebsiteTool** | Enables scraping specific elements from websites, useful for targeted data extraction. | +| **ScrapeWebsiteTool** | Facilitates scraping entire websites, ideal for comprehensive data collection. | +| **WebsiteSearchTool** | A RAG tool for searching website content, optimized for web data extraction. | +| **XMLSearchTool** | A RAG tool designed for searching within XML files, suitable for structured data formats. | +| **YoutubeChannelSearchTool** | A RAG tool for searching within YouTube channels, useful for video content analysis. | +| **YoutubeVideoSearchTool** | A RAG tool aimed at searching within YouTube videos, ideal for video data extraction. | ## Creating your own Tools - Developers can craft `custom tools` tailored for their agent’s needs or utilize pre-built options. + Developers can craft `custom tools` tailored for their agent’s needs or + utilize pre-built options. -To create your own CrewAI tools you will need to install our extra tools package: - -```bash -pip install 'crewai[tools]' -``` - -Once you do that there are two main ways for one to create a CrewAI tool: +There are two main ways for one to create a CrewAI tool: ### Subclassing `BaseTool` ```python Code -from crewai_tools import BaseTool +from crewai.tools import BaseTool + class MyCustomTool(BaseTool): name: str = "Name of my tool" @@ -167,7 +164,7 @@ class MyCustomTool(BaseTool): ### Utilizing the `tool` Decorator ```python Code -from crewai_tools import tool +from crewai.tools import tool @tool("Name of my tool") def my_tool(question: str) -> str: """Clear description for what this tool is useful for, your agent will need this information to use it.""" @@ -178,11 +175,13 @@ def my_tool(question: str) -> str: ### Custom Caching Mechanism - Tools can optionally implement a `cache_function` to fine-tune caching behavior. This function determines when to cache results based on specific conditions, offering granular control over caching logic. + Tools can optionally implement a `cache_function` to fine-tune caching + behavior. This function determines when to cache results based on specific + conditions, offering granular control over caching logic. ```python Code -from crewai_tools import tool +from crewai.tools import tool @tool def multiplication_tool(first_number: int, second_number: int) -> str: @@ -208,6 +207,6 @@ writer1 = Agent( ## Conclusion -Tools are pivotal in extending the capabilities of CrewAI agents, enabling them to undertake a broad spectrum of tasks and collaborate effectively. -When building solutions with CrewAI, leverage both custom and existing tools to empower your agents and enhance the AI ecosystem. Consider utilizing error handling, -caching mechanisms, and the flexibility of tool arguments to optimize your agents' performance and capabilities. \ No newline at end of file +Tools are pivotal in extending the capabilities of CrewAI agents, enabling them to undertake a broad spectrum of tasks and collaborate effectively. +When building solutions with CrewAI, leverage both custom and existing tools to empower your agents and enhance the AI ecosystem. Consider utilizing error handling, +caching mechanisms, and the flexibility of tool arguments to optimize your agents' performance and capabilities. diff --git a/docs/how-to/create-custom-tools.mdx b/docs/how-to/create-custom-tools.mdx index 2caab716bc..e7ee2e9a92 100644 --- a/docs/how-to/create-custom-tools.mdx +++ b/docs/how-to/create-custom-tools.mdx @@ -6,25 +6,17 @@ icon: hammer ## Creating and Utilizing Tools in CrewAI -This guide provides detailed instructions on creating custom tools for the CrewAI framework and how to efficiently manage and utilize these tools, -incorporating the latest functionalities such as tool delegation, error handling, and dynamic tool calling. It also highlights the importance of collaboration tools, +This guide provides detailed instructions on creating custom tools for the CrewAI framework and how to efficiently manage and utilize these tools, +incorporating the latest functionalities such as tool delegation, error handling, and dynamic tool calling. It also highlights the importance of collaboration tools, enabling agents to perform a wide range of actions. -### Prerequisites - -Before creating your own tools, ensure you have the crewAI extra tools package installed: - -```bash -pip install 'crewai[tools]' -``` - ### Subclassing `BaseTool` To create a personalized tool, inherit from `BaseTool` and define the necessary attributes, including the `args_schema` for input validation, and the `_run` method. ```python Code from typing import Type -from crewai_tools import BaseTool +from crewai.tools import BaseTool from pydantic import BaseModel, Field class MyToolInput(BaseModel): @@ -47,7 +39,7 @@ Alternatively, you can use the tool decorator `@tool`. This approach allows you offering a concise and efficient way to create specialized tools tailored to your needs. ```python Code -from crewai_tools import tool +from crewai.tools import tool @tool("Tool Name") def my_simple_tool(question: str) -> str: @@ -73,5 +65,5 @@ def my_cache_strategy(arguments: dict, result: str) -> bool: cached_tool.cache_function = my_cache_strategy ``` -By adhering to these guidelines and incorporating new functionalities and collaboration tools into your tool creation and management processes, +By adhering to these guidelines and incorporating new functionalities and collaboration tools into your tool creation and management processes, you can leverage the full capabilities of the CrewAI framework, enhancing both the development experience and the efficiency of your AI agents. diff --git a/src/crewai/agent.py b/src/crewai/agent.py index 937710f592..f2a0a5c310 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -10,7 +10,8 @@ from crewai.agents.crew_agent_executor import CrewAgentExecutor from crewai.llm import LLM from crewai.memory.contextual.contextual_memory import ContextualMemory -from crewai.tools.agent_tools import AgentTools +from crewai.tools.agent_tools.agent_tools import AgentTools +from crewai.tools import BaseTool from crewai.utilities import Converter, Prompts from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE from crewai.utilities.token_counter_callback import TokenCalcHandler @@ -192,7 +193,7 @@ def execute_task( self, task: Any, context: Optional[str] = None, - tools: Optional[List[Any]] = None, + tools: Optional[List[BaseTool]] = None, ) -> str: """Execute a task with the agent. @@ -259,7 +260,9 @@ def execute_task( return result - def create_agent_executor(self, tools=None, task=None) -> None: + def create_agent_executor( + self, tools: Optional[List[BaseTool]] = None, task=None + ) -> None: """Create an agent executor for the agent. Returns: @@ -332,7 +335,7 @@ def _parse_tools(self, tools: List[Any]) -> List[Any]: # type: ignore tools_list = [] try: # tentatively try to import from crewai_tools import BaseTool as CrewAITool - from crewai_tools import BaseTool as CrewAITool + from crewai.tools import BaseTool as CrewAITool for tool in tools: if isinstance(tool, CrewAITool): @@ -391,7 +394,7 @@ def _render_text_description(self, tools: List[Any]) -> str: return description - def _render_text_description_and_args(self, tools: List[Any]) -> str: + def _render_text_description_and_args(self, tools: List[BaseTool]) -> str: """Render the tool name, description, and args in plain text. Output will be in the format of: @@ -404,17 +407,7 @@ def _render_text_description_and_args(self, tools: List[Any]) -> str: """ tool_strings = [] for tool in tools: - args_schema = { - name: { - "description": field.description, - "type": field.annotation.__name__, - } - for name, field in tool.args_schema.model_fields.items() - } - description = ( - f"Tool Name: {tool.name}\nTool Description: {tool.description}" - ) - tool_strings.append(f"{description}\nTool Arguments: {args_schema}") + tool_strings.append(tool.description) return "\n".join(tool_strings) diff --git a/src/crewai/agents/agent_builder/base_agent.py b/src/crewai/agents/agent_builder/base_agent.py index f42ab31721..55315e7ff3 100644 --- a/src/crewai/agents/agent_builder/base_agent.py +++ b/src/crewai/agents/agent_builder/base_agent.py @@ -18,6 +18,7 @@ from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess from crewai.agents.cache.cache_handler import CacheHandler from crewai.agents.tools_handler import ToolsHandler +from crewai.tools import BaseTool from crewai.utilities import I18N, Logger, RPMController from crewai.utilities.config import process_config @@ -49,11 +50,11 @@ class BaseAgent(ABC, BaseModel): Methods: - execute_task(task: Any, context: Optional[str] = None, tools: Optional[List[Any]] = None) -> str: + execute_task(task: Any, context: Optional[str] = None, tools: Optional[List[BaseTool]] = None) -> str: Abstract method to execute a task. create_agent_executor(tools=None) -> None: Abstract method to create an agent executor. - _parse_tools(tools: List[Any]) -> List[Any]: + _parse_tools(tools: List[BaseTool]) -> List[Any]: Abstract method to parse tools. get_delegation_tools(agents: List["BaseAgent"]): Abstract method to set the agents task tools for handling delegation and question asking to other agents in crew. @@ -105,7 +106,7 @@ class BaseAgent(ABC, BaseModel): default=False, description="Enable agent to delegate and ask questions among each other.", ) - tools: Optional[List[Any]] = Field( + tools: Optional[List[BaseTool]] = Field( default_factory=list, description="Tools at agents' disposal" ) max_iter: Optional[int] = Field( @@ -188,7 +189,7 @@ def execute_task( self, task: Any, context: Optional[str] = None, - tools: Optional[List[Any]] = None, + tools: Optional[List[BaseTool]] = None, ) -> str: pass @@ -197,11 +198,11 @@ def create_agent_executor(self, tools=None) -> None: pass @abstractmethod - def _parse_tools(self, tools: List[Any]) -> List[Any]: + def _parse_tools(self, tools: List[BaseTool]) -> List[BaseTool]: pass @abstractmethod - def get_delegation_tools(self, agents: List["BaseAgent"]) -> List[Any]: + def get_delegation_tools(self, agents: List["BaseAgent"]) -> List[BaseTool]: """Set the task tools that init BaseAgenTools class.""" pass diff --git a/src/crewai/agents/tools_handler.py b/src/crewai/agents/tools_handler.py index c82f7de8c1..fd4bec7ee5 100644 --- a/src/crewai/agents/tools_handler.py +++ b/src/crewai/agents/tools_handler.py @@ -1,6 +1,6 @@ from typing import Any, Optional, Union -from ..tools.cache_tools import CacheTools +from ..tools.cache_tools.cache_tools import CacheTools from ..tools.tool_calling import InstructorToolCalling, ToolCalling from .cache.cache_handler import CacheHandler diff --git a/src/crewai/cli/templates/crew/tools/custom_tool.py b/src/crewai/cli/templates/crew/tools/custom_tool.py index d383180519..50bffa5056 100644 --- a/src/crewai/cli/templates/crew/tools/custom_tool.py +++ b/src/crewai/cli/templates/crew/tools/custom_tool.py @@ -1,7 +1,8 @@ +from crewai.tools import BaseTool from typing import Type -from crewai_tools import BaseTool from pydantic import BaseModel, Field + class MyCustomToolInput(BaseModel): """Input schema for MyCustomTool.""" argument: str = Field(..., description="Description of the argument.") diff --git a/src/crewai/cli/templates/flow/tools/custom_tool.py b/src/crewai/cli/templates/flow/tools/custom_tool.py index 030e575ecd..e669b6c3b5 100644 --- a/src/crewai/cli/templates/flow/tools/custom_tool.py +++ b/src/crewai/cli/templates/flow/tools/custom_tool.py @@ -1,6 +1,6 @@ from typing import Type -from crewai_tools import BaseTool +from crewai.tools import BaseTool from pydantic import BaseModel, Field diff --git a/src/crewai/cli/templates/pipeline/tools/custom_tool.py b/src/crewai/cli/templates/pipeline/tools/custom_tool.py index d383180519..fbd53cf026 100644 --- a/src/crewai/cli/templates/pipeline/tools/custom_tool.py +++ b/src/crewai/cli/templates/pipeline/tools/custom_tool.py @@ -1,7 +1,8 @@ from typing import Type -from crewai_tools import BaseTool +from crewai.tools import BaseTool from pydantic import BaseModel, Field + class MyCustomToolInput(BaseModel): """Input schema for MyCustomTool.""" argument: str = Field(..., description="Description of the argument.") diff --git a/src/crewai/cli/templates/pipeline_router/tools/custom_tool.py b/src/crewai/cli/templates/pipeline_router/tools/custom_tool.py index d383180519..fbd53cf026 100644 --- a/src/crewai/cli/templates/pipeline_router/tools/custom_tool.py +++ b/src/crewai/cli/templates/pipeline_router/tools/custom_tool.py @@ -1,7 +1,8 @@ from typing import Type -from crewai_tools import BaseTool +from crewai.tools import BaseTool from pydantic import BaseModel, Field + class MyCustomToolInput(BaseModel): """Input schema for MyCustomTool.""" argument: str = Field(..., description="Description of the argument.") diff --git a/src/crewai/cli/templates/tool/src/{{folder_name}}/tool.py b/src/crewai/cli/templates/tool/src/{{folder_name}}/tool.py index 63c653a6cf..24bc360178 100644 --- a/src/crewai/cli/templates/tool/src/{{folder_name}}/tool.py +++ b/src/crewai/cli/templates/tool/src/{{folder_name}}/tool.py @@ -1,4 +1,5 @@ -from crewai_tools import BaseTool +from crewai.tools import BaseTool + class {{class_name}}(BaseTool): name: str = "Name of my tool" diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 377b6fec95..e65024ed60 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -32,7 +32,7 @@ from crewai.tasks.conditional_task import ConditionalTask from crewai.tasks.task_output import TaskOutput from crewai.telemetry import Telemetry -from crewai.tools.agent_tools import AgentTools +from crewai.tools.agent_tools.agent_tools import AgentTools from crewai.types.usage_metrics import UsageMetrics from crewai.utilities import I18N, FileHandler, Logger, RPMController from crewai.utilities.constants import ( diff --git a/src/crewai/task.py b/src/crewai/task.py index 82baa9959d..21278af98a 100644 --- a/src/crewai/task.py +++ b/src/crewai/task.py @@ -20,6 +20,7 @@ from pydantic_core import PydanticCustomError from crewai.agents.agent_builder.base_agent import BaseAgent +from crewai.tools.base_tool import BaseTool from crewai.tasks.output_format import OutputFormat from crewai.tasks.task_output import TaskOutput from crewai.telemetry.telemetry import Telemetry @@ -91,7 +92,7 @@ class Task(BaseModel): output: Optional[TaskOutput] = Field( description="Task output, it's final result after being executed", default=None ) - tools: Optional[List[Any]] = Field( + tools: Optional[List[BaseTool]] = Field( default_factory=list, description="Tools the agent is limited to use for this task.", ) @@ -185,7 +186,7 @@ def execute_sync( self, agent: Optional[BaseAgent] = None, context: Optional[str] = None, - tools: Optional[List[Any]] = None, + tools: Optional[List[BaseTool]] = None, ) -> TaskOutput: """Execute the task synchronously.""" return self._execute_core(agent, context, tools) @@ -202,7 +203,7 @@ def execute_async( self, agent: BaseAgent | None = None, context: Optional[str] = None, - tools: Optional[List[Any]] = None, + tools: Optional[List[BaseTool]] = None, ) -> Future[TaskOutput]: """Execute the task asynchronously.""" future: Future[TaskOutput] = Future() diff --git a/src/crewai/tools/__init__.py b/src/crewai/tools/__init__.py index e69de29bb2..41819ccbc0 100644 --- a/src/crewai/tools/__init__.py +++ b/src/crewai/tools/__init__.py @@ -0,0 +1 @@ +from .base_tool import BaseTool, tool diff --git a/src/crewai/tools/agent_tools.py b/src/crewai/tools/agent_tools.py deleted file mode 100644 index 7831285cf7..0000000000 --- a/src/crewai/tools/agent_tools.py +++ /dev/null @@ -1,25 +0,0 @@ -from crewai.agents.agent_builder.utilities.base_agent_tool import BaseAgentTools - - -class AgentTools(BaseAgentTools): - """Default tools around agent delegation""" - - def tools(self): - from langchain.tools import StructuredTool - - coworkers = ", ".join([f"{agent.role}" for agent in self.agents]) - tools = [ - StructuredTool.from_function( - func=self.delegate_work, - name="Delegate work to coworker", - description=self.i18n.tools("delegate_work").format( - coworkers=coworkers - ), - ), - StructuredTool.from_function( - func=self.ask_question, - name="Ask question to coworker", - description=self.i18n.tools("ask_question").format(coworkers=coworkers), - ), - ] - return tools diff --git a/src/crewai/tools/agent_tools/agent_tools.py b/src/crewai/tools/agent_tools/agent_tools.py new file mode 100644 index 0000000000..1db55a29b5 --- /dev/null +++ b/src/crewai/tools/agent_tools/agent_tools.py @@ -0,0 +1,32 @@ +from crewai.tools.base_tool import BaseTool +from crewai.agents.agent_builder.base_agent import BaseAgent +from crewai.utilities import I18N + +from .delegate_work_tool import DelegateWorkTool +from .ask_question_tool import AskQuestionTool + + +class AgentTools: + """Manager class for agent-related tools""" + + def __init__(self, agents: list[BaseAgent], i18n: I18N = I18N()): + self.agents = agents + self.i18n = i18n + + def tools(self) -> list[BaseTool]: + """Get all available agent tools""" + coworkers = ", ".join([f"{agent.role}" for agent in self.agents]) + + delegate_tool = DelegateWorkTool( + agents=self.agents, + i18n=self.i18n, + description=self.i18n.tools("delegate_work").format(coworkers=coworkers), + ) + + ask_tool = AskQuestionTool( + agents=self.agents, + i18n=self.i18n, + description=self.i18n.tools("ask_question").format(coworkers=coworkers), + ) + + return [delegate_tool, ask_tool] diff --git a/src/crewai/tools/agent_tools/ask_question_tool.py b/src/crewai/tools/agent_tools/ask_question_tool.py new file mode 100644 index 0000000000..af739b8fc1 --- /dev/null +++ b/src/crewai/tools/agent_tools/ask_question_tool.py @@ -0,0 +1,26 @@ +from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool +from typing import Optional +from pydantic import BaseModel, Field + + +class AskQuestionToolSchema(BaseModel): + question: str = Field(..., description="The question to ask") + context: str = Field(..., description="The context for the question") + coworker: str = Field(..., description="The role/name of the coworker to ask") + + +class AskQuestionTool(BaseAgentTool): + """Tool for asking questions to coworkers""" + + name: str = "Ask question to coworker" + args_schema: type[BaseModel] = AskQuestionToolSchema + + def _run( + self, + question: str, + context: str, + coworker: Optional[str] = None, + **kwargs, + ) -> str: + coworker = self._get_coworker(coworker, **kwargs) + return self._execute(coworker, question, context) diff --git a/src/crewai/agents/agent_builder/utilities/base_agent_tool.py b/src/crewai/tools/agent_tools/base_agent_tools.py similarity index 67% rename from src/crewai/agents/agent_builder/utilities/base_agent_tool.py rename to src/crewai/tools/agent_tools/base_agent_tools.py index fef4ee9ef8..bedb2aed65 100644 --- a/src/crewai/agents/agent_builder/utilities/base_agent_tool.py +++ b/src/crewai/tools/agent_tools/base_agent_tools.py @@ -1,22 +1,19 @@ -from abc import ABC, abstractmethod -from typing import List, Optional, Union - -from pydantic import BaseModel, Field +from typing import Optional, Union +from pydantic import Field +from crewai.tools.base_tool import BaseTool from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.task import Task from crewai.utilities import I18N -class BaseAgentTools(BaseModel, ABC): - """Default tools around agent delegation""" - - agents: List[BaseAgent] = Field(description="List of agents in this crew.") - i18n: I18N = Field(default=I18N(), description="Internationalization settings.") +class BaseAgentTool(BaseTool): + """Base class for agent-related tools""" - @abstractmethod - def tools(self): - pass + agents: list[BaseAgent] = Field(description="List of available agents") + i18n: I18N = Field( + default_factory=I18N, description="Internationalization settings" + ) def _get_coworker(self, coworker: Optional[str], **kwargs) -> Optional[str]: coworker = coworker or kwargs.get("co_worker") or kwargs.get("coworker") @@ -24,27 +21,11 @@ def _get_coworker(self, coworker: Optional[str], **kwargs) -> Optional[str]: is_list = coworker.startswith("[") and coworker.endswith("]") if is_list: coworker = coworker[1:-1].split(",")[0] - return coworker - def delegate_work( - self, task: str, context: str, coworker: Optional[str] = None, **kwargs - ): - """Useful to delegate a specific task to a coworker passing all necessary context and names.""" - coworker = self._get_coworker(coworker, **kwargs) - return self._execute(coworker, task, context) - - def ask_question( - self, question: str, context: str, coworker: Optional[str] = None, **kwargs - ): - """Useful to ask a question, opinion or take from a coworker passing all necessary context and names.""" - coworker = self._get_coworker(coworker, **kwargs) - return self._execute(coworker, question, context) - def _execute( self, agent_name: Union[str, None], task: str, context: Union[str, None] - ): - """Execute the command.""" + ) -> str: try: if agent_name is None: agent_name = "" @@ -57,7 +38,6 @@ def _execute( # when it should look like this: # {"task": "....", "coworker": "...."} agent_name = agent_name.casefold().replace('"', "").replace("\n", "") - agent = [ # type: ignore # Incompatible types in assignment (expression has type "list[BaseAgent]", variable has type "str | None") available_agent for available_agent in self.agents diff --git a/src/crewai/tools/agent_tools/delegate_work_tool.py b/src/crewai/tools/agent_tools/delegate_work_tool.py new file mode 100644 index 0000000000..7f20075971 --- /dev/null +++ b/src/crewai/tools/agent_tools/delegate_work_tool.py @@ -0,0 +1,29 @@ +from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool +from typing import Optional + +from pydantic import BaseModel, Field + + +class DelegateWorkToolSchema(BaseModel): + task: str = Field(..., description="The task to delegate") + context: str = Field(..., description="The context for the task") + coworker: str = Field( + ..., description="The role/name of the coworker to delegate to" + ) + + +class DelegateWorkTool(BaseAgentTool): + """Tool for delegating work to coworkers""" + + name: str = "Delegate work to coworker" + args_schema: type[BaseModel] = DelegateWorkToolSchema + + def _run( + self, + task: str, + context: str, + coworker: Optional[str] = None, + **kwargs, + ) -> str: + coworker = self._get_coworker(coworker, **kwargs) + return self._execute(coworker, task, context) diff --git a/src/crewai/tools/base_tool.py b/src/crewai/tools/base_tool.py new file mode 100644 index 0000000000..f41fb7c0b8 --- /dev/null +++ b/src/crewai/tools/base_tool.py @@ -0,0 +1,186 @@ +from abc import ABC, abstractmethod +from typing import Any, Callable, Type, get_args, get_origin + +from langchain_core.tools import StructuredTool +from pydantic import BaseModel, ConfigDict, Field, validator +from pydantic import BaseModel as PydanticBaseModel + + +class BaseTool(BaseModel, ABC): + class _ArgsSchemaPlaceholder(PydanticBaseModel): + pass + + model_config = ConfigDict() + + name: str + """The unique name of the tool that clearly communicates its purpose.""" + description: str + """Used to tell the model how/when/why to use the tool.""" + args_schema: Type[PydanticBaseModel] = Field(default_factory=_ArgsSchemaPlaceholder) + """The schema for the arguments that the tool accepts.""" + description_updated: bool = False + """Flag to check if the description has been updated.""" + cache_function: Callable = lambda _args=None, _result=None: True + """Function that will be used to determine if the tool should be cached, should return a boolean. If None, the tool will be cached.""" + result_as_answer: bool = False + """Flag to check if the tool should be the final agent answer.""" + + @validator("args_schema", always=True, pre=True) + def _default_args_schema( + cls, v: Type[PydanticBaseModel] + ) -> Type[PydanticBaseModel]: + if not isinstance(v, cls._ArgsSchemaPlaceholder): + return v + + return type( + f"{cls.__name__}Schema", + (PydanticBaseModel,), + { + "__annotations__": { + k: v for k, v in cls._run.__annotations__.items() if k != "return" + }, + }, + ) + + def model_post_init(self, __context: Any) -> None: + self._generate_description() + + super().model_post_init(__context) + + def run( + self, + *args: Any, + **kwargs: Any, + ) -> Any: + print(f"Using Tool: {self.name}") + return self._run(*args, **kwargs) + + @abstractmethod + def _run( + self, + *args: Any, + **kwargs: Any, + ) -> Any: + """Here goes the actual implementation of the tool.""" + + def to_langchain(self) -> StructuredTool: + self._set_args_schema() + return StructuredTool( + name=self.name, + description=self.description, + args_schema=self.args_schema, + func=self._run, + ) + + @classmethod + def from_langchain(cls, tool: StructuredTool) -> "BaseTool": + if cls == Tool: + if tool.func is None: + raise ValueError("StructuredTool must have a callable 'func'") + return Tool( + name=tool.name, + description=tool.description, + args_schema=tool.args_schema, + func=tool.func, + ) + raise NotImplementedError(f"from_langchain not implemented for {cls.__name__}") + + def _set_args_schema(self): + if self.args_schema is None: + class_name = f"{self.__class__.__name__}Schema" + self.args_schema = type( + class_name, + (PydanticBaseModel,), + { + "__annotations__": { + k: v + for k, v in self._run.__annotations__.items() + if k != "return" + }, + }, + ) + + def _generate_description(self): + args_schema = { + name: { + "description": field.description, + "type": BaseTool._get_arg_annotations(field.annotation), + } + for name, field in self.args_schema.model_fields.items() + } + + self.description = f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nTool Description: {self.description}" + + @staticmethod + def _get_arg_annotations(annotation: type[Any] | None) -> str: + if annotation is None: + return "None" + + origin = get_origin(annotation) + args = get_args(annotation) + + if origin is None: + return ( + annotation.__name__ + if hasattr(annotation, "__name__") + else str(annotation) + ) + + if args: + args_str = ", ".join(BaseTool._get_arg_annotations(arg) for arg in args) + return f"{origin.__name__}[{args_str}]" + + return origin.__name__ + + +class Tool(BaseTool): + func: Callable + """The function that will be executed when the tool is called.""" + + def _run(self, *args: Any, **kwargs: Any) -> Any: + return self.func(*args, **kwargs) + + +def to_langchain( + tools: list[BaseTool | StructuredTool], +) -> list[StructuredTool]: + return [t.to_langchain() if isinstance(t, BaseTool) else t for t in tools] + + +def tool(*args): + """ + Decorator to create a tool from a function. + """ + + def _make_with_name(tool_name: str) -> Callable: + def _make_tool(f: Callable) -> BaseTool: + if f.__doc__ is None: + raise ValueError("Function must have a docstring") + if f.__annotations__ is None: + raise ValueError("Function must have type annotations") + + class_name = "".join(tool_name.split()).title() + args_schema = type( + class_name, + (PydanticBaseModel,), + { + "__annotations__": { + k: v for k, v in f.__annotations__.items() if k != "return" + }, + }, + ) + + return Tool( + name=tool_name, + description=f.__doc__, + func=f, + args_schema=args_schema, + ) + + return _make_tool + + if len(args) == 1 and callable(args[0]): + return _make_with_name(args[0].__name__)(args[0]) + if len(args) == 1 and isinstance(args[0], str): + return _make_with_name(args[0]) + raise ValueError("Invalid arguments") diff --git a/src/crewai/tools/cache_tools.py b/src/crewai/tools/cache_tools/cache_tools.py similarity index 100% rename from src/crewai/tools/cache_tools.py rename to src/crewai/tools/cache_tools/cache_tools.py diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index 71c02fc3cf..e4108c8baa 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -10,6 +10,7 @@ from crewai.agents.tools_handler import ToolsHandler from crewai.task import Task from crewai.telemetry import Telemetry +from crewai.tools import BaseTool from crewai.tools.tool_calling import InstructorToolCalling, ToolCalling from crewai.tools.tool_usage_events import ToolUsageError, ToolUsageFinished from crewai.utilities import I18N, Converter, ConverterError, Printer @@ -49,7 +50,7 @@ class ToolUsage: def __init__( self, tools_handler: ToolsHandler, - tools: List[Any], + tools: List[BaseTool], original_tools: List[Any], tools_description: str, tools_names: str, @@ -298,22 +299,7 @@ def _render(self) -> str: """Render the tool name and description in plain text.""" descriptions = [] for tool in self.tools: - args = { - name: { - "description": field.description, - "type": field.annotation.__name__, - } - for name, field in tool.args_schema.model_fields.items() - } - descriptions.append( - "\n".join( - [ - f"Tool Name: {tool.name.lower()}", - f"Tool Description: {tool.description}", - f"Tool Arguments: {args}", - ] - ) - ) + descriptions.append(tool.description) return "\n--\n".join(descriptions) def _function_calling(self, tool_string: str): diff --git a/tests/agent_test.py b/tests/agent_test.py index 154677ccfb..c4094d15cf 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest -from crewai_tools import tool from crewai import Agent, Crew, Task from crewai.agents.cache import CacheHandler @@ -14,6 +13,7 @@ from crewai.llm import LLM from crewai.tools.tool_calling import InstructorToolCalling from crewai.tools.tool_usage import ToolUsage +from crewai.tools import tool from crewai.tools.tool_usage_events import ToolUsageFinished from crewai.utilities import RPMController from crewai.utilities.events import Emitter @@ -277,9 +277,10 @@ def multiplier(first_number: int, second_number: int) -> float: "multiplier-{'first_number': 12, 'second_number': 3}": 36, } - with patch.object(CacheHandler, "read") as read, patch.object( - Emitter, "emit" - ) as emit: + with ( + patch.object(CacheHandler, "read") as read, + patch.object(Emitter, "emit") as emit, + ): read.return_value = "0" task = Task( description="What is 2 times 6? Ignore correctness and just return the result of the multiplication tool, you must use the tool.", @@ -604,7 +605,7 @@ def get_final_answer() -> float: def test_agent_respect_the_max_rpm_set_over_crew_rpm(capsys): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool @tool def get_final_answer() -> float: @@ -642,7 +643,7 @@ def get_final_answer() -> float: def test_agent_without_max_rpm_respet_crew_rpm(capsys): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool @tool def get_final_answer() -> float: @@ -696,7 +697,7 @@ def get_final_answer() -> float: def test_agent_error_on_parsing_tool(capsys): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool @tool def get_final_answer() -> float: @@ -739,7 +740,7 @@ def get_final_answer() -> float: def test_agent_remembers_output_format_after_using_tools_too_many_times(): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool @tool def get_final_answer() -> float: @@ -863,11 +864,16 @@ def learn_about_AI() -> str: from crewai.tools.tool_usage import ToolUsage - with patch.object( - instructor, "from_litellm", wraps=instructor.from_litellm - ) as mock_from_litellm, patch.object( - ToolUsage, "_original_tool_calling", side_effect=Exception("Forced exception") - ) as mock_original_tool_calling: + with ( + patch.object( + instructor, "from_litellm", wraps=instructor.from_litellm + ) as mock_from_litellm, + patch.object( + ToolUsage, + "_original_tool_calling", + side_effect=Exception("Forced exception"), + ) as mock_original_tool_calling, + ): crew.kickoff() mock_from_litellm.assert_called() mock_original_tool_calling.assert_called() @@ -894,7 +900,7 @@ def test_agent_count_formatting_error(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_tool_result_as_answer_is_the_final_answer_for_the_agent(): - from crewai_tools import BaseTool + from crewai.tools import BaseTool class MyCustomTool(BaseTool): name: str = "Get Greetings" @@ -924,7 +930,7 @@ def _run(self) -> str: @pytest.mark.vcr(filter_headers=["authorization"]) def test_tool_usage_information_is_appended_to_agent(): - from crewai_tools import BaseTool + from crewai.tools import BaseTool class MyCustomTool(BaseTool): name: str = "Decide Greetings" diff --git a/tests/agent_tools/lol.py b/tests/agent_tools/lol.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/agents/agent_builder/base_agent_test.py b/tests/agents/agent_builder/base_agent_test.py index 4e47f2271e..fcd5d58b63 100644 --- a/tests/agents/agent_builder/base_agent_test.py +++ b/tests/agents/agent_builder/base_agent_test.py @@ -2,6 +2,7 @@ from typing import Any, List, Optional from crewai.agents.agent_builder.base_agent import BaseAgent +from crewai.tools.base_tool import BaseTool from pydantic import BaseModel @@ -10,13 +11,13 @@ def execute_task( self, task: Any, context: Optional[str] = None, - tools: Optional[List[Any]] = None, + tools: Optional[List[BaseTool]] = None, ) -> str: return "" def create_agent_executor(self, tools=None) -> None: ... - def _parse_tools(self, tools: List[Any]) -> List[Any]: + def _parse_tools(self, tools: List[BaseTool]) -> List[BaseTool]: return [] def get_delegation_tools(self, agents: List["BaseAgent"]): ... diff --git a/tests/crew_test.py b/tests/crew_test.py index 6444b781c3..24b8922713 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -456,7 +456,7 @@ def test_crew_verbose_output(capsys): def test_cache_hitting_between_agents(): from unittest.mock import call, patch - from crewai_tools import tool + from crewai.tools import tool @tool def multiplier(first_number: int, second_number: int) -> float: @@ -499,7 +499,7 @@ def multiplier(first_number: int, second_number: int) -> float: def test_api_calls_throttling(capsys): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool @tool def get_final_answer() -> float: @@ -1111,7 +1111,7 @@ def crew_callback(_): def test_crew_function_calling_llm(): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool llm = "gpt-4o" @@ -1146,7 +1146,7 @@ def learn_about_AI() -> str: @pytest.mark.vcr(filter_headers=["authorization"]) def test_task_with_no_arguments(): - from crewai_tools import tool + from crewai.tools import tool @tool def return_data() -> str: @@ -1309,8 +1309,9 @@ def test_hierarchical_crew_creation_tasks_with_agents(): assert crew.manager_agent is not None assert crew.manager_agent.tools is not None - assert crew.manager_agent.tools[0].description.startswith( - "Delegate a specific task to one of the following coworkers: Senior Writer" + assert ( + "Delegate a specific task to one of the following coworkers: Senior Writer\n" + in crew.manager_agent.tools[0].description ) @@ -1337,8 +1338,9 @@ def test_hierarchical_crew_creation_tasks_with_async_execution(): crew.kickoff() assert crew.manager_agent is not None assert crew.manager_agent.tools is not None - assert crew.manager_agent.tools[0].description.startswith( + assert ( "Delegate a specific task to one of the following coworkers: Senior Writer\n" + in crew.manager_agent.tools[0].description ) @@ -1370,8 +1372,9 @@ def test_hierarchical_crew_creation_tasks_with_sync_last(): crew.kickoff() assert crew.manager_agent is not None assert crew.manager_agent.tools is not None - assert crew.manager_agent.tools[0].description.startswith( + assert ( "Delegate a specific task to one of the following coworkers: Senior Writer, Researcher, CEO\n" + in crew.manager_agent.tools[0].description ) @@ -1494,7 +1497,7 @@ def test_task_callback_on_crew(): def test_tools_with_custom_caching(): from unittest.mock import patch - from crewai_tools import tool + from crewai.tools import tool @tool def multiplcation_tool(first_number: int, second_number: int) -> int: @@ -1696,7 +1699,7 @@ def test_manager_agent_in_agents_raises_exception(): def test_manager_agent_with_tools_raises_exception(): - from crewai_tools import tool + from crewai.tools import tool @tool def testing_tool(first_number: int, second_number: int) -> int: diff --git a/tests/task_test.py b/tests/task_test.py index 1e20c94918..3b3de0bed8 100644 --- a/tests/task_test.py +++ b/tests/task_test.py @@ -15,7 +15,7 @@ def test_task_tool_reflect_agent_tools(): - from crewai_tools import tool + from crewai.tools import tool @tool def fake_tool() -> None: @@ -39,7 +39,7 @@ def fake_tool() -> None: def test_task_tool_takes_precedence_over_agent_tools(): - from crewai_tools import tool + from crewai.tools import tool @tool def fake_tool() -> None: @@ -656,7 +656,7 @@ def test_increment_delegations_for_sequential_process(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_increment_tool_errors(): - from crewai_tools import tool + from crewai.tools import tool @tool def scoring_examples() -> None: diff --git a/tests/agent_tools/__init__.py b/tests/tools/agent_tools/__init__.py similarity index 100% rename from tests/agent_tools/__init__.py rename to tests/tools/agent_tools/__init__.py diff --git a/tests/agent_tools/agent_tools_test.py b/tests/tools/agent_tools/agent_tools_test.py similarity index 95% rename from tests/agent_tools/agent_tools_test.py rename to tests/tools/agent_tools/agent_tools_test.py index 8d9345b463..9aea7b4bc2 100644 --- a/tests/agent_tools/agent_tools_test.py +++ b/tests/tools/agent_tools/agent_tools_test.py @@ -3,7 +3,7 @@ import pytest from crewai.agent import Agent -from crewai.tools.agent_tools import AgentTools +from crewai.tools.agent_tools.agent_tools import AgentTools researcher = Agent( role="researcher", @@ -11,12 +11,14 @@ backstory="You're an expert researcher, specialized in technology", allow_delegation=False, ) -tools = AgentTools(agents=[researcher]) +tools = AgentTools(agents=[researcher]).tools() +delegate_tool = tools[0] +ask_tool = tools[1] @pytest.mark.vcr(filter_headers=["authorization"]) def test_delegate_work(): - result = tools.delegate_work( + result = delegate_tool.run( coworker="researcher", task="share your take on AI Agents", context="I heard you hate them", @@ -30,8 +32,8 @@ def test_delegate_work(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_delegate_work_with_wrong_co_worker_variable(): - result = tools.delegate_work( - co_worker="researcher", + result = delegate_tool.run( + coworker="researcher", task="share your take on AI Agents", context="I heard you hate them", ) @@ -44,7 +46,7 @@ def test_delegate_work_with_wrong_co_worker_variable(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_ask_question(): - result = tools.ask_question( + result = ask_tool.run( coworker="researcher", question="do you hate AI Agents?", context="I heard you LOVE them", @@ -58,8 +60,8 @@ def test_ask_question(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_ask_question_with_wrong_co_worker_variable(): - result = tools.ask_question( - co_worker="researcher", + result = ask_tool.run( + coworker="researcher", question="do you hate AI Agents?", context="I heard you LOVE them", ) @@ -72,8 +74,8 @@ def test_ask_question_with_wrong_co_worker_variable(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_delegate_work_withwith_coworker_as_array(): - result = tools.delegate_work( - co_worker="[researcher]", + result = delegate_tool.run( + coworker="[researcher]", task="share your take on AI Agents", context="I heard you hate them", ) @@ -86,8 +88,8 @@ def test_delegate_work_withwith_coworker_as_array(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_ask_question_with_coworker_as_array(): - result = tools.ask_question( - co_worker="[researcher]", + result = ask_tool.run( + coworker="[researcher]", question="do you hate AI Agents?", context="I heard you LOVE them", ) @@ -99,7 +101,7 @@ def test_ask_question_with_coworker_as_array(): def test_delegate_work_to_wrong_agent(): - result = tools.ask_question( + result = ask_tool.run( coworker="writer", question="share your take on AI Agents", context="I heard you hate them", @@ -112,7 +114,7 @@ def test_delegate_work_to_wrong_agent(): def test_ask_question_to_wrong_agent(): - result = tools.ask_question( + result = ask_tool.run( coworker="writer", question="do you hate AI Agents?", context="I heard you LOVE them", diff --git a/tests/agent_tools/cassettes/test_ask_question.yaml b/tests/tools/agent_tools/cassettes/test_ask_question.yaml similarity index 100% rename from tests/agent_tools/cassettes/test_ask_question.yaml rename to tests/tools/agent_tools/cassettes/test_ask_question.yaml diff --git a/tests/agent_tools/cassettes/test_ask_question_with_coworker_as_array.yaml b/tests/tools/agent_tools/cassettes/test_ask_question_with_coworker_as_array.yaml similarity index 100% rename from tests/agent_tools/cassettes/test_ask_question_with_coworker_as_array.yaml rename to tests/tools/agent_tools/cassettes/test_ask_question_with_coworker_as_array.yaml diff --git a/tests/agent_tools/cassettes/test_ask_question_with_wrong_co_worker_variable.yaml b/tests/tools/agent_tools/cassettes/test_ask_question_with_wrong_co_worker_variable.yaml similarity index 100% rename from tests/agent_tools/cassettes/test_ask_question_with_wrong_co_worker_variable.yaml rename to tests/tools/agent_tools/cassettes/test_ask_question_with_wrong_co_worker_variable.yaml diff --git a/tests/agent_tools/cassettes/test_delegate_work.yaml b/tests/tools/agent_tools/cassettes/test_delegate_work.yaml similarity index 100% rename from tests/agent_tools/cassettes/test_delegate_work.yaml rename to tests/tools/agent_tools/cassettes/test_delegate_work.yaml diff --git a/tests/agent_tools/cassettes/test_delegate_work_with_wrong_co_worker_variable.yaml b/tests/tools/agent_tools/cassettes/test_delegate_work_with_wrong_co_worker_variable.yaml similarity index 100% rename from tests/agent_tools/cassettes/test_delegate_work_with_wrong_co_worker_variable.yaml rename to tests/tools/agent_tools/cassettes/test_delegate_work_with_wrong_co_worker_variable.yaml diff --git a/tests/agent_tools/cassettes/test_delegate_work_withwith_coworker_as_array.yaml b/tests/tools/agent_tools/cassettes/test_delegate_work_withwith_coworker_as_array.yaml similarity index 100% rename from tests/agent_tools/cassettes/test_delegate_work_withwith_coworker_as_array.yaml rename to tests/tools/agent_tools/cassettes/test_delegate_work_withwith_coworker_as_array.yaml diff --git a/tests/tools/test_base_tool.py b/tests/tools/test_base_tool.py new file mode 100644 index 0000000000..eca36739c2 --- /dev/null +++ b/tests/tools/test_base_tool.py @@ -0,0 +1,109 @@ +from typing import Callable +from crewai.tools import BaseTool, tool + + +def test_creating_a_tool_using_annotation(): + @tool("Name of my tool") + def my_tool(question: str) -> str: + """Clear description for what this tool is useful for, you agent will need this information to use it.""" + return question + + # Assert all the right attributes were defined + assert my_tool.name == "Name of my tool" + assert ( + my_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert my_tool.args_schema.schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + my_tool.func("What is the meaning of life?") == "What is the meaning of life?" + ) + + # Assert the langchain tool conversion worked as expected + converted_tool = my_tool.to_langchain() + assert converted_tool.name == "Name of my tool" + + assert ( + converted_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert converted_tool.args_schema.schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + converted_tool.func("What is the meaning of life?") + == "What is the meaning of life?" + ) + + +def test_creating_a_tool_using_baseclass(): + class MyCustomTool(BaseTool): + name: str = "Name of my tool" + description: str = ( + "Clear description for what this tool is useful for, you agent will need this information to use it." + ) + + def _run(self, question: str) -> str: + return question + + my_tool = MyCustomTool() + # Assert all the right attributes were defined + assert my_tool.name == "Name of my tool" + + assert ( + my_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert my_tool.args_schema.schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert my_tool.run("What is the meaning of life?") == "What is the meaning of life?" + + # Assert the langchain tool conversion worked as expected + converted_tool = my_tool.to_langchain() + assert converted_tool.name == "Name of my tool" + + assert ( + converted_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert converted_tool.args_schema.schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + converted_tool.run("What is the meaning of life?") + == "What is the meaning of life?" + ) + + +def test_setting_cache_function(): + class MyCustomTool(BaseTool): + name: str = "Name of my tool" + description: str = ( + "Clear description for what this tool is useful for, you agent will need this information to use it." + ) + cache_function: Callable = lambda: False + + def _run(self, question: str) -> str: + return question + + my_tool = MyCustomTool() + # Assert all the right attributes were defined + assert not my_tool.cache_function() + + +def test_default_cache_function_is_true(): + class MyCustomTool(BaseTool): + name: str = "Name of my tool" + description: str = ( + "Clear description for what this tool is useful for, you agent will need this information to use it." + ) + + def _run(self, question: str) -> str: + return question + + my_tool = MyCustomTool() + # Assert all the right attributes were defined + assert my_tool.cache_function() diff --git a/tests/tools/test_tool_usage.py b/tests/tools/test_tool_usage.py index 8af9b8abb8..fbe93a6288 100644 --- a/tests/tools/test_tool_usage.py +++ b/tests/tools/test_tool_usage.py @@ -3,11 +3,11 @@ from unittest.mock import MagicMock import pytest -from crewai_tools import BaseTool from pydantic import BaseModel, Field from crewai import Agent, Task from crewai.tools.tool_usage import ToolUsage +from crewai.tools import BaseTool class RandomNumberToolInput(BaseModel): @@ -103,11 +103,7 @@ def test_tool_usage_render(): rendered = tool_usage._render() # Updated checks to match the actual output - assert "Tool Name: random number generator" in rendered - assert ( - "Random Number Generator(min_value: 'integer', max_value: 'integer') - Generates a random number within a specified range min_value: 'The minimum value of the range (inclusive)', max_value: 'The maximum value of the range (inclusive)'" - in rendered - ) + assert "Tool Name: Random Number Generator" in rendered assert "Tool Arguments:" in rendered assert ( "'min_value': {'description': 'The minimum value of the range (inclusive)', 'type': 'int'}" @@ -117,3 +113,11 @@ def test_tool_usage_render(): "'max_value': {'description': 'The maximum value of the range (inclusive)', 'type': 'int'}" in rendered ) + assert ( + "Tool Description: Generates a random number within a specified range" + in rendered + ) + assert ( + "Tool Name: Random Number Generator\nTool Arguments: {'min_value': {'description': 'The minimum value of the range (inclusive)', 'type': 'int'}, 'max_value': {'description': 'The maximum value of the range (inclusive)', 'type': 'int'}}\nTool Description: Generates a random number within a specified range" + in rendered + )