fix(cli): load json runner from source checkout

This commit is contained in:
Joao Moura
2026-06-15 10:06:30 -07:00
parent bfc817bbd3
commit ce74cdf02c
2 changed files with 113 additions and 9 deletions

View File

@@ -35,12 +35,46 @@ class CrewType(Enum):
# crewai.utilities.string_utils (_VARIABLE_PATTERN), including hyphens —
# otherwise placeholders are interpolated at runtime but never prompted for.
_INPUT_PLACEHOLDER_RE = re.compile(r"(?<!{){([A-Za-z_][A-Za-z0-9_\-]*)}(?!})")
_JSON_CREW_RUNNER_CODE = (
"import os; "
"from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV; "
"from crewai_cli.run_crew import _run_json_crew; "
"_run_json_crew(trained_agents_file=os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV))"
_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV = "CREWAI_CLI_RUNNER_PACKAGE_DIR"
_CREWAI_RUNNER_SOURCE_DIR_ENV = "CREWAI_RUNNER_SOURCE_DIR"
_JSON_CREW_RUNNER_CODE = """
import importlib.util
import os
from pathlib import Path
import sys
source_dir = os.environ.get("CREWAI_RUNNER_SOURCE_DIR")
if source_dir:
sys.path.insert(0, source_dir)
package_dir = Path(os.environ["CREWAI_CLI_RUNNER_PACKAGE_DIR"])
package_spec = importlib.util.spec_from_file_location(
"crewai_cli",
package_dir / "__init__.py",
submodule_search_locations=[str(package_dir)],
)
if package_spec is None or package_spec.loader is None:
raise ImportError(f"Cannot load CrewAI CLI package from {package_dir}")
package = importlib.util.module_from_spec(package_spec)
sys.modules["crewai_cli"] = package
package_spec.loader.exec_module(package)
module_path = package_dir / "run_crew.py"
module_spec = importlib.util.spec_from_file_location("crewai_cli.run_crew", module_path)
if module_spec is None or module_spec.loader is None:
raise ImportError(f"Cannot load CrewAI CLI runner from {module_path}")
module = importlib.util.module_from_spec(module_spec)
sys.modules["crewai_cli.run_crew"] = module
module_spec.loader.exec_module(module)
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
module._run_json_crew(
trained_agents_file=os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV)
)
""".strip()
def _has_json_crew() -> bool:
@@ -238,6 +272,15 @@ def _install_json_crew_dependencies() -> None:
raise SystemExit(1) from e
def _find_local_crewai_source_dir() -> Path | None:
"""Return the repo's CrewAI source dir when running from a source checkout."""
for parent in Path(__file__).resolve().parents:
candidate = parent / "lib" / "crewai" / "src"
if (candidate / "crewai" / "project" / "json_loader.py").is_file():
return candidate
return None
def _run_json_crew_in_project_env(trained_agents_file: str | None = None) -> Any:
"""Run JSON crews from the project's uv-managed environment."""
if not (Path.cwd() / "pyproject.toml").is_file():
@@ -247,6 +290,9 @@ def _run_json_crew_in_project_env(trained_agents_file: str | None = None) -> Any
command = ["uv", "run", "--no-sync", "python", "-c", _JSON_CREW_RUNNER_CODE]
env = build_env_with_all_tool_credentials()
env[_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(Path(__file__).resolve().parent)
if local_crewai_source_dir := _find_local_crewai_source_dir():
env[_CREWAI_RUNNER_SOURCE_DIR_ENV] = str(local_crewai_source_dir)
if trained_agents_file:
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file

View File

@@ -3,6 +3,7 @@
import os
from pathlib import Path
import subprocess
import sys
import pytest
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
@@ -56,6 +57,18 @@ def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path:
trained_agents_file="trained.pkl"
)
expected_env = {
"EXISTING": "value",
run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV: str(
Path(run_crew_module.__file__).resolve().parent
),
CREWAI_TRAINED_AGENTS_FILE_ENV: "trained.pkl",
}
if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir():
expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str(
local_crewai_source_dir
)
assert install_calls == [True]
assert subprocess_calls == [
(
@@ -71,15 +84,60 @@ def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path:
"capture_output": False,
"text": True,
"check": True,
"env": {
"EXISTING": "value",
CREWAI_TRAINED_AGENTS_FILE_ENV: "trained.pkl",
},
"env": expected_env,
},
)
]
def test_json_runner_code_loads_current_cli_package_over_project_env(tmp_path: Path):
old_parent = tmp_path / "old"
old_pkg = old_parent / "crewai_cli"
old_pkg.mkdir(parents=True)
(old_pkg / "__init__.py").write_text("")
(old_pkg / "run_crew.py").write_text("raise ImportError('old package used')\n")
old_crewai_project = old_parent / "crewai" / "project"
old_crewai_project.mkdir(parents=True)
(old_parent / "crewai" / "__init__.py").write_text("")
(old_crewai_project / "__init__.py").write_text("")
(old_crewai_project / "json_loader.py").write_text(
"raise ImportError('old crewai used')\n"
)
current_pkg = tmp_path / "current" / "crewai_cli"
current_pkg.mkdir(parents=True)
marker = tmp_path / "marker.txt"
(current_pkg / "__init__.py").write_text("")
(current_pkg / "run_crew.py").write_text(
"from pathlib import Path\n"
"from crewai.project.json_loader import SOURCE\n"
"def _run_json_crew(trained_agents_file=None):\n"
f" Path({str(marker)!r}).write_text(SOURCE + ':' + (trained_agents_file or ''))\n"
)
current_crewai_project = tmp_path / "current_crewai_src" / "crewai" / "project"
current_crewai_project.mkdir(parents=True)
(tmp_path / "current_crewai_src" / "crewai" / "__init__.py").write_text("")
(current_crewai_project / "__init__.py").write_text("")
(current_crewai_project / "json_loader.py").write_text("SOURCE = 'current'\n")
env = os.environ.copy()
env["PYTHONPATH"] = str(old_parent)
env[run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(current_pkg)
env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str(
tmp_path / "current_crewai_src"
)
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = "trained.pkl"
subprocess.run(
[sys.executable, "-c", run_crew_module._JSON_CREW_RUNNER_CODE],
check=True,
env=env,
cwd=tmp_path,
)
assert marker.read_text() == "current:trained.pkl"
def test_json_run_without_pyproject_runs_in_process(monkeypatch, tmp_path: Path):
monkeypatch.chdir(tmp_path)
called: dict = {}