mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-20 18:13:49 +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:
@@ -488,7 +488,7 @@ finally:
|
||||
| `router.response_format` كصنف نموذج | احذفه؛ يولّد الإطار واحدًا. يُتجاهل المرجع أو المخطط مع تحذير |
|
||||
| تجاوزات `route_turn()` / `can_answer_from_history()` | اكتب التدفق بلغة Python، أو وجّه `do` لطريقة إلى مرجع `call: code` |
|
||||
|
||||
لا يملك `crewai run` حلقة محادثة بعد: فهو يبلّغ أن التدفق محادثاتي ويخرج بدلًا من تنفيذ جولة واحدة. شغّل التدفق المحادثاتي التعريفي من Python عبر `handle_turn()` أو `stream_turn()` أو `chat()`.
|
||||
يفتح `crewai run` واجهة المحادثة النصية للتدفق المحادثاتي التعريفي — نفس الواجهة التي يحصل عليها تدفق محادثاتي مكتوب بلغة Python. تحتاج حلقة المحادثة إلى طرفية، لذا يُبلّغ التشغيل بدون طرفية بذلك بدلًا من تنفيذ جولة واحدة؛ شغّله من Python هناك عبر `handle_turn()` أو `stream_turn()`. والتدفق الذي يستخدم `@human_feedback` أيضًا يعمل على حلقة طرفية، لأن زمن التشغيل يجمع الملاحظات عبر مُطالبة حاجبة لا تستطيع الواجهة النصية خدمتها. لا يُقبل `--inputs` مع التدفق المحادثاتي — فمدخل كل دورة هو الرسالة التي تكتبها — واستئناف جلسة عبر المعرّف غير موصول بواجهة سطر الأوامر بعد؛ استخدم `flow.handle_turn(message, session_id=...)` من Python لذلك.
|
||||
|
||||
## التتبع عبر الجولات
|
||||
|
||||
|
||||
@@ -553,7 +553,7 @@ Route labels and method names share one trigger namespace, so a handler must not
|
||||
| `router.response_format` as a model class | Omit it; the framework synthesizes one. A ref or schema is ignored with a warning |
|
||||
| `route_turn()` / `can_answer_from_history()` overrides | Author the Flow in Python, or point a method's `do` at a `call: code` ref |
|
||||
|
||||
`crewai run` has no chat loop yet: it reports that the flow is conversational and exits rather than running a single turn. Drive a declarative conversational flow from Python with `handle_turn()`, `stream_turn()` or `chat()`.
|
||||
`crewai run` opens the chat TUI for a declarative conversational flow — the same one a Python conversational Flow gets. A chat loop needs a terminal, so a headless run says so instead of running a single turn; drive it from Python there with `handle_turn()` or `stream_turn()`. A flow that also uses `@human_feedback` runs on a terminal REPL, because the runtime collects feedback with a blocking prompt the TUI cannot service. `--inputs` is not accepted for a conversational flow — each turn's input is the message you type — and resuming a session by id is not wired into the CLI yet; use `flow.handle_turn(message, session_id=...)` from Python for that.
|
||||
|
||||
## Tracing across turns
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@ finally:
|
||||
| 모델 클래스로서의 `router.response_format` | 생략하세요; 프레임워크가 생성합니다. ref나 스키마는 경고와 함께 무시됩니다 |
|
||||
| `route_turn()` / `can_answer_from_history()` 재정의 | 플로우를 Python으로 작성하거나, 메서드의 `do`를 `call: code` ref로 지정하세요 |
|
||||
|
||||
`crewai run`에는 아직 채팅 루프가 없습니다: 단일 턴을 실행하는 대신 플로우가 대화형임을 알리고 종료합니다. 선언적 대화형 플로우는 Python에서 `handle_turn()`, `stream_turn()`, `chat()`으로 실행하세요.
|
||||
`crewai run`은 선언적 대화형 플로우에 대해 채팅 TUI를 엽니다 — Python 대화형 Flow가 받는 것과 같은 TUI입니다. 채팅 루프에는 터미널이 필요하므로, 헤드리스 실행에서는 단일 턴을 실행하는 대신 그 사실을 알립니다; 그 환경에서는 Python에서 `handle_turn()` 또는 `stream_turn()`으로 실행하세요. `@human_feedback`도 사용하는 플로우는 터미널 REPL에서 실행됩니다. 런타임이 TUI가 처리할 수 없는 블로킹 프롬프트로 피드백을 수집하기 때문입니다. 대화형 플로우에서는 `--inputs`를 받지 않습니다 — 각 턴의 입력은 여러분이 입력하는 메시지입니다 — 그리고 id로 세션을 재개하는 기능은 아직 CLI에 연결되지 않았습니다; 그것이 필요하면 Python에서 `flow.handle_turn(message, session_id=...)`을 사용하세요.
|
||||
|
||||
## 턴 간 트레이싱
|
||||
|
||||
|
||||
@@ -490,7 +490,7 @@ Rótulos de rota e nomes de métodos compartilham um único namespace de gatilho
|
||||
| `router.response_format` como classe de modelo | Omita; o framework sintetiza uma. Um ref ou schema é ignorado com um aviso |
|
||||
| Overrides de `route_turn()` / `can_answer_from_history()` | Escreva o Flow em Python, ou aponte o `do` de um método para um ref `call: code` |
|
||||
|
||||
O `crewai run` ainda não tem loop de chat: ele informa que o flow é conversacional e sai, em vez de rodar um único turno. Conduza um flow conversacional declarativo pelo Python com `handle_turn()`, `stream_turn()` ou `chat()`.
|
||||
O `crewai run` abre a TUI de chat para um flow conversacional declarativo — a mesma que um Flow conversacional em Python recebe. Um loop de chat precisa de um terminal, então uma execução headless informa isso em vez de rodar um único turno; ali, conduza pelo Python com `handle_turn()` ou `stream_turn()`. Um flow que também usa `@human_feedback` roda em um REPL de terminal, porque o runtime coleta feedback com um prompt bloqueante que a TUI não consegue atender. O `--inputs` não é aceito em um flow conversacional — a entrada de cada turno é a mensagem que você digita — e retomar uma sessão por id ainda não está ligado à CLI; use `flow.handle_turn(message, session_id=...)` no Python para isso.
|
||||
|
||||
## Tracing entre turnos
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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