diff --git a/docs/edge/ar/guides/flows/conversational-flows.mdx b/docs/edge/ar/guides/flows/conversational-flows.mdx index e96814b82..c66cf26f3 100644 --- a/docs/edge/ar/guides/flows/conversational-flows.mdx +++ b/docs/edge/ar/guides/flows/conversational-flows.mdx @@ -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 لذلك. ## التتبع عبر الجولات diff --git a/docs/edge/en/guides/flows/conversational-flows.mdx b/docs/edge/en/guides/flows/conversational-flows.mdx index e42c4966b..3735fede4 100644 --- a/docs/edge/en/guides/flows/conversational-flows.mdx +++ b/docs/edge/en/guides/flows/conversational-flows.mdx @@ -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 diff --git a/docs/edge/ko/guides/flows/conversational-flows.mdx b/docs/edge/ko/guides/flows/conversational-flows.mdx index fd3a7c256..02259dc0d 100644 --- a/docs/edge/ko/guides/flows/conversational-flows.mdx +++ b/docs/edge/ko/guides/flows/conversational-flows.mdx @@ -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=...)`을 사용하세요. ## 턴 간 트레이싱 diff --git a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx index 9012bbe6e..afe46ee55 100644 --- a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx +++ b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx @@ -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 diff --git a/lib/cli/src/crewai_cli/run_declarative_flow.py b/lib/cli/src/crewai_cli/run_declarative_flow.py index 0b7cb8cd2..3042d00c0 100644 --- a/lib/cli/src/crewai_cli/run_declarative_flow.py +++ b/lib/cli/src/crewai_cli/run_declarative_flow.py @@ -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: diff --git a/lib/cli/tests/test_run_declarative_flow.py b/lib/cli/tests/test_run_declarative_flow.py index 32df4be7a..e6c1e1ff8 100644 --- a/lib/cli/tests/test_run_declarative_flow.py +++ b/lib/cli/tests/test_run_declarative_flow.py @@ -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(