Learnixo

CrewAI Multi-Agents · Lesson 6 of 16

Giving Agents Tools

Why Agents Need Tools

Without tools, a CrewAI agent can only reason about what it already knows from its training data. Tools give agents the ability to act on the world: search the web, read files, query databases, call APIs, and more.

A tool is a function that:

  1. Accepts structured input
  2. Does something (fetches data, executes code, writes to a file)
  3. Returns a string result that the agent can read and reason about

Built-In Tools from crewai-tools

The crewai-tools package ships a library of ready-made tools:

Bash
pip install crewai-tools

SerperDevTool (Web Search)

Python
from crewai_tools import SerperDevTool
import os

os.environ["SERPER_API_KEY"] = "your-serper-api-key"

search_tool = SerperDevTool()

# Attach to an agent
researcher = Agent(
    role="Research Analyst",
    goal="Find current, accurate information on any topic",
    backstory="You are a methodical researcher who verifies facts from multiple sources.",
    tools=[search_tool],
    verbose=True,
)

Get a free Serper API key at serper.dev. Each search costs a fraction of a cent.

FileReadTool

Python
from crewai_tools import FileReadTool

file_tool = FileReadTool()

# Agent can now read files you specify in the task description
analyst = Agent(
    role="Data Analyst",
    goal="Extract insights from provided data files",
    backstory="You analyze structured data files and produce concise summaries.",
    tools=[file_tool],
    verbose=True,
)

WebsiteSearchTool

Python
from crewai_tools import WebsiteSearchTool

# General web scraping
web_tool = WebsiteSearchTool()

# Or locked to a specific site
fda_tool = WebsiteSearchTool(website="https://www.fda.gov")

Other Built-In Tools

Python
from crewai_tools import (
    CodeInterpreterTool,    # Execute Python code
    CSVSearchTool,          # Search within CSV files
    PDFSearchTool,          # Search within PDF files
    DirectorySearchTool,    # Search across files in a directory
    GithubSearchTool,       # Search GitHub repositories
    YoutubeVideoSearchTool, # Search YouTube video transcripts
)

Assigning Tools Per Agent (Not Global)

A common mistake is giving all agents all tools. This is wasteful and confusing — the LLM has to decide from a long list every time it acts.

Assign only the tools each agent actually needs:

Python
from crewai_tools import SerperDevTool, FileReadTool, PDFSearchTool

search_tool = SerperDevTool()
file_tool = FileReadTool()
pdf_tool = PDFSearchTool()

# Researcher needs web search and PDF reading
researcher = Agent(
    role="Clinical Research Analyst",
    goal="Find evidence in published literature",
    backstory="You search PubMed and clinical databases for evidence.",
    tools=[search_tool, pdf_tool],   # Only what this agent needs
    verbose=True,
)

# Writer needs file reading to access templates
writer = Agent(
    role="Medical Writer",
    goal="Write structured medical documents",
    backstory="You write clear, compliant medical communications.",
    tools=[file_tool],               # Only file reading
    verbose=True,
)

# Reviewer needs no tools  it reasons from context only
reviewer = Agent(
    role="Regulatory Reviewer",
    goal="Review documents for compliance",
    backstory="You apply FDA guidelines to review medical communications.",
    tools=[],                        # No tools needed
    verbose=True,
)

Building Custom Tools with @tool

The @tool decorator is the simplest way to create a custom tool. CrewAI converts the decorated function into a tool the agent can call.

Basic Custom Tool

Python
from crewai.tools import tool

@tool("Drug Database Search")
def search_drug_database(query: str) -> str:
    """
    Search the internal drug safety database for information about a specific drug.
    Use this when you need data from the company's proprietary safety repository.
    Input: a drug name or compound identifier.
    """
    # In production this would query a real database
    # For this example, we simulate a response
    mock_db = {
        "metformin": "Adverse events: GI upset (20%), lactic acidosis (rare). Contraindicated in eGFR < 30.",
        "semaglutide": "Adverse events: nausea (44%), vomiting (24%), diarrhea (30%). Monitor for thyroid tumors.",
        "tirzepatide": "Adverse events: nausea (18-45%), diarrhea (13-30%), injection site reactions (3-7%).",
    }
    drug_name = query.lower().strip()
    result = mock_db.get(drug_name, f"No data found for '{query}' in the drug database.")
    return result

The docstring is critical — CrewAI uses it to tell the LLM when and how to call this tool.

Attach the Custom Tool to an Agent

Python
safety_analyst = Agent(
    role="Drug Safety Analyst",
    goal="Assess drug safety using internal and external data sources",
    backstory=(
        "You have access to the company's proprietary safety database "
        "and use it alongside public literature to assess safety signals."
    ),
    tools=[search_drug_database, search_tool],   # custom + built-in
    verbose=True,
)

Custom Tool with Pydantic Input Schema

For tools with multiple parameters or complex inputs, use a Pydantic schema for type safety and validation:

Python
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Optional

class DrugQueryInput(BaseModel):
    drug_name: str = Field(
        description="The name of the drug to look up"
    )
    query_type: str = Field(
        description="Type of query: 'adverse_events', 'interactions', 'contraindications', or 'all'"
    )
    severity_filter: Optional[str] = Field(
        default=None,
        description="Filter by severity: 'mild', 'moderate', 'severe'. Leave blank for all."
    )

