콘텐츠로 이동

커스텀 도구 발행 (Publish Custom Tools)

CrewAI 호환 도구를 직접 빌드하고 패키징해서 PyPI에 올리는 방법을 다뤄요. 올리면 어떤 CrewAI 사용자든 설치해서 쓸 수 있게 되죠.

개요

CrewAI의 도구(tool) 시스템은 확장할 수 있게 설계되어 있어요. 만든 도구가 다른 사람에게도 도움이 될 만하다면, 독립된 Python 라이브러리로 패키징해서 PyPI에 발행할 수 있어요. 그러면 CrewAI 저장소에 PR을 보낼 필요 없이, 모든 CrewAI 사용자가 설치해서 쓸 수 있게 됩니다.

이 가이드는 전체 과정을 짚어줘요. 도구 계약(tools contract)을 구현하는 일, 패키지를 구조화하는 일, 그리고 PyPI에 발행하는 일까지 말이죠.

내 프로젝트에서만 쓸 커스텀 도구가 필요하다면, 대신 Create Custom Tools 가이드를 보세요.

도구 계약 (The Tools Contract)

모든 CrewAI 도구는 두 가지 인터페이스 중 하나를 만족해야 해요.

옵션 1: BaseTool 서브클래스

crewai.tools.BaseTool을 상속하고 _run 메서드를 구현하는 방식이에요. name, description을 정의하고, 입력 검증을 원하면 args_schema도 선택적으로 정의하면 됩니다.

from crewai.tools import BaseTool
from pydantic import BaseModel, Field

class GeolocateInput(BaseModel):
    """Input schema for GeolocateTool."""
    address: str = Field(..., description="The street address to geolocate.")

class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."
    args_schema: type[BaseModel] = GeolocateInput

    def _run(self, address: str) -> str:
        # Your implementation here
        return f"40.7128, -74.0060"

옵션 2: @tool 데코레이터

더 간단한 도구라면 @tool 데코레이터가 함수를 CrewAI 도구로 바꿔줘요. 이때 함수는 docstring(도구 설명으로 쓰여요)과 타입 애너테이션을 반드시 가져야 해요.

from crewai.tools import tool

@tool("Geolocate")
def geolocate(address: str) -> str:
    """Converts a street address into latitude/longitude coordinates."""
    return "40.7128, -74.0060"

핵심 요구 사항

어느 방식을 쓰든 도구는 다음을 갖춰야 해요.

  • name — 짧고 설명적인 식별자.
  • description — 에이전트에게 언제, 어떻게 도구를 쓸지 알려줘요. 에이전트가 도구를 얼마나 잘 쓰는지에 직접 영향을 주기 때문에, 명확하고 구체적으로 적어야 해요.
  • _run(BaseTool) 또는 함수 본문(@tool) — 동기(synchronous) 실행 로직.
  • 모든 매개변수와 반환 값에 타입 애너테이션.
  • 문자열 결과를 반환하거나, 구조화된 결과를 위한 선택적 Pydantic 출력 스키마를 정의.

선택: 비동기 지원

도구가 I/O 바운드(I/O-bound) 작업을 한다면 _arun을 구현해서 비동기로 실행할 수 있어요.

class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."

    def _run(self, address: str) -> str:
        # Sync implementation
        ...

    async def _arun(self, address: str) -> str:
        # Async implementation
        ...

선택: args_schema로 입력 검증

args_schema로 Pydantic 모델을 정의하면 자동 입력 검증과 명확한 에러 메시지를 얻을 수 있어요. 제공하지 않으면 CrewAI가 _run 메서드의 시그니처에서 유추해요.

from pydantic import BaseModel, Field

class TranslateInput(BaseModel):
    """Input schema for TranslateTool."""
    text: str = Field(..., description="The text to translate.")
    target_language: str = Field(
        default="en",
        description="ISO 639-1 language code for the target language.",
    )

발행할 도구에서는 명시적 스키마를 권장해요. 에이전트 동작이 더 좋아지고, 사용자에게 더 명확한 문서가 되니까요.

선택: result_schema로 타입 있는 출력

도구가 구조화된 데이터를 반환한다면 Pydantic 출력 모델을 정의해요. 발행한 도구에서는 이걸 기본값으로 삼는 게 좋아요. 사용자와 에이전트가 이름 있는 필드에 의존할 수 있으니까요.

직접 Python 호출에서는 도구가 반환한 값을 그대로 받아요. 에이전트가 도구를 쓰면, CrewAI는 출력 모델에 기반한 JSON을 에이전트에게 보내줘요.

CrewAI는 Pydantic 반환 애너테이션에서 출력 스키마를 유추할 수 있어요.

from crewai.tools import BaseTool
from pydantic import BaseModel, Field

