Compare commits

..

4 Commits

Author SHA1 Message Date
Lorenze Jay
ff2fd74ee2 Merge branch 'main' into feat/open-sandbox-tool 2026-05-12 12:42:00 -07:00
github-actions[bot]
812468e1b9 chore: update tool specifications 2026-05-11 17:17:50 +00:00
Lorenze Jay
0f78d824e9 Merge branch 'main' into feat/open-sandbox-tool 2026-05-11 10:16:32 -07:00
iris-clawd
9c981e175b feat(tools): add OpenSandbox sandbox tool with optional deps
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 23:02:12 +00:00
17 changed files with 2404 additions and 2273 deletions

View File

@@ -29,7 +29,6 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,6 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -313,9 +313,9 @@ flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Second run - pass the ID to load the persisted state
# Second run - state is automatically loaded
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
result2 = flow2.kickoff()
print(f"Second run result: {result2}") # Will be higher due to persisted state
```

View File

@@ -1,131 +0,0 @@
---
title: Platform Tools CLI
description: Create, publish, and install custom tools on the CrewAI platform using the CLI.
icon: terminal
mode: "wide"
---
## Overview
The CrewAI CLI provides commands to manage custom tools on the **CrewAI platform** — a hosted tool registry that lets you share tools within your organization without publishing to PyPI.
| Command | Purpose |
|---------|---------|
| `crewai tool create <handle>` | Scaffold a new tool project |
| `crewai tool publish` | Publish the tool to the CrewAI platform |
| `crewai tool install <handle>` | Install a platform tool into your crew project |
<Note type="info" title="Platform vs PyPI">
These commands manage tools on the **CrewAI platform registry**. If you want to publish a standalone Python package to PyPI instead, see the [Publish Custom Tools to PyPI](/en/guides/tools/publish-custom-tools) guide.
</Note>
## Prerequisites
- **CrewAI CLI** installed (`pip install crewai`)
- **Authenticated** with the platform — run `crewai login` first
---
## Step 1: Create a Tool Project
Scaffold a new tool project:
```bash
crewai tool create my_custom_tool
```
This generates a project structure with the boilerplate you need to start building your tool.
<Tip>
The `handle` is the unique identifier for your tool on the platform. Choose something descriptive and specific to what the tool does.
</Tip>
### Implement Your Tool
Edit the generated tool file to add your logic. The tool follows the standard CrewAI tools contract — you can subclass `BaseTool` or use the `@tool` decorator:
```python
from crewai.tools import BaseTool
class MyCustomTool(BaseTool):
name: str = "My Custom Tool"
description: str = "Description of what this tool does — be specific so agents know when to use it."
def _run(self, argument: str) -> str:
# Your tool logic here
return "result"
```
For the full tools API reference (input schemas, caching, async support, error handling), see the [Create Custom Tools](/en/learn/create-custom-tools) guide.
---
## Step 2: Publish to the Platform
From your tool project directory, publish it to the CrewAI platform:
```bash
crewai tool publish
```
### Options
| Flag | Description |
|------|-------------|
| `--force` | Bypass Git remote validations |
Tools are published privately to your organization by default.
---
## Step 3: Install a Platform Tool
To install a tool that's been published to the platform:
```bash
crewai tool install my_custom_tool
```
Once installed, you can use the tool in your crew like any other tool — assign it to an agent via the `tools` parameter.
---
## Full Lifecycle Example
```bash
# 1. Authenticate with the platform
crewai login
# 2. Scaffold a new tool
crewai tool create weather_lookup
# 3. Implement your logic in the generated project
cd weather_lookup
# ... edit the tool file ...
# 4. Publish to the platform
crewai tool publish
# 5. In another project, install and use it
crewai tool install weather_lookup
```
---
## Platform Tools vs PyPI Packages
| | Platform Tools | PyPI Packages |
|---|---|---|
| **Publish** | `crewai tool publish` | `uv build` + `uv publish` |
| **Registry** | CrewAI platform | PyPI |
| **Install** | `crewai tool install <handle>` | `pip install <package>` |
| **Auth** | `crewai login` | PyPI account + token |
| **Visibility** | Organization-scoped (private) | Always public |
| **Guide** | This page | [Publish Custom Tools](/en/guides/tools/publish-custom-tools) |
---
## Related
- [Create Custom Tools](/en/learn/create-custom-tools) — Python API reference for building tools (BaseTool, @tool decorator)
- [Publish Custom Tools to PyPI](/en/guides/tools/publish-custom-tools) — package and distribute tools as standalone Python libraries

View File

@@ -12,9 +12,7 @@ incorporating the latest functionalities such as tool delegation, error handling
enabling agents to perform a wide range of actions.
<Tip>
**Want to publish your tool to the CrewAI platform?** Use the CLI to scaffold, publish, and share tools directly on the platform — see the [Platform Tools CLI](/en/guides/tools/platform-tools-cli) guide.
**Prefer publishing to PyPI?** Check out the [Publish Custom Tools](/en/guides/tools/publish-custom-tools) guide to package and distribute your tool as a standalone Python library.
**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`

View File

@@ -29,7 +29,6 @@ from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"

View File

