Compare commits

...

5 Commits

Author SHA1 Message Date
Lucas Gomide
b859a50de7 docs: adding a lot of missinge vent listeners 2026-03-20 15:27:28 -03:00
Greyson LaLonde
f7de8b2d28 fix(devtools): consolidate prerelease changelogs into stable releases
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
2026-03-19 17:16:18 -04:00
Greyson LaLonde
8886f11672 docs: add publish custom tools guide with translations
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
2026-03-19 11:15:56 -04:00
Rip&Tear
713fa7d01b fix: prevent path traversal in FileWriterTool (#4895)
* fix: add base_dir path containment to FileWriterTool

os.path.join does not prevent traversal — joining "./" with "../../../etc/cron.d/pwned"
resolves cleanly outside any intended scope. The tool also called os.makedirs on
the unvalidated path, meaning it would create arbitrary directory structures.

Adds a base_dir parameter that uses os.path.realpath() to resolve the final path
(including symlinks) before checking containment. Any filename or directory argument
that resolves outside base_dir is rejected before any filesystem operation occurs.

When base_dir is not set the tool behaves as before — only use that in fully
sandboxed environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: make directory relative to base_dir for better UX

When base_dir is set, the directory arg is now treated as a subdirectory
of base_dir rather than an absolute path. This means the LLM only needs
to specify a filename (and optionally a relative subdirectory) — it does
not need to repeat the base_dir path.

  FileWriterTool(base_dir="./output")
  → filename="report.txt"            writes to ./output/report.txt
  → filename="f.txt", directory="sub" writes to ./output/sub/f.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove directory field from LLM schema when base_dir is set

When a developer sets base_dir, they control where files are written.
The LLM should only supply filename and content — not a directory path.

Adds ScopedFileWriterToolInput (no directory field) which is used when
base_dir is provided at construction, following the same pattern as
FileReadTool/ScrapeWebsiteTool.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent path traversal in FileWriterTool without interface changes

Adds containment check inside _run() using os.path.realpath() to ensure
the resolved file path stays within the resolved directory. Blocks ../
sequences, absolute filenames, and symlink escapes transparently —
no schema or interface changes required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use Path.is_relative_to() for path containment check

Replaces startswith(real_directory + os.sep) with Path.is_relative_to(),
which does a proper path-component comparison. This avoids the edge case
where real_directory == "/" produces a "//" prefix, and is safe on
case-insensitive filesystems. Also explicitly rejects the case where
the filepath resolves to the directory itself (not a valid file target).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: fix portability issues in path traversal tests

- test_blocks_traversal_in_filename: use a sibling temp dir instead of
  asserting against a potentially pre-existing ../outside.txt
- test_blocks_absolute_path_in_filename: use a temp-dir-derived absolute
  path instead of hardcoding /etc/passwd
- test_blocks_symlink_escape: symlink to a temp "outside" dir instead of
  /etc, assert target file was not created

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
2026-03-19 20:11:45 +08:00
Greyson LaLonde
929d756ae2 chore: add coding tool environment detection via telemetry events 2026-03-19 07:34:11 -04:00
24 changed files with 1564 additions and 29 deletions

View File

@@ -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",

View File

@@ -196,12 +196,19 @@ CrewAI provides a wide range of events that you can listen for:
- **CrewTrainStartedEvent**: Emitted when a Crew starts training
- **CrewTrainCompletedEvent**: Emitted when a Crew completes training
- **CrewTrainFailedEvent**: Emitted when a Crew fails to complete training
- **CrewTestResultEvent**: Emitted when a Crew test result is available. Contains the quality score, execution duration, and model used.
### Agent Events
- **AgentExecutionStartedEvent**: Emitted when an Agent starts executing a task
- **AgentExecutionCompletedEvent**: Emitted when an Agent completes executing a task
- **AgentExecutionErrorEvent**: Emitted when an Agent encounters an error during execution
- **LiteAgentExecutionStartedEvent**: Emitted when a LiteAgent starts executing. Contains the agent info, tools, and messages.
- **LiteAgentExecutionCompletedEvent**: Emitted when a LiteAgent completes execution. Contains the agent info and output.
- **LiteAgentExecutionErrorEvent**: Emitted when a LiteAgent encounters an error during execution. Contains the agent info and error message.
- **AgentEvaluationStartedEvent**: Emitted when an agent evaluation starts. Contains the agent ID, agent role, optional task ID, and iteration number.
- **AgentEvaluationCompletedEvent**: Emitted when an agent evaluation completes. Contains the agent ID, agent role, optional task ID, iteration number, metric category, and score.
- **AgentEvaluationFailedEvent**: Emitted when an agent evaluation fails. Contains the agent ID, agent role, optional task ID, iteration number, and error message.
### Task Events
@@ -242,16 +249,26 @@ CrewAI provides a wide range of events that you can listen for:
- **LLMGuardrailStartedEvent**: Emitted when a guardrail validation starts. Contains details about the guardrail being applied and retry count.
- **LLMGuardrailCompletedEvent**: Emitted when a guardrail validation completes. Contains details about validation success/failure, results, and error messages if any.
- **LLMGuardrailFailedEvent**: Emitted when a guardrail validation fails. Contains the error message and retry count.
### Flow Events
- **FlowCreatedEvent**: Emitted when a Flow is created
- **FlowStartedEvent**: Emitted when a Flow starts execution
- **FlowFinishedEvent**: Emitted when a Flow completes execution
- **FlowPausedEvent**: Emitted when a Flow is paused waiting for human feedback. Contains the flow name, flow ID, method name, current state, message shown when requesting feedback, and optional list of possible outcomes for routing.
- **FlowPlotEvent**: Emitted when a Flow is plotted
- **MethodExecutionStartedEvent**: Emitted when a Flow method starts execution
- **MethodExecutionFinishedEvent**: Emitted when a Flow method completes execution
- **MethodExecutionFailedEvent**: Emitted when a Flow method fails to complete execution
- **MethodExecutionPausedEvent**: Emitted when a Flow method is paused waiting for human feedback. Contains the flow name, method name, current state, flow ID, message shown when requesting feedback, and optional list of possible outcomes for routing.
### Human In The Loop Events
- **FlowInputRequestedEvent**: Emitted when a Flow requests user input via `Flow.ask()`. Contains the flow name, method name, the question or prompt being shown to the user, and optional metadata (e.g., user ID, channel, session context).
- **FlowInputReceivedEvent**: Emitted when user input is received after `Flow.ask()`. Contains the flow name, method name, the original question, the user's response (or `None` if timed out), optional request metadata, and optional response metadata from the provider (e.g., who responded, thread ID, timestamps).
- **HumanFeedbackRequestedEvent**: Emitted when a `@human_feedback` decorated method requires input from a human reviewer. Contains the flow name, method name, the method output shown to the human for review, the message displayed when requesting feedback, and optional list of possible outcomes for routing.
- **HumanFeedbackReceivedEvent**: Emitted when a human provides feedback in response to a `@human_feedback` decorated method. Contains the flow name, method name, the raw text feedback provided by the human, and the collapsed outcome string (if emit was specified).
### LLM Events
@@ -259,6 +276,7 @@ CrewAI provides a wide range of events that you can listen for:
- **LLMCallCompletedEvent**: Emitted when an LLM call completes
- **LLMCallFailedEvent**: Emitted when an LLM call fails
- **LLMStreamChunkEvent**: Emitted for each chunk received during streaming LLM responses
- **LLMThinkingChunkEvent**: Emitted when a thinking/reasoning chunk is received from a thinking model. Contains the chunk text and optional response ID.
### Memory Events
@@ -270,6 +288,79 @@ CrewAI provides a wide range of events that you can listen for:
- **MemorySaveFailedEvent**: Emitted when a memory save operation fails. Contains the value, metadata, agent role, and error message.
- **MemoryRetrievalStartedEvent**: Emitted when memory retrieval for a task prompt starts. Contains the optional task ID.
- **MemoryRetrievalCompletedEvent**: Emitted when memory retrieval for a task prompt completes successfully. Contains the task ID, memory content, and retrieval execution time.
- **MemoryRetrievalFailedEvent**: Emitted when memory retrieval for a task prompt fails. Contains the optional task ID and error message.
### Reasoning Events
- **AgentReasoningStartedEvent**: Emitted when an agent starts reasoning about a task. Contains the agent role, task ID, and attempt number.
- **AgentReasoningCompletedEvent**: Emitted when an agent finishes its reasoning process. Contains the agent role, task ID, the plan produced, and whether the agent is ready to proceed.
- **AgentReasoningFailedEvent**: Emitted when the reasoning process fails. Contains the agent role, task ID, and error message.
### Observation Events
- **StepObservationStartedEvent**: Emitted when the Planner begins observing a step's result. Fires after every step execution, before the observation LLM call. Contains the agent role, step number, and step description.
- **StepObservationCompletedEvent**: Emitted when the Planner finishes observing a step's result. Contains whether the step completed successfully, key information learned, whether the remaining plan is still valid, whether a full replan is needed, and suggested refinements.
- **StepObservationFailedEvent**: Emitted when the observation LLM call itself fails. The system defaults to continuing the plan. Contains the error message.
- **PlanRefinementEvent**: Emitted when the Planner refines upcoming step descriptions without a full replan. Contains the number of refined steps and the refinements applied.
- **PlanReplanTriggeredEvent**: Emitted when the Planner triggers a full replan because the remaining plan was deemed fundamentally wrong. Contains the replan reason, replan count, and number of completed steps preserved.
- **GoalAchievedEarlyEvent**: Emitted when the Planner detects the goal was achieved early and remaining steps will be skipped. Contains the number of steps remaining and steps completed.
### A2A (Agent-to-Agent) Events
#### Delegation Events
- **A2ADelegationStartedEvent**: Emitted when A2A delegation starts. Contains the endpoint URL, task description, agent ID, context ID, whether it's multiturn, turn number, agent card metadata, protocol version, provider info, and optional skill ID.
- **A2ADelegationCompletedEvent**: Emitted when A2A delegation completes. Contains the completion status (`completed`, `input_required`, `failed`, etc.), result, error message, context ID, and agent card metadata.
- **A2AParallelDelegationStartedEvent**: Emitted when parallel delegation to multiple A2A agents begins. Contains the list of endpoints and the task description.
- **A2AParallelDelegationCompletedEvent**: Emitted when parallel delegation to multiple A2A agents completes. Contains the list of endpoints, success count, failure count, and results summary.
#### Conversation Events
- **A2AConversationStartedEvent**: Emitted once at the beginning of a multiturn A2A conversation, before the first message exchange. Contains the agent ID, endpoint, context ID, agent card metadata, protocol version, and provider info.
- **A2AMessageSentEvent**: Emitted when a message is sent to the A2A agent. Contains the message content, turn number, context ID, message ID, and whether it's multiturn.
- **A2AResponseReceivedEvent**: Emitted when a response is received from the A2A agent. Contains the response content, turn number, context ID, message ID, status, and whether it's the final response.
- **A2AConversationCompletedEvent**: Emitted once at the end of a multiturn A2A conversation. Contains the final status (`completed` or `failed`), final result, error message, context ID, and total number of turns.
#### Streaming Events
- **A2AStreamingStartedEvent**: Emitted when streaming mode begins for A2A delegation. Contains the task ID, context ID, endpoint, turn number, and whether it's multiturn.
- **A2AStreamingChunkEvent**: Emitted when a streaming chunk is received. Contains the chunk text, chunk index, whether it's the final chunk, task ID, context ID, and turn number.
#### Polling & Push Notification Events
- **A2APollingStartedEvent**: Emitted when polling mode begins for A2A delegation. Contains the task ID, context ID, polling interval in seconds, and endpoint.
- **A2APollingStatusEvent**: Emitted on each polling iteration. Contains the task ID, context ID, current task state, elapsed seconds, and poll count.
- **A2APushNotificationRegisteredEvent**: Emitted when a push notification callback is registered. Contains the task ID, context ID, callback URL, and endpoint.
- **A2APushNotificationReceivedEvent**: Emitted when a push notification is received from the remote A2A agent. Contains the task ID, context ID, and current state.
- **A2APushNotificationSentEvent**: Emitted when a push notification is sent to a callback URL. Contains the task ID, context ID, callback URL, state, whether delivery succeeded, and optional error message.
- **A2APushNotificationTimeoutEvent**: Emitted when push notification wait times out. Contains the task ID, context ID, and timeout duration in seconds.
#### Connection & Authentication Events
- **A2AAgentCardFetchedEvent**: Emitted when an agent card is successfully fetched. Contains the endpoint, agent name, agent card metadata, protocol version, provider info, whether it was cached, and fetch time in milliseconds.
- **A2AAuthenticationFailedEvent**: Emitted when authentication to an A2A agent fails. Contains the endpoint, auth type attempted (e.g., `bearer`, `oauth2`, `api_key`), error message, and HTTP status code.
- **A2AConnectionErrorEvent**: Emitted when a connection error occurs during A2A communication. Contains the endpoint, error message, error type (e.g., `timeout`, `connection_refused`, `dns_error`), HTTP status code, and the operation being attempted.
- **A2ATransportNegotiatedEvent**: Emitted when transport protocol is negotiated with an A2A agent. Contains the negotiated transport, negotiated URL, selection source (`client_preferred`, `server_preferred`, `fallback`), and client/server supported transports.
- **A2AContentTypeNegotiatedEvent**: Emitted when content types are negotiated with an A2A agent. Contains the client/server input/output modes, negotiated input/output modes, and whether negotiation succeeded.
#### Artifact Events
- **A2AArtifactReceivedEvent**: Emitted when an artifact is received from a remote A2A agent. Contains the task ID, artifact ID, artifact name, description, MIME type, size in bytes, and whether content should be appended.
#### Server Task Events
- **A2AServerTaskStartedEvent**: Emitted when an A2A server task execution starts. Contains the task ID and context ID.
- **A2AServerTaskCompletedEvent**: Emitted when an A2A server task execution completes. Contains the task ID, context ID, and result.
- **A2AServerTaskCanceledEvent**: Emitted when an A2A server task execution is canceled. Contains the task ID and context ID.
- **A2AServerTaskFailedEvent**: Emitted when an A2A server task execution fails. Contains the task ID, context ID, and error message.
#### Context Lifecycle Events
- **A2AContextCreatedEvent**: Emitted when an A2A context is created. Contexts group related tasks in a conversation or workflow. Contains the context ID and creation timestamp.
- **A2AContextExpiredEvent**: Emitted when an A2A context expires due to TTL. Contains the context ID, creation timestamp, age in seconds, and task count.
- **A2AContextIdleEvent**: Emitted when an A2A context becomes idle (no activity for the configured threshold). Contains the context ID, idle time in seconds, and task count.
- **A2AContextCompletedEvent**: Emitted when all tasks in an A2A context complete. Contains the context ID, total tasks, and duration in seconds.
- **A2AContextPrunedEvent**: Emitted when an A2A context is pruned (deleted). Contains the context ID, task count, and age in seconds.
## Event Handler Structure

View 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()],
# ...
)
```

View File

@@ -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.

View File

@@ -195,12 +195,19 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
- **CrewTrainStartedEvent**: Crew가 훈련을 시작할 때 발생
- **CrewTrainCompletedEvent**: Crew가 훈련을 완료할 때 발생
- **CrewTrainFailedEvent**: Crew가 훈련을 완료하지 못할 때 발생
- **CrewTestResultEvent**: Crew 테스트 결과가 사용 가능할 때 발생합니다. 품질 점수, 실행 시간, 사용된 모델을 포함합니다.
### 에이전트 이벤트
- **AgentExecutionStartedEvent**: 에이전트가 작업 실행을 시작할 때 발생함
- **AgentExecutionCompletedEvent**: 에이전트가 작업 실행을 완료할 때 발생함
- **AgentExecutionErrorEvent**: 에이전트가 실행 도중 오류를 만날 때 발생함
- **LiteAgentExecutionStartedEvent**: LiteAgent가 실행을 시작할 때 발생합니다. 에이전트 정보, 도구, 메시지를 포함합니다.
- **LiteAgentExecutionCompletedEvent**: LiteAgent가 실행을 완료할 때 발생합니다. 에이전트 정보와 출력을 포함합니다.
- **LiteAgentExecutionErrorEvent**: LiteAgent가 실행 중 오류를 만날 때 발생합니다. 에이전트 정보와 오류 메시지를 포함합니다.
- **AgentEvaluationStartedEvent**: 에이전트 평가가 시작될 때 발생합니다. 에이전트 ID, 에이전트 역할, 선택적 태스크 ID, 반복 횟수를 포함합니다.
- **AgentEvaluationCompletedEvent**: 에이전트 평가가 완료될 때 발생합니다. 에이전트 ID, 에이전트 역할, 선택적 태스크 ID, 반복 횟수, 메트릭 카테고리, 점수를 포함합니다.
- **AgentEvaluationFailedEvent**: 에이전트 평가가 실패할 때 발생합니다. 에이전트 ID, 에이전트 역할, 선택적 태스크 ID, 반복 횟수, 오류 메시지를 포함합니다.
### 작업 이벤트
@@ -218,6 +225,16 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
- **ToolExecutionErrorEvent**: 도구 실행 중 오류가 발생할 때 발생함
- **ToolSelectionErrorEvent**: 도구 선택 시 오류가 발생할 때 발생함
### MCP 이벤트
- **MCPConnectionStartedEvent**: MCP 서버 연결을 시작할 때 발생합니다. 서버 이름, URL, 전송 유형, 연결 시간 초과, 재연결 시도 여부를 포함합니다.
- **MCPConnectionCompletedEvent**: MCP 서버에 성공적으로 연결될 때 발생합니다. 서버 이름, 연결 시간(밀리초), 재연결 여부를 포함합니다.
- **MCPConnectionFailedEvent**: MCP 서버 연결이 실패할 때 발생합니다. 서버 이름, 오류 메시지, 오류 유형(`timeout`, `authentication`, `network` 등)을 포함합니다.
- **MCPToolExecutionStartedEvent**: MCP 도구 실행을 시작할 때 발생합니다. 서버 이름, 도구 이름, 도구 인수를 포함합니다.
- **MCPToolExecutionCompletedEvent**: MCP 도구 실행이 성공적으로 완료될 때 발생합니다. 서버 이름, 도구 이름, 결과, 실행 시간(밀리초)을 포함합니다.
- **MCPToolExecutionFailedEvent**: MCP 도구 실행이 실패할 때 발생합니다. 서버 이름, 도구 이름, 오류 메시지, 오류 유형(`timeout`, `validation`, `server_error` 등)을 포함합니다.
- **MCPConfigFetchFailedEvent**: MCP 서버 구성을 가져오는 데 실패할 때 발생합니다(예: 계정에서 MCP가 연결되지 않았거나, API 오류, 구성을 가져온 후 연결 실패). slug, 오류 메시지, 오류 유형(`not_connected`, `api_error`, `connection_failed`)을 포함합니다.
### 지식 이벤트
- **KnowledgeRetrievalStartedEvent**: 지식 검색이 시작될 때 발생
@@ -231,16 +248,26 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
- **LLMGuardrailStartedEvent**: 가드레일 검증이 시작될 때 발생합니다. 적용되는 가드레일에 대한 세부 정보와 재시도 횟수를 포함합니다.
- **LLMGuardrailCompletedEvent**: 가드레일 검증이 완료될 때 발생합니다. 검증의 성공/실패, 결과 및 오류 메시지(있는 경우)에 대한 세부 정보를 포함합니다.
- **LLMGuardrailFailedEvent**: 가드레일 검증이 실패할 때 발생합니다. 오류 메시지와 재시도 횟수를 포함합니다.
### Flow 이벤트
- **FlowCreatedEvent**: Flow가 생성될 때 발생
- **FlowStartedEvent**: Flow가 실행을 시작할 때 발생
- **FlowFinishedEvent**: Flow가 실행을 완료할 때 발생
- **FlowPausedEvent**: 사람의 피드백을 기다리며 Flow가 일시 중지될 때 발생합니다. Flow 이름, Flow ID, 메서드 이름, 현재 상태, 피드백 요청 시 표시되는 메시지, 라우팅을 위한 선택적 결과 목록을 포함합니다.
- **FlowPlotEvent**: Flow가 플롯될 때 발생
- **MethodExecutionStartedEvent**: Flow 메서드가 실행을 시작할 때 발생
- **MethodExecutionFinishedEvent**: Flow 메서드가 실행을 완료할 때 발생
- **MethodExecutionFailedEvent**: Flow 메서드가 실행을 완료하지 못할 때 발생
- **MethodExecutionPausedEvent**: 사람의 피드백을 기다리며 Flow 메서드가 일시 중지될 때 발생합니다. Flow 이름, 메서드 이름, 현재 상태, Flow ID, 피드백 요청 시 표시되는 메시지, 라우팅을 위한 선택적 결과 목록을 포함합니다.
### Human In The Loop 이벤트
- **FlowInputRequestedEvent**: `Flow.ask()`를 통해 Flow가 사용자 입력을 요청할 때 발생합니다. Flow 이름, 메서드 이름, 사용자에게 표시되는 질문 또는 프롬프트, 선택적 메타데이터(예: 사용자 ID, 채널, 세션 컨텍스트)를 포함합니다.
- **FlowInputReceivedEvent**: `Flow.ask()` 이후 사용자 입력이 수신될 때 발생합니다. Flow 이름, 메서드 이름, 원래 질문, 사용자의 응답(시간 초과 시 `None`), 선택적 요청 메타데이터, 프로바이더의 선택적 응답 메타데이터(예: 응답자, 스레드 ID, 타임스탬프)를 포함합니다.
- **HumanFeedbackRequestedEvent**: `@human_feedback` 데코레이터가 적용된 메서드가 사람 리뷰어의 입력을 필요로 할 때 발생합니다. Flow 이름, 메서드 이름, 사람에게 검토를 위해 표시되는 메서드 출력, 피드백 요청 시 표시되는 메시지, 라우팅을 위한 선택적 결과 목록을 포함합니다.
- **HumanFeedbackReceivedEvent**: `@human_feedback` 데코레이터가 적용된 메서드에 대해 사람이 피드백을 제공할 때 발생합니다. Flow 이름, 메서드 이름, 사람이 제공한 원본 텍스트 피드백, 축약된 결과 문자열(emit이 지정된 경우)을 포함합니다.
### LLM 이벤트
@@ -248,6 +275,7 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
- **LLMCallCompletedEvent**: LLM 호출이 완료될 때 발생
- **LLMCallFailedEvent**: LLM 호출이 실패할 때 발생
- **LLMStreamChunkEvent**: 스트리밍 LLM 응답 중 각 청크를 받을 때마다 발생
- **LLMThinkingChunkEvent**: thinking 모델에서 사고/추론 청크가 수신될 때 발생합니다. 청크 텍스트와 선택적 응답 ID를 포함합니다.
### 메모리 이벤트
@@ -259,6 +287,79 @@ CrewAI는 여러분이 청취할 수 있는 다양한 이벤트를 제공합니
- **MemorySaveFailedEvent**: 메모리 저장 작업에 실패할 때 발생합니다. 값, 메타데이터, agent 역할, 오류 메시지를 포함합니다.
- **MemoryRetrievalStartedEvent**: 태스크 프롬프트를 위한 메모리 검색이 시작될 때 발생합니다. 선택적 태스크 ID를 포함합니다.
- **MemoryRetrievalCompletedEvent**: 태스크 프롬프트를 위한 메모리 검색이 성공적으로 완료될 때 발생합니다. 태스크 ID, 메모리 내용, 검색 실행 시간을 포함합니다.
- **MemoryRetrievalFailedEvent**: 태스크 프롬프트를 위한 메모리 검색이 실패할 때 발생합니다. 선택적 태스크 ID와 오류 메시지를 포함합니다.
### 추론 이벤트
- **AgentReasoningStartedEvent**: 에이전트가 태스크에 대한 추론을 시작할 때 발생합니다. 에이전트 역할, 태스크 ID, 시도 횟수를 포함합니다.
- **AgentReasoningCompletedEvent**: 에이전트가 추론 과정을 마칠 때 발생합니다. 에이전트 역할, 태스크 ID, 생성된 계획, 에이전트가 진행할 준비가 되었는지 여부를 포함합니다.
- **AgentReasoningFailedEvent**: 추론 과정이 실패할 때 발생합니다. 에이전트 역할, 태스크 ID, 오류 메시지를 포함합니다.
### 관찰 이벤트
- **StepObservationStartedEvent**: Planner가 단계 결과를 관찰하기 시작할 때 발생합니다. 매 단계 실행 후, 관찰 LLM 호출 전에 발생합니다. 에이전트 역할, 단계 번호, 단계 설명을 포함합니다.
- **StepObservationCompletedEvent**: Planner가 단계 결과 관찰을 마칠 때 발생합니다. 단계 성공 여부, 학습된 핵심 정보, 남은 계획의 유효성, 전체 재계획 필요 여부, 제안된 개선 사항을 포함합니다.
- **StepObservationFailedEvent**: 관찰 LLM 호출 자체가 실패할 때 발생합니다. 시스템은 기본적으로 계획을 계속 진행합니다. 오류 메시지를 포함합니다.
- **PlanRefinementEvent**: Planner가 전체 재계획 없이 다음 단계 설명을 개선할 때 발생합니다. 개선된 단계 수와 적용된 개선 사항을 포함합니다.
- **PlanReplanTriggeredEvent**: 남은 계획이 근본적으로 잘못된 것으로 판단되어 Planner가 전체 재계획을 트리거할 때 발생합니다. 재계획 이유, 재계획 횟수, 보존된 완료 단계 수를 포함합니다.
- **GoalAchievedEarlyEvent**: Planner가 목표가 조기에 달성되었음을 감지하고 나머지 단계를 건너뛸 때 발생합니다. 남은 단계 수와 완료된 단계 수를 포함합니다.
### A2A (Agent-to-Agent) 이벤트
#### 위임 이벤트
- **A2ADelegationStartedEvent**: A2A 위임이 시작될 때 발생합니다. 엔드포인트 URL, 태스크 설명, 에이전트 ID, 컨텍스트 ID, 멀티턴 여부, 턴 번호, agent card 메타데이터, 프로토콜 버전, 프로바이더 정보, 선택적 skill ID를 포함합니다.
- **A2ADelegationCompletedEvent**: A2A 위임이 완료될 때 발생합니다. 완료 상태(`completed`, `input_required`, `failed` 등), 결과, 오류 메시지, 컨텍스트 ID, agent card 메타데이터를 포함합니다.
- **A2AParallelDelegationStartedEvent**: 여러 A2A 에이전트로의 병렬 위임이 시작될 때 발생합니다. 엔드포인트 목록과 태스크 설명을 포함합니다.
- **A2AParallelDelegationCompletedEvent**: 여러 A2A 에이전트로의 병렬 위임이 완료될 때 발생합니다. 엔드포인트 목록, 성공 수, 실패 수, 결과 요약을 포함합니다.
#### 대화 이벤트
- **A2AConversationStartedEvent**: 멀티턴 A2A 대화 시작 시 한 번 발생합니다. 첫 번째 메시지 교환 전에 발생합니다. 에이전트 ID, 엔드포인트, 컨텍스트 ID, agent card 메타데이터, 프로토콜 버전, 프로바이더 정보를 포함합니다.
- **A2AMessageSentEvent**: A2A 에이전트에 메시지가 전송될 때 발생합니다. 메시지 내용, 턴 번호, 컨텍스트 ID, 메시지 ID, 멀티턴 여부를 포함합니다.
- **A2AResponseReceivedEvent**: A2A 에이전트로부터 응답이 수신될 때 발생합니다. 응답 내용, 턴 번호, 컨텍스트 ID, 메시지 ID, 상태, 최종 응답 여부를 포함합니다.
- **A2AConversationCompletedEvent**: 멀티턴 A2A 대화 종료 시 한 번 발생합니다. 최종 상태(`completed` 또는 `failed`), 최종 결과, 오류 메시지, 컨텍스트 ID, 총 턴 수를 포함합니다.
#### 스트리밍 이벤트
- **A2AStreamingStartedEvent**: A2A 위임을 위한 스트리밍 모드가 시작될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 엔드포인트, 턴 번호, 멀티턴 여부를 포함합니다.
- **A2AStreamingChunkEvent**: 스트리밍 청크가 수신될 때 발생합니다. 청크 텍스트, 청크 인덱스, 최종 청크 여부, 태스크 ID, 컨텍스트 ID, 턴 번호를 포함합니다.
#### 폴링 및 푸시 알림 이벤트
- **A2APollingStartedEvent**: A2A 위임을 위한 폴링 모드가 시작될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 폴링 간격(초), 엔드포인트를 포함합니다.
- **A2APollingStatusEvent**: 각 폴링 반복 시 발생합니다. 태스크 ID, 컨텍스트 ID, 현재 태스크 상태, 경과 시간, 폴링 횟수를 포함합니다.
- **A2APushNotificationRegisteredEvent**: 푸시 알림 콜백이 등록될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 콜백 URL, 엔드포인트를 포함합니다.
- **A2APushNotificationReceivedEvent**: 원격 A2A 에이전트로부터 푸시 알림이 수신될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 현재 상태를 포함합니다.
- **A2APushNotificationSentEvent**: 콜백 URL로 푸시 알림이 전송될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 콜백 URL, 상태, 전달 성공 여부, 선택적 오류 메시지를 포함합니다.
- **A2APushNotificationTimeoutEvent**: 푸시 알림 대기가 시간 초과될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 시간 초과 시간(초)을 포함합니다.
#### 연결 및 인증 이벤트
- **A2AAgentCardFetchedEvent**: agent card가 성공적으로 가져올 때 발생합니다. 엔드포인트, 에이전트 이름, agent card 메타데이터, 프로토콜 버전, 프로바이더 정보, 캐시 여부, 가져오기 시간(밀리초)을 포함합니다.
- **A2AAuthenticationFailedEvent**: A2A 에이전트 인증이 실패할 때 발생합니다. 엔드포인트, 시도된 인증 유형(예: `bearer`, `oauth2`, `api_key`), 오류 메시지, HTTP 상태 코드를 포함합니다.
- **A2AConnectionErrorEvent**: A2A 통신 중 연결 오류가 발생할 때 발생합니다. 엔드포인트, 오류 메시지, 오류 유형(예: `timeout`, `connection_refused`, `dns_error`), HTTP 상태 코드, 시도 중인 작업을 포함합니다.
- **A2ATransportNegotiatedEvent**: A2A 에이전트와 전송 프로토콜이 협상될 때 발생합니다. 협상된 전송, 협상된 URL, 선택 소스(`client_preferred`, `server_preferred`, `fallback`), 클라이언트/서버 지원 전송을 포함합니다.
- **A2AContentTypeNegotiatedEvent**: A2A 에이전트와 콘텐츠 유형이 협상될 때 발생합니다. 클라이언트/서버 입출력 모드, 협상된 입출력 모드, 협상 성공 여부를 포함합니다.
#### 아티팩트 이벤트
- **A2AArtifactReceivedEvent**: 원격 A2A 에이전트로부터 아티팩트가 수신될 때 발생합니다. 태스크 ID, 아티팩트 ID, 아티팩트 이름, 설명, MIME 유형, 크기(바이트), 콘텐츠 추가 여부를 포함합니다.
#### 서버 태스크 이벤트
- **A2AServerTaskStartedEvent**: A2A 서버 태스크 실행이 시작될 때 발생합니다. 태스크 ID와 컨텍스트 ID를 포함합니다.
- **A2AServerTaskCompletedEvent**: A2A 서버 태스크 실행이 완료될 때 발생합니다. 태스크 ID, 컨텍스트 ID, 결과를 포함합니다.
- **A2AServerTaskCanceledEvent**: A2A 서버 태스크 실행이 취소될 때 발생합니다. 태스크 ID와 컨텍스트 ID를 포함합니다.
- **A2AServerTaskFailedEvent**: A2A 서버 태스크 실행이 실패할 때 발생합니다. 태스크 ID, 컨텍스트 ID, 오류 메시지를 포함합니다.
#### 컨텍스트 수명 주기 이벤트
- **A2AContextCreatedEvent**: A2A 컨텍스트가 생성될 때 발생합니다. 컨텍스트는 대화 또는 워크플로우에서 관련 태스크를 그룹화합니다. 컨텍스트 ID와 생성 타임스탬프를 포함합니다.
- **A2AContextExpiredEvent**: TTL로 인해 A2A 컨텍스트가 만료될 때 발생합니다. 컨텍스트 ID, 생성 타임스탬프, 수명(초), 태스크 수를 포함합니다.
- **A2AContextIdleEvent**: A2A 컨텍스트가 유휴 상태가 될 때(설정된 임계값 동안 활동 없음) 발생합니다. 컨텍스트 ID, 유휴 시간(초), 태스크 수를 포함합니다.
- **A2AContextCompletedEvent**: A2A 컨텍스트의 모든 태스크가 완료될 때 발생합니다. 컨텍스트 ID, 총 태스크 수, 지속 시간(초)을 포함합니다.
- **A2AContextPrunedEvent**: A2A 컨텍스트가 정리(삭제)될 때 발생합니다. 컨텍스트 ID, 태스크 수, 수명(초)을 포함합니다.
## 이벤트 핸들러 구조

View 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`에서 읽거나 내용을 직접 붙여넣으세요.

