Files
crewAI/lib/cli/src/crewai_cli/input_prompt.py
Joao Moura 96546e37c3 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
2026-07-07 12:03:54 -07:00

85 lines
2.9 KiB
Python

"""Shared interactive prompting for runtime inputs (flows and crews).
``crewai run`` asks the user for values that were not provided up front — for a
declarative flow (derived from its state schema) and for a declarative (JSON)
crew (derived from the ``{placeholder}`` references in its agents and tasks).
Both paths go through this module so the experience is identical: the same
header, the same per-field prompt styling, and prompt chrome on stderr so
stdout carries only the run's result.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable
import difflib
import json
import sys
from typing import Any
import click
from crewai_cli.utils import enable_prompt_line_editing, is_dmn_mode_enabled
def parse_inputs_json(inputs: str | None) -> dict[str, Any] | None:
"""Parse a ``--inputs`` JSON object, exiting with a pointed error if invalid."""
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 closest_name(key: str, candidates: Iterable[str]) -> str | None:
"""Nearest candidate name to a likely typo, if one is close enough."""
matches = difflib.get_close_matches(key, list(candidates), n=1, cutoff=0.7)
return matches[0] if matches else None
def is_interactive() -> bool:
"""Prompt only in an interactive terminal, never in non-interactive mode."""
return not is_dmn_mode_enabled() and sys.stdin.isatty()
def prompt_for_inputs(
names: list[str],
*,
title: str,
subtitle: str,
describe: Callable[[str], str | None] | None = None,
coerce: Callable[[str, str], Any] | None = None,
) -> dict[str, Any]:
"""Prompt for each name and return ``{name: value}``.
``describe(name)`` returns an optional hint shown dim above the prompt (used
by flows to surface a field's schema description). ``coerce(name, raw)``
converts the typed string to the stored value (used by flows to coerce to
the field's JSON-schema type); by default the raw string is kept as-is.
Prompt chrome is written to stderr so stdout carries only the run result.
"""
enable_prompt_line_editing()
click.echo(err=True)
click.secho(f" {title}", fg="cyan", bold=True, err=True)
click.secho(f" {subtitle}", dim=True, err=True)
collected: dict[str, Any] = {}
for name in names:
if describe is not None and (hint := describe(name)):
click.secho(f" {hint}", dim=True, err=True)
raw = click.prompt(
click.style(f" {name}", fg="cyan"),
prompt_suffix=click.style(" > ", fg="bright_white"),
)
collected[name] = coerce(name, raw) if coerce is not None else raw
return collected