@@ -24,63 +24,7 @@ Os flows permitem que você crie fluxos de trabalho estruturados e orientados po
Vamos criar um Flow simples no qual você usará a OpenAI para gerar uma cidade aleatória em uma tarefa e, em seguida, usará essa cidade para gerar uma curiosidade em outra tarefa.
```python Code
from crewai.flow.flow import Flow, listen, start
from dotenv import load_dotenv
from litellm import completion
load_dotenv()
class ExampleFlow(Flow):
model = "gpt-4o-mini"
@start()
def generate_city(self):
print("Starting flow")
# Cada estado do flow recebe automaticamente um ID único
print(f"Flow State ID: {self.state['id']}")
response = completion(
model=self.model,
messages=[
{
"role": "user",
"content": "Return the name of a random city in the world.",
},
],
)
random_city = response["choices"][0]["message"]["content"]
# Armazena a cidade no nosso estado
self.state["city"] = random_city
print(f"Random City: {random_city}")
return random_city
@listen(generate_city)
def generate_fun_fact(self, random_city):
response = completion(
model=self.model,
messages=[
{
"role": "user",
"content": f"Tell me a fun fact about {random_city}",
},
],
)
fun_fact = response["choices"][0]["message"]["content"]
# Armazena a curiosidade no nosso estado
self.state["fun_fact"] = fun_fact
return fun_fact
flow = ExampleFlow()
flow.plot()
result = flow.kickoff()
print(f"Generated fun fact: {result}")
# (O código não é traduzido)
```
Na ilustração acima, criamos um Flow simples que gera uma cidade aleatória usando a OpenAI e depois cria uma curiosidade sobre essa cidade. O Flow consiste em duas tarefas: `generate_city` e `generate_fun_fact`. A tarefa `generate_city` é o ponto de início do Flow, enquanto a tarefa `generate_fun_fact` fica escutando o resultado da tarefa `generate_city`.
@@ -112,16 +56,12 @@ O decorador `@listen()` pode ser usado de várias formas:
1. **Escutando um Método pelo Nome**: Você pode passar o nome do método ao qual deseja escutar como string. Quando esse método concluir, o método ouvinte será chamado.
```python Code
@listen("generate_city")
def generate_fun_fact(self, random_city):
# Implementação
# (O código não é traduzido)
```
2. **Escutando um Método Diretamente**: Você pode passar o próprio método. Quando esse método concluir, o método ouvinte será chamado.
```python Code
@listen(generate_city)
def generate_fun_fact(self, random_city):
# Implementação
# (O código não é traduzido)
```
### Saída de um Flow
@@ -136,24 +76,7 @@ Veja como acessar a saída final:
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, listen, start
class OutputExampleFlow(Flow):
@start()
def first_method(self):
return "Output from first_method"
@listen(first_method)
def second_method(self, first_output):
return f"Second method received: {first_output}"
flow = OutputExampleFlow()
flow.plot("my_flow_plot")
final_output = flow.kickoff()
print("---- Final Output ----")
print(final_output)
# (O código não é traduzido)
```
```text Output
@@ -174,34 +97,8 @@ Além de recuperar a saída final, você pode acessar e atualizar o estado dentr
Veja um exemplo de como atualizar e acessar o estado:
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ExampleState(BaseModel):
counter: int = 0
message: str = ""
class StateExampleFlow(Flow[ExampleState]):
@start()
def first_method(self):
self.state.message = "Hello from first_method"
self.state.counter += 1
@listen(first_method)
def second_method(self):
self.state.message += " - updated by second_method"
self.state.counter += 1
return self.state.message
flow = StateExampleFlow()
flow.plot("my_flow_plot")
final_output = flow.kickoff()
print(f"Final Output: {final_output}")
print("Final State:")
print(flow.state)
# (O código não é traduzido)
```
```text Output
@@ -231,33 +128,7 @@ Essa abordagem oferece flexibilidade, permitindo que o desenvolvedor adicione ou
Mesmo com estados não estruturados, os flows do CrewAI geram e mantêm automaticamente um identificador único (UUID) para cada instância de estado.
```python Code
from crewai.flow.flow import Flow, listen, start
class UnstructuredExampleFlow(Flow):
@start()
def first_method(self):
# O estado inclui automaticamente um campo 'id'
print(f"State ID: {self.state['id']}")
self.state['counter'] = 0
self.state['message'] = "Hello from structured flow"
@listen(first_method)
def second_method(self):
self.state['counter'] += 1
self.state['message'] += " - updated"
@listen(second_method)
def third_method(self):
self.state['counter'] += 1
self.state['message'] += " - updated again"
print(f"State after third_method: {self.state}")
flow = UnstructuredExampleFlow()
flow.plot("my_flow_plot")
flow.kickoff()
# (O código não é traduzido)
```
![Flow Visual image](/images/crewai-flow-3.png)
@@ -277,39 +148,7 @@ Ao usar modelos como o `BaseModel` da Pydantic, os desenvolvedores podem definir
Cada estado nos flows do CrewAI recebe automaticamente um identificador único (UUID) para ajudar no rastreamento e gerenciamento. Esse ID é gerado e mantido automaticamente pelo sistema de flows.
```python Code
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ExampleState(BaseModel):
# Nota: o campo 'id' é adicionado automaticamente a todos os estados
counter: int = 0
message: str = ""
class StructuredExampleFlow(Flow[ExampleState]):
@start()
def first_method(self):
# Acesse o ID gerado automaticamente, se necessário
print(f"State ID: {self.state.id}")
self.state.message = "Hello from structured flow"
@listen(first_method)
def second_method(self):
self.state.counter += 1
self.state.message += " - updated"
@listen(second_method)
def third_method(self):
self.state.counter += 1
self.state.message += " - updated again"
print(f"State after third_method: {self.state}")
flow = StructuredExampleFlow()
flow.kickoff()
# (O código não é traduzido)
```
![Flow Visual image](/images/crewai-flow-3.png)
@@ -343,19 +182,7 @@ O decorador @persist permite a persistência automática do estado nos flows do
Quando aplicado no nível da classe, o decorador @persist garante a persistência automática de todos os estados dos métodos do flow:
```python
@persist # Usa SQLiteFlowPersistence por padrão
class MyFlow(Flow[MyState]):
@start()
def initialize_flow(self):
# Este método terá seu estado persistido automaticamente
self.state.counter = 1
print("Initialized flow. State ID:", self.state.id)
@listen(initialize_flow)
def next_step(self):
# O estado (incluindo self.state.id) é recarregado automaticamente
self.state.counter += 1
print("Flow state is persisted. Counter:", self.state.counter)
# (O código não é traduzido)
```
### Persistência no Nível de Método
@@ -363,14 +190,7 @@ class MyFlow(Flow[MyState]):
Para um controle mais granular, você pode aplicar @persist em métodos específicos:
```python
class AnotherFlow(Flow[dict]):
@persist # Persiste apenas o estado deste método
@start()
def begin(self):
if "runs" not in self.state:
self.state["runs"] = 0
self.state["runs"] += 1
print("Method-level persisted runs:", self.state["runs"])
# (O código não é traduzido)
```
### Forking de Estado Persistido
@@ -462,29 +282,8 @@ A arquitetura de persistência enfatiza precisão técnica e opções de persona
A função `or_` nos flows permite escutar múltiplos métodos e acionar o método ouvinte quando qualquer um dos métodos especificados gerar uma saída.
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, listen, or_, start
class OrExampleFlow(Flow):
@start()
def start_method(self):
return "Hello from the start method"
@listen(start_method)
def second_method(self):
return "Hello from the second method"
@listen(or_(start_method, second_method))
def logger(self, result):
print(f"Logger: {result}")
flow = OrExampleFlow()
flow.plot("my_flow_plot")
flow.kickoff()
# (O código não é traduzido)
```
```text Output
@@ -503,28 +302,8 @@ A função `or_` serve para escutar vários métodos e disparar o método ouvint
A função `and_` nos flows permite escutar múltiplos métodos e acionar o método ouvinte apenas quando todos os métodos especificados emitirem uma saída.
<CodeGroup>
```python Code
from crewai.flow.flow import Flow, and_, listen, start
class AndExampleFlow(Flow):
@start()
def start_method(self):
self.state["greeting"] = "Hello from the start method"
@listen(start_method)
def second_method(self):
self.state["joke"] = "What do computers eat? Microchips."
@listen(and_(start_method, second_method))
def logger(self):
print("---- Logger ----")
print(self.state)
flow = AndExampleFlow()
flow.plot()
flow.kickoff()
# (O código não é traduzido)
```
```text Output
@@ -544,42 +323,8 @@ O decorador `@router()` nos flows permite definir lógica de roteamento condicio
Você pode especificar diferentes rotas conforme a saída do método, permitindo controlar o fluxo de execução de forma dinâmica.
<CodeGroup>
```python Code
import random
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class ExampleState(BaseModel):
success_flag: bool = False
class RouterFlow(Flow[ExampleState]):
@start()
def start_method(self):
print("Starting the structured flow")
random_boolean = random.choice([True, False])
self.state.success_flag = random_boolean
@router(start_method)
def second_method(self):
if self.state.success_flag:
return "success"
else:
return "failed"
@listen("success")
def third_method(self):
print("Third method running")
@listen("failed")
def fourth_method(self):
print("Fourth method running")
flow = RouterFlow()
flow.plot("my_flow_plot")
flow.kickoff()
# (O código não é traduzido)
```
```text Output
@@ -656,105 +401,7 @@ Para um guia completo sobre feedback humano em flows, incluindo feedback assínc
Os agentes podem ser integrados facilmente aos seus flows, oferecendo uma alternativa leve às crews completas quando você precisar executar tarefas simples e focadas. Veja um exemplo de como utilizar um agente em um flow para realizar uma pesquisa de mercado:
```python
import asyncio
from typing import Any, Dict, List
from crewai_tools import SerperDevTool
from pydantic import BaseModel, Field
from crewai.agent import Agent
from crewai.flow.flow import Flow, listen, start
# Define um formato de saída estruturado
class MarketAnalysis(BaseModel):
key_trends: List[str] = Field(description="List of identified market trends")
market_size: str = Field(description="Estimated market size")
competitors: List[str] = Field(description="Major competitors in the space")
# Define o estado do flow
class MarketResearchState(BaseModel):
product: str = ""
analysis: MarketAnalysis | None = None
# Cria uma classe de flow
class MarketResearchFlow(Flow[MarketResearchState]):
@start()
def initialize_research(self) -> Dict[str, Any]:
print(f"Starting market research for {self.state.product}")
return {"product": self.state.product}
@listen(initialize_research)
async def analyze_market(self) -> Dict[str, Any]:
# Cria um agente para pesquisa de mercado
analyst = Agent(
role="Market Research Analyst",
goal=f"Analyze the market for {self.state.product}",
backstory="You are an experienced market analyst with expertise in "
"identifying market trends and opportunities.",
tools=[SerperDevTool()],
verbose=True,
)
# Define a consulta de pesquisa
query = f"""
Research the market for {self.state.product}. Include:
1. Key market trends
2. Market size
3. Major competitors
Format your response according to the specified structure.
"""
# Executa a análise com formato de saída estruturado
result = await analyst.kickoff_async(query, response_format=MarketAnalysis)
if result.pydantic:
print("result", result.pydantic)
else:
print("result", result)
# Retorna a análise para atualizar o estado
return {"analysis": result.pydantic}
@listen(analyze_market)
def present_results(self, analysis) -> None:
print("\nMarket Analysis Results")
print("=====================")
if isinstance(analysis, dict):
# Se recebemos um dict com a chave 'analysis', extrai o objeto de análise real
market_analysis = analysis.get("analysis")
else:
market_analysis = analysis
if market_analysis and isinstance(market_analysis, MarketAnalysis):
print("\nKey Market Trends:")
for trend in market_analysis.key_trends:
print(f"- {trend}")
print(f"\nMarket Size: {market_analysis.market_size}")
print("\nMajor Competitors:")
for competitor in market_analysis.competitors:
print(f"- {competitor}")
else:
print("No structured analysis data available.")
print("Raw analysis:", analysis)
# Exemplo de uso
async def run_flow():
flow = MarketResearchFlow()
flow.plot("MarketResearchFlowPlot")
result = await flow.kickoff_async(inputs={"product": "AI-powered chatbots"})
return result
# Executa o flow
if __name__ == "__main__":
asyncio.run(run_flow())
# (O código não é traduzido)
```
![Flow Visual image](/images/crewai-flow-7.png)
@@ -816,50 +463,7 @@ No arquivo `main.py`, você cria seu flow e conecta as crews. É possível defin
Veja um exemplo de como conectar a `poem_crew` no arquivo `main.py`:
```python Code
#!/usr/bin/env python
from random import randint
from pydantic import BaseModel
from crewai.flow.flow import Flow, listen, start
from .crews.poem_crew.poem_crew import PoemCrew
class PoemState(BaseModel):
sentence_count: int = 1
poem: str = ""
class PoemFlow(Flow[PoemState]):
@start()
def generate_sentence_count(self):
print("Generating sentence count")
self.state.sentence_count = randint(1, 5)
@listen(generate_sentence_count)
def generate_poem(self):
print("Generating poem")
result = PoemCrew().crew().kickoff(inputs={"sentence_count": self.state.sentence_count})
print("Poem generated", result.raw)
self.state.poem = result.raw
@listen(generate_poem)
def save_poem(self):
print("Saving poem")
with open("poem.txt", "w") as f:
f.write(self.state.poem)
def kickoff():
poem_flow = PoemFlow()
poem_flow.kickoff()
def plot():
poem_flow = PoemFlow()
poem_flow.plot("PoemFlowPlot")
if __name__ == "__main__":
kickoff()
plot()
# (O código não é traduzido)
```
Neste exemplo, a classe `PoemFlow` define um fluxo que gera a quantidade de frases, usa a `PoemCrew` para gerar um poema e, depois, salva o poema em um arquivo. O flow inicia com o método `kickoff()`, e o gráfico é gerado pelo método `plot()`.
@@ -911,8 +515,7 @@ O CrewAI oferece duas formas práticas de gerar plots dos seus flows:
Se estiver trabalhando diretamente com uma instância do flow, basta chamar o método `plot()` do objeto. Isso criará um arquivo HTML com o plot interativo do seu flow.
```python Code
# Considerando que você já tem uma instância do flow
flow.plot("my_flow_plot")
# (O código não é traduzido)
```
Esse comando gera um arquivo chamado `my_flow_plot.html` no diretório atual. Abra esse arquivo em um navegador para visualizar o plot interativo.

