Compare commits

..

2 Commits

Author SHA1 Message Date
Devin AI
f28e3e0be8 Fix lint error: remove unused 're' import from test file
Co-Authored-By: João <joao@crewai.com>
2025-06-24 06:21:34 +00:00
Devin AI
10a55bd210 Fix CLI documentation to reflect actual provider count and two-step process
- Update docs/concepts/cli.mdx to remove outdated 'top 5 most common LLM providers' reference
- Replace with accurate description of 12 available providers plus 'other' option
- Document the two-step process: select provider, then select model
- Add comprehensive test to prevent documentation drift in the future
- Test validates that docs stay in sync with actual CLI implementation

Fixes #3054

Co-Authored-By: João <joao@crewai.com>
2025-06-24 06:15:05 +00:00
4 changed files with 100 additions and 176 deletions

View File

@@ -285,25 +285,32 @@ Watch this video tutorial for a step-by-step demonstration of deploying your cre
### 11. API Keys
When running ```crewai create crew``` command, the CLI will first show you the top 5 most common LLM providers and ask you to select one.
When running ```crewai create crew``` command, the CLI will show you a list of available LLM providers to choose from, followed by model selection for your chosen provider.
Once you've selected an LLM provider, you will be prompted for API keys.
Once you've selected an LLM provider and model, you will be prompted for API keys.
#### Initial API key providers
#### Available LLM Providers
The CLI will initially prompt for API keys for the following services:
The CLI will show you the following LLM providers to choose from:
* OpenAI
* Groq
* Anthropic
* Google Gemini
* NVIDIA NIM
* Groq
* Hugging Face
* Ollama
* Watson
* AWS Bedrock
* Azure
* Cerebras
* SambaNova
When you select a provider, the CLI will prompt you to enter your API key.
When you select a provider, the CLI will then show you available models for that provider and prompt you to enter your API key.
#### Other Options
If you select option 6, you will be able to select from a list of LiteLLM supported providers.
If you select "other", you will be able to select from a list of LiteLLM supported providers.
When you select a provider, the CLI will prompt you to enter the Key name and the API key.

View File

