mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 06:08:15 +00:00
Implement DMN mode support in crew creation and execution (#6194)
* Implement DMN mode support in crew creation and execution - Added `is_dmn_mode_enabled` utility to check for enterprise non-interactive mode based on the `CREWAI_DMN` environment variable. - Updated `create` function in `cli.py` to enforce required parameters when DMN mode is active, raising appropriate usage errors. - Enhanced `create_crew` and `create_json_crew` functions to skip provider prompts and handle folder existence checks in DMN mode. - Introduced non-interactive defaults for agent and task creation in DMN mode, ensuring seamless project setup without user input. - Modified `run_crew` to bypass TUI and handle runtime inputs directly when in DMN mode, improving execution flow for JSON-defined crews. - Added tests to validate DMN mode behavior, ensuring correct handling of required inputs and non-interactive defaults. * Implement DMN mode support in crew creation and execution - Introduced `is_dmn_mode_enabled()` utility to check for non-interactive mode based on the `CREWAI_DMN` environment variable. - Updated `create` function to enforce required parameters when DMN mode is active, raising appropriate usage errors. - Modified `create_crew` and `create_json_crew` functions to skip provider prompts and utilize non-interactive defaults in DMN mode. - Enhanced `run_crew` to bypass TUI and handle runtime inputs directly in DMN mode, ensuring smooth execution without user interaction. - Added tests to validate DMN mode behavior, including requirements for type and name, and ensuring proper handling of existing folders and missing inputs.
This commit is contained in:
@@ -17,6 +17,7 @@ from crewai_cli.user_data import (
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
read_toml,
|
||||
)
|
||||
|
||||
@@ -162,7 +163,13 @@ def create(
|
||||
classic: bool = False,
|
||||
) -> None:
|
||||
"""Create a new crew, or flow."""
|
||||
dmn_mode = is_dmn_mode_enabled()
|
||||
if not type:
|
||||
if dmn_mode:
|
||||
raise click.UsageError(
|
||||
"TYPE is required when CREWAI_DMN is set. "
|
||||
"Use `crewai create crew <name>` or `crewai create flow <name>`."
|
||||
)
|
||||
from crewai_cli.tui_picker import pick
|
||||
|
||||
options = [
|
||||
@@ -177,11 +184,15 @@ def create(
|
||||
raise SystemExit(0)
|
||||
click.echo()
|
||||
if not name:
|
||||
if dmn_mode:
|
||||
raise click.UsageError("NAME is required when CREWAI_DMN is set.")
|
||||
enable_prompt_line_editing()
|
||||
name = click.prompt(
|
||||
click.style(f" Name of your {type}", fg="cyan", bold=True),
|
||||
prompt_suffix=click.style(" › ", fg="bright_white"), # noqa: RUF001
|
||||
)
|
||||
if dmn_mode:
|
||||
skip_provider = True
|
||||
if type == "crew":
|
||||
if classic:
|
||||
from crewai_cli.create_crew import create_crew
|
||||
|
||||
@@ -11,7 +11,12 @@ from crewai_cli.provider import (
|
||||
select_model,
|
||||
select_provider,
|
||||
)
|
||||
from crewai_cli.utils import copy_template, load_env_vars, write_env_file
|
||||
from crewai_cli.utils import (
|
||||
copy_template,
|
||||
is_dmn_mode_enabled,
|
||||
load_env_vars,
|
||||
write_env_file,
|
||||
)
|
||||
|
||||
|
||||
def get_reserved_script_names() -> set[str]:
|
||||
@@ -120,6 +125,8 @@ def create_folder_structure(
|
||||
folder_path = Path(folder_name)
|
||||
|
||||
if folder_path.exists():
|
||||
if is_dmn_mode_enabled():
|
||||
raise click.ClickException(f"Folder {folder_name} already exists.")
|
||||
if not click.confirm(
|
||||
f"Folder {folder_name} already exists. Do you want to override it?"
|
||||
):
|
||||
@@ -201,6 +208,8 @@ def create_crew(
|
||||
) -> None:
|
||||
folder_path, folder_name, class_name = create_folder_structure(name, parent_folder)
|
||||
env_vars = load_env_vars(folder_path)
|
||||
if is_dmn_mode_enabled():
|
||||
skip_provider = True
|
||||
if not skip_provider:
|
||||
if not provider:
|
||||
provider_models = get_provider_data()
|
||||
|
||||
@@ -14,7 +14,12 @@ from rich.text import Text
|
||||
|
||||
from crewai_cli.constants import ENV_VARS
|
||||
from crewai_cli.tui_picker import pick_many, pick_one
|
||||
from crewai_cli.utils import enable_prompt_line_editing, load_env_vars, write_env_file
|
||||
from crewai_cli.utils import (
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
load_env_vars,
|
||||
write_env_file,
|
||||
)
|
||||
|
||||
|
||||
# ── Provider / model data ───────────────────────────────────────
|
||||
@@ -641,6 +646,43 @@ def _wizard_agents_and_tasks(
|
||||
return agents, tasks, crew_settings
|
||||
|
||||
|
||||
def _default_agents_and_tasks(
|
||||
default_llm: str | None = None,
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
|
||||
"""Return deterministic scaffold data for non-interactive project creation."""
|
||||
llm = default_llm or "openai/gpt-4o"
|
||||
agents = [
|
||||
{
|
||||
"name": "researcher",
|
||||
"role": "Senior Researcher",
|
||||
"goal": "Research the requested topic and identify useful findings.",
|
||||
"backstory": (
|
||||
"You are an experienced researcher who finds relevant information "
|
||||
"and presents it clearly."
|
||||
),
|
||||
"llm": llm,
|
||||
"tools": [],
|
||||
"planning": False,
|
||||
"allow_delegation": False,
|
||||
}
|
||||
]
|
||||
tasks = [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research current AI trends and write a concise summary.",
|
||||
"expected_output": "A concise markdown report with key findings.",
|
||||
"agent": "researcher",
|
||||
"context": [],
|
||||
}
|
||||
]
|
||||
crew_settings = {
|
||||
"process": "sequential",
|
||||
"memory": False,
|
||||
"inputs": {},
|
||||
}
|
||||
return agents, tasks, crew_settings
|
||||
|
||||
|
||||
# ── JSONC generation from wizard data ──────────────────────────
|
||||
|
||||
|
||||
@@ -1029,7 +1071,9 @@ def create_json_crew(
|
||||
import keyword
|
||||
import shutil
|
||||
|
||||
enable_prompt_line_editing()
|
||||
dmn_mode = is_dmn_mode_enabled()
|
||||
if not dmn_mode:
|
||||
enable_prompt_line_editing()
|
||||
|
||||
name = name.rstrip("/")
|
||||
if not name.strip():
|
||||
@@ -1048,6 +1092,8 @@ def create_json_crew(
|
||||
|
||||
folder_path = Path(folder_name)
|
||||
if folder_path.exists():
|
||||
if dmn_mode:
|
||||
raise click.ClickException(f"Folder {folder_name} already exists.")
|
||||
if not click.confirm(f"Folder {folder_name} already exists. Override?"):
|
||||
click.secho("Cancelled.", fg="yellow")
|
||||
sys.exit(0)
|
||||
@@ -1056,10 +1102,14 @@ def create_json_crew(
|
||||
click.echo()
|
||||
click.secho(f" Creating crew: {name}", fg="green", bold=True)
|
||||
|
||||
agents, tasks, crew_settings = _wizard_agents_and_tasks(
|
||||
skip_provider=skip_provider,
|
||||
default_llm=_default_model_for_provider(provider),
|
||||
)
|
||||
default_llm = _default_model_for_provider(provider)
|
||||
if dmn_mode:
|
||||
agents, tasks, crew_settings = _default_agents_and_tasks(default_llm)
|
||||
else:
|
||||
agents, tasks, crew_settings = _wizard_agents_and_tasks(
|
||||
skip_provider=skip_provider,
|
||||
default_llm=default_llm,
|
||||
)
|
||||
|
||||
# Create directories
|
||||
folder_path.mkdir(parents=True)
|
||||
@@ -1104,7 +1154,7 @@ def create_json_crew(
|
||||
(folder_path / "skills" / ".gitkeep").write_text("", encoding="utf-8")
|
||||
|
||||
# Setup .env with API keys
|
||||
if not skip_provider:
|
||||
if not skip_provider and not dmn_mode:
|
||||
models = list({a["llm"] for a in agents})
|
||||
for model in models:
|
||||
_setup_env(folder_path, model)
|
||||
|
||||
@@ -17,6 +17,7 @@ from packaging import version
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
read_toml,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_version
|
||||
@@ -202,6 +203,35 @@ def _prepare_json_crew_for_tui(crew: Any) -> None:
|
||||
agent.llm.stream = True
|
||||
|
||||
|
||||
def _runtime_inputs_without_prompt(
|
||||
crew: Any, default_inputs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Return runtime inputs in non-interactive mode or exit on missing values."""
|
||||
inputs = dict(default_inputs or {})
|
||||
missing = _missing_input_names(crew, inputs)
|
||||
if missing:
|
||||
missing_list = ", ".join(missing)
|
||||
click.echo(
|
||||
"Missing runtime inputs for CREWAI_DMN mode: "
|
||||
f"{missing_list}. Add them to the `inputs` object in crew.json(c).",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
return inputs
|
||||
|
||||
|
||||
def _run_json_crew_without_tui(crew_path: Path) -> Any:
|
||||
"""Run a JSON-defined crew with plain terminal output."""
|
||||
with _json_loading_status("Preparing crew..."):
|
||||
crew, default_inputs = _load_json_crew(crew_path)
|
||||
|
||||
runtime_inputs = _runtime_inputs_without_prompt(crew, default_inputs)
|
||||
result = crew.kickoff(inputs=runtime_inputs)
|
||||
if result is not None:
|
||||
click.echo(str(result))
|
||||
return result
|
||||
|
||||
|
||||
def _run_json_crew(trained_agents_file: str | None = None) -> Any:
|
||||
"""Load and run a JSON-defined crew."""
|
||||
from dotenv import load_dotenv
|
||||
@@ -219,6 +249,9 @@ def _run_json_crew(trained_agents_file: str | None = None) -> Any:
|
||||
if crew_path is None:
|
||||
raise FileNotFoundError("No crew.jsonc or crew.json found")
|
||||
|
||||
if is_dmn_mode_enabled():
|
||||
return _run_json_crew_without_tui(crew_path)
|
||||
|
||||
crew_run_app_cls, crew, default_inputs, task_names, agent_names = (
|
||||
_load_json_crew_for_tui(crew_path)
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ __all__ = [
|
||||
"get_project_description",
|
||||
"get_project_name",
|
||||
"get_project_version",
|
||||
"is_dmn_mode_enabled",
|
||||
"load_env_vars",
|
||||
"parse_toml",
|
||||
"read_toml",
|
||||
@@ -41,6 +42,14 @@ __all__ = [
|
||||
console = Console()
|
||||
|
||||
|
||||
def is_dmn_mode_enabled() -> bool:
|
||||
"""Return True when the enterprise non-interactive mode is enabled."""
|
||||
value = os.environ.get("CREWAI_DMN")
|
||||
if value is None:
|
||||
return False
|
||||
return value.strip().lower() not in {"", "0", "false", "no", "off"}
|
||||
|
||||
|
||||
def enable_prompt_line_editing() -> None:
|
||||
"""Enable cursor movement/history editing for Click text prompts when available."""
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest import mock
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from crewai_cli.cli import (
|
||||
create,
|
||||
deploy_create,
|
||||
deploy_list,
|
||||
deploy_logs,
|
||||
@@ -157,6 +158,28 @@ def test_run_rejects_inputs_without_definition(run_flow_definition, run_crew, ru
|
||||
run_crew.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.create_json_crew.create_json_crew")
|
||||
def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner):
|
||||
result = runner.invoke(create, ["crew", "DMN Crew"], env={"CREWAI_DMN": "True"})
|
||||
|
||||
assert result.exit_code == 0
|
||||
create_json_crew.assert_called_once_with("DMN Crew", None, True)
|
||||
|
||||
|
||||
def test_create_requires_type_in_dmn_mode(runner):
|
||||
result = runner.invoke(create, env={"CREWAI_DMN": "True"})
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "TYPE is required when CREWAI_DMN is set" in result.output
|
||||
|
||||
|
||||
def test_create_requires_name_in_dmn_mode(runner):
|
||||
result = runner.invoke(create, ["flow"], env={"CREWAI_DMN": "True"})
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "NAME is required when CREWAI_DMN is set" in result.output
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.AuthenticationCommand")
|
||||
def test_login(command, runner):
|
||||
mock_auth = command.return_value
|
||||
|
||||
@@ -812,3 +812,34 @@ def test_json_wizard_task_reprompts_on_cancelled_agent_pick(monkeypatch):
|
||||
|
||||
assert len(pick_calls) == 2
|
||||
assert task["agent"] == "second_agent"
|
||||
|
||||
|
||||
def test_json_create_dmn_mode_uses_non_interactive_defaults(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("CREWAI_DMN", "True")
|
||||
monkeypatch.setattr(
|
||||
json_crew,
|
||||
"_wizard_agents_and_tasks",
|
||||
lambda **_: pytest.fail("DMN mode must not run the wizard"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
json_crew,
|
||||
"_setup_env",
|
||||
lambda *_args, **_kwargs: pytest.fail("DMN mode must not prompt for env vars"),
|
||||
)
|
||||
|
||||
json_crew.create_json_crew("DMN Crew", provider="anthropic", skip_provider=False)
|
||||
|
||||
project_root = tmp_path / "dmn_crew"
|
||||
assert (project_root / "crew.jsonc").exists()
|
||||
assert (project_root / "agents" / "researcher.jsonc").exists()
|
||||
assert not (project_root / ".env").exists()
|
||||
|
||||
crew_template = (project_root / "crew.jsonc").read_text()
|
||||
agent_template = (project_root / "agents" / "researcher.jsonc").read_text()
|
||||
|
||||
assert '"memory": false' in crew_template
|
||||
assert '"description": "Research current AI trends and write a concise summary."' in (
|
||||
crew_template
|
||||
)
|
||||
assert '"llm": "anthropic/claude-opus-4-6"' in agent_template
|
||||
|
||||
@@ -402,6 +402,7 @@ def test_missing_input_names_accepts_hyphenated_placeholders():
|
||||
|
||||
def _patch_tui_run(monkeypatch, status: str):
|
||||
"""Stub the TUI pieces of _run_json_crew so only exit handling runs."""
|
||||
monkeypatch.delenv("CREWAI_DMN", raising=False)
|
||||
|
||||
class FakeApp:
|
||||
def __init__(self, **kwargs):
|
||||
@@ -446,6 +447,84 @@ def test_run_json_crew_completed_status_returns_result(monkeypatch, tmp_path: Pa
|
||||
assert run_crew_module._run_json_crew() == "result"
|
||||
|
||||
|
||||
def test_run_json_crew_dmn_mode_bypasses_tui(monkeypatch, tmp_path: Path, capsys):
|
||||
from types import SimpleNamespace
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("CREWAI_DMN", "True")
|
||||
crew_path = tmp_path / "crew.jsonc"
|
||||
crew_path.write_text("{}")
|
||||
kickoff_calls = []
|
||||
|
||||
class FakeCrew:
|
||||
name = "Demo"
|
||||
agents = [SimpleNamespace(role="Researcher", goal="Research", backstory="")]
|
||||
tasks = [
|
||||
SimpleNamespace(
|
||||
description="Research",
|
||||
expected_output="Findings",
|
||||
output_file=None,
|
||||
)
|
||||
]
|
||||
|
||||
def kickoff(self, inputs):
|
||||
kickoff_calls.append(inputs)
|
||||
return "plain result"
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "find_crew_json_file", lambda: crew_path)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_load_json_crew",
|
||||
lambda _path: (FakeCrew(), {"topic": "AI"}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_load_json_crew_for_tui",
|
||||
lambda _path: pytest.fail("DMN mode must not start the TUI loader"),
|
||||
)
|
||||
|
||||
assert run_crew_module._run_json_crew() == "plain result"
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert kickoff_calls == [{"topic": "AI"}]
|
||||
assert "plain result" in captured.out
|
||||
|
||||
|
||||
def test_run_json_crew_dmn_mode_exits_on_missing_inputs(
|
||||
monkeypatch, tmp_path: Path, capsys
|
||||
):
|
||||
from types import SimpleNamespace
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("CREWAI_DMN", "True")
|
||||
crew_path = tmp_path / "crew.jsonc"
|
||||
crew_path.write_text("{}")
|
||||
crew = SimpleNamespace(
|
||||
agents=[
|
||||
SimpleNamespace(
|
||||
role="Researcher",
|
||||
goal="Research {topic}",
|
||||
backstory="",
|
||||
)
|
||||
],
|
||||
tasks=[],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "find_crew_json_file", lambda: crew_path)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_load_json_crew",
|
||||
lambda _path: (crew, {}),
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_crew_module._run_json_crew()
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert exc_info.value.code == 1
|
||||
assert "Missing runtime inputs for CREWAI_DMN mode: topic" in captured.err
|
||||
|
||||
|
||||
def test_has_json_crew_defers_to_declared_flow_type(monkeypatch, tmp_path: Path):
|
||||
"""A flow project containing a stray crew.jsonc must still run as a flow."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
@@ -102,6 +102,23 @@ def test_tree_copy_to_existing_directory(temp_tree):
|
||||
shutil.rmtree(dest_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["1", "true", "True", "yes", "anything"])
|
||||
def test_is_dmn_mode_enabled_for_truthy_values(monkeypatch, value):
|
||||
monkeypatch.setenv("CREWAI_DMN", value)
|
||||
|
||||
assert utils.is_dmn_mode_enabled() is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [None, "", "0", "false", "no", "off"])
|
||||
def test_is_dmn_mode_enabled_for_falsey_values(monkeypatch, value):
|
||||
if value is None:
|
||||
monkeypatch.delenv("CREWAI_DMN", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("CREWAI_DMN", value)
|
||||
|
||||
assert utils.is_dmn_mode_enabled() is False
|
||||
|
||||
|
||||
# Tests for extract_available_exports, get_crews, get_flows, fetch_crews,
|
||||
# is_valid_tool live in lib/crewai/tests/cli/test_utils.py — the canonical
|
||||
# implementations are in crewai.utilities.project_utils.
|
||||
|
||||
Reference in New Issue
Block a user