View File

@@ -63,60 +63,7 @@ Com estado não estruturado:
Veja um exemplo simples de gerenciamento de estado não estruturado:
```python
from crewai.flow.flow import Flow, listen, start
class UnstructuredStateFlow(Flow):
@start()
def initialize_data(self):
print("Initializing flow data")
# Adiciona pares chave-valor ao estado
self.state["user_name"] = "Alex"
self.state["preferences"] = {
"theme": "dark",
"language": "English"
}
self.state["items"] = []
# O estado do flow recebe automaticamente um ID único
print(f"Flow ID: {self.state['id']}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Previous step returned: {previous_result}")
# Acessa e modifica o estado
user = self.state["user_name"]
print(f"Processing data for {user}")
# Adiciona itens a uma lista no estado
self.state["items"].append("item1")
self.state["items"].append("item2")
# Adiciona um novo par chave-valor
self.state["processed"] = True
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Acessa múltiplos valores do estado
user = self.state["user_name"]
theme = self.state["preferences"]["theme"]
items = self.state["items"]
processed = self.state.get("processed", False)
summary = f"User {user} has {len(items)} items with {theme} theme. "
summary += "Data is processed." if processed else "Data is not processed."
return summary
# Executa o flow
flow = UnstructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
# código não traduzido
```
### Quando Usar Estado Não Estruturado
@@ -147,63 +94,7 @@ Ao utilizar estado estruturado:
Veja como implementar o gerenciamento de estado estruturado:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
# Define o modelo de estado
class UserPreferences(BaseModel):
theme: str = "light"
language: str = "English"
class AppState(BaseModel):
user_name: str = ""
preferences: UserPreferences = UserPreferences()
items: List[str] = []
processed: bool = False
completion_percentage: float = 0.0
# Cria um flow com estado tipado
class StructuredStateFlow(Flow[AppState]):
@start()
def initialize_data(self):
print("Initializing flow data")
# Define valores do estado (com checagem de tipo)
self.state.user_name = "Taylor"
self.state.preferences.theme = "dark"
# O campo ID está disponível automaticamente
print(f"Flow ID: {self.state.id}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Processing data for {self.state.user_name}")
# Modifica o estado (com checagem de tipo)
self.state.items.append("item1")
self.state.items.append("item2")
self.state.processed = True
self.state.completion_percentage = 50.0
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Acessa o estado (com autocompletar)
summary = f"User {self.state.user_name} has {len(self.state.items)} items "
summary += f"with {self.state.preferences.theme} theme. "
summary += "Data is processed." if self.state.processed else "Data is not processed."
summary += f" Completion: {self.state.completion_percentage}%"
return summary
# Executa o flow
flow = StructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
# código não traduzido
```
### Benefícios do Estado Estruturado
@@ -247,29 +138,7 @@ Independente de você usar estado estruturado ou não estruturado, é possível
Métodos do flow podem retornar valores que serão passados como argumento para métodos listeners:
```python
from crewai.flow.flow import Flow, listen, start
class DataPassingFlow(Flow):
@start()
def generate_data(self):
# Este valor de retorno será passado para os métodos listeners
return "Generated data"
@listen(generate_data)
def process_data(self, data_from_previous_step):
print(f"Received: {data_from_previous_step}")
# Você pode modificar os dados e repassá-los adiante
processed_data = f"{data_from_previous_step} - processed"
# Também atualiza o estado
self.state["last_processed"] = processed_data
return processed_data
@listen(process_data)
def finalize_data(self, processed_data):
print(f"Received processed data: {processed_data}")
# Acessa tanto os dados passados quanto o estado
last_processed = self.state.get("last_processed", "")
return f"Final: {processed_data} (from state: {last_processed})"
# código não traduzido
```
Esse padrão permite combinar passagem de dados direta com atualizações de estado para obter máxima flexibilidade.
@@ -287,36 +156,7 @@ O decorador `@persist()` automatiza a persistência de estado, salvando o estado
Ao aplicar em nível de classe, `@persist()` salva o estado após cada execução de método:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
value: int = 0
@persist() # Aplica à classe inteira do flow
class PersistentCounterFlow(Flow[CounterState]):
@start()
def increment(self):
self.state.value += 1
print(f"Incremented to {self.state.value}")
return self.state.value
@listen(increment)
def double(self, value):
self.state.value = value * 2
print(f"Doubled to {self.state.value}")
return self.state.value
# Primeira execução
flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Segunda execução - passa o ID para carregar o estado persistido
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
print(f"Second run result: {result2}") # Será maior devido ao estado persistido
# código não traduzido
```
#### Persistência em Nível de Método
@@ -324,26 +164,7 @@ print(f"Second run result: {result2}") # Será maior devido ao estado persistid
Para mais controle, você pode aplicar `@persist()` em métodos específicos:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
class SelectivePersistFlow(Flow):
@start()
def first_step(self):
self.state["count"] = 1
return "First step"
@persist() # Persiste apenas após este método
@listen(first_step)
def important_step(self, prev_result):
self.state["count"] += 1
self.state["important_data"] = "This will be persisted"
return "Important step completed"
@listen(important_step)
def final_step(self, prev_result):
self.state["count"] += 1
return f"Complete with count {self.state['count']}"
# código não traduzido
```
#### Forking de Estado Persistido
@@ -395,45 +216,7 @@ Notas sobre o comportamento:
Você pode usar o estado para implementar lógicas condicionais complexas em seus flows:
```python
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class PaymentState(BaseModel):
amount: float = 0.0
is_approved: bool = False
retry_count: int = 0
class PaymentFlow(Flow[PaymentState]):
@start()
def process_payment(self):
# Simula o processamento do pagamento
self.state.amount = 100.0
self.state.is_approved = self.state.amount < 1000
return "Payment processed"
@router(process_payment)
def check_approval(self, previous_result):
if self.state.is_approved:
return "approved"
elif self.state.retry_count < 3:
return "retry"
else:
return "rejected"
@listen("approved")
def handle_approval(self):
return f"Payment of ${self.state.amount} approved!"
@listen("retry")
def handle_retry(self):
self.state.retry_count += 1
print(f"Retrying payment (attempt {self.state.retry_count})...")
# Aqui poderia ser implementada a lógica de retry
return "Retry initiated"
@listen("rejected")
def handle_rejection(self):
return f"Payment of ${self.state.amount} rejected after {self.state.retry_count} retries."
# código não traduzido
```
### Manipulações Complexas de Estado
@@ -441,60 +224,7 @@ class PaymentFlow(Flow[PaymentState]):
Para transformar estados complexos, você pode criar métodos dedicados:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
from typing import List, Dict
class UserData(BaseModel):
name: str
active: bool = True
login_count: int = 0
class ComplexState(BaseModel):
users: Dict[str, UserData] = {}
active_user_count: int = 0
class TransformationFlow(Flow[ComplexState]):
@start()
def initialize(self):
# Adiciona alguns usuários
self.add_user("alice", "Alice")
self.add_user("bob", "Bob")
self.add_user("charlie", "Charlie")
return "Initialized"
@listen(initialize)
def process_users(self, _):
# Incrementa contagens de login
for user_id in self.state.users:
self.increment_login(user_id)
# Desativa um usuário
self.deactivate_user("bob")
# Atualiza a contagem de ativos
self.update_active_count()
return f"Processed {len(self.state.users)} users"
# Métodos auxiliares para transformações de estado
def add_user(self, user_id: str, name: str):
self.state.users[user_id] = UserData(name=name)
self.update_active_count()
def increment_login(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].login_count += 1
def deactivate_user(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].active = False
self.update_active_count()
def update_active_count(self):
self.state.active_user_count = sum(
1 for user in self.state.users.values() if user.active
)
# código não traduzido
```
Esse padrão de criar métodos auxiliares mantém seus métodos de flow limpos, enquanto permite manipulações complexas de estado.
@@ -508,71 +238,7 @@ Um dos padrões mais poderosos na CrewAI é combinar o gerenciamento de estado d
Você pode usar o estado do flow para parametrizar crews:
```python
from crewai.flow.flow import Flow, listen, start
from crewai import Agent, Crew, Process, Task
from pydantic import BaseModel
class ResearchState(BaseModel):
topic: str = ""
depth: str = "medium"
results: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def get_parameters(self):
# Em uma aplicação real, isso pode vir da entrada do usuário
self.state.topic = "Artificial Intelligence Ethics"
self.state.depth = "deep"
return "Parameters set"
@listen(get_parameters)
def execute_research(self, _):
# Cria os agentes
researcher = Agent(
role="Research Specialist",
goal=f"Research {self.state.topic} in {self.state.depth} detail",
backstory="You are an expert researcher with a talent for finding accurate information."
)
writer = Agent(
role="Content Writer",
goal="Transform research into clear, engaging content",
backstory="You excel at communicating complex ideas clearly and concisely."
)
# Cria as tarefas
research_task = Task(
description=f"Research {self.state.topic} with {self.state.depth} analysis",
expected_output="Comprehensive research notes in markdown format",
agent=researcher
)
writing_task = Task(
description=f"Create a summary on {self.state.topic} based on the research",
expected_output="Well-written article in markdown format",
agent=writer,
context=[research_task]
)
# Cria e executa a crew
research_crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
verbose=True
)
# Executa a crew e armazena o resultado no estado
result = research_crew.kickoff()
self.state.results = result.raw
return "Research completed"
@listen(execute_research)
def summarize_results(self, _):
# Acessa os resultados armazenados
result_length = len(self.state.results)
return f"Research on {self.state.topic} completed with {result_length} characters of results."
# código não traduzido
```
### Manipulando Saídas de Crews no Estado
@@ -580,21 +246,7 @@ class ResearchFlow(Flow[ResearchState]):
Quando um crew finaliza, é possível processar sua saída e armazená-la no estado do flow:
```python
@listen(execute_crew)
def process_crew_results(self, _):
# Faz parsing dos resultados brutos (assumindo saída em JSON)
import json
try:
results_dict = json.loads(self.state.raw_results)
self.state.processed_results = {
"title": results_dict.get("title", ""),
"main_points": results_dict.get("main_points", []),
"conclusion": results_dict.get("conclusion", "")
}
return "Results processed successfully"
except json.JSONDecodeError:
self.state.error = "Failed to parse crew results as JSON"
return "Error processing results"
# código não traduzido
```
## Boas Práticas para Gerenciamento de Estado
@@ -604,19 +256,7 @@ def process_crew_results(self, _):
Projete seu estado para conter somente o necessário:
```python
# Abrangente demais
class BloatedState(BaseModel):
user_data: Dict = {}
system_settings: Dict = {}
temporary_calculations: List = []
debug_info: Dict = {}
# ...muitos outros campos
# Melhor: estado focado
class FocusedState(BaseModel):
user_id: str
preferences: Dict[str, str]
completion_status: Dict[str, bool]
# Exemplo não traduzido
```
### 2. Use Estado Estruturado em Flows Complexos
@@ -624,23 +264,7 @@ class FocusedState(BaseModel):
À medida que seus flows evoluem em complexidade, o estado estruturado se torna cada vez mais valioso:
```python
# Flow simples pode usar estado não estruturado
class SimpleGreetingFlow(Flow):
@start()
def greet(self):
self.state["name"] = "World"
return f"Hello, {self.state['name']}!"
# Flow complexo se beneficia de estado estruturado
class UserRegistrationState(BaseModel):
username: str
email: str
verification_status: bool = False
registration_date: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
class RegistrationFlow(Flow[UserRegistrationState]):
# Métodos com acesso ao estado fortemente tipado
# Exemplo não traduzido
```
### 3. Documente Transições de Estado
@@ -648,18 +272,7 @@ class RegistrationFlow(Flow[UserRegistrationState]):
Para flows complexos, documente como o estado muda ao longo da execução:
```python
@start()
def initialize_order(self):
"""
Initialize order state with empty values.
State before: {}
State after: {order_id: str, items: [], status: 'new'}
"""
self.state.order_id = str(uuid.uuid4())
self.state.items = []
self.state.status = "new"
return "Order initialized"
# Exemplo não traduzido
```
### 4. Trate Erros de Estado de Forma Elegante
@@ -667,18 +280,7 @@ def initialize_order(self):
Implemente tratamento de erros ao acessar o estado:
```python
@listen(previous_step)
def process_data(self, _):
try:
# Tenta acessar um valor que pode não existir
user_preference = self.state.preferences.get("theme", "default")
except (AttributeError, KeyError):
# Trata o erro de forma elegante
self.state.errors = self.state.get("errors", [])
self.state.errors.append("Failed to access preferences")
user_preference = "default"
return f"Used preference: {user_preference}"
# Exemplo não traduzido
```
### 5. Use o Estado Para Acompanhar o Progresso
@@ -686,30 +288,7 @@ def process_data(self, _):
Aproveite o estado para monitorar o progresso em flows de longa duração:
```python
class ProgressTrackingFlow(Flow):
@start()
def initialize(self):
self.state["total_steps"] = 3
self.state["current_step"] = 0
self.state["progress"] = 0.0
self.update_progress()
return "Initialized"
def update_progress(self):
"""Helper method to calculate and update progress"""
if self.state.get("total_steps", 0) > 0:
self.state["progress"] = (self.state.get("current_step", 0) /
self.state["total_steps"]) * 100
print(f"Progress: {self.state['progress']:.1f}%")
@listen(initialize)
def step_one(self, _):
# Realiza o trabalho...
self.state["current_step"] = 1
self.update_progress()
return "Step 1 complete"
# Etapas adicionais...
# Exemplo não traduzido
```
### 6. Prefira Operações Imutáveis Quando Possível
@@ -717,22 +296,7 @@ class ProgressTrackingFlow(Flow):
Especialmente com estado estruturado, prefira operações imutáveis para maior clareza:
```python
# Em vez de modificar listas no local:
self.state.items.append(new_item) # Operação mutável
# Considere criar um novo estado:
from pydantic import BaseModel
from typing import List
class ItemState(BaseModel):
items: List[str] = []
class ImmutableFlow(Flow[ItemState]):
@start()
def add_item(self):
# Cria uma nova lista com o item adicionado
self.state.items = [*self.state.items, "new item"]
return "Item added"
# Exemplo não traduzido
```
## Depurando o Estado do Flow
@@ -742,24 +306,7 @@ class ImmutableFlow(Flow[ItemState]):
Ao desenvolver, adicione logs para acompanhar mudanças no estado:
```python
import logging
logging.basicConfig(level=logging.INFO)
class LoggingFlow(Flow):
def log_state(self, step_name):
logging.info(f"State after {step_name}: {self.state}")
@start()
def initialize(self):
self.state["counter"] = 0
self.log_state("initialize")
return "Initialized"
@listen(initialize)
def increment(self, _):
self.state["counter"] += 1
self.log_state("increment")
return f"Incremented to {self.state['counter']}"
# Exemplo não traduzido
```
### Visualizando o Estado
@@ -767,30 +314,7 @@ class LoggingFlow(Flow):
Você pode adicionar métodos para visualizar seu estado durante o debug:
```python
def visualize_state(self):
"""Create a simple visualization of the current state"""
import json
from rich.console import Console
from rich.panel import Panel
console = Console()
if hasattr(self.state, "model_dump"):
# Pydantic v2
state_dict = self.state.model_dump()
elif hasattr(self.state, "dict"):
# Pydantic v1
state_dict = self.state.dict()
else:
# Estado não estruturado
state_dict = dict(self.state)
# Remove o id para uma saída mais limpa
if "id" in state_dict:
state_dict.pop("id")
state_json = json.dumps(state_dict, indent=2, default=str)
console.print(Panel(state_json, title="Current Flow State"))
# Exemplo não traduzido
```
## Conclusão

