diff --git a/src/crewai_tools/__init__.py b/src/crewai_tools/__init__.py index 4886dbc57..f4c03ba0e 100644 --- a/src/crewai_tools/__init__.py +++ b/src/crewai_tools/__init__.py @@ -12,16 +12,16 @@ from .tools import ( ApifyActorsTool, ArxivPaperTool, BraveSearchTool, - BrightDataWebUnlockerTool, - BrightDataSearchTool, BrightDataDatasetTool, + BrightDataSearchTool, + BrightDataWebUnlockerTool, BrowserbaseLoadTool, CodeDocsSearchTool, CodeInterpreterTool, ComposioTool, - ContextualAIQueryTool, ContextualAICreateAgentTool, ContextualAIParseTool, + ContextualAIQueryTool, ContextualAIRerankTool, CouchbaseFTSVectorSearchTool, CrewaiEnterpriseTools, @@ -38,6 +38,7 @@ from .tools import ( FirecrawlCrawlWebsiteTool, FirecrawlScrapeWebsiteTool, FirecrawlSearchTool, + GenerateCrewaiAutomationTool, GithubSearchTool, HyperbrowserLoadTool, InvokeCrewAIAutomationTool, diff --git a/src/crewai_tools/tools/__init__.py b/src/crewai_tools/tools/__init__.py index 886c27ad1..bf1a166d9 100644 --- a/src/crewai_tools/tools/__init__.py +++ b/src/crewai_tools/tools/__init__.py @@ -2,13 +2,20 @@ from .ai_mind_tool.ai_mind_tool import AIMindTool from .apify_actors_tool.apify_actors_tool import ApifyActorsTool from .arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool 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 .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool from .code_interpreter_tool.code_interpreter_tool import CodeInterpreterTool 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 ContextualAICreateAgentTool +from .contextualai_create_agent_tool.contextual_create_agent_tool import ( + ContextualAICreateAgentTool, +) 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 .couchbase_tool.couchbase_tool import CouchbaseFTSVectorSearchTool from .crewai_enterprise_tools.crewai_enterprise_tools import CrewaiEnterpriseTools @@ -29,9 +36,14 @@ from .firecrawl_scrape_website_tool.firecrawl_scrape_website_tool import ( FirecrawlScrapeWebsiteTool, ) 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 .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 .linkup.linkup_search_tool import LinkupSearchTool from .llamaindex_tool.llamaindex_tool import LlamaIndexTool @@ -108,9 +120,4 @@ from .youtube_channel_search_tool.youtube_channel_search_tool import ( YoutubeChannelSearchTool, ) 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 diff --git a/src/crewai_tools/tools/generate_crewai_automation_tool/README.md b/src/crewai_tools/tools/generate_crewai_automation_tool/README.md new file mode 100644 index 000000000..c70741ca4 --- /dev/null +++ b/src/crewai_tools/tools/generate_crewai_automation_tool/README.md @@ -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() +``` \ No newline at end of file diff --git a/src/crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py b/src/crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py new file mode 100644 index 000000000..3d52ae3fa --- /dev/null +++ b/src/crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py @@ -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 diff --git a/tests/tools/generate_crewai_automation_tool_test.py b/tests/tools/generate_crewai_automation_tool_test.py new file mode 100644 index 000000000..715e00804 --- /dev/null +++ b/tests/tools/generate_crewai_automation_tool_test.py @@ -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