View 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()],
# ...
)
```

View File

@@ -9,6 +9,10 @@ mode: "wide"
이 가이드는 CrewAI 프레임워크를 위한 커스텀 툴을 생성하는 방법과 최신 기능(툴 위임, 오류 처리, 동적 툴 호출 등)을 통합하여 이러한 툴을 효율적으로 관리하고 활용하는 방법에 대해 자세히 안내합니다. 또한 협업 툴의 중요성을 강조하며, 에이전트가 다양한 작업을 수행할 수 있도록 지원합니다.
<Tip>
**커뮤니티에 도구를 배포하고 싶으신가요?** 다른 사용자에게도 유용한 도구를 만들고 있다면, [커스텀 도구 배포하기](/ko/guides/tools/publish-custom-tools) 가이드에서 도구를 패키징하고 PyPI에 배포하는 방법을 알아보세요.
</Tip>
### `BaseTool` 서브클래싱
개인화된 툴을 생성하려면 `BaseTool`을 상속받고, 입력 검증을 위한 `args_schema`와 `_run` 메서드를 포함한 필요한 속성들을 정의해야 합니다.

View File

@@ -196,12 +196,19 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
- **CrewTrainStartedEvent**: Emitido ao iniciar o treinamento de um Crew
- **CrewTrainCompletedEvent**: Emitido ao concluir o treinamento de um Crew
- **CrewTrainFailedEvent**: Emitido ao falhar no treinamento de um Crew
- **CrewTestResultEvent**: Emitido quando um resultado de teste de Crew está disponível. Contém a pontuação de qualidade, duração da execução e modelo utilizado.
### Eventos de Agent
- **AgentExecutionStartedEvent**: Emitido quando um Agent inicia a execução de uma tarefa
- **AgentExecutionCompletedEvent**: Emitido quando um Agent conclui a execução de uma tarefa
- **AgentExecutionErrorEvent**: Emitido quando um Agent encontra um erro durante a execução
- **LiteAgentExecutionStartedEvent**: Emitido quando um LiteAgent inicia a execução. Contém as informações do agente, ferramentas e mensagens.
- **LiteAgentExecutionCompletedEvent**: Emitido quando um LiteAgent conclui a execução. Contém as informações do agente e a saída.
- **LiteAgentExecutionErrorEvent**: Emitido quando um LiteAgent encontra um erro durante a execução. Contém as informações do agente e a mensagem de erro.
- **AgentEvaluationStartedEvent**: Emitido quando uma avaliação de agente é iniciada. Contém o ID do agente, papel do agente, ID da tarefa opcional e número da iteração.
- **AgentEvaluationCompletedEvent**: Emitido quando uma avaliação de agente é concluída. Contém o ID do agente, papel do agente, ID da tarefa opcional, número da iteração, categoria da métrica e pontuação.
- **AgentEvaluationFailedEvent**: Emitido quando uma avaliação de agente falha. Contém o ID do agente, papel do agente, ID da tarefa opcional, número da iteração e mensagem de erro.
### Eventos de Task
@@ -219,6 +226,16 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
- **ToolExecutionErrorEvent**: Emitido quando ocorre erro na execução de uma ferramenta
- **ToolSelectionErrorEvent**: Emitido ao ocorrer erro na seleção de uma ferramenta
### Eventos de MCP
- **MCPConnectionStartedEvent**: Emitido ao iniciar a conexão com um servidor MCP. Contém o nome do servidor, URL, tipo de transporte, timeout de conexão e se é uma tentativa de reconexão.
- **MCPConnectionCompletedEvent**: Emitido ao conectar com sucesso a um servidor MCP. Contém o nome do servidor, duração da conexão em milissegundos e se foi uma reconexão.
- **MCPConnectionFailedEvent**: Emitido quando a conexão com um servidor MCP falha. Contém o nome do servidor, mensagem de erro e tipo de erro (`timeout`, `authentication`, `network`, etc.).
- **MCPToolExecutionStartedEvent**: Emitido ao iniciar a execução de uma ferramenta MCP. Contém o nome do servidor, nome da ferramenta e argumentos da ferramenta.
- **MCPToolExecutionCompletedEvent**: Emitido quando a execução de uma ferramenta MCP é concluída com sucesso. Contém o nome do servidor, nome da ferramenta, resultado e duração da execução em milissegundos.
- **MCPToolExecutionFailedEvent**: Emitido quando a execução de uma ferramenta MCP falha. Contém o nome do servidor, nome da ferramenta, mensagem de erro e tipo de erro (`timeout`, `validation`, `server_error`, etc.).
- **MCPConfigFetchFailedEvent**: Emitido quando a obtenção da configuração de um servidor MCP falha (ex.: o MCP não está conectado na sua conta, erro de API ou falha de conexão após a configuração ser obtida). Contém o slug, mensagem de erro e tipo de erro (`not_connected`, `api_error`, `connection_failed`).
### Eventos de Knowledge
- **KnowledgeRetrievalStartedEvent**: Emitido ao iniciar recuperação de conhecimento
@@ -232,16 +249,26 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
- **LLMGuardrailStartedEvent**: Emitido ao iniciar validação dos guardrails. Contém detalhes do guardrail aplicado e tentativas.
- **LLMGuardrailCompletedEvent**: Emitido ao concluir validação dos guardrails. Contém detalhes sobre sucesso/falha na validação, resultados e mensagens de erro, se houver.
- **LLMGuardrailFailedEvent**: Emitido quando a validação do guardrail falha. Contém a mensagem de erro e o número de tentativas.
### Eventos de Flow
- **FlowCreatedEvent**: Emitido ao criar um Flow
- **FlowStartedEvent**: Emitido ao iniciar a execução de um Flow
- **FlowFinishedEvent**: Emitido ao concluir a execução de um Flow
- **FlowPausedEvent**: Emitido quando um Flow é pausado aguardando feedback humano. Contém o nome do flow, ID do flow, nome do método, estado atual, mensagem exibida ao solicitar feedback e lista opcional de resultados possíveis para roteamento.
- **FlowPlotEvent**: Emitido ao plotar um Flow
- **MethodExecutionStartedEvent**: Emitido ao iniciar a execução de um método do Flow
- **MethodExecutionFinishedEvent**: Emitido ao concluir a execução de um método do Flow
- **MethodExecutionFailedEvent**: Emitido ao falhar na execução de um método do Flow
- **MethodExecutionPausedEvent**: Emitido quando um método do Flow é pausado aguardando feedback humano. Contém o nome do flow, nome do método, estado atual, ID do flow, mensagem exibida ao solicitar feedback e lista opcional de resultados possíveis para roteamento.
### Eventos de Human In The Loop
- **FlowInputRequestedEvent**: Emitido quando um Flow solicita entrada do usuário via `Flow.ask()`. Contém o nome do flow, nome do método, a pergunta ou prompt exibido ao usuário e metadados opcionais (ex.: ID do usuário, canal, contexto da sessão).
- **FlowInputReceivedEvent**: Emitido quando a entrada do usuário é recebida após `Flow.ask()`. Contém o nome do flow, nome do método, a pergunta original, a resposta do usuário (ou `None` se expirou), metadados opcionais da solicitação e metadados opcionais da resposta do provedor (ex.: quem respondeu, ID do thread, timestamps).
- **HumanFeedbackRequestedEvent**: Emitido quando um método decorado com `@human_feedback` requer entrada de um revisor humano. Contém o nome do flow, nome do método, a saída do método exibida ao humano para revisão, a mensagem exibida ao solicitar feedback e lista opcional de resultados possíveis para roteamento.
- **HumanFeedbackReceivedEvent**: Emitido quando um humano fornece feedback em resposta a um método decorado com `@human_feedback`. Contém o nome do flow, nome do método, o texto bruto do feedback fornecido pelo humano e a string de resultado consolidada (se emit foi especificado).
### Eventos de LLM
@@ -249,6 +276,91 @@ O CrewAI fornece uma ampla variedade de eventos para escuta:
- **LLMCallCompletedEvent**: Emitido ao concluir uma chamada LLM
- **LLMCallFailedEvent**: Emitido ao falhar uma chamada LLM
- **LLMStreamChunkEvent**: Emitido para cada chunk recebido durante respostas em streaming do LLM
- **LLMThinkingChunkEvent**: Emitido quando um chunk de pensamento/raciocínio é recebido de um modelo de pensamento. Contém o texto do chunk e ID de resposta opcional.
### Eventos de Memória
- **MemoryQueryStartedEvent**: Emitido quando uma consulta de memória é iniciada. Contém a consulta, limite e threshold de pontuação opcional.
- **MemoryQueryCompletedEvent**: Emitido quando uma consulta de memória é concluída com sucesso. Contém a consulta, resultados, limite, threshold de pontuação e tempo de execução da consulta.
- **MemoryQueryFailedEvent**: Emitido quando uma consulta de memória falha. Contém a consulta, limite, threshold de pontuação e mensagem de erro.
- **MemorySaveStartedEvent**: Emitido quando uma operação de salvamento de memória é iniciada. Contém o valor a ser salvo, metadados e papel do agente opcional.
- **MemorySaveCompletedEvent**: Emitido quando uma operação de salvamento de memória é concluída com sucesso. Contém o valor salvo, metadados, papel do agente e tempo de salvamento.
- **MemorySaveFailedEvent**: Emitido quando uma operação de salvamento de memória falha. Contém o valor, metadados, papel do agente e mensagem de erro.
- **MemoryRetrievalStartedEvent**: Emitido quando a recuperação de memória para um prompt de tarefa é iniciada. Contém o ID da tarefa opcional.
- **MemoryRetrievalCompletedEvent**: Emitido quando a recuperação de memória para um prompt de tarefa é concluída com sucesso. Contém o ID da tarefa, conteúdo da memória e tempo de execução da recuperação.
- **MemoryRetrievalFailedEvent**: Emitido quando a recuperação de memória para um prompt de tarefa falha. Contém o ID da tarefa opcional e mensagem de erro.
### Eventos de Raciocínio
- **AgentReasoningStartedEvent**: Emitido quando um agente começa a raciocinar sobre uma tarefa. Contém o papel do agente, ID da tarefa e número da tentativa.
- **AgentReasoningCompletedEvent**: Emitido quando um agente finaliza seu processo de raciocínio. Contém o papel do agente, ID da tarefa, o plano produzido e se o agente está pronto para prosseguir.
- **AgentReasoningFailedEvent**: Emitido quando o processo de raciocínio falha. Contém o papel do agente, ID da tarefa e mensagem de erro.
### Eventos de Observação
- **StepObservationStartedEvent**: Emitido quando o Planner começa a observar o resultado de um passo. Disparado após cada execução de passo, antes da chamada LLM de observação. Contém o papel do agente, número do passo e descrição do passo.
- **StepObservationCompletedEvent**: Emitido quando o Planner finaliza a observação do resultado de um passo. Contém se o passo foi concluído com sucesso, informações-chave aprendidas, se o plano restante ainda é válido, se é necessário um replanejamento completo e refinamentos sugeridos.
- **StepObservationFailedEvent**: Emitido quando a chamada LLM de observação falha. O sistema continua o plano por padrão. Contém a mensagem de erro.
- **PlanRefinementEvent**: Emitido quando o Planner refina descrições de passos futuros sem replanejamento completo. Contém o número de passos refinados e os refinamentos aplicados.
- **PlanReplanTriggeredEvent**: Emitido quando o Planner dispara um replanejamento completo porque o plano restante foi considerado fundamentalmente incorreto. Contém o motivo do replanejamento, contagem de replanejamentos e número de passos concluídos preservados.
- **GoalAchievedEarlyEvent**: Emitido quando o Planner detecta que o objetivo foi alcançado antecipadamente e os passos restantes serão ignorados. Contém o número de passos restantes e passos concluídos.
### Eventos A2A (Agent-to-Agent)
#### Eventos de Delegação
- **A2ADelegationStartedEvent**: Emitido quando a delegação A2A é iniciada. Contém a URL do endpoint, descrição da tarefa, ID do agente, ID do contexto, se é multiturn, número do turno, metadados do agent card, versão do protocolo, informações do provedor e ID da skill opcional.
- **A2ADelegationCompletedEvent**: Emitido quando a delegação A2A é concluída. Contém o status de conclusão (`completed`, `input_required`, `failed`, etc.), resultado, mensagem de erro, ID do contexto e metadados do agent card.
- **A2AParallelDelegationStartedEvent**: Emitido quando a delegação paralela para múltiplos agentes A2A é iniciada. Contém a lista de endpoints e a descrição da tarefa.
- **A2AParallelDelegationCompletedEvent**: Emitido quando a delegação paralela para múltiplos agentes A2A é concluída. Contém a lista de endpoints, contagem de sucessos, contagem de falhas e resumo dos resultados.
#### Eventos de Conversação
- **A2AConversationStartedEvent**: Emitido uma vez no início de uma conversação multiturn A2A, antes da primeira troca de mensagens. Contém o ID do agente, endpoint, ID do contexto, metadados do agent card, versão do protocolo e informações do provedor.
- **A2AMessageSentEvent**: Emitido quando uma mensagem é enviada ao agente A2A. Contém o conteúdo da mensagem, número do turno, ID do contexto, ID da mensagem e se é multiturn.
- **A2AResponseReceivedEvent**: Emitido quando uma resposta é recebida do agente A2A. Contém o conteúdo da resposta, número do turno, ID do contexto, ID da mensagem, status e se é a resposta final.
- **A2AConversationCompletedEvent**: Emitido uma vez ao final de uma conversação multiturn A2A. Contém o status final (`completed` ou `failed`), resultado final, mensagem de erro, ID do contexto e número total de turnos.
#### Eventos de Streaming
- **A2AStreamingStartedEvent**: Emitido quando o modo streaming é iniciado para delegação A2A. Contém o ID da tarefa, ID do contexto, endpoint, número do turno e se é multiturn.
- **A2AStreamingChunkEvent**: Emitido quando um chunk de streaming é recebido. Contém o texto do chunk, índice do chunk, se é o chunk final, ID da tarefa, ID do contexto e número do turno.
#### Eventos de Polling e Push Notification
- **A2APollingStartedEvent**: Emitido quando o modo polling é iniciado para delegação A2A. Contém o ID da tarefa, ID do contexto, intervalo de polling em segundos e endpoint.
- **A2APollingStatusEvent**: Emitido em cada iteração de polling. Contém o ID da tarefa, ID do contexto, estado atual da tarefa, segundos decorridos e contagem de polls.
- **A2APushNotificationRegisteredEvent**: Emitido quando um callback de push notification é registrado. Contém o ID da tarefa, ID do contexto, URL do callback e endpoint.
- **A2APushNotificationReceivedEvent**: Emitido quando uma push notification é recebida do agente A2A remoto. Contém o ID da tarefa, ID do contexto e estado atual.
- **A2APushNotificationSentEvent**: Emitido quando uma push notification é enviada para uma URL de callback. Contém o ID da tarefa, ID do contexto, URL do callback, estado, se a entrega foi bem-sucedida e mensagem de erro opcional.
- **A2APushNotificationTimeoutEvent**: Emitido quando a espera por push notification expira. Contém o ID da tarefa, ID do contexto e duração do timeout em segundos.
#### Eventos de Conexão e Autenticação
- **A2AAgentCardFetchedEvent**: Emitido quando um agent card é obtido com sucesso. Contém o endpoint, nome do agente, metadados do agent card, versão do protocolo, informações do provedor, se foi do cache e tempo de busca em milissegundos.
- **A2AAuthenticationFailedEvent**: Emitido quando a autenticação com um agente A2A falha. Contém o endpoint, tipo de autenticação tentada (ex.: `bearer`, `oauth2`, `api_key`), mensagem de erro e código de status HTTP.
- **A2AConnectionErrorEvent**: Emitido quando ocorre um erro de conexão durante a comunicação A2A. Contém o endpoint, mensagem de erro, tipo de erro (ex.: `timeout`, `connection_refused`, `dns_error`), código de status HTTP e a operação sendo tentada.
- **A2ATransportNegotiatedEvent**: Emitido quando o protocolo de transporte é negociado com um agente A2A. Contém o transporte negociado, URL negociada, fonte de seleção (`client_preferred`, `server_preferred`, `fallback`) e transportes suportados pelo cliente/servidor.
- **A2AContentTypeNegotiatedEvent**: Emitido quando os tipos de conteúdo são negociados com um agente A2A. Contém os modos de entrada/saída do cliente/servidor, modos de entrada/saída negociados e se a negociação foi bem-sucedida.
#### Eventos de Artefatos
- **A2AArtifactReceivedEvent**: Emitido quando um artefato é recebido de um agente A2A remoto. Contém o ID da tarefa, ID do artefato, nome do artefato, descrição, tipo MIME, tamanho em bytes e se o conteúdo deve ser concatenado.
#### Eventos de Tarefa do Servidor
- **A2AServerTaskStartedEvent**: Emitido quando a execução de uma tarefa do servidor A2A é iniciada. Contém o ID da tarefa e ID do contexto.
- **A2AServerTaskCompletedEvent**: Emitido quando a execução de uma tarefa do servidor A2A é concluída. Contém o ID da tarefa, ID do contexto e resultado.
- **A2AServerTaskCanceledEvent**: Emitido quando a execução de uma tarefa do servidor A2A é cancelada. Contém o ID da tarefa e ID do contexto.
- **A2AServerTaskFailedEvent**: Emitido quando a execução de uma tarefa do servidor A2A falha. Contém o ID da tarefa, ID do contexto e mensagem de erro.
#### Eventos de Ciclo de Vida do Contexto
- **A2AContextCreatedEvent**: Emitido quando um contexto A2A é criado. Contextos agrupam tarefas relacionadas em uma conversação ou workflow. Contém o ID do contexto e timestamp de criação.
- **A2AContextExpiredEvent**: Emitido quando um contexto A2A expira devido ao TTL. Contém o ID do contexto, timestamp de criação, idade em segundos e contagem de tarefas.
- **A2AContextIdleEvent**: Emitido quando um contexto A2A fica inativo (sem atividade pelo threshold configurado). Contém o ID do contexto, tempo de inatividade em segundos e contagem de tarefas.
- **A2AContextCompletedEvent**: Emitido quando todas as tarefas em um contexto A2A são concluídas. Contém o ID do contexto, total de tarefas e duração em segundos.
- **A2AContextPrunedEvent**: Emitido quando um contexto A2A é podado (deletado). Contém o ID do contexto, contagem de tarefas e idade em segundos.
## Estrutura dos Handlers de Evento

View 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.

View 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()],
# ...
)
```

