fix(cli): open the conversational TUI for a declarative chat flow (#7060)

* fix(cli): open the conversational TUI for a declarative chat flow

`crewai run` refused a declarative conversational flow and told the user to
drive it from Python. That was wrong: the conversational TUI already exists and
already does this job. `CrewRunApp(conversational=True)` renders a chat pane and
drives `handle_turn` per message (crew_run_tui.py:833-935), and
`kickoff_flow._run_conversational_flow_tui` launches it for a Python
conversational Flow.

A declaration-built flow satisfies everything that TUI needs -- `handle_turn`,
a settable `defer_trace_finalization`, and `finalize_session_traces()` -- so it
now routes there instead of exiting.

A chat loop still needs a terminal. A headless run (`is_interactive()` false,
which folds in CREWAI_DMN) says what it would have needed rather than kicking
off a single turn and presenting that as the whole conversation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): do not send a human-feedback chat flow to the Textual TUI

A declaration can carry both a `conversational:` block and a method with
`human_feedback:` -- verified, both predicates return True on the same flow.
Routing it to the chat TUI hangs: the runtime collects feedback with a blocking
`input()` (flow/runtime/__init__.py:3719) that Textual cannot service, so the
prompt is never shown. The STEPS TUI already declines these for exactly this
reason. Such a flow now falls back to the terminal REPL, which can prompt.

Also updates the guide in en/ar/ko/pt-BR: it still said `crewai run` has no
chat loop and exits, which is now the opposite of what the CLI does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): reject --inputs on a conversational flow instead of dropping it

The conversational branch returns before `_resolve_flow_inputs`, and the TUI
calls `handle_turn(message)` -- which owns the kickoff inputs, passing
`{"id": session_id}` itself. So any `--inputs` value was silently discarded and
the conversation ran as if it had been applied. It now errors, and says that
resuming a session by id is not wired up yet rather than implying it worked.

Also corrects the Arabic guide: `مُوجّه محجوز` reads as "reserved router", not
the blocking prompt it describes.

Both found by CodeRabbit on #7060.

* fix(cli): document the conversational routing exceptions

Three review follow-ups:

- The Arabic guide read خدمته (masculine) against the feminine
  مُطالبة introduced by the last fix.
- Both docstrings described a routing path that now has exceptions: a
  conversational declaration rejects --inputs and skips state-schema
  resolution, and a human-feedback one uses the terminal REPL.
- The --inputs rejection test accepted SystemExit(0); it now pins code 1.

Found by CodeRabbit on #7060.

* fix(cli): reject --inputs on a chat flow even when it parses empty

parse_inputs_json returns {} both when the option is absent and when the user
passes --inputs "{}", so the falsy check started the TUI for the second case
while the docs said it was unsupported. The conversational path now takes
whether the option was supplied, not what it parsed to.

Documents the restriction in en, ar, ko and pt-BR.

Found by CodeRabbit on #7060.

* test(cli): pin the headless conversational exit status

pytest.raises(SystemExit) also accepts SystemExit(0), so the error path could
regress to a successful exit unnoticed.

Found by CodeRabbit on #7060.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Moura
2026-08-21 06:06:40 -03:00
committed by GitHub
parent 456c67d7c2
commit 4718b190d1
6 changed files with 210 additions and 18 deletions

View File

@@ -59,6 +59,10 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N
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.
A conversational declaration takes none of that: each turn's input is the
message typed into the chat, so ``--inputs`` is rejected and no
state-schema resolution runs.
"""
# 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
@@ -74,15 +78,8 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N
flow = load_declarative_flow(definition)
if _flow_is_conversational(flow):
click.secho(
" This flow declares `conversational`, and `crewai run` has no chat "
"loop yet — it would run a single turn and exit.\n"
" Drive it from Python for now: `flow.chat()` for a terminal REPL, "
"or `flow.handle_turn(message, session_id=...)` per message.",
fg="yellow",
err=True,
)
raise SystemExit(1)
_run_conversational_declarative_flow(flow, inputs is not None)
return
resolved_inputs = _resolve_flow_inputs(flow, provided)
@@ -108,6 +105,62 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N
click.echo(_format_result(result))
def _run_conversational_declarative_flow(
flow: Flow[Any], inputs_supplied: bool
) -> None:
"""Run a declarative chat flow on the conversational TUI.
The same TUI a Python conversational Flow gets from ``crewai run``; it
drives ``handle_turn`` per message. A chat loop needs a terminal, so a
headless run says what it would have needed rather than kicking off one
turn and exiting as if that were the whole conversation.
Two flows do not reach that TUI: one passed ``--inputs``, which it has
nowhere to put, and one using ``@human_feedback``, which needs the
terminal ``flow.chat()`` REPL because the runtime collects feedback with a
blocking prompt Textual cannot service.
"""
if inputs_supplied:
# Whether ``--inputs`` was passed at all, not whether it parsed to
# anything: ``--inputs '{}'`` is a request for something unsupported and
# has to be answered, not silently accepted as no inputs.
# The TUI calls ``handle_turn(message)``, which owns the kickoff inputs
# (it passes ``{"id": session_id}`` itself). There is nowhere to put
# these without fighting it, so say so rather than accepting them and
# running a conversation that quietly ignored them.
click.secho(
" `--inputs` is not supported for a conversational flow: each turn's "
"input is the message you type.\n"
" Resuming a session by id is not wired up yet — use "
"`flow.handle_turn(message, session_id=...)` from Python for that.",
fg="red",
err=True,
)
raise SystemExit(1)
if not is_interactive():
click.secho(
" This flow is conversational, which needs an interactive terminal.\n"
" Drive it from Python instead: `flow.handle_turn(message, "
"session_id=...)` per message, or `flow.stream_turn(...)` to stream.",
fg="yellow",
err=True,
)
raise SystemExit(1)
if _flow_uses_human_feedback(flow):
# Same reason the STEPS TUI declines these: the runtime collects feedback
# with a blocking ``input()`` (flow/runtime/__init__.py), which Textual
# cannot service -- the prompt would never be shown and the run would
# hang. A terminal REPL can, so fall back to one.
flow.chat()
return
from crewai_cli.kickoff_flow import _run_conversational_flow_tui
_run_conversational_flow_tui(flow)
def _run_declarative_flow_tui(
flow: Flow[Any], resolved_inputs: dict[str, Any] | None
) -> Any: