From eb281c6d73490efddea0c3b26c74a68289fdafa0 Mon Sep 17 00:00:00 2001 From: Mateus Braga Date: Fri, 11 Sep 2026 13:54:05 +0000 Subject: [PATCH] fix(tools): fall back to CREWAI_API_URL/CREWAI_BEARER_TOKEN env vars InvokeCrewAIAutomationTool required crew_api_url and crew_bearer_token as positional __init__ arguments with no environment-variable fallback, even though docs.crewai.com documents CREWAI_API_URL/CREWAI_BEARER_TOKEN as alternatives. It also could not be instantiated with zero arguments, which blocks CrewAI AMP Studio's "Invoke Amp Automation" internal tool: the Studio runtime resolves tools by class reference and instantiates them with no arguments. - crew_api_url/crew_bearer_token become optional, falling back to CREWAI_API_URL/CREWAI_BEARER_TOKEN (explicit args still win), mirroring the env-var pattern already used by GenerateCrewaiAutomationTool. - crew_name/crew_description become optional too, defaulting to the tool's existing generic name/description, so InvokeCrewAIAutomationTool() never raises at construction time. - Declare env_vars: list[EnvVar] so the tool catalog surfaces the two env vars, matching the sibling tool. - Raise a clear ValueError at use time when crew_api_url/crew_bearer_token are still missing, instead of failing inside `requests` or with the previous confusing TypeError about positional arguments. - Update the tool's README and the edge docs page (docs/edge/en/tools/integration/crewaiautomationtool.mdx) so the "Tool Arguments" table matches the corrected code. Fully backward compatible: existing positional/keyword constructor calls are unchanged. tool.specs.json is left untouched; the "Generate Tool Specifications" CI workflow regenerates and commits it automatically once this is pushed. crewAIInc/crewAI-tools (the previous home of this tool) is archived and can no longer receive pushes, so this fix targets the actively maintained copy of the tool under lib/crewai-tools/ instead. --- .../integration/crewaiautomationtool.mdx | 12 +- .../invoke_crewai_automation_tool/README.md | 47 ++++- .../invoke_crewai_automation_tool.py | 115 +++++++++-- .../invoke_crewai_automation_tool_test.py | 182 ++++++++++++++++++ 4 files changed, 328 insertions(+), 28 deletions(-) create mode 100644 lib/crewai-tools/tests/tools/invoke_crewai_automation_tool_test.py diff --git a/docs/edge/en/tools/integration/crewaiautomationtool.mdx b/docs/edge/en/tools/integration/crewaiautomationtool.mdx index 62f1a4cea..26951c1e1 100644 --- a/docs/edge/en/tools/integration/crewaiautomationtool.mdx +++ b/docs/edge/en/tools/integration/crewaiautomationtool.mdx @@ -68,13 +68,17 @@ print(result) | Argument | Type | Required | Default | Description | |:---------|:-----|:---------|:--------|:------------| -| **crew_api_url** | `str` | Yes | None | Base URL of the CrewAI Platform automation API | -| **crew_bearer_token** | `str` | Yes | None | Bearer token for API authentication | -| **crew_name** | `str` | Yes | None | Name of the crew automation | -| **crew_description** | `str` | Yes | None | Description of what the crew automation does | +| **crew_api_url** | `str` | No | `None` | Base URL of the CrewAI Platform automation API. Falls back to the `CREWAI_API_URL` environment variable when omitted. | +| **crew_bearer_token** | `str` | No | `None` | Bearer token for API authentication. Falls back to the `CREWAI_BEARER_TOKEN` environment variable when omitted. | +| **crew_name** | `str` | No | generic name | Name of the crew automation. Set explicitly outside CrewAI AMP Studio so the LLM sees a meaningful tool name. | +| **crew_description** | `str` | No | generic description | Description of what the crew automation does. Set explicitly for the same reason as `crew_name`. | | **max_polling_time** | `int` | No | 600 | Maximum time in seconds to wait for task completion | | **crew_inputs** | `dict` | No | None | Dictionary defining custom input schema fields | +If `crew_api_url`/`crew_bearer_token` are still missing at run time (neither passed +explicitly nor available via the environment variables below), the tool raises a clear +error explaining what's missing. + ## Environment Variables ```bash diff --git a/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/README.md index 58ab4bbcc..fd4d6ff08 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/README.md +++ b/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/README.md @@ -39,6 +39,28 @@ tool = InvokeCrewAIAutomationTool( result = tool.run() ``` +### Using Environment Variables + +`crew_api_url` and `crew_bearer_token` can also be provided via environment variables +instead of constructor arguments, which is convenient for keeping credentials out of code: + +```shell +export CREWAI_API_URL="https://data-analysis-crew-[...].crewai.com" +export CREWAI_BEARER_TOKEN="your_bearer_token_here" +``` + +```python +from crewai_tools import InvokeCrewAIAutomationTool + +# crew_api_url/crew_bearer_token are read from CREWAI_API_URL/CREWAI_BEARER_TOKEN +tool = InvokeCrewAIAutomationTool( + crew_name="Data Analysis Crew", + crew_description="Analyzes data and generates insights" +) +``` + +Explicit constructor arguments always take precedence over the environment variables. + ### Advanced Usage with Custom Inputs ```python @@ -109,18 +131,29 @@ result = crew.kickoff() ## Arguments -### Required Parameters - -- `crew_api_url` (str): Base URL of the CrewAI Platform automation API -- `crew_bearer_token` (str): Bearer token for API authentication -- `crew_name` (str): Name of the crew automation -- `crew_description` (str): Description of what the crew automation does - ### Optional Parameters +- `crew_api_url` (str): Base URL of the CrewAI Platform automation API. Falls back to the + `CREWAI_API_URL` environment variable when omitted. +- `crew_bearer_token` (str): Bearer token for API authentication. Falls back to the + `CREWAI_BEARER_TOKEN` environment variable when omitted. +- `crew_name` (str): Name of the crew automation. Defaults to a generic tool name; set it + explicitly so the LLM sees a meaningful tool name and description in its tool list. +- `crew_description` (str): Description of what the crew automation does. Defaults to a + generic description for the same reason as `crew_name`. - `max_polling_time` (int): Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes) - `crew_inputs` (dict): Dictionary defining custom input schema fields using Pydantic Field objects +All of the above are optional at construction time, so `InvokeCrewAIAutomationTool()` never +fails to instantiate. If `crew_api_url`/`crew_bearer_token` are still missing (neither passed +explicitly nor available via `CREWAI_API_URL`/`CREWAI_BEARER_TOKEN`) when the tool is actually +run, it raises a clear `ValueError` explaining what is missing. + +## Environment Variables + +- `CREWAI_API_URL`: Alternative to passing `crew_api_url`. +- `CREWAI_BEARER_TOKEN`: Alternative to passing `crew_bearer_token`. + ## Custom Input Schema When defining `crew_inputs`, use Pydantic Field objects to specify the input parameters. These have to be compatible with the crew automation you are invoking: diff --git a/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py b/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py index 2f1e7868e..6fccfdb57 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py @@ -1,11 +1,20 @@ +import os import time from typing import Any -from crewai.tools import BaseTool +from crewai.tools import BaseTool, EnvVar from pydantic import BaseModel, Field, create_model import requests +# Generic fallbacks used when the tool is instantiated without a crew_name / +# crew_description, e.g. by a runtime that resolves tools by class reference +# and instantiates them with no arguments (see the "Zero-argument +# instantiation" note in the class docstring below). +DEFAULT_TOOL_NAME = "invoke_amp_automation" +DEFAULT_TOOL_DESCRIPTION = "Invokes an CrewAI Platform Automation using API" + + class InvokeCrewAIAutomationInput(BaseModel): """Input schema for InvokeCrewAIAutomationTool.""" @@ -59,32 +68,72 @@ class InvokeCrewAIAutomationTool(BaseTool): ... }, ... ) ... ] + + Configuring the API url and bearer token via environment variables, instead of + passing them as constructor arguments: + >>> import os + >>> os.environ["CREWAI_API_URL"] = "https://canary-crew-[...].crewai.com" + >>> os.environ["CREWAI_BEARER_TOKEN"] = "[Your token: abcdef012345]" + >>> tool = InvokeCrewAIAutomationTool( + ... crew_name="State of AI Report", + ... crew_description="Retrieves a report on state of AI for a given year.", + ... ) + + Zero-argument instantiation: + `crew_api_url`/`crew_bearer_token` fall back to the `CREWAI_API_URL` / + `CREWAI_BEARER_TOKEN` environment variables, and `crew_name` / + `crew_description` fall back to a generic name/description, so + `InvokeCrewAIAutomationTool()` never raises at construction time. This + matters because some runtimes (e.g. the CrewAI AMP Studio "Invoke Amp + Automation" internal tool) resolve tools by class reference and instantiate + them with no arguments. If the tool is still unconfigured (no explicit + arguments and no environment variables) when it is actually invoked, it + raises a clear `ValueError` instead of attempting the HTTP request. """ - name: str = "invoke_amp_automation" - description: str = "Invokes an CrewAI Platform Automation using API" + name: str = DEFAULT_TOOL_NAME + description: str = DEFAULT_TOOL_DESCRIPTION args_schema: type[BaseModel] = InvokeCrewAIAutomationInput - crew_api_url: str - crew_bearer_token: str + crew_api_url: str | None = None + crew_bearer_token: str | None = None max_polling_time: int = 10 * 60 + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="CREWAI_API_URL", + description="Base URL of the crew/flow API to invoke. Alternative to passing crew_api_url.", + required=True, + ), + EnvVar( + name="CREWAI_BEARER_TOKEN", + description="Bearer token used to authenticate against crew_api_url. Alternative to passing crew_bearer_token.", + required=True, + ), + ] + ) def __init__( self, - crew_api_url: str, - crew_bearer_token: str, - crew_name: str, - crew_description: str, + crew_api_url: str | None = None, + crew_bearer_token: str | None = None, + crew_name: str = DEFAULT_TOOL_NAME, + crew_description: str = DEFAULT_TOOL_DESCRIPTION, max_polling_time: int = 10 * 60, crew_inputs: dict[str, Any] | None = None, ): """Initialize the InvokeCrewAIAutomationTool. Args: - crew_api_url: Base URL of the crew API service - crew_bearer_token: Bearer token for API authentication - crew_name: Name of the crew to invoke - crew_description: Description of the crew to invoke + crew_api_url: Base URL of the crew API service. If omitted, falls back to + the CREWAI_API_URL environment variable. + crew_bearer_token: Bearer token for API authentication. If omitted, falls + back to the CREWAI_BEARER_TOKEN environment variable. + crew_name: Name of the crew to invoke. Defaults to a generic tool name so + the tool can be instantiated without arguments; set it explicitly so + the LLM sees a meaningful tool name in its tool list. + crew_description: Description of the crew to invoke. Defaults to a generic + description for the same reason as crew_name. max_polling_time: Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes) crew_inputs: Optional dictionary defining custom input schema fields """ @@ -102,15 +151,45 @@ class InvokeCrewAIAutomationTool(BaseTool): else: args_schema = InvokeCrewAIAutomationInput + # Explicit constructor arguments win over the environment variables. + resolved_api_url = crew_api_url or os.getenv("CREWAI_API_URL") + resolved_bearer_token = crew_bearer_token or os.getenv("CREWAI_BEARER_TOKEN") + super().__init__( - name=crew_name, - description=crew_description, + name=crew_name or DEFAULT_TOOL_NAME, + description=crew_description or DEFAULT_TOOL_DESCRIPTION, args_schema=args_schema, - crew_api_url=crew_api_url, - crew_bearer_token=crew_bearer_token, + crew_api_url=resolved_api_url, + crew_bearer_token=resolved_bearer_token, max_polling_time=max_polling_time, ) + def _ensure_configured(self) -> None: + """Raise a clear, actionable error if the tool has no API url/token. + + crew_api_url and crew_bearer_token are optional at construction time (they + may be resolved later from the environment, or simply not set yet when the + tool is instantiated with no arguments). This is checked once, right before + the tool is actually used, so a missing configuration produces an explicit + error message instead of a confusing failure deep inside `requests` (e.g. an + "Invalid URL 'None/kickoff'" error) or a bare 401 from the API. + """ + missing = [ + env_name + for value, env_name in ( + (self.crew_api_url, "CREWAI_API_URL"), + (self.crew_bearer_token, "CREWAI_BEARER_TOKEN"), + ) + if not value + ] + if missing: + raise ValueError( + "InvokeCrewAIAutomationTool is not configured: missing " + f"{' and '.join(missing)}. Pass crew_api_url/crew_bearer_token " + "explicitly when creating the tool, or set the " + f"{' and '.join(missing)} environment variable(s)." + ) + def _kickoff_crew(self, inputs: dict[str, Any]) -> dict[str, Any]: """Start a new crew task. @@ -154,6 +233,8 @@ class InvokeCrewAIAutomationTool(BaseTool): def _run(self, **kwargs: Any) -> str: """Execute the crew invocation tool.""" + self._ensure_configured() + if kwargs is None: kwargs = {} diff --git a/lib/crewai-tools/tests/tools/invoke_crewai_automation_tool_test.py b/lib/crewai-tools/tests/tools/invoke_crewai_automation_tool_test.py new file mode 100644 index 000000000..3a33f6275 --- /dev/null +++ b/lib/crewai-tools/tests/tools/invoke_crewai_automation_tool_test.py @@ -0,0 +1,182 @@ +import os +from unittest.mock import MagicMock, patch + +from crewai_tools.tools.invoke_crewai_automation_tool.invoke_crewai_automation_tool import ( + DEFAULT_TOOL_DESCRIPTION, + DEFAULT_TOOL_NAME, + InvokeCrewAIAutomationTool, +) +from pydantic import Field +import pytest + + +@pytest.fixture(autouse=True) +def clean_env(): + """Ensure CREWAI_API_URL/CREWAI_BEARER_TOKEN never leak between tests.""" + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("CREWAI_API_URL", None) + os.environ.pop("CREWAI_BEARER_TOKEN", None) + yield + + +def test_zero_argument_instantiation_does_not_raise(): + """A runtime that resolves tools by class reference (e.g. CrewAI AMP Studio's + "Invoke Amp Automation" internal tool) instantiates the tool with no arguments. + This must never raise a TypeError about missing positional arguments.""" + tool = InvokeCrewAIAutomationTool() + + assert tool.name == DEFAULT_TOOL_NAME + assert DEFAULT_TOOL_DESCRIPTION in tool.description + assert tool.crew_api_url is None + assert tool.crew_bearer_token is None + + +def test_existing_positional_keyword_call_shape_still_works(): + """Backward compatibility: the documented/previous required-arguments call shape + keeps working unchanged.""" + tool = InvokeCrewAIAutomationTool( + "https://api.example.com", + "explicit_token", + "My Crew", + "Description of what the crew does", + ) + + assert tool.crew_api_url == "https://api.example.com" + assert tool.crew_bearer_token == "explicit_token" + assert tool.name == "My Crew" + assert "Description of what the crew does" in tool.description + + kwarg_tool = InvokeCrewAIAutomationTool( + crew_api_url="https://api.example.com", + crew_bearer_token="explicit_token", + crew_name="My Crew", + crew_description="Description of what the crew does", + max_polling_time=120, + ) + assert kwarg_tool.crew_api_url == "https://api.example.com" + assert kwarg_tool.crew_bearer_token == "explicit_token" + assert kwarg_tool.max_polling_time == 120 + + +def test_env_var_fallback_for_url_and_token(): + with patch.dict( + os.environ, + { + "CREWAI_API_URL": "https://from-env.crewai.com", + "CREWAI_BEARER_TOKEN": "env_token", + }, + ): + tool = InvokeCrewAIAutomationTool( + crew_name="My Crew", crew_description="Does things" + ) + + assert tool.crew_api_url == "https://from-env.crewai.com" + assert tool.crew_bearer_token == "env_token" + assert tool.name == "My Crew" + assert "Does things" in tool.description + + +def test_explicit_arguments_take_precedence_over_env_vars(): + with patch.dict( + os.environ, + { + "CREWAI_API_URL": "https://from-env.crewai.com", + "CREWAI_BEARER_TOKEN": "env_token", + }, + ): + tool = InvokeCrewAIAutomationTool( + crew_api_url="https://explicit.crewai.com", + crew_bearer_token="explicit_token", + crew_name="My Crew", + crew_description="Does things", + ) + + assert tool.crew_api_url == "https://explicit.crewai.com" + assert tool.crew_bearer_token == "explicit_token" + + +def test_missing_configuration_raises_clear_error_on_use(): + """No explicit args and no env vars: construction succeeds, but running the + tool must raise a clear, actionable error instead of failing deep inside + `requests` or the CrewAI Platform API with a bare 401.""" + tool = InvokeCrewAIAutomationTool( + crew_name="My Crew", crew_description="Does things" + ) + + with pytest.raises(ValueError) as exc_info: + tool.run(prompt="hello") + + message = str(exc_info.value) + assert "CREWAI_API_URL" in message + assert "CREWAI_BEARER_TOKEN" in message + + +def test_partial_configuration_raises_clear_error_on_use(): + """Only one of the two required values is configured.""" + tool = InvokeCrewAIAutomationTool( + crew_api_url="https://api.example.com", + crew_name="My Crew", + crew_description="Does things", + ) + + with pytest.raises(ValueError) as exc_info: + tool.run(prompt="hello") + + message = str(exc_info.value) + assert "CREWAI_BEARER_TOKEN" in message + assert "CREWAI_API_URL" not in message + + +@patch("requests.get") +@patch("requests.post") +def test_successful_run_with_env_var_configuration(mock_post, mock_get): + with patch.dict( + os.environ, + { + "CREWAI_API_URL": "https://from-env.crewai.com", + "CREWAI_BEARER_TOKEN": "env_token", + }, + ): + tool = InvokeCrewAIAutomationTool( + crew_name="My Crew", crew_description="Does things" + ) + + kickoff_response = MagicMock() + kickoff_response.json.return_value = {"kickoff_id": "kickoff-123"} + mock_post.return_value = kickoff_response + + status_response = MagicMock() + status_response.json.return_value = {"state": "success", "result": "42"} + mock_get.return_value = status_response + + result = tool.run(prompt="hello") + + assert result == "42" + mock_post.assert_called_once_with( + "https://from-env.crewai.com/kickoff", + headers={ + "Authorization": "Bearer env_token", + "Content-Type": "application/json", + }, + json={"inputs": {"prompt": "hello"}}, + timeout=30, + ) + + +def test_dynamic_crew_inputs_schema_still_works(): + custom_inputs = { + "year": Field(..., description="Year to retrieve the report for (integer)"), + "region": Field(default="global", description="Geographic region"), + } + + tool = InvokeCrewAIAutomationTool( + crew_api_url="https://api.example.com", + crew_bearer_token="token", + crew_name="State of AI Report", + crew_description="Retrieves a report on state of AI for a given year.", + crew_inputs=custom_inputs, + ) + + schema_fields = tool.args_schema.model_fields + assert "year" in schema_fields + assert "region" in schema_fields