mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-25 16:55:11 +00:00
fix(cli): unify crewai run flow input resolution and prompt from the state schema (#6466)
* fix(cli): unify `crewai run` flow input resolution; prompt from state schema
`crewai run` resolved the configured [tool.crewai] flow, but `--inputs` was
hard-gated behind `--definition` and routed through a separate branch — the two
ways of pointing at the same flow didn't share resolution, and required inputs
were never detected, prompted, or validated (a missing field only blew up at
runtime).
Now inputs and definition come from one place:
- Remove the "--inputs requires --definition" gate (cli.py, run_crew.py,
run_declarative_flow.py). `--inputs` alone resolves the configured flow,
exactly like a bare `crewai run`; `--definition` is purely an override. The
project-env re-exec forwards `--inputs` instead of rejecting it.
- Read the flow's state schema from the runtime Flow instance
(`type(flow.state).model_json_schema()`), which is reliable for both inline
`json_schema` and ref-imported `pydantic` state (the static definition's
json_schema is None for the common ref case).
- Plain `crewai run` detects required state fields (minus those satisfied by
state defaults) and prompts for them interactively, showing each field's
description; skipped in non-interactive / CREWAI_DMN mode.
- Validate against the schema before kickoff: pointed
"Missing required input 'x' — <description>" errors, and warn on unknown keys
with a did-you-mean suggestion (catches typos like `prospect_emai`).
`--inputs` on a non-flow project now errors clearly ("only supported for
declarative flows") instead of the old confusing gate.
Tests: schema-driven prompt/validate/override paths, unknown-key warning,
defaults-satisfy-required, type validation, and re-exec input forwarding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
* fix(cli): forward reserved `id` input to flow kickoff; ruff format
- Cursor: the schema filter treated an `id` key in --inputs as unknown and
dropped it, regressing kickoff's persistence-restore support (inputs["id"]).
Let `id` pass through untouched (test: reserved_id_input_is_forwarded).
- Apply ruff format to run_declarative_flow.py (fixes the lint-run
`ruff format --check` step).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
* fix(cli): don't block persistence-restore resume on schema validation
Cursor (High): `crewai run --inputs '{"id":"…"}'` is a persistence resume —
kickoff hydrates full state from storage, so schema-required fields may come
from the restored state rather than --inputs. The new required-field
prompt/validation was erroring/prompting before kickoff, breaking resume. When
`id` is present in --inputs, forward the inputs unchanged and skip the
prompt/validation. Test: test_id_only_input_skips_required_validation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
* fix(cli): load project .env in the declarative-flow runner
The declarative-flow path never loaded .env — flow projects (type = "flow")
missed API keys/config that crew projects pick up. The JSON-crew path loads
Path.cwd()/.env with override=True (run_crew._run_json_crew); mirror that at
the top of run_declarative_flow() so flow projects behave the same regardless
of where crewai is installed. Test: run_declarative_flow_loads_project_env.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
* feat(cli): unify runtime-input prompting across declarative flows and crews
Declarative (JSON) crews now resolve inputs the same way declarative flows
do, via a shared crewai_cli.input_prompt module (prompt_for_inputs,
parse_inputs_json, closest_name, is_interactive):
- accept --inputs (previously rejected for crews), forwarded to the crew
subprocess via CREWAI_JSON_CREW_INPUTS and validated before spinning up uv
- layer --inputs over the crew's declared `inputs` defaults
- prompt for missing {placeholder}s with the same UX as flows, and error
cleanly with a pointed per-name message when non-interactive
- warn on unknown keys with a "did you mean" suggestion
Unlike flows — whose state schema is authoritative, so unknown keys are
dropped — the crew placeholder scan is heuristic (agent/task text fields
only), so unrecognized keys are warned about but kept, to avoid discarding a
value a field the scan doesn't cover may rely on.
--inputs remains rejected for classic (Python/YAML) crews, which take their
inputs from main.py. run_declarative_flow's private input helpers move to the
shared module with no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
* test(crewai): update mirrored CLI test after run_crew input refactor
lib/crewai/tests/cli/test_run_crew.py imports crewai_cli internals and is
collected by the lib/crewai test job (Run Tests). It still imported
_prompt_for_missing_inputs, which was replaced by _resolve_crew_inputs, so
the module failed to import — erroring pytest at collection and cancelling
the rest of the matrix via fail-fast.
Point it at _resolve_crew_inputs and patch the prompt in the shared
crewai_cli.input_prompt module where prompting now lives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
* fix(cli): filter unknown --inputs keys even on flow persistence restore
Review follow-up: the `id` (persistence-restore) branch of
_resolve_flow_inputs returned the raw payload, so typo keys passed alongside
`id` skipped the unknown-key warning/drop and reached kickoff — which can
fail strict (extra="forbid") flow state models. The restore path now still
warns on and drops unknown keys (keeping `id` and known state fields); it
only skips the required-field prompt and pre-kickoff validation, which
persistence hydrates. Regression test: test_id_restore_still_drops_unknown_keys.
Also drop the duplicate module import in test_input_prompt.py (both `import`
and `from ... import` of crewai_cli.input_prompt) flagged by the code-quality
bot; monkeypatching now uses the string target form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,12 @@ import click
|
||||
from crewai_core.project import ProjectDefinitionError, configured_project_definition
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai_cli.input_prompt import (
|
||||
closest_name,
|
||||
is_interactive,
|
||||
parse_inputs_json,
|
||||
prompt_for_inputs,
|
||||
)
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
|
||||
|
||||
@@ -20,10 +26,13 @@ def run_declarative_flow_in_project_env(
|
||||
run_declarative_flow(definition=definition, inputs=inputs)
|
||||
return
|
||||
|
||||
# Re-run inside the project env (so the flow loads with the project's deps).
|
||||
# The configured definition is re-resolved there; forward --inputs so the
|
||||
# in-env run kicks off with the same values instead of losing them.
|
||||
command = ["uv", "run", "crewai", "run"]
|
||||
if inputs is not None:
|
||||
raise click.UsageError("--inputs is only supported with --definition")
|
||||
|
||||
_execute_declarative_flow_command(["uv", "run", "crewai", "run"])
|
||||
command += ["--inputs", inputs]
|
||||
_execute_declarative_flow_command(command)
|
||||
|
||||
|
||||
def plot_declarative_flow_in_project_env(definition: str | Path) -> None:
|
||||
@@ -36,12 +45,29 @@ def plot_declarative_flow_in_project_env(definition: str | Path) -> None:
|
||||
|
||||
|
||||
def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> None:
|
||||
"""Run a declarative flow from a definition path."""
|
||||
parsed_inputs = _parse_inputs(inputs)
|
||||
"""Run a declarative flow from a definition path.
|
||||
|
||||
Inputs come from one place: the flow's own state schema. Any ``--inputs``
|
||||
JSON is layered on top as an override, missing required fields are prompted
|
||||
for interactively, and everything is validated against the schema before
|
||||
kickoff — so a bare ``crewai run`` on a configured flow just works.
|
||||
"""
|
||||
# Load the project's .env before kickoff, mirroring the JSON-crew path
|
||||
# (run_crew._run_json_crew) so flow projects pick up API keys/config the
|
||||
# same way regardless of where crewai is installed.
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_file = Path.cwd() / ".env"
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
provided = parse_inputs_json(inputs) or {}
|
||||
|
||||
flow = load_declarative_flow(definition)
|
||||
resolved_inputs = _resolve_flow_inputs(flow, provided)
|
||||
|
||||
try:
|
||||
flow = load_declarative_flow(definition)
|
||||
result = flow.kickoff(inputs=parsed_inputs)
|
||||
result = flow.kickoff(inputs=resolved_inputs or None)
|
||||
except Exception as exc:
|
||||
click.echo(
|
||||
f"An error occurred while running the declarative flow: {exc}", err=True
|
||||
@@ -51,6 +77,152 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N
|
||||
click.echo(_format_result(result))
|
||||
|
||||
|
||||
def _resolve_flow_inputs(flow: Any, provided: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Resolve kickoff inputs from the flow's state schema.
|
||||
|
||||
Warns on unknown keys, prompts for missing required fields (unless
|
||||
non-interactive), and validates types before kickoff. Exits with a pointed
|
||||
message when a required input is still missing or an input is invalid.
|
||||
"""
|
||||
schema = _flow_state_schema(flow)
|
||||
if schema is None:
|
||||
# dict / unschematized state — nothing to derive; pass inputs through.
|
||||
return dict(provided)
|
||||
|
||||
properties = {
|
||||
name: spec
|
||||
for name, spec in (schema.get("properties") or {}).items()
|
||||
if name != "id"
|
||||
}
|
||||
state_model = type(flow.state)
|
||||
defaults = _flow_state_defaults(flow)
|
||||
|
||||
# ``id`` signals a persistence restore: kickoff hydrates the full state from
|
||||
# storage, so required fields may come from the restored state rather than
|
||||
# --inputs. We still filter the rest of the payload below, but skip the
|
||||
# required-field prompt and pre-kickoff validation, which would otherwise
|
||||
# fail on fields the resume will supply.
|
||||
restoring = "id" in provided
|
||||
|
||||
# Unknown keys are almost always typos — warn and drop them (they'd fail
|
||||
# structured-state validation at kickoff anyway). ``id`` is a reserved
|
||||
# kickoff key rather than a state field, so forward it untouched.
|
||||
collected: dict[str, Any] = {}
|
||||
for key, value in provided.items():
|
||||
if key == "id":
|
||||
collected["id"] = value
|
||||
continue
|
||||
if key in properties:
|
||||
collected[key] = value
|
||||
continue
|
||||
suggestion = closest_name(key, properties)
|
||||
hint = f" Did you mean '{suggestion}'?" if suggestion else ""
|
||||
click.secho(
|
||||
f" Ignoring unknown input '{key}' — not in the flow's state schema.{hint}",
|
||||
fg="yellow",
|
||||
err=True,
|
||||
)
|
||||
|
||||
if restoring:
|
||||
return collected
|
||||
|
||||
missing = _missing_required(state_model, {**defaults, **collected})
|
||||
if missing and _is_interactive():
|
||||
collected.update(
|
||||
prompt_for_inputs(
|
||||
missing,
|
||||
title="Flow inputs",
|
||||
subtitle="This flow needs the following to run.",
|
||||
describe=lambda name: (properties.get(name) or {}).get("description"),
|
||||
coerce=lambda name, raw: _coerce_input(raw, properties.get(name) or {}),
|
||||
)
|
||||
)
|
||||
missing = _missing_required(state_model, {**defaults, **collected})
|
||||
|
||||
if missing:
|
||||
for name in missing:
|
||||
description = (properties.get(name) or {}).get("description")
|
||||
suffix = f" — {description}" if description else ""
|
||||
click.secho(
|
||||
f" Missing required input '{name}'{suffix}", fg="red", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
_validate_flow_inputs(state_model, {**defaults, **collected})
|
||||
return collected
|
||||
|
||||
|
||||
def _is_interactive() -> bool:
|
||||
"""Prompt only in an interactive terminal, never in non-interactive mode."""
|
||||
return is_interactive()
|
||||
|
||||
|
||||
def _flow_state_schema(flow: Any) -> dict[str, Any] | None:
|
||||
"""Return the flow's state JSON schema, or ``None`` for dict/plain state."""
|
||||
state = getattr(flow, "state", None)
|
||||
if state is None or isinstance(state, dict):
|
||||
return None
|
||||
model_json_schema = getattr(type(state), "model_json_schema", None)
|
||||
if not callable(model_json_schema):
|
||||
return None
|
||||
try:
|
||||
schema = model_json_schema()
|
||||
except Exception:
|
||||
return None
|
||||
return schema if isinstance(schema, dict) else None
|
||||
|
||||
|
||||
def _flow_state_defaults(flow: Any) -> dict[str, Any]:
|
||||
"""Declared state defaults (``state.default``) from the flow definition."""
|
||||
state_definition = getattr(getattr(flow, "_definition", None), "state", None)
|
||||
default = getattr(state_definition, "default", None)
|
||||
return dict(default) if isinstance(default, dict) else {}
|
||||
|
||||
|
||||
def _missing_required(state_model: Any, values: dict[str, Any]) -> list[str]:
|
||||
"""Required state fields not satisfied by ``values`` (defaults + inputs)."""
|
||||
try:
|
||||
state_model.model_validate(values)
|
||||
except ValidationError as exc:
|
||||
return [
|
||||
str(error["loc"][0])
|
||||
for error in exc.errors()
|
||||
if error.get("type") == "missing" and error.get("loc")
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _validate_flow_inputs(state_model: Any, values: dict[str, Any]) -> None:
|
||||
"""Validate inputs against the state schema; exit with pointed type errors."""
|
||||
try:
|
||||
state_model.model_validate(values)
|
||||
except ValidationError as exc:
|
||||
for error in exc.errors():
|
||||
location = ".".join(str(part) for part in error.get("loc", ()))
|
||||
click.secho(
|
||||
f" Invalid input '{location}': {error.get('msg')}", fg="red", err=True
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
|
||||
def _coerce_input(raw: str, spec: dict[str, Any]) -> Any:
|
||||
"""Best-effort coerce a prompted string to the field's JSON-schema type."""
|
||||
field_type = spec.get("type")
|
||||
if field_type == "integer":
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return raw
|
||||
if field_type == "number":
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
return raw
|
||||
if field_type == "boolean":
|
||||
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
||||
return raw
|
||||
|
||||
|
||||
def plot_declarative_flow(definition: str | Path) -> None:
|
||||
"""Plot a declarative flow from a definition path."""
|
||||
try:
|
||||
@@ -152,23 +324,6 @@ def _has_project_file(project_root: Path | None = None) -> bool:
|
||||
return (root / "pyproject.toml").is_file()
|
||||
|
||||
|
||||
def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
|
||||
if inputs is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(inputs)
|
||||
except json.JSONDecodeError as exc:
|
||||
click.echo(f"Invalid --inputs JSON: {exc}", err=True)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
click.echo("Invalid --inputs JSON: expected an object.", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def _format_result(result: Any) -> str:
|
||||
raw_result = getattr(result, "raw", result)
|
||||
if isinstance(raw_result, str):
|
||||
|
||||
Reference in New Issue
Block a user