class DrugSafetyDatabaseTool(BaseTool):
    name: str = "Drug Safety Database"
    description: str = (
        "Query the internal drug safety database. "
        "Use this for adverse event data, drug interactions, and contraindications. "
        "Always use this before the web search tool when looking for safety data."
    )
    args_schema: type[BaseModel] = DrugQueryInput

    def _run(self, drug_name: str, query_type: str, severity_filter: Optional[str] = None) -> str:
        # Simulate database query
        data = self._query_database(drug_name, query_type, severity_filter)
        return data

    def _query_database(self, drug: str, qtype: str, severity: Optional[str]) -> str:
        # In production: actual DB call here
        results = {
            "metformin": {
                "adverse_events": [
                    {"event": "GI upset", "incidence": "20%", "severity": "mild"},
                    {"event": "Lactic acidosis", "incidence": "0.01%", "severity": "severe"},
                    {"event": "B12 deficiency", "incidence": "7%", "severity": "moderate"},
                ],
                "contraindications": [
                    "eGFR below 30 mL/min/1.73m2",
                    "Acute metabolic acidosis",
                    "Radiological contrast procedures",
                ],
                "interactions": [
                    "Alcohol: increases lactic acidosis risk",
                    "Iodinated contrast: hold 48h before/after",
                ],
            }
        }

        drug_data = results.get(drug.lower(), {})
        if not drug_data:
            return f"No data found for {drug} in the safety database."

        if qtype == "adverse_events" or qtype == "all":
            events = drug_data.get("adverse_events", [])
            if severity:
                events = [e for e in events if e["severity"] == severity]
            output_lines = [f"Adverse Events for {drug}:"]
            for event in events:
                output_lines.append(
                    f"  - {event['event']}: {event['incidence']} incidence ({event['severity']})"
                )
            return "\n".join(output_lines)

        return f"Query type '{qtype}' not recognized."

# Instantiate and attach
db_tool = DrugSafetyDatabaseTool()

analyst = Agent(
    role="Drug Safety Analyst",
    goal="Provide comprehensive drug safety assessments",
    backstory=(
        "You use the internal safety database as your primary source, "
        "supplementing with web searches for external data."
    ),
    tools=[db_tool, search_tool],
    verbose=True,
)

Full Example: Research Agent with Two Tools

Python
from crewai import Agent, Task, Crew, Process, LLM
from crewai_tools import SerperDevTool
from crewai.tools import tool

# Custom tool
@tool("Internal Drug Registry")
def search_drug_registry(drug_name: str) -> str:
    """
    Search the internal drug registry for approved compounds and their status.
    Use this first when asked about any specific drug or compound.
    Returns: approval status, indication, and key safety notes.
    """
    registry = {
        "semaglutide": {
            "status": "Approved",
            "brand": "Ozempic / Wegovy",
            "indications": ["Type 2 diabetes (Ozempic)", "Obesity (Wegovy)"],
            "safety_notes": "Monitor for thyroid C-cell tumors, pancreatitis",
        },
        "tirzepatide": {
            "status": "Approved",
            "brand": "Mounjaro / Zepbound",
            "indications": ["Type 2 diabetes (Mounjaro)", "Obesity (Zepbound)"],
            "safety_notes": "GI adverse events common; monitor renal function",
        },
    }
    entry = registry.get(drug_name.lower())
    if not entry:
        return f"Drug '{drug_name}' not found in the internal registry."
    return (
        f"Drug: {drug_name.title()}\n"
        f"Brand: {entry['brand']}\n"
        f"Status: {entry['status']}\n"
        f"Indications: {', '.join(entry['indications'])}\n"
        f"Safety Notes: {entry['safety_notes']}"
    )

# Built-in tool
web_search = SerperDevTool()

# Agent with both tools
drug_researcher = Agent(
    role="Senior Drug Researcher",
    goal=(
        "Provide comprehensive, accurate information on drugs by combining "
        "internal registry data with the latest published research."
    ),
    backstory=(
        "You are a senior research scientist with a PharmD. "
        "You always check the internal registry first for authoritative data, "
        "then supplement with a web search for the latest evidence. "
        "You cite your sources and note any discrepancy between the registry "
        "and published literature."
    ),
    tools=[search_drug_registry, web_search],
    llm=LLM(model="gpt-4o"),
    verbose=True,
)

research_task = Task(
    description=(
        "Research semaglutide. Check the internal registry first, "
        "then search for the latest Phase 4 safety data published in 2025 or 2026."
    ),
    expected_output=(
        "A research summary with: "
        "1) Registry data (approval status, indications, key safety notes), "
        "2) Latest published safety findings (with source citations), "
        "3) Any discrepancies between registry data and recent literature."
    ),
    agent=drug_researcher,
)

crew = Crew(
    agents=[drug_researcher],
    tasks=[research_task],
    process=Process.sequential,
    verbose=True,
)

result = crew.kickoff()
print(result.raw)

Tool Best Practices

| Practice | Why | |----------|-----| | Write clear, specific docstrings | CrewAI uses the docstring to tell the LLM when to use each tool | | Name tools descriptively | "Internal Drug Registry" beats "Tool 1" | | Assign only relevant tools per agent | Fewer tools = less confusion and fewer hallucinated tool calls | | Return strings, not dicts or objects | Agents read tool output as text | | Handle errors gracefully | Return an error message string rather than raising exceptions | | Use Pydantic schemas for multi-parameter tools | Prevents the LLM from misformatting arguments |


Summary

  • CrewAI ships a tool library via crewai-tools — web search, file reading, PDF search, and more
  • Custom tools use @tool for simple functions or BaseTool for Pydantic-typed inputs
  • Assign tools per agent, not globally — keep each agent's toolset minimal and focused
  • The tool docstring is the agent's instruction for when and how to use the tool
  • Return strings from tools — agents parse text, not Python objects