class GeolocateResult(BaseModel):
    latitude: float = Field(..., description="Latitude in decimal degrees.")
    longitude: float = Field(..., description="Longitude in decimal degrees.")

class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."

    def _run(self, address: str) -> GeolocateResult:
        if "1600 Pennsylvania" in address:
            return GeolocateResult(latitude=38.8977, longitude=-77.0365)
        return GeolocateResult(latitude=40.7128, longitude=-74.0060)

도구가 딕셔너리를 반환한다면 result_schema를 명시적으로 설정하세요.

class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."
    result_schema: type[BaseModel] = GeolocateResult

    def _run(self, address: str) -> dict[str, float]:
        if "1600 Pennsylvania" in address:
            return {"latitude": 38.8977, "longitude": -77.0365}
        return {"latitude": 40.7128, "longitude": -74.0060}

에이전트가 JSON 대신 짧은 텍스트 요약을 받도록 하고 싶다면, BaseTool 서브클래스에서 format_output_for_agent를 오버라이드하세요.

class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."

    def _run(self, address: str) -> GeolocateResult:
        if "1600 Pennsylvania" in address:
            return GeolocateResult(latitude=38.8977, longitude=-77.0365)
        return GeolocateResult(latitude=40.7128, longitude=-74.0060)

    def format_output_for_agent(self, raw_result: object) -> str:
        result = GeolocateResult.model_validate(raw_result)
        return f"Latitude {result.latitude}, longitude {result.longitude}"

이 오버라이드는 에이전트가 보는 것만 바꿔요. 패키지를 직접 쓰는 사용자는 여전히 tool.run(...)에서 평범한 값을 받습니다.

선택: 환경 변수

도구가 API 키나 다른 설정을 요구한다면 env_vars로 선언해서 사용자에게 무엇을 설정해야 하는지 알려줘요.

from crewai.tools import BaseTool, EnvVar

class GeolocateTool(BaseTool):
    name: str = "Geolocate"
    description: str = "Converts a street address into latitude/longitude coordinates."
    env_vars: list[EnvVar] = [
        EnvVar(
            name="GEOCODING_API_KEY",
            description="API key for the geocoding service.",
            required=True,
        ),
    ]

    def _run(self, address: str) -> str:
        ...

패키지 구조 (Package Structure)

프로젝트를 표준 Python 패키지로 구조화하세요. 권장하는 레이아웃은 이래요.

crewai-geolocate/
├── pyproject.toml
├── LICENSE
├── README.md
└── src/
    └── crewai_geolocate/
        ├── __init__.py
        └── tools.py

pyproject.toml

[project]
name = "crewai-geolocate"
version = "0.1.0"
description = "A CrewAI tool for geolocating street addresses."
requires-python = ">=3.10"
dependencies = [
    "crewai",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

crewai를 의존성으로 선언하면 사용자가 호환 버전을 자동으로 받게 돼요.

init.py

사용자가 도구 클래스를 바로 import 할 수 있도록 다시 내보내세요.

from crewai_geolocate.tools import GeolocateTool

__all__ = ["GeolocateTool"]

네이밍 규칙

  • 패키지 이름: crewai- 접두사를 사용하세요 (예: crewai-geolocate). 이러면 사용자가 PyPI에서 검색할 때 찾기 쉬워져요.
  • 모듈 이름: 언더스코어를 사용하세요 (예: crewai_geolocate).
  • 도구 클래스 이름: Tool로 끝나는 PascalCase를 사용하세요 (예: GeolocateTool).

도구 테스트하기

발행하기 전에 도구가 크루 안에서 잘 동작하는지 확인하세요.

from crewai import Agent, Crew, Task
from crewai_geolocate import GeolocateTool

agent = Agent(
    role="Location Analyst",
    goal="Find coordinates for given addresses.",
    backstory="An expert in geospatial data.",
    tools=[GeolocateTool()],
)

task = Task(
    description="Find the coordinates of 1600 Pennsylvania Avenue, Washington, DC.",
    expected_output="The latitude and longitude of the address.",
    agent=agent,
)

crew = Crew(agents=[agent], tasks=[task])
result = crew.kickoff()
print(result)

PyPI에 발행하기

도구를 테스트하고 준비가 됐다면:

# 패키지 빌드
uv build

# PyPI에 발행
uv publish

처음 발행하는 경우라면 PyPI 계정과 API 토큰이 필요해요.

발행한 뒤

사용자는 다음 명령으로 도구를 설치할 수 있어요.

pip install crewai-geolocate

또는 uv를 쓴다면:

uv add crewai-geolocate

그리고 크루에서 이렇게 사용합니다.

from crewai_geolocate import GeolocateTool

agent = Agent(
    role="Location Analyst",
    tools=[GeolocateTool()],
    # ...
)