Consolidates pytest config, standardizes env handling, reorganizes cassette layout, removes outdated VCR configs, improves sync with threading.Condition, updates event-waiting logic, ensures cleanup, regenerates Gemini cassettes, and reverts unintended test changes.
12 KiB
Building CrewAI Tools
This guide shows you how to build high‑quality CrewAI tools that match the patterns in this repository and are ready to be merged. It focuses on: architecture, conventions, environment variables, dependencies, testing, documentation, and a complete example.
Who this is for
- Contributors creating new tools under
crewai_tools/tools/* - Maintainers reviewing PRs for consistency and DX
Quick‑start checklist
- Create a new folder under
crewai_tools/tools/<your_tool_name>/with aREADME.mdand a<your_tool_name>.py. - Implement a class that ends with
Tooland subclassesBaseTool(orRagToolwhen appropriate). - Define a Pydantic
args_schemawith explicit field descriptions and validation. - Declare
env_varsandpackage_dependenciesin the class when needed. - Lazily initialize clients in
__init__or_runand handle missing credentials with clear errors. - Implement
_run(...) -> str | dictand, if needed,_arun(...). - Add tests under
tests/tools/(unit, no real network calls; mock or record safely). - Add a concise tool
README.mdwith usage and required env vars. - If you add optional dependencies, register them in
pyproject.tomlunder[project.optional-dependencies]and reference that extra in your tool docs. - Run
uv run pytestandpre-commit run -alocally; ensure green.
Tool anatomy and conventions
BaseTool pattern
All tools follow this structure:
from typing import Any, List, Optional, Type
import os
from pydantic import BaseModel, Field
from crewai.tools import BaseTool, EnvVar
class MyToolInput(BaseModel):
"""Input schema for MyTool."""
query: str = Field(..., description="Your input description here")
limit: int = Field(5, ge=1, le=50, description="Max items to return")
class MyTool(BaseTool):
name: str = "My Tool"
description: str = "Explain succinctly what this tool does and when to use it."
args_schema: Type[BaseModel] = MyToolInput
# Only include when applicable
env_vars: List[EnvVar] = [
EnvVar(name="MY_API_KEY", description="API key for My service", required=True),
]
package_dependencies: List[str] = ["my-sdk"]
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
# Lazy import to keep base install light
try:
import my_sdk # noqa: F401
except Exception as exc:
raise ImportError(
"Missing optional dependency 'my-sdk'. Install with: \n"
" uv add crewai-tools --extra my-sdk\n"
"or\n"
" pip install my-sdk\n"
) from exc
if "MY_API_KEY" not in os.environ:
raise ValueError("Environment variable MY_API_KEY is required for MyTool")
def _run(self, query: str, limit: int = 5, **_: Any) -> str:
"""Synchronous execution. Return a concise string or JSON string."""
# Implement your logic here; do not print. Return the content.
# Handle errors gracefully, return clear messages.
return f"Processed {query} with limit={limit}"
async def _arun(self, *args: Any, **kwargs: Any) -> str:
"""Optional async counterpart if your client supports it."""
# Prefer delegating to _run when the client is thread-safe
return self._run(*args, **kwargs)
Key points:
- Class name must end with
Toolto be auto‑discovered by our tooling. - Use
args_schemafor inputs; always includedescriptionand validation. - Validate env vars early and fail with actionable errors.
- Keep outputs deterministic and compact; favor
str(possibly JSON‑encoded) or small dicts converted to strings. - Avoid printing; return the final string.
Error handling
- Wrap network and I/O with try/except and return a helpful message. See
BraveSearchTooland others for patterns. - Validate required inputs and environment configuration with clear messages.
- Keep exceptions user‑friendly; do not leak stack traces.
Rate limiting and retries
- If the upstream API enforces request pacing, implement minimal rate limiting (see
BraveSearchTool). - Consider idempotency and backoff for transient errors where appropriate.
Async support
- Implement
_arunonly if your library has a true async client or your sync calls are thread‑safe. - Otherwise, delegate
_arunto_runas in multiple existing tools.
Returning values
- Return a string (or JSON string) that’s ready to display in an agent transcript.
- If returning structured data, keep it small and human‑readable. Use stable keys and ordering.
RAG tools and adapters
If your tool is a knowledge source, consider extending RagTool and/or creating an adapter.
RagToolexposesadd(...)and aquery(question: str) -> strcontract through anAdapter.- See
crewai_tools/tools/rag/rag_tool.pyand adapters likeembedchain_adapter.pyandlancedb_adapter.py.
Minimal adapter example:
from typing import Any
from pydantic import BaseModel
from crewai_tools.tools.rag.rag_tool import Adapter, RagTool
class MemoryAdapter(Adapter):
store: list[str] = []
def add(self, text: str, **_: Any) -> None:
self.store.append(text)
def query(self, question: str) -> str:
# naive demo: return all text containing any word from the question
tokens = set(question.lower().split())
hits = [t for t in self.store if tokens & set(t.lower().split())]
return "\n".join(hits) if hits else "No relevant content found."
class MemoryRagTool(RagTool):
name: str = "In‑memory RAG"
description: str = "Toy RAG that stores text in memory and returns matches."
adapter: Adapter = MemoryAdapter()
When using external vector DBs (MongoDB, Qdrant, Weaviate), study the existing tools to follow indexing, embedding, and query configuration patterns closely.
Toolkits (multiple related tools)
Some integrations expose a toolkit (a group of tools) rather than a single class. See Bedrock browser_toolkit.py and code_interpreter_toolkit.py.
Guidelines:
- Provide small, focused
BaseToolclasses for each operation (e.g.,navigate,click,extract_text). - Offer a helper
create_<name>_toolkit(...) -> Tuple[ToolkitClass, List[BaseTool]]to create tools and manage resources. - If you open external resources (browsers, interpreters), support cleanup methods and optionally context manager usage.
Environment variables and dependencies
env_vars
- Declare as
env_vars: List[EnvVar]withname,description,required, and optionaldefault. - Validate presence in
__init__or on first_runcall.
Dependencies
- List runtime packages in
package_dependencieson the class. - If they are genuinely optional, add an extra under
[project.optional-dependencies]inpyproject.toml(e.g.,tavily-python,serpapi,scrapfly-sdk). - Use lazy imports to avoid hard deps for users who don’t need the tool.
Testing
Place tests under tests/tools/ and follow these rules:
- Do not hit real external services in CI. Use mocks, fakes, or recorded fixtures where allowed.
- Validate input validation, env var handling, error messages, and happy path output formatting.
- Keep tests fast and deterministic.
Example skeleton (tests/tools/my_tool_test.py):
import os
import pytest
from crewai_tools.tools.my_tool.my_tool import MyTool
def test_requires_env_var(monkeypatch):
monkeypatch.delenv("MY_API_KEY", raising=False)
with pytest.raises(ValueError):
MyTool()
def test_happy_path(monkeypatch):
monkeypatch.setenv("MY_API_KEY", "test")
tool = MyTool()
result = tool.run(query="hello", limit=2)
assert "hello" in result
Run locally:
uv run pytest
pre-commit run -a
Documentation
Each tool must include a README.md in its folder with:
- What it does and when to use it
- Required env vars and optional extras (with install snippet)
- Minimal usage example
Update the root README.md only if the tool introduces a new category or notable capability.
Discovery and specs
Our internal tooling discovers classes whose names end with Tool. Keep your class exported from the module path under crewai_tools/tools/... to be picked up by scripts like crewai_tools.generate_tool_specs.py.
Full example: “Weather Search Tool”
This example demonstrates: args_schema, env_vars, package_dependencies, lazy imports, validation, and robust error handling.
# file: crewai_tools/tools/weather_tool/weather_tool.py
from typing import Any, List, Optional, Type
import os
import requests
from pydantic import BaseModel, Field
from crewai.tools import BaseTool, EnvVar
class WeatherToolInput(BaseModel):
"""Input schema for WeatherTool."""
city: str = Field(..., description="City name, e.g., 'Berlin'")
country: Optional[str] = Field(None, description="ISO country code, e.g., 'DE'")
units: str = Field(
default="metric",
description="Units system: 'metric' or 'imperial'",
pattern=r"^(metric|imperial)$",
)
class WeatherTool(BaseTool):
name: str = "Weather Search"
description: str = (
"Look up current weather for a city using a public weather API."
)
args_schema: Type[BaseModel] = WeatherToolInput
env_vars: List[EnvVar] = [
EnvVar(
name="WEATHER_API_KEY",
description="API key for the weather service",
required=True,
),
]
package_dependencies: List[str] = ["requests"]
base_url: str = "https://api.openweathermap.org/data/2.5/weather"
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
if "WEATHER_API_KEY" not in os.environ:
raise ValueError("WEATHER_API_KEY is required for WeatherTool")
def _run(self, city: str, country: Optional[str] = None, units: str = "metric") -> str:
try:
q = f"{city},{country}" if country else city
params = {
"q": q,
"units": units,
"appid": os.environ["WEATHER_API_KEY"],
}
resp = requests.get(self.base_url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
main = data.get("weather", [{}])[0].get("main", "Unknown")
desc = data.get("weather", [{}])[0].get("description", "")
temp = data.get("main", {}).get("temp")
feels = data.get("main", {}).get("feels_like")
city_name = data.get("name", city)
return (
f"Weather in {city_name}: {main} ({desc}). "
f"Temperature: {temp}°, feels like {feels}°."
)
except requests.Timeout:
return "Weather service timed out. Please try again later."
except requests.HTTPError as e:
return f"Weather service error: {e.response.status_code} {e.response.text[:120]}"
except Exception as e:
return f"Unexpected error fetching weather: {e}"
Folder layout:
crewai_tools/tools/weather_tool/
├─ weather_tool.py
└─ README.md
And README.md should document env vars and usage.
PR checklist
- Tool lives under
crewai_tools/tools/<name>/ - Class ends with
Tooland subclassesBaseTool(orRagTool) - Precise
args_schemawith descriptions and validation env_varsdeclared (if any) and validatedpackage_dependenciesand optional extras added inpyproject.toml(if any)- Clear error handling; no prints
- Unit tests added (
tests/tools/), fast and deterministic - Tool
README.mdwith usage and env vars pre-commitandpytestpass locally
Tips for great DX
- Keep responses short and useful—agents quote your tool output directly.
- Validate early; fail fast with actionable guidance.
- Prefer lazy imports; minimize default install surface.
- Mirror patterns from similar tools in this repo for a consistent developer experience.
Happy building!