mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-23 07:08:14 +00:00
Create tool for generating automations in Studio (#438)
* Create tool for generating automations in Studio This commit creates a tool to use CrewAI Enterprise API to generate crews using CrewAI Studio. * Replace CREWAI_BASE_URL with CREWAI_PLUS_URL * Add missing /crewai_plus in URL
This commit is contained in:
@@ -12,16 +12,16 @@ from .tools import (
|
|||||||
ApifyActorsTool,
|
ApifyActorsTool,
|
||||||
ArxivPaperTool,
|
ArxivPaperTool,
|
||||||
BraveSearchTool,
|
BraveSearchTool,
|
||||||
BrightDataWebUnlockerTool,
|
|
||||||
BrightDataSearchTool,
|
|
||||||
BrightDataDatasetTool,
|
BrightDataDatasetTool,
|
||||||
|
BrightDataSearchTool,
|
||||||
|
BrightDataWebUnlockerTool,
|
||||||
BrowserbaseLoadTool,
|
BrowserbaseLoadTool,
|
||||||
CodeDocsSearchTool,
|
CodeDocsSearchTool,
|
||||||
CodeInterpreterTool,
|
CodeInterpreterTool,
|
||||||
ComposioTool,
|
ComposioTool,
|
||||||
ContextualAIQueryTool,
|
|
||||||
ContextualAICreateAgentTool,
|
ContextualAICreateAgentTool,
|
||||||
ContextualAIParseTool,
|
ContextualAIParseTool,
|
||||||
|
ContextualAIQueryTool,
|
||||||
ContextualAIRerankTool,
|
ContextualAIRerankTool,
|
||||||
CouchbaseFTSVectorSearchTool,
|
CouchbaseFTSVectorSearchTool,
|
||||||
CrewaiEnterpriseTools,
|
CrewaiEnterpriseTools,
|
||||||
@@ -38,6 +38,7 @@ from .tools import (
|
|||||||
FirecrawlCrawlWebsiteTool,
|
FirecrawlCrawlWebsiteTool,
|
||||||
FirecrawlScrapeWebsiteTool,
|
FirecrawlScrapeWebsiteTool,
|
||||||
FirecrawlSearchTool,
|
FirecrawlSearchTool,
|
||||||
|
GenerateCrewaiAutomationTool,
|
||||||
GithubSearchTool,
|
GithubSearchTool,
|
||||||
HyperbrowserLoadTool,
|
HyperbrowserLoadTool,
|
||||||
InvokeCrewAIAutomationTool,
|
InvokeCrewAIAutomationTool,
|
||||||
|
|||||||
@@ -2,13 +2,20 @@ from .ai_mind_tool.ai_mind_tool import AIMindTool
|
|||||||
from .apify_actors_tool.apify_actors_tool import ApifyActorsTool
|
from .apify_actors_tool.apify_actors_tool import ApifyActorsTool
|
||||||
from .arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool
|
from .arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool
|
||||||
from .brave_search_tool.brave_search_tool import BraveSearchTool
|
from .brave_search_tool.brave_search_tool import BraveSearchTool
|
||||||
|
from .brightdata_tool import (
|
||||||
|
BrightDataDatasetTool,
|
||||||
|
BrightDataSearchTool,
|
||||||
|
BrightDataWebUnlockerTool,
|
||||||
|
)
|
||||||
from .browserbase_load_tool.browserbase_load_tool import BrowserbaseLoadTool
|
from .browserbase_load_tool.browserbase_load_tool import BrowserbaseLoadTool
|
||||||
from .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool
|
from .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool
|
||||||
from .code_interpreter_tool.code_interpreter_tool import CodeInterpreterTool
|
from .code_interpreter_tool.code_interpreter_tool import CodeInterpreterTool
|
||||||
from .composio_tool.composio_tool import ComposioTool
|
from .composio_tool.composio_tool import ComposioTool
|
||||||
from .contextualai_query_tool.contextual_query_tool import ContextualAIQueryTool
|
from .contextualai_create_agent_tool.contextual_create_agent_tool import (
|
||||||
from .contextualai_create_agent_tool.contextual_create_agent_tool import ContextualAICreateAgentTool
|
ContextualAICreateAgentTool,
|
||||||
|
)
|
||||||
from .contextualai_parse_tool.contextual_parse_tool import ContextualAIParseTool
|
from .contextualai_parse_tool.contextual_parse_tool import ContextualAIParseTool
|
||||||
|
from .contextualai_query_tool.contextual_query_tool import ContextualAIQueryTool
|
||||||
from .contextualai_rerank_tool.contextual_rerank_tool import ContextualAIRerankTool
|
from .contextualai_rerank_tool.contextual_rerank_tool import ContextualAIRerankTool
|
||||||
from .couchbase_tool.couchbase_tool import CouchbaseFTSVectorSearchTool
|
from .couchbase_tool.couchbase_tool import CouchbaseFTSVectorSearchTool
|
||||||
from .crewai_enterprise_tools.crewai_enterprise_tools import CrewaiEnterpriseTools
|
from .crewai_enterprise_tools.crewai_enterprise_tools import CrewaiEnterpriseTools
|
||||||
@@ -29,9 +36,14 @@ from .firecrawl_scrape_website_tool.firecrawl_scrape_website_tool import (
|
|||||||
FirecrawlScrapeWebsiteTool,
|
FirecrawlScrapeWebsiteTool,
|
||||||
)
|
)
|
||||||
from .firecrawl_search_tool.firecrawl_search_tool import FirecrawlSearchTool
|
from .firecrawl_search_tool.firecrawl_search_tool import FirecrawlSearchTool
|
||||||
|
from .generate_crewai_automation_tool.generate_crewai_automation_tool import (
|
||||||
|
GenerateCrewaiAutomationTool,
|
||||||
|
)
|
||||||
from .github_search_tool.github_search_tool import GithubSearchTool
|
from .github_search_tool.github_search_tool import GithubSearchTool
|
||||||
from .hyperbrowser_load_tool.hyperbrowser_load_tool import HyperbrowserLoadTool
|
from .hyperbrowser_load_tool.hyperbrowser_load_tool import HyperbrowserLoadTool
|
||||||
from .invoke_crewai_automation_tool.invoke_crewai_automation_tool import InvokeCrewAIAutomationTool
|
from .invoke_crewai_automation_tool.invoke_crewai_automation_tool import (
|
||||||
|
InvokeCrewAIAutomationTool,
|
||||||
|
)
|
||||||
from .json_search_tool.json_search_tool import JSONSearchTool
|
from .json_search_tool.json_search_tool import JSONSearchTool
|
||||||
from .linkup.linkup_search_tool import LinkupSearchTool
|
from .linkup.linkup_search_tool import LinkupSearchTool
|
||||||
from .llamaindex_tool.llamaindex_tool import LlamaIndexTool
|
from .llamaindex_tool.llamaindex_tool import LlamaIndexTool
|
||||||
@@ -108,9 +120,4 @@ from .youtube_channel_search_tool.youtube_channel_search_tool import (
|
|||||||
YoutubeChannelSearchTool,
|
YoutubeChannelSearchTool,
|
||||||
)
|
)
|
||||||
from .youtube_video_search_tool.youtube_video_search_tool import YoutubeVideoSearchTool
|
from .youtube_video_search_tool.youtube_video_search_tool import YoutubeVideoSearchTool
|
||||||
from .brightdata_tool import (
|
|
||||||
BrightDataDatasetTool,
|
|
||||||
BrightDataSearchTool,
|
|
||||||
BrightDataWebUnlockerTool
|
|
||||||
)
|
|
||||||
from .zapier_action_tool.zapier_action_tool import ZapierActionTools
|
from .zapier_action_tool.zapier_action_tool import ZapierActionTools
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# GenerateCrewaiAutomationTool
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The GenerateCrewaiAutomationTool integrates with CrewAI Studio API to generate complete CrewAI automations from natural language descriptions. It translates high-level requirements into functional CrewAI implementations and returns direct links to Studio projects.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Set your CrewAI Personal Access Token (CrewAI Enterprise > Settings > Account > Personal Access Token):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CREWAI_PERSONAL_ACCESS_TOKEN="your_personal_access_token_here"
|
||||||
|
export CREWAI_PLUS_URL="https://app.crewai.com" # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai_tools import GenerateCrewaiAutomationTool
|
||||||
|
from crewai import Agent, Task, Crew
|
||||||
|
|
||||||
|
# Initialize tool
|
||||||
|
tool = GenerateCrewaiAutomationTool()
|
||||||
|
|
||||||
|
# Generate automation
|
||||||
|
result = tool.run(
|
||||||
|
prompt="Generate a CrewAI automation that scrapes websites and stores data in a database",
|
||||||
|
organization_id="org_123" # optional but recommended
|
||||||
|
)
|
||||||
|
|
||||||
|
print(result)
|
||||||
|
# Output: Generated CrewAI Studio project URL: https://studio.crewai.com/project/abc123
|
||||||
|
|
||||||
|
# Use with agent
|
||||||
|
agent = Agent(
|
||||||
|
role="Automation Architect",
|
||||||
|
goal="Generate CrewAI automations",
|
||||||
|
backstory="Expert at creating automated workflows",
|
||||||
|
tools=[tool]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Create a lead qualification automation",
|
||||||
|
agent=agent,
|
||||||
|
expected_output="Studio project URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
result = crew.kickoff()
|
||||||
|
```
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import os
|
||||||
|
from typing import List, Optional, Type
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from crewai.tools import BaseTool, EnvVar
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateCrewaiAutomationToolSchema(BaseModel):
|
||||||
|
prompt: str = Field(
|
||||||
|
description="The prompt to generate the CrewAI automation, e.g. 'Generate a CrewAI automation that will scrape the website and store the data in a database.'"
|
||||||
|
)
|
||||||
|
organization_id: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="The identifier for the CrewAI Enterprise organization. If not specified, a default organization will be used.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateCrewaiAutomationTool(BaseTool):
|
||||||
|
name: str = "Generate CrewAI Automation"
|
||||||
|
description: str = (
|
||||||
|
"A tool that leverages CrewAI Studio's capabilities to automatically generate complete CrewAI "
|
||||||
|
"automations based on natural language descriptions. It translates high-level requirements into "
|
||||||
|
"functional CrewAI implementations."
|
||||||
|
)
|
||||||
|
args_schema: Type[BaseModel] = GenerateCrewaiAutomationToolSchema
|
||||||
|
crewai_enterprise_url: str = Field(
|
||||||
|
default_factory=lambda: os.getenv("CREWAI_PLUS_URL", "https://app.crewai.com"),
|
||||||
|
description="The base URL of CrewAI Enterprise. If not provided, it will be loaded from the environment variable CREWAI_PLUS_URL with default https://app.crewai.com.",
|
||||||
|
)
|
||||||
|
personal_access_token: Optional[str] = Field(
|
||||||
|
default_factory=lambda: os.getenv("CREWAI_PERSONAL_ACCESS_TOKEN"),
|
||||||
|
description="The user's Personal Access Token to access CrewAI Enterprise API. If not provided, it will be loaded from the environment variable CREWAI_PERSONAL_ACCESS_TOKEN.",
|
||||||
|
)
|
||||||
|
env_vars: List[EnvVar] = [
|
||||||
|
EnvVar(
|
||||||
|
name="CREWAI_PERSONAL_ACCESS_TOKEN",
|
||||||
|
description="Personal Access Token for CrewAI Enterprise API",
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
EnvVar(
|
||||||
|
name="CREWAI_PLUS_URL",
|
||||||
|
description="Base URL for CrewAI Enterprise API",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _run(self, **kwargs) -> str:
|
||||||
|
input_data = GenerateCrewaiAutomationToolSchema(**kwargs)
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.crewai_enterprise_url}/crewai_plus/api/v1/studio",
|
||||||
|
headers=self._get_headers(input_data.organization_id),
|
||||||
|
json={"prompt": input_data.prompt},
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
studio_project_url = response.json().get("url")
|
||||||
|
return f"Generated CrewAI Studio project URL: {studio_project_url}"
|
||||||
|
|
||||||
|
def _get_headers(self, organization_id: Optional[str] = None) -> dict:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.personal_access_token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization_id:
|
||||||
|
headers["X-Crewai-Organization-Id"] = organization_id
|
||||||
|
|
||||||
|
return headers
|
||||||
187
tests/tools/generate_crewai_automation_tool_test.py
Normal file
187
tests/tools/generate_crewai_automation_tool_test.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from crewai_tools.tools.generate_crewai_automation_tool.generate_crewai_automation_tool import (
|
||||||
|
GenerateCrewaiAutomationTool,
|
||||||
|
GenerateCrewaiAutomationToolSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_env():
|
||||||
|
with patch.dict(os.environ, {"CREWAI_PERSONAL_ACCESS_TOKEN": "test_token"}):
|
||||||
|
os.environ.pop("CREWAI_PLUS_URL", None)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tool():
|
||||||
|
return GenerateCrewaiAutomationTool()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def custom_url_tool():
|
||||||
|
with patch.dict(os.environ, {"CREWAI_PLUS_URL": "https://custom.crewai.com"}):
|
||||||
|
return GenerateCrewaiAutomationTool()
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_initialization(tool):
|
||||||
|
assert tool.crewai_enterprise_url == "https://app.crewai.com"
|
||||||
|
assert tool.personal_access_token == "test_token"
|
||||||
|
assert tool.name == "Generate CrewAI Automation"
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_base_url_from_environment(custom_url_tool):
|
||||||
|
assert custom_url_tool.crewai_enterprise_url == "https://custom.crewai.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_personal_access_token_from_environment(tool):
|
||||||
|
assert tool.personal_access_token == "test_token"
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_prompt_only():
|
||||||
|
schema = GenerateCrewaiAutomationToolSchema(
|
||||||
|
prompt="Create a web scraping automation"
|
||||||
|
)
|
||||||
|
assert schema.prompt == "Create a web scraping automation"
|
||||||
|
assert schema.organization_id is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_prompt_with_organization_id():
|
||||||
|
schema = GenerateCrewaiAutomationToolSchema(
|
||||||
|
prompt="Create automation", organization_id="org-123"
|
||||||
|
)
|
||||||
|
assert schema.prompt == "Create automation"
|
||||||
|
assert schema.organization_id == "org-123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_prompt_validation():
|
||||||
|
schema = GenerateCrewaiAutomationToolSchema(prompt="")
|
||||||
|
assert schema.prompt == ""
|
||||||
|
assert schema.organization_id is None
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_successful_generation_without_org_id(mock_post, tool):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"url": "https://app.crewai.com/studio/project-123"
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = tool.run(prompt="Create automation")
|
||||||
|
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== "Generated CrewAI Studio project URL: https://app.crewai.com/studio/project-123"
|
||||||
|
)
|
||||||
|
mock_post.assert_called_once_with(
|
||||||
|
"https://app.crewai.com/crewai_plus/api/v1/studio",
|
||||||
|
headers={
|
||||||
|
"Authorization": "Bearer test_token",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
json={"prompt": "Create automation"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_successful_generation_with_org_id(mock_post, tool):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"url": "https://app.crewai.com/studio/project-456"
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = tool.run(prompt="Create automation", organization_id="org-456")
|
||||||
|
|
||||||
|
assert (
|
||||||
|
result
|
||||||
|
== "Generated CrewAI Studio project URL: https://app.crewai.com/studio/project-456"
|
||||||
|
)
|
||||||
|
mock_post.assert_called_once_with(
|
||||||
|
"https://app.crewai.com/crewai_plus/api/v1/studio",
|
||||||
|
headers={
|
||||||
|
"Authorization": "Bearer test_token",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"X-Crewai-Organization-Id": "org-456",
|
||||||
|
},
|
||||||
|
json={"prompt": "Create automation"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_custom_base_url_usage(mock_post, custom_url_tool):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"url": "https://custom.crewai.com/studio/project-789"
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
custom_url_tool.run(prompt="Create automation")
|
||||||
|
|
||||||
|
mock_post.assert_called_once_with(
|
||||||
|
"https://custom.crewai.com/crewai_plus/api/v1/studio",
|
||||||
|
headers={
|
||||||
|
"Authorization": "Bearer test_token",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
json={"prompt": "Create automation"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_api_error_response_handling(mock_post, tool):
|
||||||
|
mock_post.return_value.raise_for_status.side_effect = requests.HTTPError(
|
||||||
|
"400 Bad Request"
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(requests.HTTPError):
|
||||||
|
tool.run(prompt="Create automation")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_network_error_handling(mock_post, tool):
|
||||||
|
mock_post.side_effect = requests.ConnectionError("Network unreachable")
|
||||||
|
|
||||||
|
with pytest.raises(requests.ConnectionError):
|
||||||
|
tool.run(prompt="Create automation")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("requests.post")
|
||||||
|
def test_api_response_missing_url(mock_post, tool):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {"status": "success"}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = tool.run(prompt="Create automation")
|
||||||
|
|
||||||
|
assert result == "Generated CrewAI Studio project URL: None"
|
||||||
|
|
||||||
|
|
||||||
|
def test_authorization_header_construction(tool):
|
||||||
|
headers = tool._get_headers()
|
||||||
|
|
||||||
|
assert headers["Authorization"] == "Bearer test_token"
|
||||||
|
assert headers["Content-Type"] == "application/json"
|
||||||
|
assert headers["Accept"] == "application/json"
|
||||||
|
assert "X-Crewai-Organization-Id" not in headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_authorization_header_with_org_id(tool):
|
||||||
|
headers = tool._get_headers(organization_id="org-123")
|
||||||
|
|
||||||
|
assert headers["Authorization"] == "Bearer test_token"
|
||||||
|
assert headers["X-Crewai-Organization-Id"] == "org-123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_personal_access_token():
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
tool = GenerateCrewaiAutomationTool()
|
||||||
|
assert tool.personal_access_token is None
|
||||||
Reference in New Issue
Block a user