View File

@@ -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`.

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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."""

View 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,
)

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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:

View 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())

View File

@@ -5,6 +5,7 @@ from pathlib import Path
import subprocess
import sys
import time
from typing import Final, Literal
import click
from dotenv import load_dotenv
@@ -250,7 +251,9 @@ def add_docs_version(docs_json_path: Path, version: str) -> bool:
return True
_PT_BR_MONTHS = {
ChangelogLang = Literal["en", "pt-BR", "ko"]
_PT_BR_MONTHS: Final[dict[int, str]] = {
1: "jan",
2: "fev",
3: "mar",
@@ -265,7 +268,9 @@ _PT_BR_MONTHS = {
12: "dez",
}
_CHANGELOG_LOCALES: dict[str, dict[str, str]] = {
_CHANGELOG_LOCALES: Final[
dict[ChangelogLang, dict[Literal["link_text", "language_name"], str]]
] = {
"en": {
"link_text": "View release on GitHub",
"language_name": "English",
@@ -283,7 +288,7 @@ _CHANGELOG_LOCALES: dict[str, dict[str, str]] = {
def translate_release_notes(
release_notes: str,
lang: str,
lang: ChangelogLang,
client: OpenAI,
) -> str:
"""Translate release notes into the target language using OpenAI.
@@ -326,7 +331,7 @@ def translate_release_notes(
return release_notes
def _format_changelog_date(lang: str) -> str:
def _format_changelog_date(lang: ChangelogLang) -> str:
"""Format today's date for a changelog entry in the given language."""
from datetime import datetime
@@ -342,7 +347,7 @@ def update_changelog(
changelog_path: Path,
version: str,
release_notes: str,
lang: str = "en",
lang: ChangelogLang = "en",
) -> bool:
"""Prepend a new release entry to a docs changelog file.
@@ -475,6 +480,23 @@ def get_packages(lib_dir: Path) -> list[Path]:
return packages
PrereleaseIndicator = Literal["a", "b", "rc", "alpha", "beta", "dev"]
_PRERELEASE_INDICATORS: Final[tuple[PrereleaseIndicator, ...]] = (
"a",
"b",
"rc",
"alpha",
"beta",
"dev",
)
def _is_prerelease(version: str) -> bool:
"""Check if a version string represents a pre-release."""
v = version.lower().lstrip("v")
return any(indicator in v for indicator in _PRERELEASE_INDICATORS)
def get_commits_from_last_tag(tag_name: str, version: str) -> tuple[str, str]:
"""Get commits from the last tag, excluding current version.
@@ -489,6 +511,9 @@ def get_commits_from_last_tag(tag_name: str, version: str) -> tuple[str, str]:
all_tags = run_command(["git", "tag", "--sort=-version:refname"]).split("\n")
prev_tags = [t for t in all_tags if t and t != tag_name and t != f"v{version}"]
if not _is_prerelease(version):
prev_tags = [t for t in prev_tags if not _is_prerelease(t)]
if prev_tags:
last_tag = prev_tags[0]
commit_range = f"{last_tag}..HEAD"
@@ -678,20 +703,28 @@ def _generate_release_notes(
with console.status("[cyan]Generating release notes..."):
try:
prev_bump_commit = run_command(
prev_bump_output = run_command(
[
"git",
"log",
"--grep=^feat: bump versions to",
"--format=%H",
"-n",
"2",
"--format=%H %s",
]
)
commits_list = prev_bump_commit.strip().split("\n")
bump_entries = [
line for line in prev_bump_output.strip().split("\n") if line.strip()
]
if len(commits_list) > 1:
prev_commit = commits_list[1]
is_stable = not _is_prerelease(version)
prev_commit = None
for entry in bump_entries[1:]:
bump_ver = entry.split("feat: bump versions to", 1)[-1].strip()
if is_stable and _is_prerelease(bump_ver):
continue
prev_commit = entry.split()[0]
break
if prev_commit:
commit_range = f"{prev_commit}..HEAD"
commits = run_command(
["git", "log", commit_range, "--pretty=format:%s"]
@@ -777,10 +810,7 @@ def _generate_release_notes(
"\n[green]✓[/green] Using generated release notes without editing"
)
is_prerelease = any(
indicator in version.lower()
for indicator in ["a", "b", "rc", "alpha", "beta", "dev"]
)
is_prerelease = _is_prerelease(version)
return release_notes, openai_client, is_prerelease
@@ -799,7 +829,7 @@ def _update_docs_and_create_pr(
The docs branch name if a PR was created, None otherwise.
"""
docs_json_path = cwd / "docs" / "docs.json"
changelog_langs = ["en", "pt-BR", "ko"]
changelog_langs: list[ChangelogLang] = ["en", "pt-BR", "ko"]
if not dry_run:
docs_files_staged: list[str] = []