View File

@@ -142,6 +142,9 @@ contextual = [
daytona = [
"daytona~=0.140.0",
]
opensandbox = [
"opensandbox>=0.1.8",
]
e2b = [
"e2b~=2.20.0",

View File

@@ -119,6 +119,11 @@ from crewai_tools.tools.multion_tool.multion_tool import MultiOnTool
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool
from crewai_tools.tools.ocr_tool.ocr_tool import OCRTool
from crewai_tools.tools.open_sandbox_tool import (
OpenSandboxBaseTool,
OpenSandboxExecTool,
OpenSandboxFileTool,
)
from crewai_tools.tools.oxylabs_amazon_product_scraper_tool.oxylabs_amazon_product_scraper_tool import (
OxylabsAmazonProductScraperTool,
)
@@ -282,6 +287,9 @@ __all__ = [
"MySQLSearchTool",
"NL2SQLTool",
"OCRTool",
"OpenSandboxBaseTool",
"OpenSandboxExecTool",
"OpenSandboxFileTool",
"OxylabsAmazonProductScraperTool",
"OxylabsAmazonSearchScraperTool",
"OxylabsGoogleSearchScraperTool",

View File

@@ -109,6 +109,11 @@ from crewai_tools.tools.multion_tool.multion_tool import MultiOnTool
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool
from crewai_tools.tools.ocr_tool.ocr_tool import OCRTool
from crewai_tools.tools.open_sandbox_tool import (
OpenSandboxBaseTool,
OpenSandboxExecTool,
OpenSandboxFileTool,
)
from crewai_tools.tools.oxylabs_amazon_product_scraper_tool.oxylabs_amazon_product_scraper_tool import (
OxylabsAmazonProductScraperTool,
)
@@ -266,6 +271,9 @@ __all__ = [
"MySQLSearchTool",
"NL2SQLTool",
"OCRTool",
"OpenSandboxBaseTool",
"OpenSandboxExecTool",
"OpenSandboxFileTool",
"OxylabsAmazonProductScraperTool",
"OxylabsAmazonSearchScraperTool",
"OxylabsGoogleSearchScraperTool",

View File

@@ -0,0 +1,16 @@
from crewai_tools.tools.open_sandbox_tool.open_sandbox_base_tool import (
OpenSandboxBaseTool,
)
from crewai_tools.tools.open_sandbox_tool.open_sandbox_exec_tool import (
OpenSandboxExecTool,
)
from crewai_tools.tools.open_sandbox_tool.open_sandbox_file_tool import (
OpenSandboxFileTool,
)
__all__ = [
"OpenSandboxBaseTool",
"OpenSandboxExecTool",
"OpenSandboxFileTool",
]

View File

@@ -0,0 +1,229 @@
from __future__ import annotations
import atexit
import logging
import os
import threading
from typing import Any, ClassVar
from crewai.tools import BaseTool, EnvVar
from pydantic import ConfigDict, Field, PrivateAttr
logger = logging.getLogger(__name__)
class OpenSandboxBaseTool(BaseTool):
"""Shared base for tools that act on an Open Sandbox sandbox.
Lifecycle modes:
- persistent=False (default): create a fresh sandbox per `_run` call and
kill it when the call returns. Safer and stateless — nothing leaks if
the agent forgets cleanup.
- persistent=True: lazily create a single sandbox on first use, cache it
on the instance, and register an atexit hook to kill it at process
exit. Cheaper across many calls and lets files/state carry over.
- sandbox_id=<existing>: attach to a sandbox the caller already owns.
Never killed by the tool.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
package_dependencies: list[str] = Field(default_factory=lambda: ["opensandbox"])
api_key: str | None = Field(
default_factory=lambda: os.getenv("OPEN_SANDBOX_API_KEY"),
description="Open Sandbox API key. Falls back to OPEN_SANDBOX_API_KEY env var.",
json_schema_extra={"required": False},
)
domain: str | None = Field(
default_factory=lambda: os.getenv("OPEN_SANDBOX_DOMAIN"),
description=(
"Open Sandbox management API domain (e.g. 'api.opensandbox.io'). "
"Falls back to OPEN_SANDBOX_DOMAIN env var."
),
json_schema_extra={"required": False},
)
protocol: str | None = Field(
default=None,
description="Protocol for the management API ('http' or 'https').",
json_schema_extra={"required": False},
)
persistent: bool = Field(
default=False,
description=(
"If True, reuse one sandbox across all calls to this tool instance "
"and kill it at process exit. Default False creates and kills a "
"fresh sandbox per call."
),
)
sandbox_id: str | None = Field(
default=None,
description=(
"Attach to an existing sandbox by id instead of creating a new one. "
"The tool will never kill a sandbox it did not create."
),
)
create_params: dict[str, Any] | None = Field(
default=None,
description=(
"Optional kwargs forwarded to SandboxSync.create when creating a "
"sandbox (e.g. image, env, resource, metadata, entrypoint)."
),
)
sandbox_timeout: float = Field(
default=60.0,
description=(
"Timeout in seconds to wait for sandbox readiness on create/connect."
),
)
env_vars: list[EnvVar] = Field(
default_factory=lambda: [
EnvVar(
name="OPEN_SANDBOX_API_KEY",
description="API key for Open Sandbox service",
required=False,
),
EnvVar(
name="OPEN_SANDBOX_DOMAIN",
description="Open Sandbox management API domain (optional)",
required=False,
),
]
)
_client: Any | None = PrivateAttr(default=None)
_persistent_sandbox: Any | None = PrivateAttr(default=None)
_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
_cleanup_registered: bool = PrivateAttr(default=False)
_sdk_cache: ClassVar[dict[str, Any]] = {}
@classmethod
def _import_sdk(cls) -> dict[str, Any]:
if cls._sdk_cache:
return cls._sdk_cache
try:
from opensandbox.config.connection_sync import ConnectionConfigSync
from opensandbox.models.execd import RunCommandOpts
from opensandbox.models.filesystem import SearchEntry, WriteEntry
from opensandbox.sync.sandbox import SandboxSync
except ImportError as exc:
raise ImportError(
"The 'opensandbox' package is required for Open Sandbox tools. "
"Install it with: uv add opensandbox (or) pip install opensandbox"
) from exc
cls._sdk_cache = {
"SandboxSync": SandboxSync,
"ConnectionConfigSync": ConnectionConfigSync,
"RunCommandOpts": RunCommandOpts,
"WriteEntry": WriteEntry,
"SearchEntry": SearchEntry,
}
return cls._sdk_cache
def _get_client(self) -> Any:
"""Return a cached ConnectionConfigSync built from this tool's fields.
Open Sandbox has no separate "client" object — connection settings are
carried by ConnectionConfigSync and passed into SandboxSync.create /
SandboxSync.connect. We cache one config per tool instance.
"""
if self._client is not None:
return self._client
sdk = self._import_sdk()
config_kwargs: dict[str, Any] = {}
if self.api_key:
config_kwargs["api_key"] = self.api_key
if self.domain:
config_kwargs["domain"] = self.domain
if self.protocol:
config_kwargs["protocol"] = self.protocol
self._client = sdk["ConnectionConfigSync"](**config_kwargs)
return self._client
def _build_create_kwargs(self) -> dict[str, Any]:
return dict(self.create_params) if self.create_params else {}
def _acquire_sandbox(self) -> tuple[Any, bool]:
"""Return (sandbox, should_kill_after_use)."""
sdk = self._import_sdk()
config = self._get_client()
if self.sandbox_id:
sandbox = sdk["SandboxSync"].connect(
self.sandbox_id,
connection_config=config,
connect_timeout=_seconds_to_timedelta(self.sandbox_timeout),
)
return sandbox, False
if self.persistent:
with self._lock:
if self._persistent_sandbox is None:
self._persistent_sandbox = sdk["SandboxSync"].create(
connection_config=config,
ready_timeout=_seconds_to_timedelta(self.sandbox_timeout),
**self._build_create_kwargs(),
)
if not self._cleanup_registered:
atexit.register(self.close)
self._cleanup_registered = True
return self._persistent_sandbox, False
sandbox = sdk["SandboxSync"].create(
connection_config=config,
ready_timeout=_seconds_to_timedelta(self.sandbox_timeout),
**self._build_create_kwargs(),
)
return sandbox, True
def _release_sandbox(self, sandbox: Any, should_kill: bool) -> None:
if not should_kill:
return
try:
sandbox.kill()
except Exception:
logger.debug(
"Best-effort sandbox kill failed after ephemeral use; "
"the sandbox may need manual termination.",
exc_info=True,
)
try:
sandbox.close()
except Exception:
logger.debug(
"Best-effort sandbox local-resource close failed after ephemeral use.",
exc_info=True,
)
def close(self) -> None:
"""Kill the cached persistent sandbox if one exists."""
with self._lock:
sandbox = self._persistent_sandbox
self._persistent_sandbox = None
if sandbox is None:
return
try:
sandbox.kill()
except Exception:
logger.debug(
"Best-effort persistent sandbox kill failed at close(); "
"the sandbox may need manual termination.",
exc_info=True,
)
try:
sandbox.close()
except Exception:
logger.debug(
"Best-effort persistent sandbox local-resource close failed at close().",
exc_info=True,
)
def _seconds_to_timedelta(seconds: float) -> Any:
from datetime import timedelta
return timedelta(seconds=seconds)

View File

@@ -0,0 +1,102 @@
from __future__ import annotations
from builtins import type as type_
from datetime import timedelta
from typing import Any
from pydantic import BaseModel, Field
from crewai_tools.tools.open_sandbox_tool.open_sandbox_base_tool import (
OpenSandboxBaseTool,
)
class OpenSandboxExecToolSchema(BaseModel):
command: str = Field(..., description="Shell command to execute in the sandbox.")
cwd: str | None = Field(
default=None,
description="Working directory to run the command in. Defaults to the sandbox work dir.",
)
env: dict[str, str] | None = Field(
default=None,
description="Optional environment variables to set for this command.",
)
timeout: int | None = Field(
default=None,
description="Maximum seconds to wait for the command to finish.",
)
class OpenSandboxExecTool(OpenSandboxBaseTool):
"""Run a shell command inside an Open Sandbox sandbox."""
name: str = "Open Sandbox Exec"
description: str = (
"Execute a shell command inside an Open Sandbox sandbox and return the "
"exit code and combined output. Use this to run builds, package installs, "
"git operations, or any one-off shell command."
)
args_schema: type_[BaseModel] = OpenSandboxExecToolSchema
def _run(
self,
command: str,
cwd: str | None = None,
env: dict[str, str] | None = None,
timeout: int | None = None,
) -> Any:
sdk = self._import_sdk()
sandbox, should_kill = self._acquire_sandbox()
try:
opts = self._build_run_opts(sdk, cwd=cwd, env=env, timeout=timeout)
execution = sandbox.commands.run(command, opts=opts)
return {
"exit_code": getattr(execution, "exit_code", None),
"result": getattr(execution, "text", None),
"artifacts": _collect_artifacts(execution),
}
finally:
self._release_sandbox(sandbox, should_kill)
@staticmethod
def _build_run_opts(
sdk: dict[str, Any],
*,
cwd: str | None,
env: dict[str, str] | None,
timeout: int | None,
) -> Any | None:
if cwd is None and env is None and timeout is None:
return None
kwargs: dict[str, Any] = {}
if cwd is not None:
kwargs["working_directory"] = cwd
if env is not None:
kwargs["envs"] = env
if timeout is not None:
kwargs["timeout"] = timedelta(seconds=timeout)
return sdk["RunCommandOpts"](**kwargs)
def _collect_artifacts(execution: Any) -> dict[str, Any] | None:
logs = getattr(execution, "logs", None)
stderr_msgs = getattr(logs, "stderr", None) if logs is not None else None
results = getattr(execution, "result", None)
error = getattr(execution, "error", None)
if not stderr_msgs and not results and error is None:
return None
return {
"stderr": [getattr(m, "text", str(m)) for m in stderr_msgs or []],
"results": [getattr(r, "text", None) for r in results or []],
"error": _serialize_error(error),
}
def _serialize_error(error: Any) -> dict[str, Any] | None:
if error is None:
return None
return {
"name": getattr(error, "name", None),
"value": getattr(error, "value", None),
"traceback": getattr(error, "traceback", None),
}

View File

@@ -0,0 +1,228 @@
from __future__ import annotations
import base64
from builtins import type as type_
import logging
import posixpath
from typing import Any, Literal
from pydantic import BaseModel, Field, model_validator
from crewai_tools.tools.open_sandbox_tool.open_sandbox_base_tool import (
OpenSandboxBaseTool,
)
logger = logging.getLogger(__name__)
FileAction = Literal["read", "write", "append", "list", "delete", "mkdir", "info"]
class OpenSandboxFileToolSchema(BaseModel):
action: FileAction = Field(
...,
description=(
"The filesystem action to perform: 'read' (returns file contents), "
"'write' (create or replace a file with content), 'append' (append "
"content to an existing file — use this for writing large files in "
"chunks to avoid hitting tool-call size limits), 'list' (lists a "
"directory), 'delete' (removes a file/dir), 'mkdir' (creates a "
"directory), 'info' (returns file metadata)."
),
)
path: str = Field(..., description="Absolute path inside the sandbox.")
content: str | None = Field(
default=None,
description=(
"Content to write or append. If omitted for 'write', an empty file "
"is created. For files larger than a few KB, prefer one 'write' "
"with empty content followed by multiple 'append' calls of ~4KB "
"each to stay within tool-call payload limits."
),
)
binary: bool = Field(
default=False,
description=(
"For 'write': treat content as base64 and upload raw bytes. "
"For 'read': return contents as base64 instead of decoded utf-8."
),
)
recursive: bool = Field(
default=False,
description="For action='delete': remove a directory recursively.",
)
mode: int = Field(
default=755,
description="For action='mkdir': Unix file mode as an integer (default 755).",
)
@model_validator(mode="after")
def _validate_action_args(self) -> OpenSandboxFileToolSchema:
if self.action == "append" and self.content is None:
raise ValueError(
"action='append' requires 'content'. Pass the chunk to append "
"in the 'content' field."
)
return self
class OpenSandboxFileTool(OpenSandboxBaseTool):
"""Read, write, and manage files inside an Open Sandbox sandbox.
Notes:
- Most useful with `persistent=True` or an explicit `sandbox_id`. With the
default ephemeral mode, files disappear when this tool call finishes.
"""
name: str = "Open Sandbox File"
description: str = (
"Perform filesystem operations inside an Open Sandbox sandbox: read a "
"file, write content to a path, append content to an existing file, "
"list a directory, delete a path, make a directory, or fetch file "
"metadata. For files larger than a few KB, create the file with "
"action='write' and empty content, then send the body via multiple "
"'append' calls of ~4KB each to stay within tool-call payload limits."
)
args_schema: type_[BaseModel] = OpenSandboxFileToolSchema
def _run(
self,
action: FileAction,
path: str,
content: str | None = None,
binary: bool = False,
recursive: bool = False,
mode: int = 755,
) -> Any:
sandbox, should_kill = self._acquire_sandbox()
try:
if action == "read":
return self._read(sandbox, path, binary=binary)
if action == "write":
return self._write(sandbox, path, content or "", binary=binary)
if action == "append":
return self._append(sandbox, path, content or "", binary=binary)
if action == "list":
return self._list(sandbox, path)
if action == "delete":
return self._delete(sandbox, path, recursive=recursive)
if action == "mkdir":
return self._mkdir(sandbox, path, mode=mode)
if action == "info":
return self._info(sandbox, path)
raise ValueError(f"Unknown action: {action}")
finally:
self._release_sandbox(sandbox, should_kill)
def _read(self, sandbox: Any, path: str, *, binary: bool) -> dict[str, Any]:
if binary:
data: bytes = sandbox.files.read_bytes(path)
return {
"path": path,
"encoding": "base64",
"content": base64.b64encode(data).decode("ascii"),
}
try:
text: str = sandbox.files.read_file(path)
return {"path": path, "encoding": "utf-8", "content": text}
except UnicodeDecodeError:
data = sandbox.files.read_bytes(path)
return {
"path": path,
"encoding": "base64",
"content": base64.b64encode(data).decode("ascii"),
"note": "File was not valid utf-8; returned as base64.",
}
def _write(
self, sandbox: Any, path: str, content: str, *, binary: bool
) -> dict[str, Any]:
payload = base64.b64decode(content) if binary else content.encode("utf-8")
self._ensure_parent_dir(sandbox, path)
sandbox.files.write_file(path, payload)
return {"status": "written", "path": path, "bytes": len(payload)}
def _append(
self, sandbox: Any, path: str, content: str, *, binary: bool
) -> dict[str, Any]:
chunk = base64.b64decode(content) if binary else content.encode("utf-8")
self._ensure_parent_dir(sandbox, path)
try:
existing: bytes = sandbox.files.read_bytes(path)
except Exception:
existing = b""
payload = existing + chunk
sandbox.files.write_file(path, payload)
return {
"status": "appended",
"path": path,
"appended_bytes": len(chunk),
"total_bytes": len(payload),
}
def _ensure_parent_dir(self, sandbox: Any, path: str) -> None:
"""Make sure the parent directory of `path` exists.
Best-effort mkdir of the parent; any error (e.g. already exists) is
swallowed because create_directories may not be idempotent.
"""
parent = posixpath.dirname(path)
if not parent or parent in ("/", "."):
return
sdk = self._import_sdk()
try:
sandbox.files.create_directories([sdk["WriteEntry"](path=parent, mode=755)])
except Exception:
logger.debug(
"Best-effort parent-directory create failed for %s; "
"assuming it already exists and proceeding with the write.",
parent,
exc_info=True,
)
def _mkdir(self, sandbox: Any, path: str, *, mode: int) -> dict[str, Any]:
sdk = self._import_sdk()
sandbox.files.create_directories([sdk["WriteEntry"](path=path, mode=mode)])
return {"status": "created", "path": path, "mode": mode}
def _delete(self, sandbox: Any, path: str, *, recursive: bool) -> dict[str, Any]:
if recursive:
sandbox.files.delete_directories([path])
else:
sandbox.files.delete_files([path])
return {"status": "deleted", "path": path}
def _list(self, sandbox: Any, path: str) -> dict[str, Any]:
sdk = self._import_sdk()
entries = sandbox.files.search(sdk["SearchEntry"](path=path, pattern="*"))
return {
"path": path,
"entries": [self._entry_info_to_dict(entry) for entry in entries],
}
def _info(self, sandbox: Any, path: str) -> dict[str, Any]:
info_map = sandbox.files.get_file_info([path])
info = info_map.get(path) if hasattr(info_map, "get") else None
if info is None:
return {"path": path, "found": False}
return self._entry_info_to_dict(info)
@staticmethod
def _entry_info_to_dict(info: Any) -> dict[str, Any]:
fields = (
"path",
"mode",
"owner",
"group",
"size",
"modified_at",
"created_at",
)
out: dict[str, Any] = {}
for field in fields:
value = getattr(info, field, None)
if hasattr(value, "isoformat"):
value = value.isoformat()
out[field] = value
return out

View File

@@ -16318,6 +16318,565 @@
"type": "object"
}
},
{
"description": "",
"env_vars": [
{
"default": null,
"description": "API key for Open Sandbox service",
"name": "OPEN_SANDBOX_API_KEY",
"required": false
},
{
"default": null,
"description": "Open Sandbox management API domain (optional)",
"name": "OPEN_SANDBOX_DOMAIN",
"required": false
}
],
"humanized_name": "OpenSandboxBaseTool",
"init_params_schema": {
"$defs": {
"EnvVar": {
"properties": {
"default": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Default"
},
"description": {
"title": "Description",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"required": {
"default": true,
"title": "Required",
"type": "boolean"
}
},
"required": [
"name",
"description"
],
"title": "EnvVar",
"type": "object"
}
},
"description": "Shared base for tools that act on an Open Sandbox sandbox.\n\nLifecycle modes:\n - persistent=False (default): create a fresh sandbox per `_run` call and\n kill it when the call returns. Safer and stateless \u2014 nothing leaks if\n the agent forgets cleanup.\n - persistent=True: lazily create a single sandbox on first use, cache it\n on the instance, and register an atexit hook to kill it at process\n exit. Cheaper across many calls and lets files/state carry over.\n - sandbox_id=<existing>: attach to a sandbox the caller already owns.\n Never killed by the tool.",
"properties": {
"api_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Open Sandbox API key. Falls back to OPEN_SANDBOX_API_KEY env var.",
"required": false,
"title": "Api Key"
},
"create_params": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional kwargs forwarded to SandboxSync.create when creating a sandbox (e.g. image, env, resource, metadata, entrypoint).",
"title": "Create Params"
},
"domain": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Open Sandbox management API domain (e.g. 'api.opensandbox.io'). Falls back to OPEN_SANDBOX_DOMAIN env var.",
"required": false,
"title": "Domain"
},
"persistent": {
"default": false,
"description": "If True, reuse one sandbox across all calls to this tool instance and kill it at process exit. Default False creates and kills a fresh sandbox per call.",
"title": "Persistent",
"type": "boolean"
},
"protocol": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Protocol for the management API ('http' or 'https').",
"required": false,
"title": "Protocol"
},
"sandbox_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Attach to an existing sandbox by id instead of creating a new one. The tool will never kill a sandbox it did not create.",
"title": "Sandbox Id"
},
"sandbox_timeout": {
"default": 60.0,
"description": "Timeout in seconds to wait for sandbox readiness on create/connect.",
"title": "Sandbox Timeout",
"type": "number"
}
},
"required": [],
"title": "OpenSandboxBaseTool",
"type": "object"
},
"name": "OpenSandboxBaseTool",
"package_dependencies": [
"opensandbox"
],
"run_params_schema": {
"properties": {},
"title": "_ArgsSchemaPlaceholder",
"type": "object"
}
},
{
"description": "Execute a shell command inside an Open Sandbox sandbox and return the exit code and combined output. Use this to run builds, package installs, git operations, or any one-off shell command.",
"env_vars": [
{
"default": null,
"description": "API key for Open Sandbox service",
"name": "OPEN_SANDBOX_API_KEY",
"required": false
},
{
"default": null,
"description": "Open Sandbox management API domain (optional)",
"name": "OPEN_SANDBOX_DOMAIN",
"required": false
}
],
"humanized_name": "Open Sandbox Exec",
"init_params_schema": {
"$defs": {
"EnvVar": {
"properties": {
"default": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Default"
},
"description": {
"title": "Description",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"required": {
"default": true,
"title": "Required",
"type": "boolean"
}
},
"required": [
"name",
"description"
],
"title": "EnvVar",
"type": "object"
}
},
"description": "Run a shell command inside an Open Sandbox sandbox.",
"properties": {
"api_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Open Sandbox API key. Falls back to OPEN_SANDBOX_API_KEY env var.",
"required": false,
"title": "Api Key"
},
"create_params": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional kwargs forwarded to SandboxSync.create when creating a sandbox (e.g. image, env, resource, metadata, entrypoint).",
"title": "Create Params"
},
"domain": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Open Sandbox management API domain (e.g. 'api.opensandbox.io'). Falls back to OPEN_SANDBOX_DOMAIN env var.",
"required": false,
"title": "Domain"
},
"persistent": {
"default": false,
"description": "If True, reuse one sandbox across all calls to this tool instance and kill it at process exit. Default False creates and kills a fresh sandbox per call.",
"title": "Persistent",
"type": "boolean"
},
"protocol": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Protocol for the management API ('http' or 'https').",
"required": false,
"title": "Protocol"
},
"sandbox_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Attach to an existing sandbox by id instead of creating a new one. The tool will never kill a sandbox it did not create.",
"title": "Sandbox Id"
},
"sandbox_timeout": {
"default": 60.0,
"description": "Timeout in seconds to wait for sandbox readiness on create/connect.",
"title": "Sandbox Timeout",
"type": "number"
}
},
"required": [],
"title": "OpenSandboxExecTool",
"type": "object"
},
"name": "OpenSandboxExecTool",
"package_dependencies": [
"opensandbox"
],
"run_params_schema": {
"properties": {
"command": {
"description": "Shell command to execute in the sandbox.",
"title": "Command",
"type": "string"
},
"cwd": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Working directory to run the command in. Defaults to the sandbox work dir.",
"title": "Cwd"
},
"env": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional environment variables to set for this command.",
"title": "Env"
},
"timeout": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Maximum seconds to wait for the command to finish.",
"title": "Timeout"
}
},
"required": [
"command"
],
"title": "OpenSandboxExecToolSchema",
"type": "object"
}
},
{
"description": "Perform filesystem operations inside an Open Sandbox sandbox: read a file, write content to a path, append content to an existing file, list a directory, delete a path, make a directory, or fetch file metadata. For files larger than a few KB, create the file with action='write' and empty content, then send the body via multiple 'append' calls of ~4KB each to stay within tool-call payload limits.",
"env_vars": [
{
"default": null,
"description": "API key for Open Sandbox service",
"name": "OPEN_SANDBOX_API_KEY",
"required": false
},
{
"default": null,
"description": "Open Sandbox management API domain (optional)",
"name": "OPEN_SANDBOX_DOMAIN",
"required": false
}
],
"humanized_name": "Open Sandbox File",
"init_params_schema": {
"$defs": {
"EnvVar": {
"properties": {
"default": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Default"
},
"description": {
"title": "Description",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"required": {
"default": true,
"title": "Required",
"type": "boolean"
}
},
"required": [
"name",
"description"
],
"title": "EnvVar",
"type": "object"
}
},
"description": "Read, write, and manage files inside an Open Sandbox sandbox.\n\nNotes:\n - Most useful with `persistent=True` or an explicit `sandbox_id`. With the\n default ephemeral mode, files disappear when this tool call finishes.",
"properties": {
"api_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Open Sandbox API key. Falls back to OPEN_SANDBOX_API_KEY env var.",
"required": false,
"title": "Api Key"
},
"create_params": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional kwargs forwarded to SandboxSync.create when creating a sandbox (e.g. image, env, resource, metadata, entrypoint).",
"title": "Create Params"
},
"domain": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Open Sandbox management API domain (e.g. 'api.opensandbox.io'). Falls back to OPEN_SANDBOX_DOMAIN env var.",
"required": false,
"title": "Domain"
},
"persistent": {
"default": false,
"description": "If True, reuse one sandbox across all calls to this tool instance and kill it at process exit. Default False creates and kills a fresh sandbox per call.",
"title": "Persistent",
"type": "boolean"
},
"protocol": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Protocol for the management API ('http' or 'https').",
"required": false,
"title": "Protocol"
},
"sandbox_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Attach to an existing sandbox by id instead of creating a new one. The tool will never kill a sandbox it did not create.",
"title": "Sandbox Id"
},
"sandbox_timeout": {
"default": 60.0,
"description": "Timeout in seconds to wait for sandbox readiness on create/connect.",
"title": "Sandbox Timeout",
"type": "number"
}
},
"required": [],
"title": "OpenSandboxFileTool",
"type": "object"
},
"name": "OpenSandboxFileTool",
"package_dependencies": [
"opensandbox"
],
"run_params_schema": {
"properties": {
"action": {
"description": "The filesystem action to perform: 'read' (returns file contents), 'write' (create or replace a file with content), 'append' (append content to an existing file \u2014 use this for writing large files in chunks to avoid hitting tool-call size limits), 'list' (lists a directory), 'delete' (removes a file/dir), 'mkdir' (creates a directory), 'info' (returns file metadata).",
"enum": [
"read",
"write",
"append",
"list",
"delete",
"mkdir",
"info"
],
"title": "Action",
"type": "string"
},
"binary": {
"default": false,
"description": "For 'write': treat content as base64 and upload raw bytes. For 'read': return contents as base64 instead of decoded utf-8.",
"title": "Binary",
"type": "boolean"
},
"content": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Content to write or append. If omitted for 'write', an empty file is created. For files larger than a few KB, prefer one 'write' with empty content followed by multiple 'append' calls of ~4KB each to stay within tool-call payload limits.",
"title": "Content"
},
"mode": {
"default": 755,
"description": "For action='mkdir': Unix file mode as an integer (default 755).",
"title": "Mode",
"type": "integer"
},
"path": {
"description": "Absolute path inside the sandbox.",
"title": "Path",
"type": "string"
},
"recursive": {
"default": false,
"description": "For action='delete': remove a directory recursively.",
"title": "Recursive",
"type": "boolean"
}
},
"required": [
"action",
"path"
],
"title": "OpenSandboxFileToolSchema",
"type": "object"
}
},
{
"description": "Scrape Amazon product pages with Oxylabs Amazon Product Scraper",
"env_vars": [