@@ -94,18 +94,17 @@ def _get_project_attribute(
attribute = _get_nested_value(pyproject_content, keys)
except FileNotFoundError:
console.print(f"Error: {pyproject_path} not found.", style="bold red")
print(f"Error: {pyproject_path} not found.")
except KeyError:
console.print(f"Error: {pyproject_path} is not a valid pyproject.toml file.", style="bold red")
print(f"Error: {pyproject_path} is not a valid pyproject.toml file.")
except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: # type: ignore
console.print(
print(
f"Error: {pyproject_path} is not a valid TOML file."
if sys.version_info >= (3, 11)
else f"Error reading the pyproject.toml file: {e}",
style="bold red",
else f"Error reading the pyproject.toml file: {e}"
)
except Exception as e:
console.print(f"Error reading the pyproject.toml file: {e}", style="bold red")
print(f"Error reading the pyproject.toml file: {e}")
if require and not attribute:
console.print(
@@ -138,9 +137,9 @@ def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
return env_dict
except FileNotFoundError:
console.print(f"Error: {env_file_path} not found.", style="bold red")
print(f"Error: {env_file_path} not found.")
except Exception as e:
console.print(f"Error reading the .env file: {e}", style="bold red")
print(f"Error reading the .env file: {e}")
return {}
@@ -256,69 +255,50 @@ def write_env_file(folder_path, env_vars):
def get_crews(crew_path: str = "crew.py", require: bool = False) -> list[Crew]:
"""Get the crew instances from a file."""
"""Get the crew instances from the a file."""
crew_instances = []
try:
import importlib.util
# Add the current directory to sys.path to ensure imports resolve correctly
current_dir = os.getcwd()
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
# If we're not in src directory but there's a src directory, add it to path
src_dir = os.path.join(current_dir, "src")
if os.path.isdir(src_dir) and src_dir not in sys.path:
sys.path.insert(0, src_dir)
# Search in both current directory and src directory if it exists
search_paths = [".", "src"] if os.path.isdir("src") else ["."]
for search_path in search_paths:
for root, _, files in os.walk(search_path):
if crew_path in files and "cli/templates" not in root:
crew_os_path = os.path.join(root, crew_path)
for root, _, files in os.walk("."):
if crew_path in files:
crew_os_path = os.path.join(root, crew_path)
try:
spec = importlib.util.spec_from_file_location(
"crew_module", crew_os_path
)
if not spec or not spec.loader:
continue
module = importlib.util.module_from_spec(spec)
try:
spec = importlib.util.spec_from_file_location(
"crew_module", crew_os_path
)
if not spec or not spec.loader:
continue
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
try:
spec.loader.exec_module(module)
for attr_name in dir(module):
module_attr = getattr(module, attr_name)
for attr_name in dir(module):
module_attr = getattr(module, attr_name)
try:
crew_instances.extend(fetch_crews(module_attr))
except Exception as e:
console.print(f"Error processing attribute {attr_name}: {e}", style="bold red")
continue
try:
crew_instances.extend(fetch_crews(module_attr))
except Exception as e:
print(f"Error processing attribute {attr_name}: {e}")
continue
# If we found crew instances, break out of the loop
if crew_instances:
break
except Exception as exec_error:
print(f"Error executing module: {exec_error}")
import traceback
except Exception as exec_error:
console.print(f"Error executing module: {exec_error}", style="bold red")
except (ImportError, AttributeError) as e:
if require:
console.print(
f"Error importing crew from {crew_path}: {str(e)}",
style="bold red",
)
print(f"Traceback: {traceback.format_exc()}")
except (ImportError, AttributeError) as e:
if require:
console.print(
f"Error importing crew from {crew_path}: {str(e)}",
style="bold red",
)
continue
# If we found crew instances in this search path, break out of the search paths loop
if crew_instances:
break
if require and not crew_instances:
if require:
console.print("No valid Crew instance found in crew.py", style="bold red")
raise SystemExit
@@ -338,15 +318,11 @@ def get_crew_instance(module_attr) -> Crew | None:
and module_attr.is_crew_class
):
return module_attr().crew()
try:
if (ismethod(module_attr) or isfunction(module_attr)) and get_type_hints(
module_attr
).get("return") is Crew:
return module_attr()
except Exception:
return None
if isinstance(module_attr, Crew):
if (ismethod(module_attr) or isfunction(module_attr)) and get_type_hints(
module_attr
).get("return") is Crew:
return module_attr()
elif isinstance(module_attr, Crew):
return module_attr
else:
return None
@@ -426,8 +402,7 @@ def _load_tools_from_init(init_file: Path) -> list[dict[str, Any]]:
if not hasattr(module, "__all__"):
console.print(
f"Warning: No __all__ defined in {init_file}",
style="bold yellow",
f"[bold yellow]Warning: No __all__ defined in {init_file}[/bold yellow]"
)
raise SystemExit(1)

View File

@@ -261,104 +261,3 @@ __all__ = ['MyTool']
captured = capsys.readouterr()
assert "was never closed" in captured.out
@pytest.fixture
def mock_crew():
from crewai.crew import Crew
class MockCrew(Crew):
def __init__(self):
pass
return MockCrew()
@pytest.fixture
def temp_crew_project():
with tempfile.TemporaryDirectory() as temp_dir:
old_cwd = os.getcwd()
os.chdir(temp_dir)
crew_content = """
from crewai.crew import Crew
from crewai.agent import Agent
def create_crew() -> Crew:
agent = Agent(role="test", goal="test", backstory="test")
return Crew(agents=[agent], tasks=[])
# Direct crew instance
direct_crew = Crew(agents=[], tasks=[])
"""
with open("crew.py", "w") as f:
f.write(crew_content)
os.makedirs("src", exist_ok=True)
with open(os.path.join("src", "crew.py"), "w") as f:
f.write(crew_content)
# Create a src/templates directory that should be ignored
os.makedirs(os.path.join("src", "templates"), exist_ok=True)
with open(os.path.join("src", "templates", "crew.py"), "w") as f:
f.write("# This should be ignored")
yield temp_dir
os.chdir(old_cwd)
def test_get_crews_finds_valid_crews(temp_crew_project, monkeypatch, mock_crew):
def mock_fetch_crews(module_attr):
return [mock_crew]
monkeypatch.setattr(utils, "fetch_crews", mock_fetch_crews)
crews = utils.get_crews()
assert len(crews) > 0
assert mock_crew in crews
def test_get_crews_with_nonexistent_file(temp_crew_project):
crews = utils.get_crews(crew_path="nonexistent.py", require=False)
assert len(crews) == 0
def test_get_crews_with_required_nonexistent_file(temp_crew_project, capsys):
with pytest.raises(SystemExit):
utils.get_crews(crew_path="nonexistent.py", require=True)
captured = capsys.readouterr()
assert "No valid Crew instance found" in captured.out
def test_get_crews_with_invalid_module(temp_crew_project, capsys):
with open("crew.py", "w") as f:
f.write("import nonexistent_module\n")
crews = utils.get_crews(crew_path="crew.py", require=False)
assert len(crews) == 0
with pytest.raises(SystemExit):
utils.get_crews(crew_path="crew.py", require=True)
captured = capsys.readouterr()
assert "Error" in captured.out
def test_get_crews_ignores_template_directories(temp_crew_project, monkeypatch, mock_crew):
template_crew_detected = False
def mock_fetch_crews(module_attr):
nonlocal template_crew_detected
if hasattr(module_attr, "__file__") and "templates" in module_attr.__file__:
template_crew_detected = True
return [mock_crew]
monkeypatch.setattr(utils, "fetch_crews", mock_fetch_crews)
utils.get_crews()
assert not template_crew_detected

View File

@@ -0,0 +1,43 @@
from pathlib import Path
from crewai.cli.constants import PROVIDERS
def test_cli_documentation_matches_providers():
"""Test that CLI documentation accurately reflects the available providers."""
docs_path = Path(__file__).parent.parent / "docs" / "concepts" / "cli.mdx"
with open(docs_path, 'r') as f:
docs_content = f.read()
assert "top 5" not in docs_content.lower(), "Documentation should not mention 'top 5' providers"
assert "5 most common" not in docs_content.lower(), "Documentation should not mention '5 most common' providers"
assert "list of available LLM providers" in docs_content or "following LLM providers" in docs_content, \
"Documentation should mention the availability of multiple LLM providers"
assert len(PROVIDERS) > 5, f"Expected more than 5 providers, but found {len(PROVIDERS)}"
key_providers = ["OpenAI", "Anthropic", "Gemini"]
for provider in key_providers:
assert provider in docs_content, f"Key provider {provider} should be mentioned in documentation"
def test_providers_list_matches_constants():
"""Test that the actual PROVIDERS list has the expected providers."""
expected_providers = [
"openai",
"anthropic",
"gemini",
"nvidia_nim",
"groq",
"huggingface",
"ollama",
"watson",
"bedrock",
"azure",
"cerebras",
"sambanova",
]
assert PROVIDERS == expected_providers, f"PROVIDERS list has changed. Expected {expected_providers}, got {PROVIDERS}"
assert len(PROVIDERS) == 12, f"Expected 12 providers, but found {len(PROVIDERS)}"