mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-22 10:56:50 +00:00
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:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -627,18 +628,156 @@ methods:
|
||||
"""
|
||||
|
||||
|
||||
def test_run_declarative_flow_refuses_a_conversational_flow(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
def test_run_declarative_flow_opens_the_conversational_tui(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A declarative chat flow gets the same TUI a Python one does."""
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8")
|
||||
|
||||
launched: list[Any] = []
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui", launched.append
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path))
|
||||
|
||||
assert len(launched) == 1
|
||||
assert launched[0]._is_conversational_enabled() is True
|
||||
|
||||
|
||||
def test_conversational_flow_does_not_take_the_steps_tui(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The STEPS TUI renders method progress and cannot drive a chat turn."""
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui", lambda flow: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"_run_declarative_flow_tui",
|
||||
lambda *a, **k: pytest.fail("conversational flow reached the STEPS TUI"),
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path))
|
||||
|
||||
|
||||
def test_conversational_flow_rejects_inputs(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""`--inputs` has nowhere to go: handle_turn owns the kickoff inputs."""
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui",
|
||||
lambda flow: pytest.fail("should have rejected --inputs before the TUI"),
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(definition_path), '{"topic": "AI"}'
|
||||
)
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
err = capsys.readouterr().err
|
||||
assert "`--inputs` is not supported for a conversational flow" in err
|
||||
assert "session_id" in err
|
||||
|
||||
|
||||
def test_conversational_flow_rejects_an_empty_inputs_object(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""`--inputs '{}'` asked for something unsupported; answer it, don't ignore it."""
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui",
|
||||
lambda flow: pytest.fail("an empty --inputs object still passed --inputs"),
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path), "{}")
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
assert (
|
||||
"`--inputs` is not supported for a conversational flow"
|
||||
in capsys.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
def test_conversational_flow_without_inputs_still_opens_the_tui(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
launched: list[Any] = []
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui", launched.append
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path), None)
|
||||
|
||||
assert len(launched) == 1
|
||||
|
||||
|
||||
def test_conversational_human_feedback_flow_avoids_the_tui(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Textual cannot service the runtime's blocking feedback prompt.
|
||||
|
||||
The STEPS TUI already declines these for that reason; a conversational one
|
||||
must too, or the prompt is never shown and the run hangs.
|
||||
"""
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(
|
||||
CONVERSATIONAL_FLOW_YAML.replace(
|
||||
" listen: order\n",
|
||||
" listen: order\n human_feedback:\n message: Approve?\n",
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
chatted: list[str] = []
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.kickoff_flow._run_conversational_flow_tui",
|
||||
lambda flow: pytest.fail("human-feedback flow reached the Textual TUI"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai.flow.flow.Flow.chat", lambda self, **kw: chatted.append("repl")
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path))
|
||||
|
||||
assert chatted == ["repl"]
|
||||
|
||||
|
||||
def test_conversational_flow_headless_explains_instead_of_one_turn(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(CONVERSATIONAL_FLOW_YAML, encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: False)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_declarative_flow_module.run_declarative_flow(str(definition_path))
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
err = capsys.readouterr().err
|
||||
assert "has no chat loop yet" in err
|
||||
assert "flow.chat()" in err
|
||||
assert "needs an interactive terminal" in err
|
||||
assert "handle_turn" in err
|
||||
|
||||
|
||||
def test_run_declarative_flow_still_runs_a_disabled_conversational_flow(
|
||||
|
||||
Reference in New Issue
Block a user