mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-03-19 18:28:14 +00:00
Compare commits
3 Commits
fix/bad-cr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8886f11672 | ||
|
|
713fa7d01b | ||
|
|
929d756ae2 |
105
docs/docs.json
105
docs/docs.json
@@ -115,6 +115,13 @@
|
||||
"en/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Tools",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"en/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Coding Tools",
|
||||
"icon": "terminal",
|
||||
@@ -575,6 +582,13 @@
|
||||
"en/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Tools",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"en/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Coding Tools",
|
||||
"icon": "terminal",
|
||||
@@ -1035,6 +1049,13 @@
|
||||
"en/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Tools",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"en/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Coding Tools",
|
||||
"icon": "terminal",
|
||||
@@ -1525,6 +1546,20 @@
|
||||
"pt-BR/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ferramentas",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"pt-BR/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ferramentas de Codificação",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"pt-BR/guides/coding-tools/agents-md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Avançado",
|
||||
"icon": "gear",
|
||||
@@ -1964,6 +1999,20 @@
|
||||
"pt-BR/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ferramentas",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"pt-BR/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ferramentas de Codificação",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"pt-BR/guides/coding-tools/agents-md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Avançado",
|
||||
"icon": "gear",
|
||||
@@ -2403,6 +2452,20 @@
|
||||
"pt-BR/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ferramentas",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"pt-BR/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Ferramentas de Codificação",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"pt-BR/guides/coding-tools/agents-md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Avançado",
|
||||
"icon": "gear",
|
||||
@@ -2872,6 +2935,20 @@
|
||||
"ko/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "도구",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"ko/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "코딩 도구",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"ko/guides/coding-tools/agents-md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "고급",
|
||||
"icon": "gear",
|
||||
@@ -3323,6 +3400,20 @@
|
||||
"ko/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "도구",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"ko/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "코딩 도구",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"ko/guides/coding-tools/agents-md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "고급",
|
||||
"icon": "gear",
|
||||
@@ -3774,6 +3865,20 @@
|
||||
"ko/guides/flows/mastering-flow-state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "도구",
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"ko/guides/tools/publish-custom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "코딩 도구",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"ko/guides/coding-tools/agents-md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "고급",
|
||||
"icon": "gear",
|
||||
|
||||
244
docs/en/guides/tools/publish-custom-tools.mdx
Normal file
244
docs/en/guides/tools/publish-custom-tools.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: Publish Custom Tools
|
||||
description: How to build, package, and publish your own CrewAI-compatible tools to PyPI so any CrewAI user can install and use them.
|
||||
icon: box-open
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CrewAI's tool system is designed to be extended. If you've built a tool that could benefit others, you can package it as a standalone Python library, publish it to PyPI, and make it available to any CrewAI user — no PR to the CrewAI repo required.
|
||||
|
||||
This guide walks through the full process: implementing the tools contract, structuring your package, and publishing to PyPI.
|
||||
|
||||
<Note type="info" title="Not looking to publish?">
|
||||
If you just need a custom tool for your own project, see the [Create Custom Tools](/en/learn/create-custom-tools) guide instead.
|
||||
</Note>
|
||||
|
||||
## The Tools Contract
|
||||
|
||||
Every CrewAI tool must satisfy one of two interfaces:
|
||||
|
||||
### Option 1: Subclass `BaseTool`
|
||||
|
||||
Subclass `crewai.tools.BaseTool` and implement the `_run` method. Define `name`, `description`, and optionally an `args_schema` for input validation.
|
||||
|
||||
```python
|
||||
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"
|
||||
```
|
||||
|
||||
### Option 2: Use the `@tool` Decorator
|
||||
|
||||
For simpler tools, the `@tool` decorator turns a function into a CrewAI tool. The function **must** have a docstring (used as the tool description) and type annotations.
|
||||
|
||||
```python
|
||||
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"
|
||||
```
|
||||
|
||||
### Key Requirements
|
||||
|
||||
Regardless of which approach you use, your tool must:
|
||||
|
||||
- Have a **`name`** — a short, descriptive identifier.
|
||||
- Have a **`description`** — tells the agent when and how to use the tool. This directly affects how well agents use your tool, so be clear and specific.
|
||||
- Implement **`_run`** (BaseTool) or provide a **function body** (@tool) — the synchronous execution logic.
|
||||
- Use **type annotations** on all parameters and return values.
|
||||
- Return a **string** result (or something that can be meaningfully converted to one).
|
||||
|
||||
### Optional: Async Support
|
||||
|
||||
If your tool performs I/O-bound work, implement `_arun` for async execution:
|
||||
|
||||
```python
|
||||
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
|
||||
...
|
||||
```
|
||||
|
||||
### Optional: Input Validation with `args_schema`
|
||||
|
||||
Define a Pydantic model as your `args_schema` to get automatic input validation and clear error messages. If you don't provide one, CrewAI will infer it from your `_run` method's signature.
|
||||
|
||||
```python
|
||||
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.",
|
||||
)
|
||||
```
|
||||
|
||||
Explicit schemas are recommended for published tools — they produce better agent behavior and clearer documentation for your users.
|
||||
|
||||
### Optional: Environment Variables
|
||||
|
||||
If your tool requires API keys or other configuration, declare them with `env_vars` so users know what to set:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
Structure your project as a standard Python package. Here's a recommended layout:
|
||||
|
||||
```
|
||||
crewai-geolocate/
|
||||
├── pyproject.toml
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── src/
|
||||
└── crewai_geolocate/
|
||||
├── __init__.py
|
||||
└── tools.py
|
||||
```
|
||||
|
||||
### `pyproject.toml`
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
Declare `crewai` as a dependency so users get a compatible version automatically.
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
Re-export your tool classes so users can import them directly:
|
||||
|
||||
```python
|
||||
from crewai_geolocate.tools import GeolocateTool
|
||||
|
||||
__all__ = ["GeolocateTool"]
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Package name**: Use the prefix `crewai-` (e.g., `crewai-geolocate`). This makes your tool discoverable when users search PyPI.
|
||||
- **Module name**: Use underscores (e.g., `crewai_geolocate`).
|
||||
- **Tool class name**: Use PascalCase ending in `Tool` (e.g., `GeolocateTool`).
|
||||
|
||||
## Testing Your Tool
|
||||
|
||||
Before publishing, verify your tool works within a crew:
|
||||
|
||||
```python
|
||||
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)
|
||||
```
|
||||
|
||||
## Publishing to PyPI
|
||||
|
||||
Once your tool is tested and ready:
|
||||
|
||||
```bash
|
||||
# Build the package
|
||||
uv build
|
||||
|
||||
# Publish to PyPI
|
||||
uv publish
|
||||
```
|
||||
|
||||
If this is your first time publishing, you'll need a [PyPI account](https://pypi.org/account/register/) and an [API token](https://pypi.org/help/#apitoken).
|
||||
|
||||
### After Publishing
|
||||
|
||||
Users can install your tool with:
|
||||
|
||||
```bash
|
||||
pip install crewai-geolocate
|
||||
```
|
||||
|
||||
Or with uv:
|
||||
|
||||
```bash
|
||||
uv add crewai-geolocate
|
||||
```
|
||||
|
||||
Then use it in their crews:
|
||||
|
||||
```python
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
@@ -11,6 +11,10 @@ This guide provides detailed instructions on creating custom tools for the CrewA
|
||||
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.
|
||||
|
||||
<Tip>
|
||||
**Want to publish your tool for the community?** If you're building a tool that others could benefit from, check out the [Publish Custom Tools](/en/guides/tools/publish-custom-tools) guide to learn how to package and distribute your tool on PyPI.
|
||||
</Tip>
|
||||
|
||||
### 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.
|
||||
|
||||
61
docs/ko/guides/coding-tools/agents-md.mdx
Normal file
61
docs/ko/guides/coding-tools/agents-md.mdx
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: 코딩 도구
|
||||
description: AGENTS.md를 사용하여 CrewAI 프로젝트 전반에서 코딩 에이전트와 IDE를 안내합니다.
|
||||
icon: terminal
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## AGENTS.md를 사용하는 이유
|
||||
|
||||
`AGENTS.md`는 가벼운 저장소 로컬 지침 파일로, 코딩 에이전트에게 일관되고 프로젝트별 안내를 제공합니다. 프로젝트 루트에 배치하고 어시스턴트가 작업하는 방식(컨벤션, 명령어, 아키텍처 노트, 가드레일)에 대한 신뢰할 수 있는 소스로 활용하세요.
|
||||
|
||||
## CLI로 프로젝트 생성
|
||||
|
||||
CrewAI CLI를 사용하여 프로젝트를 스캐폴딩하면, `AGENTS.md`가 루트에 자동으로 추가됩니다.
|
||||
|
||||
```bash
|
||||
# Crew
|
||||
crewai create crew my_crew
|
||||
|
||||
# Flow
|
||||
crewai create flow my_flow
|
||||
|
||||
# Tool repository
|
||||
crewai tool create my_tool
|
||||
```
|
||||
|
||||
## 도구 설정: 어시스턴트에 AGENTS.md 연결
|
||||
|
||||
### Codex
|
||||
|
||||
Codex는 저장소에 배치된 `AGENTS.md` 파일로 안내할 수 있습니다. 컨벤션, 명령어, 워크플로우 기대치 등 지속적인 프로젝트 컨텍스트를 제공하는 데 사용하세요.
|
||||
|
||||
### Claude Code
|
||||
|
||||
Claude Code는 프로젝트 메모리를 `CLAUDE.md`에 저장합니다. `/init`으로 부트스트랩하고 `/memory`로 편집할 수 있습니다. Claude Code는 `CLAUDE.md` 내에서 임포트도 지원하므로, `@AGENTS.md`와 같은 한 줄을 추가하여 공유 지침을 중복 없이 가져올 수 있습니다.
|
||||
|
||||
간단하게 다음과 같이 사용할 수 있습니다:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md CLAUDE.md
|
||||
```
|
||||
|
||||
### Gemini CLI와 Google Antigravity
|
||||
|
||||
Gemini CLI와 Antigravity는 저장소 루트 및 상위 디렉토리에서 프로젝트 컨텍스트 파일(기본값: `GEMINI.md`)을 로드합니다. Gemini CLI 설정에서 `context.fileName`을 설정하여 `AGENTS.md`를 대신(또는 추가로) 읽도록 구성할 수 있습니다. 예를 들어, `AGENTS.md`만 설정하거나 각 도구의 형식을 유지하고 싶다면 `AGENTS.md`와 `GEMINI.md`를 모두 포함할 수 있습니다.
|
||||
|
||||
간단하게 다음과 같이 사용할 수 있습니다:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md GEMINI.md
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
Cursor는 `AGENTS.md`를 프로젝트 지침 파일로 지원합니다. 프로젝트 루트에 배치하여 Cursor의 코딩 어시스턴트에 안내를 제공하세요.
|
||||
|
||||
### Windsurf
|
||||
|
||||
Claude Code는 Windsurf와의 공식 통합을 제공합니다. Windsurf 내에서 Claude Code를 사용하는 경우, 위의 Claude Code 안내를 따르고 `CLAUDE.md`에서 `AGENTS.md`를 임포트하세요.
|
||||
|
||||
Windsurf의 네이티브 어시스턴트를 사용하는 경우, 프로젝트 규칙 또는 지침 기능(사용 가능한 경우)을 구성하여 `AGENTS.md`에서 읽거나 내용을 직접 붙여넣으세요.
|
||||
244
docs/ko/guides/tools/publish-custom-tools.mdx
Normal file
244
docs/ko/guides/tools/publish-custom-tools.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: 커스텀 도구 배포하기
|
||||
description: PyPI에 게시할 수 있는 CrewAI 호환 도구를 빌드, 패키징, 배포하는 방법을 안내합니다.
|
||||
icon: box-open
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
CrewAI의 도구 시스템은 확장 가능하도록 설계되었습니다. 다른 사용자에게도 유용한 도구를 만들었다면, 독립적인 Python 라이브러리로 패키징하여 PyPI에 게시하고 모든 CrewAI 사용자가 사용할 수 있도록 할 수 있습니다. CrewAI 저장소에 PR을 보낼 필요가 없습니다.
|
||||
|
||||
이 가이드에서는 도구 계약 구현, 패키지 구조화, PyPI 게시까지의 전체 과정을 안내합니다.
|
||||
|
||||
<Note type="info" title="배포할 계획이 없으신가요?">
|
||||
프로젝트 내에서만 사용할 커스텀 도구가 필요하다면 [커스텀 도구 생성](/ko/learn/create-custom-tools) 가이드를 참고하세요.
|
||||
</Note>
|
||||
|
||||
## 도구 계약
|
||||
|
||||
모든 CrewAI 도구는 다음 두 가지 인터페이스 중 하나를 충족해야 합니다:
|
||||
|
||||
### 옵션 1: `BaseTool` 서브클래싱
|
||||
|
||||
`crewai.tools.BaseTool`을 서브클래싱하고 `_run` 메서드를 구현합니다. `name`, `description`, 그리고 선택적으로 입력 검증을 위한 `args_schema`를 정의합니다.
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateInput(BaseModel):
|
||||
"""GeolocateTool의 입력 스키마."""
|
||||
address: str = Field(..., description="지오코딩할 도로명 주소.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
|
||||
args_schema: type[BaseModel] = GeolocateInput
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# 구현 로직
|
||||
return f"40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### 옵션 2: `@tool` 데코레이터 사용
|
||||
|
||||
간단한 도구의 경우, `@tool` 데코레이터로 함수를 CrewAI 도구로 변환할 수 있습니다. 함수에는 반드시 독스트링(도구 설명으로 사용됨)과 타입 어노테이션이 있어야 합니다.
|
||||
|
||||
```python
|
||||
from crewai.tools import tool
|
||||
|
||||
|
||||
@tool("Geolocate")
|
||||
def geolocate(address: str) -> str:
|
||||
"""도로명 주소를 위도/경도 좌표로 변환합니다."""
|
||||
return "40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### 핵심 요구사항
|
||||
|
||||
어떤 방식을 사용하든, 도구는 다음을 충족해야 합니다:
|
||||
|
||||
- **`name`** — 짧고 설명적인 식별자.
|
||||
- **`description`** — 에이전트에게 도구를 언제, 어떻게 사용할지 알려줍니다. 에이전트가 도구를 얼마나 잘 활용하는지에 직접적으로 영향을 미치므로 명확하고 구체적으로 작성하세요.
|
||||
- **`_run`** (BaseTool) 또는 **함수 본문** (@tool) 구현 — 동기 실행 로직.
|
||||
- 모든 매개변수와 반환 값에 **타입 어노테이션** 사용.
|
||||
- **문자열** 결과를 반환 (또는 의미 있게 문자열로 변환 가능한 값).
|
||||
|
||||
### 선택사항: 비동기 지원
|
||||
|
||||
I/O 바운드 작업을 수행하는 도구의 경우 비동기 실행을 위해 `_arun`을 구현합니다:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# 동기 구현
|
||||
...
|
||||
|
||||
async def _arun(self, address: str) -> str:
|
||||
# 비동기 구현
|
||||
...
|
||||
```
|
||||
|
||||
### 선택사항: `args_schema`를 통한 입력 검증
|
||||
|
||||
Pydantic 모델을 `args_schema`로 정의하면 자동 입력 검증과 명확한 에러 메시지를 받을 수 있습니다. 제공하지 않으면 CrewAI가 `_run` 메서드의 시그니처에서 추론합니다.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TranslateInput(BaseModel):
|
||||
"""TranslateTool의 입력 스키마."""
|
||||
text: str = Field(..., description="번역할 텍스트.")
|
||||
target_language: str = Field(
|
||||
default="en",
|
||||
description="대상 언어의 ISO 639-1 언어 코드.",
|
||||
)
|
||||
```
|
||||
|
||||
배포용 도구에는 명시적 스키마를 권장합니다 — 에이전트 동작이 개선되고 사용자에게 더 명확한 문서를 제공합니다.
|
||||
|
||||
### 선택사항: 환경 변수
|
||||
|
||||
도구에 API 키나 기타 설정이 필요한 경우, `env_vars`로 선언하여 사용자가 무엇을 설정해야 하는지 알 수 있도록 합니다:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "도로명 주소를 위도/경도 좌표로 변환합니다."
|
||||
env_vars: list[EnvVar] = [
|
||||
EnvVar(
|
||||
name="GEOCODING_API_KEY",
|
||||
description="지오코딩 서비스 API 키.",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
## 패키지 구조
|
||||
|
||||
프로젝트를 표준 Python 패키지로 구성합니다. 권장 레이아웃:
|
||||
|
||||
```
|
||||
crewai-geolocate/
|
||||
├── pyproject.toml
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── src/
|
||||
└── crewai_geolocate/
|
||||
├── __init__.py
|
||||
└── tools.py
|
||||
```
|
||||
|
||||
### `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "crewai-geolocate"
|
||||
version = "0.1.0"
|
||||
description = "도로명 주소를 지오코딩하는 CrewAI 도구."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"crewai",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
```
|
||||
|
||||
사용자가 자동으로 호환 버전을 받을 수 있도록 `crewai`를 의존성으로 선언합니다.
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
사용자가 직접 import할 수 있도록 도구 클래스를 re-export합니다:
|
||||
|
||||
```python
|
||||
from crewai_geolocate.tools import GeolocateTool
|
||||
|
||||
__all__ = ["GeolocateTool"]
|
||||
```
|
||||
|
||||
### 명명 규칙
|
||||
|
||||
- **패키지 이름**: `crewai-` 접두사를 사용합니다 (예: `crewai-geolocate`). PyPI에서 검색할 때 도구를 쉽게 찾을 수 있습니다.
|
||||
- **모듈 이름**: 밑줄을 사용합니다 (예: `crewai_geolocate`).
|
||||
- **도구 클래스 이름**: `Tool`로 끝나는 PascalCase를 사용합니다 (예: `GeolocateTool`).
|
||||
|
||||
## 도구 테스트
|
||||
|
||||
게시 전에 도구가 크루 내에서 작동하는지 확인합니다:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
goal="주어진 주소의 좌표를 찾습니다.",
|
||||
backstory="지리공간 데이터 전문가.",
|
||||
tools=[GeolocateTool()],
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="1600 Pennsylvania Avenue, Washington, DC의 좌표를 찾으세요.",
|
||||
expected_output="해당 주소의 위도와 경도.",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## PyPI에 게시하기
|
||||
|
||||
도구 테스트를 완료하고 준비가 되면:
|
||||
|
||||
```bash
|
||||
# 패키지 빌드
|
||||
uv build
|
||||
|
||||
# PyPI에 게시
|
||||
uv publish
|
||||
```
|
||||
|
||||
처음 게시하는 경우 [PyPI 계정](https://pypi.org/account/register/)과 [API 토큰](https://pypi.org/help/#apitoken)이 필요합니다.
|
||||
|
||||
### 게시 후
|
||||
|
||||
사용자는 다음과 같이 도구를 설치할 수 있습니다:
|
||||
|
||||
```bash
|
||||
pip install crewai-geolocate
|
||||
```
|
||||
|
||||
또는 uv를 사용하여:
|
||||
|
||||
```bash
|
||||
uv add crewai-geolocate
|
||||
```
|
||||
|
||||
그런 다음 크루에서 사용합니다:
|
||||
|
||||
```python
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Location Analyst",
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
@@ -9,6 +9,10 @@ mode: "wide"
|
||||
|
||||
이 가이드는 CrewAI 프레임워크를 위한 커스텀 툴을 생성하는 방법과 최신 기능(툴 위임, 오류 처리, 동적 툴 호출 등)을 통합하여 이러한 툴을 효율적으로 관리하고 활용하는 방법에 대해 자세히 안내합니다. 또한 협업 툴의 중요성을 강조하며, 에이전트가 다양한 작업을 수행할 수 있도록 지원합니다.
|
||||
|
||||
<Tip>
|
||||
**커뮤니티에 도구를 배포하고 싶으신가요?** 다른 사용자에게도 유용한 도구를 만들고 있다면, [커스텀 도구 배포하기](/ko/guides/tools/publish-custom-tools) 가이드에서 도구를 패키징하고 PyPI에 배포하는 방법을 알아보세요.
|
||||
</Tip>
|
||||
|
||||
### `BaseTool` 서브클래싱
|
||||
|
||||
개인화된 툴을 생성하려면 `BaseTool`을 상속받고, 입력 검증을 위한 `args_schema`와 `_run` 메서드를 포함한 필요한 속성들을 정의해야 합니다.
|
||||
|
||||
61
docs/pt-BR/guides/coding-tools/agents-md.mdx
Normal file
61
docs/pt-BR/guides/coding-tools/agents-md.mdx
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Ferramentas de Codificação
|
||||
description: Use o AGENTS.md para guiar agentes de codificação e IDEs em seus projetos CrewAI.
|
||||
icon: terminal
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Por que AGENTS.md
|
||||
|
||||
`AGENTS.md` é um arquivo de instruções leve e local do repositório que fornece aos agentes de codificação orientações consistentes e específicas do projeto. Mantenha-o na raiz do projeto e trate-o como a fonte da verdade para como você deseja que os assistentes trabalhem: convenções, comandos, notas de arquitetura e proteções.
|
||||
|
||||
## Criar um Projeto com o CLI
|
||||
|
||||
Use o CLI do CrewAI para criar a estrutura de um projeto, e o `AGENTS.md` será automaticamente adicionado na raiz.
|
||||
|
||||
```bash
|
||||
# Crew
|
||||
crewai create crew my_crew
|
||||
|
||||
# Flow
|
||||
crewai create flow my_flow
|
||||
|
||||
# Tool repository
|
||||
crewai tool create my_tool
|
||||
```
|
||||
|
||||
## Configuração de Ferramentas: Direcione Assistentes para o AGENTS.md
|
||||
|
||||
### Codex
|
||||
|
||||
O Codex pode ser guiado por arquivos `AGENTS.md` colocados no seu repositório. Use-os para fornecer contexto persistente do projeto, como convenções, comandos e expectativas de fluxo de trabalho.
|
||||
|
||||
### Claude Code
|
||||
|
||||
O Claude Code armazena a memória do projeto em `CLAUDE.md`. Você pode inicializá-lo com `/init` e editá-lo usando `/memory`. O Claude Code também suporta importações dentro do `CLAUDE.md`, então você pode adicionar uma única linha como `@AGENTS.md` para incluir as instruções compartilhadas sem duplicá-las.
|
||||
|
||||
Você pode simplesmente usar:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md CLAUDE.md
|
||||
```
|
||||
|
||||
### Gemini CLI e Google Antigravity
|
||||
|
||||
O Gemini CLI e o Antigravity carregam um arquivo de contexto do projeto (padrão: `GEMINI.md`) da raiz do repositório e diretórios pais. Você pode configurá-lo para ler o `AGENTS.md` em vez disso (ou além) definindo `context.fileName` nas configurações do Gemini CLI. Por exemplo, defina apenas para `AGENTS.md`, ou inclua tanto `AGENTS.md` quanto `GEMINI.md` se quiser manter o formato de cada ferramenta.
|
||||
|
||||
Você pode simplesmente usar:
|
||||
|
||||
```bash
|
||||
mv AGENTS.md GEMINI.md
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
O Cursor suporta `AGENTS.md` como arquivo de instruções do projeto. Coloque-o na raiz do projeto para fornecer orientação ao assistente de codificação do Cursor.
|
||||
|
||||
### Windsurf
|
||||
|
||||
O Claude Code fornece uma integração oficial com o Windsurf. Se você usa o Claude Code dentro do Windsurf, siga a orientação do Claude Code acima e importe o `AGENTS.md` a partir do `CLAUDE.md`.
|
||||
|
||||
Se você está usando o assistente nativo do Windsurf, configure o recurso de regras ou instruções do projeto (se disponível) para ler o `AGENTS.md` ou cole o conteúdo diretamente.
|
||||
244
docs/pt-BR/guides/tools/publish-custom-tools.mdx
Normal file
244
docs/pt-BR/guides/tools/publish-custom-tools.mdx
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: Publicar Ferramentas Personalizadas
|
||||
description: Como construir, empacotar e publicar suas próprias ferramentas compatíveis com CrewAI no PyPI para que qualquer usuário do CrewAI possa instalá-las e usá-las.
|
||||
icon: box-open
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O sistema de ferramentas do CrewAI foi projetado para ser extensível. Se você construiu uma ferramenta que pode beneficiar outros, pode empacotá-la como uma biblioteca Python independente, publicá-la no PyPI e disponibilizá-la para qualquer usuário do CrewAI — sem necessidade de PR para o repositório do CrewAI.
|
||||
|
||||
Este guia percorre todo o processo: implementação do contrato de ferramentas, estruturação do pacote e publicação no PyPI.
|
||||
|
||||
<Note type="info" title="Não pretende publicar?">
|
||||
Se você precisa apenas de uma ferramenta personalizada para seu próprio projeto, consulte o guia [Criar Ferramentas Personalizadas](/pt-BR/learn/create-custom-tools).
|
||||
</Note>
|
||||
|
||||
## O Contrato de Ferramentas
|
||||
|
||||
Toda ferramenta CrewAI deve satisfazer uma das duas interfaces:
|
||||
|
||||
### Opção 1: Subclassificar `BaseTool`
|
||||
|
||||
Subclassifique `crewai.tools.BaseTool` e implemente o método `_run`. Defina `name`, `description` e, opcionalmente, um `args_schema` para validação de entrada.
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateInput(BaseModel):
|
||||
"""Esquema de entrada para GeolocateTool."""
|
||||
address: str = Field(..., description="O endereço para geolocalizar.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converte um endereço em coordenadas de latitude/longitude."
|
||||
args_schema: type[BaseModel] = GeolocateInput
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# Sua implementação aqui
|
||||
return f"40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### Opção 2: Usar o Decorador `@tool`
|
||||
|
||||
Para ferramentas mais simples, o decorador `@tool` transforma uma função em uma ferramenta CrewAI. A função **deve** ter uma docstring (usada como descrição da ferramenta) e anotações de tipo.
|
||||
|
||||
```python
|
||||
from crewai.tools import tool
|
||||
|
||||
|
||||
@tool("Geolocate")
|
||||
def geolocate(address: str) -> str:
|
||||
"""Converte um endereço em coordenadas de latitude/longitude."""
|
||||
return "40.7128, -74.0060"
|
||||
```
|
||||
|
||||
### Requisitos Essenciais
|
||||
|
||||
Independentemente da abordagem escolhida, sua ferramenta deve:
|
||||
|
||||
- Ter um **`name`** — um identificador curto e descritivo.
|
||||
- Ter uma **`description`** — informa ao agente quando e como usar a ferramenta. Isso afeta diretamente a qualidade do uso da ferramenta pelo agente, então seja claro e específico.
|
||||
- Implementar **`_run`** (BaseTool) ou fornecer um **corpo de função** (@tool) — a lógica de execução síncrona.
|
||||
- Usar **anotações de tipo** em todos os parâmetros e valores de retorno.
|
||||
- Retornar um resultado em **string** (ou algo que possa ser convertido de forma significativa).
|
||||
|
||||
### Opcional: Suporte Assíncrono
|
||||
|
||||
Se sua ferramenta realiza operações de I/O, implemente `_arun` para execução assíncrona:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converte um endereço em coordenadas de latitude/longitude."
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
# Implementação síncrona
|
||||
...
|
||||
|
||||
async def _arun(self, address: str) -> str:
|
||||
# Implementação assíncrona
|
||||
...
|
||||
```
|
||||
|
||||
### Opcional: Validação de Entrada com `args_schema`
|
||||
|
||||
Defina um modelo Pydantic como seu `args_schema` para obter validação automática de entrada e mensagens de erro claras. Se não fornecer um, o CrewAI irá inferi-lo da assinatura do seu método `_run`.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TranslateInput(BaseModel):
|
||||
"""Esquema de entrada para TranslateTool."""
|
||||
text: str = Field(..., description="O texto a ser traduzido.")
|
||||
target_language: str = Field(
|
||||
default="en",
|
||||
description="Código de idioma ISO 639-1 para o idioma de destino.",
|
||||
)
|
||||
```
|
||||
|
||||
Esquemas explícitos são recomendados para ferramentas publicadas — produzem melhor comportamento do agente e documentação mais clara para seus usuários.
|
||||
|
||||
### Opcional: Variáveis de Ambiente
|
||||
|
||||
Se sua ferramenta requer chaves de API ou outra configuração, declare-as com `env_vars` para que os usuários saibam o que configurar:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converte um endereço em coordenadas de latitude/longitude."
|
||||
env_vars: list[EnvVar] = [
|
||||
EnvVar(
|
||||
name="GEOCODING_API_KEY",
|
||||
description="Chave de API para o serviço de geocodificação.",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _run(self, address: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
## Estrutura do Pacote
|
||||
|
||||
Estruture seu projeto como um pacote Python padrão. Layout recomendado:
|
||||
|
||||
```
|
||||
crewai-geolocate/
|
||||
├── pyproject.toml
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── src/
|
||||
└── crewai_geolocate/
|
||||
├── __init__.py
|
||||
└── tools.py
|
||||
```
|
||||
|
||||
### `pyproject.toml`
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "crewai-geolocate"
|
||||
version = "0.1.0"
|
||||
description = "Uma ferramenta CrewAI para geolocalizar endereços."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"crewai",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
```
|
||||
|
||||
Declare `crewai` como dependência para que os usuários obtenham automaticamente uma versão compatível.
|
||||
|
||||
### `__init__.py`
|
||||
|
||||
Re-exporte suas classes de ferramenta para que os usuários possam importá-las diretamente:
|
||||
|
||||
```python
|
||||
from crewai_geolocate.tools import GeolocateTool
|
||||
|
||||
__all__ = ["GeolocateTool"]
|
||||
```
|
||||
|
||||
### Convenções de Nomenclatura
|
||||
|
||||
- **Nome do pacote**: Use o prefixo `crewai-` (ex.: `crewai-geolocate`). Isso torna sua ferramenta fácil de encontrar no PyPI.
|
||||
- **Nome do módulo**: Use underscores (ex.: `crewai_geolocate`).
|
||||
- **Nome da classe da ferramenta**: Use PascalCase terminando em `Tool` (ex.: `GeolocateTool`).
|
||||
|
||||
## Testando sua Ferramenta
|
||||
|
||||
Antes de publicar, verifique se sua ferramenta funciona dentro de uma crew:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Analista de Localização",
|
||||
goal="Encontrar coordenadas para os endereços fornecidos.",
|
||||
backstory="Um especialista em dados geoespaciais.",
|
||||
tools=[GeolocateTool()],
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Encontre as coordenadas de 1600 Pennsylvania Avenue, Washington, DC.",
|
||||
expected_output="A latitude e longitude do endereço.",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Publicando no PyPI
|
||||
|
||||
Quando sua ferramenta estiver testada e pronta:
|
||||
|
||||
```bash
|
||||
# Construir o pacote
|
||||
uv build
|
||||
|
||||
# Publicar no PyPI
|
||||
uv publish
|
||||
```
|
||||
|
||||
Se é sua primeira vez publicando, você precisará de uma [conta no PyPI](https://pypi.org/account/register/) e um [token de API](https://pypi.org/help/#apitoken).
|
||||
|
||||
### Após a Publicação
|
||||
|
||||
Os usuários podem instalar sua ferramenta com:
|
||||
|
||||
```bash
|
||||
pip install crewai-geolocate
|
||||
```
|
||||
|
||||
Ou com uv:
|
||||
|
||||
```bash
|
||||
uv add crewai-geolocate
|
||||
```
|
||||
|
||||
E então usá-la em suas crews:
|
||||
|
||||
```python
|
||||
from crewai_geolocate import GeolocateTool
|
||||
|
||||
agent = Agent(
|
||||
role="Analista de Localização",
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
@@ -11,6 +11,10 @@ Este guia traz instruções detalhadas sobre como criar ferramentas personalizad
|
||||
incorporando funcionalidades recentes, como delegação de ferramentas, tratamento de erros e chamada dinâmica de ferramentas. Destaca também a importância de ferramentas de colaboração,
|
||||
permitindo que agentes executem uma ampla gama de ações.
|
||||
|
||||
<Tip>
|
||||
**Quer publicar sua ferramenta para a comunidade?** Se você está construindo uma ferramenta que pode beneficiar outros, confira o guia [Publicar Ferramentas Personalizadas](/pt-BR/guides/tools/publish-custom-tools) para aprender como empacotar e distribuir sua ferramenta no PyPI.
|
||||
</Tip>
|
||||
|
||||
### Subclassificando `BaseTool`
|
||||
|
||||
Para criar uma ferramenta personalizada, herde de `BaseTool` e defina os atributos necessários, incluindo o `args_schema` para validação de entrada e o método `_run`.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
@@ -30,27 +31,39 @@ class FileWriterTool(BaseTool):
|
||||
|
||||
def _run(self, **kwargs: Any) -> str:
|
||||
try:
|
||||
directory = kwargs.get("directory") or "./"
|
||||
filename = kwargs["filename"]
|
||||
|
||||
filepath = os.path.join(directory, filename)
|
||||
|
||||
# Prevent path traversal: the resolved path must be strictly inside
|
||||
# the resolved directory. This blocks ../sequences, absolute paths in
|
||||
# filename, and symlink escapes regardless of how directory is set.
|
||||
# is_relative_to() does a proper path-component comparison that is
|
||||
# safe on case-insensitive filesystems and avoids the "// " edge case
|
||||
# that plagues startswith(real_directory + os.sep).
|
||||
# We also reject the case where filepath resolves to the directory
|
||||
# itself, since that is not a valid file target.
|
||||
real_directory = Path(directory).resolve()
|
||||
real_filepath = Path(filepath).resolve()
|
||||
if not real_filepath.is_relative_to(real_directory) or real_filepath == real_directory:
|
||||
return "Error: Invalid file path — the filename must not escape the target directory."
|
||||
|
||||
if kwargs.get("directory"):
|
||||
os.makedirs(kwargs["directory"], exist_ok=True)
|
||||
os.makedirs(real_directory, exist_ok=True)
|
||||
|
||||
# Construct the full path
|
||||
filepath = os.path.join(kwargs.get("directory") or "", kwargs["filename"])
|
||||
|
||||
# Convert overwrite to boolean
|
||||
kwargs["overwrite"] = strtobool(kwargs["overwrite"])
|
||||
|
||||
# Check if file exists and overwrite is not allowed
|
||||
if os.path.exists(filepath) and not kwargs["overwrite"]:
|
||||
return f"File {filepath} already exists and overwrite option was not passed."
|
||||
if os.path.exists(real_filepath) and not kwargs["overwrite"]:
|
||||
return f"File {real_filepath} already exists and overwrite option was not passed."
|
||||
|
||||
# Write content to the file
|
||||
mode = "w" if kwargs["overwrite"] else "x"
|
||||
with open(filepath, mode) as file:
|
||||
with open(real_filepath, mode) as file:
|
||||
file.write(kwargs["content"])
|
||||
return f"Content successfully written to {filepath}"
|
||||
return f"Content successfully written to {real_filepath}"
|
||||
except FileExistsError:
|
||||
return (
|
||||
f"File {filepath} already exists and overwrite option was not passed."
|
||||
f"File {real_filepath} already exists and overwrite option was not passed."
|
||||
)
|
||||
except KeyError as e:
|
||||
return f"An error occurred while accessing key: {e!s}"
|
||||
|
||||
@@ -135,3 +135,59 @@ def test_file_exists_error_handling(tool, temp_env, overwrite):
|
||||
|
||||
assert "already exists and overwrite option was not passed" in result
|
||||
assert read_file(path) == "Pre-existing content"
|
||||
|
||||
|
||||
# --- Path traversal prevention ---
|
||||
|
||||
def test_blocks_traversal_in_filename(tool, temp_env):
|
||||
# Create a sibling "outside" directory so we can assert nothing was written there.
|
||||
outside_dir = tempfile.mkdtemp()
|
||||
outside_file = os.path.join(outside_dir, "outside.txt")
|
||||
try:
|
||||
result = tool._run(
|
||||
filename=f"../{os.path.basename(outside_dir)}/outside.txt",
|
||||
directory=temp_env["temp_dir"],
|
||||
content="should not be written",
|
||||
overwrite=True,
|
||||
)
|
||||
assert "Error" in result
|
||||
assert not os.path.exists(outside_file)
|
||||
finally:
|
||||
shutil.rmtree(outside_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_blocks_absolute_path_in_filename(tool, temp_env):
|
||||
# Use a temp file outside temp_dir as the absolute target so we don't
|
||||
# depend on /etc/passwd existing or being writable on the host.
|
||||
outside_dir = tempfile.mkdtemp()
|
||||
outside_file = os.path.join(outside_dir, "target.txt")
|
||||
try:
|
||||
result = tool._run(
|
||||
filename=outside_file,
|
||||
directory=temp_env["temp_dir"],
|
||||
content="should not be written",
|
||||
overwrite=True,
|
||||
)
|
||||
assert "Error" in result
|
||||
assert not os.path.exists(outside_file)
|
||||
finally:
|
||||
shutil.rmtree(outside_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_blocks_symlink_escape(tool, temp_env):
|
||||
# Symlink inside temp_dir pointing to a separate temp "outside" directory.
|
||||
outside_dir = tempfile.mkdtemp()
|
||||
outside_file = os.path.join(outside_dir, "target.txt")
|
||||
link = os.path.join(temp_env["temp_dir"], "escape")
|
||||
os.symlink(outside_dir, link)
|
||||
try:
|
||||
result = tool._run(
|
||||
filename="escape/target.txt",
|
||||
directory=temp_env["temp_dir"],
|
||||
content="should not be written",
|
||||
overwrite=True,
|
||||
)
|
||||
assert "Error" in result
|
||||
assert not os.path.exists(outside_file)
|
||||
finally:
|
||||
shutil.rmtree(outside_dir, ignore_errors=True)
|
||||
|
||||
@@ -75,6 +75,7 @@ from crewai.utilities.agent_utils import (
|
||||
)
|
||||
from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE
|
||||
from crewai.utilities.converter import Converter, ConverterError
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.guardrail import process_guardrail
|
||||
from crewai.utilities.guardrail_types import GuardrailType
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
@@ -364,6 +365,7 @@ class Agent(BaseAgent):
|
||||
ValueError: If the max execution time is not a positive integer.
|
||||
RuntimeError: If the agent execution fails for other reasons.
|
||||
"""
|
||||
get_env_context()
|
||||
# Only call handle_reasoning for legacy CrewAgentExecutor
|
||||
# For AgentExecutor, planning is handled in AgentExecutor.generate_plan()
|
||||
if self.executor_class is not AgentExecutor:
|
||||
|
||||
@@ -98,6 +98,7 @@ from crewai.types.streaming import CrewStreamingOutput
|
||||
from crewai.types.usage_metrics import UsageMetrics
|
||||
from crewai.utilities.constants import NOT_SPECIFIED, TRAINING_DATA_FILE
|
||||
from crewai.utilities.crew.models import CrewContext
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.evaluators.crew_evaluator_handler import CrewEvaluator
|
||||
from crewai.utilities.evaluators.task_evaluator import TaskEvaluator
|
||||
from crewai.utilities.file_handler import FileHandler
|
||||
@@ -679,6 +680,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
Returns:
|
||||
CrewOutput or CrewStreamingOutput if streaming is enabled.
|
||||
"""
|
||||
get_env_context()
|
||||
if self.stream:
|
||||
enable_agent_streaming(self.agents)
|
||||
ctx = StreamingContext()
|
||||
|
||||
@@ -34,6 +34,12 @@ from crewai.events.types.crew_events import (
|
||||
CrewTrainFailedEvent,
|
||||
CrewTrainStartedEvent,
|
||||
)
|
||||
from crewai.events.types.env_events import (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowFinishedEvent,
|
||||
@@ -143,6 +149,23 @@ class EventListener(BaseEventListener):
|
||||
# ----------- CREW EVENTS -----------
|
||||
|
||||
def setup_listeners(self, crewai_event_bus: CrewAIEventsBus) -> None:
|
||||
|
||||
@crewai_event_bus.on(CCEnvEvent)
|
||||
def on_cc_env(_: Any, event: CCEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(CodexEnvEvent)
|
||||
def on_codex_env(_: Any, event: CodexEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(CursorEnvEvent)
|
||||
def on_cursor_env(_: Any, event: CursorEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(DefaultEnvEvent)
|
||||
def on_default_env(_: Any, event: DefaultEnvEvent) -> None:
|
||||
self._telemetry.env_context_span(event.type)
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
self.formatter.handle_crew_started(event.crew_name or "Crew", source.id)
|
||||
|
||||
@@ -58,6 +58,12 @@ from crewai.events.types.crew_events import (
|
||||
CrewKickoffFailedEvent,
|
||||
CrewKickoffStartedEvent,
|
||||
)
|
||||
from crewai.events.types.env_events import (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.events.types.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowFinishedEvent,
|
||||
@@ -192,6 +198,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
if self._listeners_setup:
|
||||
return
|
||||
|
||||
self._register_env_event_handlers(crewai_event_bus)
|
||||
self._register_flow_event_handlers(crewai_event_bus)
|
||||
self._register_context_event_handlers(crewai_event_bus)
|
||||
self._register_action_event_handlers(crewai_event_bus)
|
||||
@@ -200,6 +207,25 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
self._listeners_setup = True
|
||||
|
||||
def _register_env_event_handlers(self, event_bus: CrewAIEventsBus) -> None:
|
||||
"""Register handlers for environment context events."""
|
||||
|
||||
@event_bus.on(CCEnvEvent)
|
||||
def on_cc_env(source: Any, event: CCEnvEvent) -> None:
|
||||
self._handle_action_event("cc_env", source, event)
|
||||
|
||||
@event_bus.on(CodexEnvEvent)
|
||||
def on_codex_env(source: Any, event: CodexEnvEvent) -> None:
|
||||
self._handle_action_event("codex_env", source, event)
|
||||
|
||||
@event_bus.on(CursorEnvEvent)
|
||||
def on_cursor_env(source: Any, event: CursorEnvEvent) -> None:
|
||||
self._handle_action_event("cursor_env", source, event)
|
||||
|
||||
@event_bus.on(DefaultEnvEvent)
|
||||
def on_default_env(source: Any, event: DefaultEnvEvent) -> None:
|
||||
self._handle_action_event("default_env", source, event)
|
||||
|
||||
def _register_flow_event_handlers(self, event_bus: CrewAIEventsBus) -> None:
|
||||
"""Register handlers for flow events."""
|
||||
|
||||
|
||||
36
lib/crewai/src/crewai/events/types/env_events.py
Normal file
36
lib/crewai/src/crewai/events/types/env_events.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import Field, TypeAdapter
|
||||
|
||||
from crewai.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class CCEnvEvent(BaseEvent):
|
||||
type: Literal["cc_env"] = "cc_env"
|
||||
|
||||
|
||||
class CodexEnvEvent(BaseEvent):
|
||||
type: Literal["codex_env"] = "codex_env"
|
||||
|
||||
|
||||
class CursorEnvEvent(BaseEvent):
|
||||
type: Literal["cursor_env"] = "cursor_env"
|
||||
|
||||
|
||||
class DefaultEnvEvent(BaseEvent):
|
||||
type: Literal["default_env"] = "default_env"
|
||||
|
||||
|
||||
EnvContextEvent = Annotated[
|
||||
CCEnvEvent | CodexEnvEvent | CursorEnvEvent | DefaultEnvEvent,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
env_context_event_adapter: TypeAdapter[EnvContextEvent] = TypeAdapter(EnvContextEvent)
|
||||
|
||||
ENV_CONTEXT_EVENT_TYPES: tuple[type[BaseEvent], ...] = (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
@@ -110,6 +110,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from crewai.flow.visualization import build_flow_structure, render_interactive
|
||||
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
|
||||
from crewai.utilities.env import get_env_context
|
||||
from crewai.utilities.streaming import (
|
||||
TaskInfo,
|
||||
create_async_chunk_generator,
|
||||
@@ -1770,6 +1771,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
Returns:
|
||||
The final output from the flow or FlowStreamingOutput if streaming.
|
||||
"""
|
||||
get_env_context()
|
||||
if self.stream:
|
||||
result_holder: list[Any] = []
|
||||
current_task_info: TaskInfo = {
|
||||
|
||||
@@ -986,6 +986,22 @@ class Telemetry:
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def env_context_span(self, tool: str) -> None:
|
||||
"""Records the coding tool environment context."""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Environment Context")
|
||||
self._add_attribute(
|
||||
span,
|
||||
"crewai_version",
|
||||
version("crewai"),
|
||||
)
|
||||
self._add_attribute(span, "tool", tool)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def human_feedback_span(
|
||||
self,
|
||||
event_type: str,
|
||||
|
||||
@@ -8,6 +8,21 @@ TRAINED_AGENTS_DATA_FILE: Final[str] = "trained_agents_data.pkl"
|
||||
KNOWLEDGE_DIRECTORY: Final[str] = "knowledge"
|
||||
MAX_FILE_NAME_LENGTH: Final[int] = 255
|
||||
EMITTER_COLOR: Final[PrinterColor] = "bold_blue"
|
||||
CC_ENV_VAR: Final[str] = "CLAUDECODE"
|
||||
CODEX_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"CODEX_CI",
|
||||
"CODEX_MANAGED_BY_NPM",
|
||||
"CODEX_SANDBOX",
|
||||
"CODEX_SANDBOX_NETWORK_DISABLED",
|
||||
"CODEX_THREAD_ID",
|
||||
)
|
||||
CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
|
||||
"CURSOR_AGENT",
|
||||
"CURSOR_EXTENSION_HOST_ROLE",
|
||||
"CURSOR_SANDBOX",
|
||||
"CURSOR_TRACE_ID",
|
||||
"CURSOR_WORKSPACE_LABEL",
|
||||
)
|
||||
|
||||
|
||||
class _NotSpecified:
|
||||
|
||||
39
lib/crewai/src/crewai/utilities/env.py
Normal file
39
lib/crewai/src/crewai/utilities/env.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import contextvars
|
||||
import os
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.env_events import (
|
||||
CCEnvEvent,
|
||||
CodexEnvEvent,
|
||||
CursorEnvEvent,
|
||||
DefaultEnvEvent,
|
||||
)
|
||||
from crewai.utilities.constants import CC_ENV_VAR, CODEX_ENV_VARS, CURSOR_ENV_VARS
|
||||
|
||||
|
||||
_env_context_emitted: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||
"_env_context_emitted", default=False
|
||||
)
|
||||
|
||||
|
||||
def _is_codex_env() -> bool:
|
||||
return any(os.environ.get(var) for var in CODEX_ENV_VARS)
|
||||
|
||||
|
||||
def _is_cursor_env() -> bool:
|
||||
return any(os.environ.get(var) for var in CURSOR_ENV_VARS)
|
||||
|
||||
|
||||
def get_env_context() -> None:
|
||||
if _env_context_emitted.get():
|
||||
return
|
||||
_env_context_emitted.set(True)
|
||||
|
||||
if os.environ.get(CC_ENV_VAR):
|
||||
crewai_event_bus.emit(None, CCEnvEvent())
|
||||
elif _is_codex_env():
|
||||
crewai_event_bus.emit(None, CodexEnvEvent())
|
||||
elif _is_cursor_env():
|
||||
crewai_event_bus.emit(None, CursorEnvEvent())
|
||||
else:
|
||||
crewai_event_bus.emit(None, DefaultEnvEvent())
|
||||
Reference in New Issue
Block a user