From 0391febc6cbdde16b5664641c99f8036a8f9466e Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 22 Jun 2026 14:04:45 -0700 Subject: [PATCH 1/6] Allow `@router()` as start method of a flow (#6288) This commit fixes a bug where a router method could not be the start method of a flow. This is useful when you want to route against the initial state, or even stack two routers. --- lib/crewai/src/crewai/flow/dsl/_listen.py | 4 +- lib/crewai/src/crewai/flow/dsl/_router.py | 22 +++--- lib/crewai/src/crewai/flow/dsl/_start.py | 4 +- lib/crewai/src/crewai/flow/dsl/_utils.py | 19 +++++ lib/crewai/src/crewai/flow/flow_definition.py | 8 -- .../src/crewai/flow/runtime/__init__.py | 8 +- lib/crewai/tests/test_flow.py | 48 ++++++++++++ lib/crewai/tests/test_flow_definition.py | 73 +++++++++++++++---- 8 files changed, 150 insertions(+), 36 deletions(-) diff --git a/lib/crewai/src/crewai/flow/dsl/_listen.py b/lib/crewai/src/crewai/flow/dsl/_listen.py index 37c9a9d25..b964532a2 100644 --- a/lib/crewai/src/crewai/flow/dsl/_listen.py +++ b/lib/crewai/src/crewai/flow/dsl/_listen.py @@ -8,8 +8,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger from crewai.flow.dsl._utils import ( P, R, + _merge_flow_method_definition, _method_action, - _set_flow_method_definition, ) from crewai.flow.flow_definition import FlowMethodDefinition from crewai.flow.flow_wrappers import ListenMethod @@ -45,7 +45,7 @@ def listen(condition: FlowTrigger) -> FlowMethodDecorator: def decorator(func: Callable[P, R]) -> ListenMethod[P, R]: wrapper = ListenMethod(func) - _set_flow_method_definition( + _merge_flow_method_definition( wrapper, FlowMethodDefinition( do=_method_action(func), diff --git a/lib/crewai/src/crewai/flow/dsl/_router.py b/lib/crewai/src/crewai/flow/dsl/_router.py index 3edbf33ba..7f9941e42 100644 --- a/lib/crewai/src/crewai/flow/dsl/_router.py +++ b/lib/crewai/src/crewai/flow/dsl/_router.py @@ -19,8 +19,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger from crewai.flow.dsl._utils import ( P, R, + _merge_flow_method_definition, _method_action, - _set_flow_method_definition, ) from crewai.flow.flow_definition import FlowMethodDefinition from crewai.flow.flow_wrappers import RouterMethod @@ -95,7 +95,7 @@ def _normalize_router_emit(value: Sequence[Any] | str) -> list[str]: def router( - condition: FlowTrigger, + condition: FlowTrigger | None = None, *, emit: Sequence[str] | str | None = None, ) -> FlowMethodDecorator: @@ -107,6 +107,7 @@ def router( Args: condition: Specifies when the router should execute. Can be: + - None: no listen trigger, used when stacking with @start() or @listen() - str: Route label or method name that triggers this router - FlowCondition: Result from or_() or and_(), including nested conditions - Flow method reference: A method whose completion triggers this router @@ -146,14 +147,17 @@ def router( else: router_events = _get_router_return_events(func) or [] - _set_flow_method_definition( + method_definition_kwargs: dict[str, Any] = { + "do": _method_action(func), + "router": True, + "emit": router_events or None, + } + if condition is not None: + method_definition_kwargs["listen"] = _to_definition_condition(condition) + + _merge_flow_method_definition( wrapper, - FlowMethodDefinition( - do=_method_action(func), - listen=_to_definition_condition(condition), - router=True, - emit=router_events or None, - ), + FlowMethodDefinition(**method_definition_kwargs), ) return wrapper diff --git a/lib/crewai/src/crewai/flow/dsl/_start.py b/lib/crewai/src/crewai/flow/dsl/_start.py index fe9f82974..5a41bc9df 100644 --- a/lib/crewai/src/crewai/flow/dsl/_start.py +++ b/lib/crewai/src/crewai/flow/dsl/_start.py @@ -8,8 +8,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger from crewai.flow.dsl._utils import ( P, R, + _merge_flow_method_definition, _method_action, - _set_flow_method_definition, ) from crewai.flow.flow_definition import FlowMethodDefinition from crewai.flow.flow_wrappers import StartMethod @@ -54,7 +54,7 @@ def start( def decorator(func: Callable[P, R]) -> StartMethod[P, R]: wrapper = StartMethod(func) - _set_flow_method_definition( + _merge_flow_method_definition( wrapper, FlowMethodDefinition( do=_method_action(func), diff --git a/lib/crewai/src/crewai/flow/dsl/_utils.py b/lib/crewai/src/crewai/flow/dsl/_utils.py index 99d60a9e3..684264b28 100644 --- a/lib/crewai/src/crewai/flow/dsl/_utils.py +++ b/lib/crewai/src/crewai/flow/dsl/_utils.py @@ -106,6 +106,25 @@ def _get_flow_method_definition(method: Any) -> FlowMethodDefinition | None: return None +def _merge_flow_method_definition( + wrapper: FlowMethod[P, R], + definition: FlowMethodDefinition, +) -> None: + existing = _get_flow_method_definition(wrapper) + if existing is None: + _set_flow_method_definition(wrapper, definition) + return + + updates = { + field_name: getattr(definition, field_name) + for field_name in definition.model_fields_set + } + _set_flow_method_definition( + wrapper, + existing.model_copy(deep=True, update=updates), + ) + + def _is_json_serializable(value: Any) -> bool: try: json.dumps(value) diff --git a/lib/crewai/src/crewai/flow/flow_definition.py b/lib/crewai/src/crewai/flow/flow_definition.py index a566fcadd..5c277d3ce 100644 --- a/lib/crewai/src/crewai/flow/flow_definition.py +++ b/lib/crewai/src/crewai/flow/flow_definition.py @@ -870,14 +870,6 @@ def _validate_action_cel( def log_flow_definition_issues(definition: FlowDefinition) -> None: for method_name, method in definition.methods.items(): path = f"methods.{method_name}" - if method.router and not method.is_start and method.listen is None: - _log_flow_definition_issue( - definition.name, - code="router_without_trigger", - severity="error", - path=path, - message="router: true requires either start or listen", - ) if method.emit and not method.router: _log_flow_definition_issue( definition.name, diff --git a/lib/crewai/src/crewai/flow/runtime/__init__.py b/lib/crewai/src/crewai/flow/runtime/__init__.py index 85f150546..fa465cb71 100644 --- a/lib/crewai/src/crewai/flow/runtime/__init__.py +++ b/lib/crewai/src/crewai/flow/runtime/__init__.py @@ -3007,6 +3007,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): """ # First, handle routers repeatedly until no router triggers anymore router_results = [] + router_result_payloads: dict[str, Any] = {} router_result_to_feedback: dict[ str, Any ] = {} # Map outcome -> HumanFeedbackResult @@ -3044,6 +3045,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): router_result_str = str(router_result) router_result_event = FlowMethodName(router_result_str) router_results.append(router_result_event) + router_result_payloads[router_result_str] = ( + self.last_human_feedback + if self.last_human_feedback is not None + else router_result + ) if self.last_human_feedback is not None: router_result_to_feedback[router_result_str] = ( @@ -3064,7 +3070,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): current_trigger, router_only=False ) if listeners_triggered: - listener_result = router_result_to_feedback.get( + listener_result = router_result_payloads.get( str(current_trigger), result ) racing_group = self._get_racing_group_for_listeners( diff --git a/lib/crewai/tests/test_flow.py b/lib/crewai/tests/test_flow.py index e7bae8023..d0d0045b9 100644 --- a/lib/crewai/tests/test_flow.py +++ b/lib/crewai/tests/test_flow.py @@ -386,6 +386,54 @@ def test_router_runtime_uses_flow_definition_without_legacy_router_metadata(): assert execution_order == ["begin", "decide", "handle_left"] +def test_start_router_runtime_routes_public_dsl_return_value(): + execution_order = [] + + class StartRouterFlow(Flow): + @start() + @router(emit=["continue"]) + def decide(self): + execution_order.append("decide") + return "continue" + + @listen("continue") + def handle_continue(self, result): + execution_order.append(f"handle_continue:{result}") + return "done" + + assert StartRouterFlow().kickoff() == "done" + assert execution_order == ["decide", "handle_continue:continue"] + + +def test_start_router_runtime_chains_to_stacked_listener_router(): + execution_order = [] + + class ChainedStartRouterFlow(Flow): + @start() + @router(emit=["approved", "not_approved"]) + def first_router(self): + execution_order.append("first_router") + return "approved" + + @listen("approved") + @router(emit=["second_approval", "not_approved"]) + def second_router(self): + execution_order.append("second_router") + return "second_approval" + + @listen("second_approval") + def handle_second_approval(self, result): + execution_order.append(f"handle_second_approval:{result}") + return "done" + + assert ChainedStartRouterFlow().kickoff() == "done" + assert execution_order == [ + "first_router", + "second_router", + "handle_second_approval:second_approval", + ] + + def test_router_falsy_result_emits_runtime_event(): execution_order = [] diff --git a/lib/crewai/tests/test_flow_definition.py b/lib/crewai/tests/test_flow_definition.py index 4b5b8d37b..2aa654151 100644 --- a/lib/crewai/tests/test_flow_definition.py +++ b/lib/crewai/tests/test_flow_definition.py @@ -565,6 +565,54 @@ def test_flow_definition_classifies_start_router_from_human_feedback_emit(): assert entry_point.emit is None +def test_flow_definition_classifies_public_dsl_start_router(): + class StartRouterFlow(Flow): + @start() + @router(emit=["continue", "stop"]) + def entry_point(self): + return "continue" + + @router(emit=["resume"]) + @start() + def alternate_entry_point(self): + return "resume" + + entry_point = StartRouterFlow.flow_definition().methods["entry_point"] + alternate_entry_point = StartRouterFlow.flow_definition().methods[ + "alternate_entry_point" + ] + + assert entry_point.is_start is True + assert entry_point.router is True + assert entry_point.listen is None + assert entry_point.emit == ["continue", "stop"] + assert alternate_entry_point.is_start is True + assert alternate_entry_point.router is True + assert alternate_entry_point.listen is None + assert alternate_entry_point.emit == ["resume"] + + +def test_flow_definition_merges_stacked_listen_router(): + class ChainedRouterFlow(Flow): + @start() + @router(emit=["approved", "not_approved"]) + def first_router(self): + return "approved" + + @listen("approved") + @router(emit=["second_approval", "not_approved"]) + def second_router(self): + return "second_approval" + + methods = ChainedRouterFlow.flow_definition().methods + + assert methods["first_router"].is_start is True + assert methods["first_router"].listen is None + assert methods["second_router"].router is True + assert methods["second_router"].listen == "approved" + assert methods["second_router"].emit == ["second_approval", "not_approved"] + + def test_flow_definition_round_trips_json_and_yaml(): class RoundTripFlow(Flow): @start() @@ -883,7 +931,7 @@ def test_flow_definition_ignores_legacy_diagnostics_loaded_from_contract(): assert "diagnostics" not in definition.to_dict() -def test_router_start_false_without_listen_logs_missing_trigger(caplog): +def test_router_start_false_without_listen_is_allowed(caplog): caplog.set_level(logging.ERROR, logger="crewai.flow.flow_definition") flow_definition.FlowDefinition.from_dict( @@ -901,12 +949,7 @@ def test_router_start_false_without_listen_logs_missing_trigger(caplog): } ) - assert any( - record.levelno == logging.ERROR - and "router_without_trigger" in record.message - and "methods.decision" in record.message - for record in caplog.records - ) + assert not caplog.records def test_router_human_feedback_preserves_existing_router_metadata(): @@ -1048,7 +1091,7 @@ def test_flow_definition_cache_is_not_reused_by_subclasses(): assert set(child_definition.methods) == {"child_step"} -def test_flow_definition_logs_validation_issues_when_loaded_from_contract(caplog): +def test_flow_definition_allows_router_without_trigger(caplog): caplog.set_level(logging.WARNING, logger="crewai.flow.flow_definition") flow_definition.FlowDefinition.from_dict( @@ -1065,9 +1108,11 @@ def test_flow_definition_logs_validation_issues_when_loaded_from_contract(caplog } ) - assert any( - record.levelno == logging.ERROR - and "LoadedFlow" in record.message - and "router_without_trigger" in record.message - for record in caplog.records - ) + class StandaloneRouterFlow(Flow): + @router(emit=["continue"]) + def decision(self): + return "continue" + + StandaloneRouterFlow.flow_definition() + + assert not caplog.records From 4b2ce00a09dd58b48ebe305e372782e09acc6333 Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 22 Jun 2026 19:58:17 -0700 Subject: [PATCH 2/6] Add declarative Flow CLI support (#6294) * Add declarative Flow CLI support Currently, declarative flows can be loaded by the runtime, but the CLI still treats them as an experimental definition file instead of a first-class Flow project shape. With this PR, `crewai create flow --declarative` scaffolds a YAML-backed Flow project, and `crewai run`, `crewai flow kickoff`, and `crewai flow plot` can run against the configured definition. This also lets crew actions reference reusable crew definition files or folders and override their inputs from the Flow definition, so declarative flows can compose existing declarative crews without inlining everything. * Address code review comments --- lib/cli/src/crewai_cli/cli.py | 25 ++- lib/cli/src/crewai_cli/create_flow.py | 55 ++++- lib/cli/src/crewai_cli/kickoff_flow.py | 30 ++- lib/cli/src/crewai_cli/plot_flow.py | 30 ++- .../src/crewai_cli/run_declarative_flow.py | 212 ++++++++++++++++++ lib/cli/src/crewai_cli/run_flow_definition.py | 113 ---------- .../templates/declarative_flow/.gitignore | 5 + .../templates/declarative_flow/README.md | 17 ++ .../templates/declarative_flow/flow.yaml | 15 ++ .../templates/declarative_flow/pyproject.toml | 20 ++ lib/cli/tests/test_cli.py | 31 ++- lib/cli/tests/test_create_flow.py | 37 +++ lib/cli/tests/test_flow_commands.py | 103 +++++++++ lib/cli/tests/test_run_declarative_flow.py | 111 +++++++++ lib/cli/tests/test_run_flow_definition.py | 156 ------------- lib/crewai/src/crewai/flow/flow_definition.py | 83 +++++-- .../src/crewai/flow/runtime/_actions.py | 53 ++++- lib/crewai/tests/test_flow_from_definition.py | 189 +++++++++++++++- 18 files changed, 951 insertions(+), 334 deletions(-) create mode 100644 lib/cli/src/crewai_cli/run_declarative_flow.py delete mode 100644 lib/cli/src/crewai_cli/run_flow_definition.py create mode 100644 lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore create mode 100644 lib/cli/src/crewai_cli/templates/declarative_flow/README.md create mode 100644 lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml create mode 100644 lib/cli/src/crewai_cli/templates/declarative_flow/pyproject.toml create mode 100644 lib/cli/tests/test_create_flow.py create mode 100644 lib/cli/tests/test_flow_commands.py create mode 100644 lib/cli/tests/test_run_declarative_flow.py delete mode 100644 lib/cli/tests/test_run_flow_definition.py diff --git a/lib/cli/src/crewai_cli/cli.py b/lib/cli/src/crewai_cli/cli.py index b153885f3..1a64a74f3 100644 --- a/lib/cli/src/crewai_cli/cli.py +++ b/lib/cli/src/crewai_cli/cli.py @@ -40,12 +40,12 @@ def replay_task_command(*args: Any, **kwargs: Any) -> Any: return _replay_task_command(*args, **kwargs) -def run_flow_definition(*args: Any, **kwargs: Any) -> Any: - from crewai_cli.run_flow_definition import ( - run_flow_definition as _run_flow_definition, +def run_declarative_flow(*args: Any, **kwargs: Any) -> Any: + from crewai_cli.run_declarative_flow import ( + run_declarative_flow as _run_declarative_flow, ) - return _run_flow_definition(*args, **kwargs) + return _run_declarative_flow(*args, **kwargs) def run_crew(*args: Any, **kwargs: Any) -> Any: @@ -155,12 +155,18 @@ def uv(uv_args: tuple[str, ...]) -> None: is_flag=True, help="Use classic Python/YAML project structure instead of JSON", ) +@click.option( + "--declarative", + is_flag=True, + help="Create a declarative Flow project instead of a Python Flow project", +) def create( type: str | None, name: str | None, provider: str | None, skip_provider: bool = False, classic: bool = False, + declarative: bool = False, ) -> None: """Create a new crew, or flow.""" dmn_mode = is_dmn_mode_enabled() @@ -194,6 +200,8 @@ def create( if dmn_mode: skip_provider = True if type == "crew": + if declarative: + raise click.UsageError("--declarative can only be used with flow projects") if classic: from crewai_cli.create_crew import create_crew @@ -205,7 +213,7 @@ def create( elif type == "flow": from crewai_cli.create_flow import create_flow - create_flow(name) + create_flow(name, declarative=declarative) else: click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red") @@ -512,10 +520,7 @@ def install(context: click.Context) -> None: "--definition", type=str, default=None, - help=( - "Experimental: path to a Flow Definition YAML/JSON file, " - "or an inline YAML/JSON string." - ), + help="Experimental: path to a declarative Flow YAML/JSON file.", ) @click.option( "--inputs", @@ -537,7 +542,7 @@ def run( "Warning: `crewai run --definition` is experimental and may change without notice.", fg="yellow", ) - run_flow_definition(definition=definition, inputs=inputs) + run_declarative_flow(definition=definition, inputs=inputs) return run_crew(trained_agents_file=trained_agents_file) diff --git a/lib/cli/src/crewai_cli/create_flow.py b/lib/cli/src/crewai_cli/create_flow.py index 5042d7679..adaa3d3bf 100644 --- a/lib/cli/src/crewai_cli/create_flow.py +++ b/lib/cli/src/crewai_cli/create_flow.py @@ -5,7 +5,10 @@ import click from crewai_core.telemetry import Telemetry -def create_flow(name: str) -> None: +DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills") + + +def create_flow(name: str, *, declarative: bool = False) -> None: """Create a new flow.""" folder_name = name.replace(" ", "_").replace("-", "_").lower() class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "") @@ -20,6 +23,17 @@ def create_flow(name: str) -> None: telemetry = Telemetry() telemetry.flow_creation_span(class_name) + if declarative: + _create_declarative_flow(name, class_name, folder_name, project_root) + else: + _create_python_flow(name, class_name, folder_name, project_root) + + click.secho(f"Flow {name} created successfully!", fg="green", bold=True) + + +def _create_python_flow( + name: str, class_name: str, folder_name: str, project_root: Path +) -> None: (project_root / "src" / folder_name).mkdir(parents=True) (project_root / "src" / folder_name / "crews").mkdir(parents=True) (project_root / "src" / folder_name / "tools").mkdir(parents=True) @@ -92,4 +106,41 @@ def create_flow(name: str) -> None: fg="yellow", ) - click.secho(f"Flow {name} created successfully!", fg="green", bold=True) + +def _create_declarative_flow( + name: str, class_name: str, folder_name: str, project_root: Path +) -> None: + project_root.mkdir(parents=True) + package_root = project_root / "src" / folder_name + package_root.mkdir(parents=True) + for folder in DECLARATIVE_FLOW_FOLDERS: + (package_root / folder).mkdir() + + package_dir = Path(__file__).parent + templates_dir = package_dir / "templates" / "declarative_flow" + + agents_md_src = package_dir / "templates" / "AGENTS.md" + if agents_md_src.exists(): + shutil.copy2(agents_md_src, project_root / "AGENTS.md") + + for src_file in templates_dir.rglob("*"): + if not src_file.is_file(): + continue + + relative_path = src_file.relative_to(templates_dir) + dst_file = ( + project_root / relative_path + if relative_path.name in {".gitignore", "README.md", "pyproject.toml"} + else package_root / relative_path + ) + dst_file.parent.mkdir(parents=True, exist_ok=True) + content = src_file.read_text(encoding="utf-8") + content = content.replace("{{name}}", name) + content = content.replace("{{flow_name}}", class_name) + content = content.replace("{{folder_name}}", folder_name) + dst_file.write_text(content, encoding="utf-8") + + (project_root / ".env").write_text("OPENAI_API_KEY=YOUR_API_KEY", encoding="utf-8") + (package_root / "__init__.py").write_text("", encoding="utf-8") + for folder in DECLARATIVE_FLOW_FOLDERS: + (package_root / folder / ".gitkeep").write_text("", encoding="utf-8") diff --git a/lib/cli/src/crewai_cli/kickoff_flow.py b/lib/cli/src/crewai_cli/kickoff_flow.py index b5bc0d81e..ff5f317dd 100644 --- a/lib/cli/src/crewai_cli/kickoff_flow.py +++ b/lib/cli/src/crewai_cli/kickoff_flow.py @@ -5,19 +5,27 @@ import click def kickoff_flow() -> None: """ - Kickoff the flow by running a command in the UV environment. + Kickoff the flow from declarative config or the Python UV entrypoint. """ - command = ["uv", "run", "kickoff"] + from crewai_cli.run_declarative_flow import ( + configured_project_declarative_flow, + run_declarative_flow_in_project_env, + ) - try: - result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603 + if definition := configured_project_declarative_flow(): + run_declarative_flow_in_project_env(definition=definition) + else: + command = ["uv", "run", "kickoff"] - if result.stderr: - click.echo(result.stderr, err=True) + try: + subprocess.run( # noqa: S603 + command, capture_output=False, text=True, check=True + ) - except subprocess.CalledProcessError as e: - click.echo(f"An error occurred while running the flow: {e}", err=True) - click.echo(e.output, err=True) + except subprocess.CalledProcessError as e: + click.echo(f"An error occurred while running the flow: {e}", err=True) + raise SystemExit(1) from e - except Exception as e: - click.echo(f"An unexpected error occurred: {e}", err=True) + except Exception as e: + click.echo(f"An unexpected error occurred: {e}", err=True) + raise SystemExit(1) from e diff --git a/lib/cli/src/crewai_cli/plot_flow.py b/lib/cli/src/crewai_cli/plot_flow.py index d97ccba77..d79bdc58b 100644 --- a/lib/cli/src/crewai_cli/plot_flow.py +++ b/lib/cli/src/crewai_cli/plot_flow.py @@ -5,19 +5,27 @@ import click def plot_flow() -> None: """ - Plot the flow by running a command in the UV environment. + Plot the flow from declarative config or the Python UV entrypoint. """ - command = ["uv", "run", "plot"] + from crewai_cli.run_declarative_flow import ( + configured_project_declarative_flow, + plot_declarative_flow_in_project_env, + ) - try: - result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603 + if definition := configured_project_declarative_flow(): + plot_declarative_flow_in_project_env(definition) + else: + command = ["uv", "run", "plot"] - if result.stderr: - click.echo(result.stderr, err=True) + try: + subprocess.run( # noqa: S603 + command, capture_output=False, text=True, check=True + ) - except subprocess.CalledProcessError as e: - click.echo(f"An error occurred while plotting the flow: {e}", err=True) - click.echo(e.output, err=True) + except subprocess.CalledProcessError as e: + click.echo(f"An error occurred while plotting the flow: {e}", err=True) + raise SystemExit(1) from e - except Exception as e: - click.echo(f"An unexpected error occurred: {e}", err=True) + except Exception as e: + click.echo(f"An unexpected error occurred: {e}", err=True) + raise SystemExit(1) from e diff --git a/lib/cli/src/crewai_cli/run_declarative_flow.py b/lib/cli/src/crewai_cli/run_declarative_flow.py new file mode 100644 index 000000000..af7431b02 --- /dev/null +++ b/lib/cli/src/crewai_cli/run_declarative_flow.py @@ -0,0 +1,212 @@ +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +from typing import Any + +import click + +from crewai_cli.utils import build_env_with_all_tool_credentials + + +def run_declarative_flow_in_project_env( + definition: str, inputs: str | None = None +) -> None: + """Run a declarative flow inside the project's Python environment.""" + if is_declarative_flow_project_env() or not _has_project_file(): + run_declarative_flow(definition=definition, inputs=inputs) + return + + if inputs is not None: + raise click.UsageError("--inputs is only supported with --definition") + + _execute_declarative_flow_command(["uv", "run", "crewai", "flow", "kickoff"]) + + +def plot_declarative_flow_in_project_env(definition: str) -> None: + """Plot a declarative flow inside the project's Python environment.""" + if is_declarative_flow_project_env() or not _has_project_file(): + plot_declarative_flow(definition=definition) + return + + _execute_declarative_flow_command(["uv", "run", "crewai", "flow", "plot"]) + + +def run_declarative_flow(definition: str, inputs: str | None = None) -> None: + """Run a declarative flow from a YAML/JSON file path.""" + parsed_inputs = _parse_inputs(inputs) + + try: + flow = load_declarative_flow(definition) + result = flow.kickoff(inputs=parsed_inputs) + except Exception as exc: + click.echo( + f"An error occurred while running the declarative flow: {exc}", err=True + ) + raise SystemExit(1) from exc + + click.echo(_format_result(result)) + + +def plot_declarative_flow(definition: str) -> None: + """Plot a declarative flow from a YAML/JSON file path.""" + try: + flow = load_declarative_flow(definition) + flow.plot() + except Exception as exc: + click.echo( + f"An error occurred while plotting the declarative flow: {exc}", err=True + ) + raise SystemExit(1) from exc + + +def load_declarative_flow(definition: str) -> Any: + """Load a declarative Flow instance from a YAML/JSON file path.""" + try: + from crewai.flow.flow import Flow + from crewai.flow.flow_definition import FlowDefinition + except ImportError as exc: + click.echo( + "Running declarative flows requires the full crewai package.", + err=True, + ) + raise SystemExit(1) from exc + + definition_path = Path(definition).expanduser() + definition_source = _read_declarative_flow_source(definition_path, definition) + + flow_definition = _parse_declarative_flow( + FlowDefinition, + definition_source, + source_path=definition_path, + ) + return Flow.from_definition(flow_definition) + + +def configured_project_declarative_flow( + pyproject_data: dict[str, Any] | None = None, +) -> str | None: + """Return the configured declarative flow source for flow projects.""" + if pyproject_data is None: + try: + from crewai_cli.utils import read_toml + + pyproject_data = read_toml() + except Exception: + return None + + crewai_config = pyproject_data.get("tool", {}).get("crewai", {}) + if crewai_config.get("type") != "flow": + return None + definition = crewai_config.get("definition") + if not isinstance(definition, str): + return None + return definition.strip() or None + + +def _execute_declarative_flow_command(command: list[str]) -> None: + env = build_env_with_all_tool_credentials() + + try: + subprocess.run( # noqa: S603 + command, + capture_output=False, + text=True, + check=True, + env=env, + ) + except subprocess.CalledProcessError as e: + raise SystemExit(e.returncode) from e + except Exception as e: + click.echo( + f"An unexpected error occurred while running the declarative flow: {e}", + err=True, + ) + raise SystemExit(1) from e + + +def is_declarative_flow_project_env() -> bool: + import os + + return os.environ.get("UV_RUN_RECURSION_DEPTH") is not None + + +def _has_project_file(project_root: Path | None = None) -> bool: + root = project_root or Path.cwd() + return (root / "pyproject.toml").is_file() + + +def _parse_inputs(inputs: str | None) -> dict[str, Any] | None: + 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 _read_declarative_flow_source(path: Path, definition: str) -> str: + try: + if path.is_file(): + source = _read_declarative_flow_file(path) + elif path.exists(): + click.echo( + f"Invalid --definition path: {definition} is not a file.", err=True + ) + raise SystemExit(1) + else: + click.echo( + f"Invalid --definition path: {definition} does not exist.", err=True + ) + raise SystemExit(1) + except OSError as exc: + click.echo(f"Invalid --definition path: {definition} ({exc})", err=True) + raise SystemExit(1) from exc + + return source + + +def _read_declarative_flow_file(path: Path) -> str: + try: + source = path.read_text(encoding="utf-8") + except (OSError, UnicodeError) as exc: + click.echo( + f"Unable to read --definition path {path}: {exc}", + err=True, + ) + raise SystemExit(1) from exc + return source + + +def _parse_declarative_flow( + flow_definition_cls: type[Any], source: str, *, source_path: Path +) -> Any: + if _looks_like_json(source): + return flow_definition_cls.from_json(source, source_path=source_path) + + return flow_definition_cls.from_yaml(source, source_path=source_path) + + +def _looks_like_json(source: str) -> bool: + stripped = source.lstrip() + return stripped.startswith("{") + + +def _format_result(result: Any) -> str: + raw_result = getattr(result, "raw", result) + if isinstance(raw_result, str): + return raw_result + + try: + return json.dumps(raw_result, default=str) + except TypeError: + return str(raw_result) diff --git a/lib/cli/src/crewai_cli/run_flow_definition.py b/lib/cli/src/crewai_cli/run_flow_definition.py deleted file mode 100644 index 7acb6d9fe..000000000 --- a/lib/cli/src/crewai_cli/run_flow_definition.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -import click - - -def run_flow_definition(definition: str, inputs: str | None = None) -> None: - """Run a flow from a Flow Definition YAML/JSON string or file path.""" - try: - from crewai.flow.flow import Flow - from crewai.flow.flow_definition import FlowDefinition - except ImportError as exc: - click.echo( - "Running flows from definitions requires the full crewai package.", - err=True, - ) - raise SystemExit(1) from exc - - parsed_inputs = _parse_inputs(inputs) - definition_source = _read_definition_source(definition) - - try: - flow_definition = _parse_flow_definition(FlowDefinition, definition_source) - flow = Flow.from_definition(flow_definition) - result = flow.kickoff(inputs=parsed_inputs) - except Exception as exc: - click.echo( - f"An error occurred while running the flow definition: {exc}", err=True - ) - raise SystemExit(1) from exc - - click.echo(_format_result(result)) - - -def _parse_inputs(inputs: str | None) -> dict[str, Any] | None: - 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 _read_definition_source(definition: str) -> str: - path = Path(definition).expanduser() - try: - is_file = path.is_file() - except OSError as exc: - if _looks_like_inline_definition(definition): - return definition - click.echo(f"Invalid --definition path: {definition} ({exc})", err=True) - raise SystemExit(1) from exc - - if is_file: - try: - return path.read_text(encoding="utf-8") - except (OSError, UnicodeError) as exc: - click.echo( - f"Unable to read --definition path {path}: {exc}", - err=True, - ) - raise SystemExit(1) from exc - - try: - if path.exists(): - click.echo( - f"Invalid --definition path: {definition} is not a file.", err=True - ) - raise SystemExit(1) - except OSError as exc: - click.echo(f"Invalid --definition path: {definition} ({exc})", err=True) - raise SystemExit(1) from exc - - return definition - - -def _looks_like_inline_definition(definition: str) -> bool: - stripped = definition.lstrip() - return "\n" in definition or stripped.startswith(("{", "---")) or ":" in stripped - - -def _parse_flow_definition(flow_definition_cls: type[Any], source: str) -> Any: - if _looks_like_json(source): - return flow_definition_cls.from_json(source) - - return flow_definition_cls.from_yaml(source) - - -def _looks_like_json(source: str) -> bool: - stripped = source.lstrip() - return stripped.startswith("{") - - -def _format_result(result: Any) -> str: - raw_result = getattr(result, "raw", result) - if isinstance(raw_result, str): - return raw_result - - try: - return json.dumps(raw_result, default=str) - except TypeError: - return str(raw_result) diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore b/lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore new file mode 100644 index 000000000..9b826004b --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore @@ -0,0 +1,5 @@ +.env +.venv/ +__pycache__/ +.crewai/ +output/ diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/README.md b/lib/cli/src/crewai_cli/templates/declarative_flow/README.md new file mode 100644 index 000000000..2de72c4df --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/README.md @@ -0,0 +1,17 @@ +# {{name}} Flow + +This project defines a CrewAI Flow in `src/{{folder_name}}/flow.yaml`. + +## Install + +```bash +crewai install +``` + +## Run + +```bash +crewai flow kickoff +``` + +Edit `src/{{folder_name}}/flow.yaml` to change the flow. Add reusable crews under `src/{{folder_name}}/crews/`, custom Python tools under `src/{{folder_name}}/tools/`, and shared knowledge files under `src/{{folder_name}}/knowledge/`. diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml b/lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml new file mode 100644 index 000000000..3b07891fe --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml @@ -0,0 +1,15 @@ +schema: crewai.flow/v1 +name: {{flow_name}} +description: A declarative CrewAI Flow. + +state: + type: dict + default: + topic: AI agents + +methods: + start: + start: true + do: + call: expression + expr: state.topic diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/pyproject.toml b/lib/cli/src/crewai_cli/templates/declarative_flow/pyproject.toml new file mode 100644 index 000000000..e4a9a8693 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "{{folder_name}}" +version = "0.1.0" +description = "{{name}} using crewAI" +authors = [{ name = "Your Name", email = "you@example.com" }] +requires-python = ">=3.10,<3.14" +dependencies = [ + "crewai[tools]==1.14.8a2" +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/{{folder_name}}"] + +[tool.crewai] +type = "flow" +definition = "src/{{folder_name}}/flow.yaml" diff --git a/lib/cli/tests/test_cli.py b/lib/cli/tests/test_cli.py index 3b5ce277f..9d8802f27 100644 --- a/lib/cli/tests/test_cli.py +++ b/lib/cli/tests/test_cli.py @@ -130,8 +130,8 @@ def test_run_uses_project_runner_by_default(run_crew, runner): assert "experimental" not in result.output.lower() -@mock.patch("crewai_cli.cli.run_flow_definition") -def test_run_with_definition_uses_definition_runner(run_flow_definition, runner): +@mock.patch("crewai_cli.cli.run_declarative_flow") +def test_run_with_definition_uses_definition_runner(run_declarative_flow, runner): result = runner.invoke( run, ["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'], @@ -142,19 +142,21 @@ def test_run_with_definition_uses_definition_runner(run_flow_definition, runner) "Warning: `crewai run --definition` is experimental and may change without notice." in result.output ) - run_flow_definition.assert_called_once_with( + run_declarative_flow.assert_called_once_with( definition="flow.yaml", inputs='{"topic":"AI"}' ) @mock.patch("crewai_cli.cli.run_crew") -@mock.patch("crewai_cli.cli.run_flow_definition") -def test_run_rejects_inputs_without_definition(run_flow_definition, run_crew, runner): +@mock.patch("crewai_cli.cli.run_declarative_flow") +def test_run_rejects_inputs_without_definition( + run_declarative_flow, run_crew, runner +): result = runner.invoke(run, ["--inputs", '{"topic":"AI"}']) assert result.exit_code == 2 assert "Error: --inputs requires --definition" in result.output - run_flow_definition.assert_not_called() + run_declarative_flow.assert_not_called() run_crew.assert_not_called() @@ -166,6 +168,23 @@ def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner create_json_crew.assert_called_once_with("DMN Crew", None, True) +@mock.patch("crewai_cli.create_flow.create_flow") +def test_create_flow_declarative_uses_declarative_scaffold(create_flow, runner): + result = runner.invoke(create, ["flow", "My Flow", "--declarative"]) + + assert result.exit_code == 0 + create_flow.assert_called_once_with("My Flow", declarative=True) + + +@mock.patch("crewai_cli.create_json_crew.create_json_crew") +def test_create_crew_rejects_declarative_flag(create_json_crew, runner): + result = runner.invoke(create, ["crew", "My Crew", "--declarative"]) + + assert result.exit_code == 2 + assert "--declarative can only be used with flow projects" in result.output + create_json_crew.assert_not_called() + + def test_create_requires_type_in_dmn_mode(runner): result = runner.invoke(create, env={"CREWAI_DMN": "True"}) diff --git a/lib/cli/tests/test_create_flow.py b/lib/cli/tests/test_create_flow.py new file mode 100644 index 000000000..2fa941e58 --- /dev/null +++ b/lib/cli/tests/test_create_flow.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pathlib import Path + +from click.testing import CliRunner +from pytest import MonkeyPatch +import tomli + +from crewai_cli.cli import crewai +from crewai_cli.create_flow import create_flow + + +def test_create_flow_declarative_project_can_run( + tmp_path: Path, monkeypatch: MonkeyPatch +): + monkeypatch.chdir(tmp_path) + create_flow("Research Flow", declarative=True) + + project_root = tmp_path / "research_flow" + assert project_root.is_dir() + + pyproject = tomli.loads( + (project_root / "pyproject.toml").read_text(encoding="utf-8") + ) + assert pyproject["project"]["name"] == "research_flow" + assert pyproject["project"]["requires-python"] + assert pyproject["project"]["dependencies"] + assert (project_root / pyproject["tool"]["crewai"]["definition"]).is_file() + + monkeypatch.chdir(project_root) + result = CliRunner().invoke( + crewai, ["flow", "kickoff"], env={"UV_RUN_RECURSION_DEPTH": "1"} + ) + + assert result.exit_code == 0 + assert "Running the Flow" in result.output + assert "AI agents" in result.output diff --git a/lib/cli/tests/test_flow_commands.py b/lib/cli/tests/test_flow_commands.py new file mode 100644 index 000000000..6154ff642 --- /dev/null +++ b/lib/cli/tests/test_flow_commands.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +import subprocess + +import pytest + +import crewai_cli.kickoff_flow as kickoff_flow_module +import crewai_cli.plot_flow as plot_flow_module + + +FLOW_YAML = """\ +schema: crewai.flow/v1 +name: TestFlow +config: + suppress_flow_events: true +methods: + begin: + start: true + do: + call: expression + expr: "'AI'" +""" + + +def _write_flow_project(project_root: Path) -> None: + (project_root / "flow.yaml").write_text(FLOW_YAML, encoding="utf-8") + (project_root / "pyproject.toml").write_text( + '[project]\nname = "demo"\n\n' + '[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n', + encoding="utf-8", + ) + + +def test_kickoff_flow_runs_configured_declarative_definition( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + _write_flow_project(tmp_path) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1") + + kickoff_flow_module.kickoff_flow() + + assert capsys.readouterr().out == "AI\n" + + +def test_plot_flow_runs_configured_declarative_definition( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + _write_flow_project(tmp_path) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1") + + plot_flow_module.plot_flow() + + +@pytest.mark.parametrize( + ("command", "expected"), + [ + pytest.param(kickoff_flow_module.kickoff_flow, ["uv", "run", "kickoff"]), + pytest.param(plot_flow_module.plot_flow, ["uv", "run", "plot"]), + ], +) +def test_flow_commands_keep_python_entrypoint_without_definition( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + command: Callable[[], None], + expected: list[str], +) -> None: + subprocess_calls = [] + + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + subprocess, + "run", + lambda command, **kwargs: subprocess_calls.append((command, kwargs)), + ) + + command() + + assert subprocess_calls == [ + ( + expected, + {"capture_output": False, "text": True, "check": True}, + ) + ] + + +def test_configured_project_declarative_flow( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text( + '[tool.crewai]\ntype = "flow"\ndefinition = " flow.yaml "\n', + encoding="utf-8", + ) + + from crewai_cli.run_declarative_flow import configured_project_declarative_flow + + assert configured_project_declarative_flow() == "flow.yaml" diff --git a/lib/cli/tests/test_run_declarative_flow.py b/lib/cli/tests/test_run_declarative_flow.py new file mode 100644 index 000000000..9808d6b17 --- /dev/null +++ b/lib/cli/tests/test_run_declarative_flow.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +import crewai_cli.run_declarative_flow as run_declarative_flow_module + + +FLOW_YAML = """\ +schema: crewai.flow/v1 +name: TestFlow +config: + suppress_flow_events: true +methods: + begin: + start: true + do: + call: expression + expr: state.topic +""" + + +def test_run_declarative_flow_reads_definition_file( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + definition_path = tmp_path / "flow.yaml" + definition_path.write_text(FLOW_YAML, encoding="utf-8") + + run_declarative_flow_module.run_declarative_flow( + str(definition_path), '{"topic":"AI"}' + ) + + assert capsys.readouterr().out == "AI\n" + + +def test_run_declarative_flow_rejects_non_object_inputs( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + definition_path = tmp_path / "flow.yaml" + definition_path.write_text(FLOW_YAML, encoding="utf-8") + + with pytest.raises(SystemExit): + run_declarative_flow_module.run_declarative_flow( + str(definition_path), '["not", "an", "object"]' + ) + + assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err + + +def test_run_declarative_flow_reports_missing_file( + capsys: pytest.CaptureFixture[str], +) -> None: + with pytest.raises(SystemExit): + run_declarative_flow_module.run_declarative_flow("missing-flow.yaml") + + assert ( + "Invalid --definition path: missing-flow.yaml does not exist." + in capsys.readouterr().err + ) + + +def test_run_declarative_flow_in_project_env_uses_uv( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + subprocess_calls = [] + + monkeypatch.chdir(tmp_path) + monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False) + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + monkeypatch.setattr( + run_declarative_flow_module, + "build_env_with_all_tool_credentials", + lambda: {"EXISTING": "value"}, + ) + monkeypatch.setattr( + run_declarative_flow_module.subprocess, + "run", + lambda command, **kwargs: subprocess_calls.append((command, kwargs)), + ) + + run_declarative_flow_module.run_declarative_flow_in_project_env("flow.yaml") + + assert subprocess_calls == [ + ( + ["uv", "run", "crewai", "flow", "kickoff"], + { + "capture_output": False, + "text": True, + "check": True, + "env": {"EXISTING": "value"}, + }, + ) + ] + + +def test_run_declarative_flow_in_process_inside_uv( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1") + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + (tmp_path / "flow.yaml").write_text(FLOW_YAML, encoding="utf-8") + + run_declarative_flow_module.run_declarative_flow_in_project_env( + "flow.yaml", '{"topic":"AI"}' + ) + + assert capsys.readouterr().out == "AI\n" diff --git a/lib/cli/tests/test_run_flow_definition.py b/lib/cli/tests/test_run_flow_definition.py deleted file mode 100644 index 532f810be..000000000 --- a/lib/cli/tests/test_run_flow_definition.py +++ /dev/null @@ -1,156 +0,0 @@ -from __future__ import annotations - -import json -import sys -import types - -import pytest -import yaml - -from crewai_cli.run_flow_definition import run_flow_definition - - -class _FakeFlow: - def __init__(self, definition): - self.definition = definition - - def kickoff(self, inputs=None): - return { - "flow": self.definition["name"], - "inputs": inputs or {}, - } - - -class _FakeFlowFactory: - @classmethod - def from_definition(cls, definition): - return _FakeFlow(definition) - - -class _FakeFlowDefinition: - @classmethod - def from_yaml(cls, source): - return yaml.safe_load(source) - - @classmethod - def from_json(cls, source): - return json.loads(source) - - -@pytest.fixture -def fake_flow_runtime(monkeypatch): - crewai_module = types.ModuleType("crewai") - flow_package = types.ModuleType("crewai.flow") - flow_module = types.ModuleType("crewai.flow.flow") - flow_definition_module = types.ModuleType("crewai.flow.flow_definition") - - flow_module.Flow = _FakeFlowFactory - flow_definition_module.FlowDefinition = _FakeFlowDefinition - - monkeypatch.setitem(sys.modules, "crewai", crewai_module) - monkeypatch.setitem(sys.modules, "crewai.flow", flow_package) - monkeypatch.setitem(sys.modules, "crewai.flow.flow", flow_module) - monkeypatch.setitem( - sys.modules, "crewai.flow.flow_definition", flow_definition_module - ) - - -def _captured_json(capsys): - return json.loads(capsys.readouterr().out) - - -def test_run_flow_definition_reads_definition_file( - tmp_path, capsys, fake_flow_runtime -): - definition_path = tmp_path / "flow.yaml" - definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n") - - run_flow_definition(str(definition_path), '{"topic":"AI"}') - - assert _captured_json(capsys) == { - "flow": "TestFlow", - "inputs": {"topic": "AI"}, - } - - -@pytest.mark.parametrize( - ("definition_source", "expected_flow_name"), - [ - pytest.param( - "schema: crewai.flow/v1\nname: InlineFlow\n", - "InlineFlow", - id="inline-yaml", - ), - pytest.param( - '{"schema":"crewai.flow/v1","name":"InlineJsonFlow"}', - "InlineJsonFlow", - id="inline-json", - ), - pytest.param( - '{"schema":"crewai.flow/v1","name":"' + ("JsonFlow" * 500) + '"}', - "JsonFlow" * 500, - id="large-inline-json", - ), - ], -) -def test_run_flow_definition_accepts_inline_definitions( - definition_source, expected_flow_name, capsys, fake_flow_runtime -): - run_flow_definition(definition_source) - - assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}} - - -@pytest.mark.parametrize( - ("filename", "definition_source", "expected_flow_name"), - [ - pytest.param( - "flow.yaml", - "schema: crewai.flow/v1\nname: YamlFileFlow\n", - "YamlFileFlow", - id="yaml-file", - ), - pytest.param( - "flow.json", - '{"schema":"crewai.flow/v1","name":"JsonFlow"}', - "JsonFlow", - id="json-file", - ), - ], -) -def test_run_flow_definition_accepts_definition_files( - filename, definition_source, expected_flow_name, tmp_path, capsys, fake_flow_runtime -): - definition_path = tmp_path / filename - definition_path.write_text(definition_source) - - run_flow_definition(str(definition_path)) - - assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}} - - -def test_run_flow_definition_rejects_non_object_inputs(fake_flow_runtime, capsys): - with pytest.raises(SystemExit): - run_flow_definition("name: TestFlow", '["not", "an", "object"]') - - assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err - - -def test_run_flow_definition_reports_unreadable_file( - monkeypatch, tmp_path, capsys, fake_flow_runtime -): - definition_path = tmp_path / "flow.yaml" - definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n") - - def raise_permission_error(self, *args, **kwargs): - raise PermissionError("no access") - - monkeypatch.setattr("pathlib.Path.read_text", raise_permission_error) - - with pytest.raises(SystemExit): - run_flow_definition(str(definition_path)) - - err = capsys.readouterr().err - assert "Unable to read --definition path" in err - assert str(definition_path) in err - assert "no access" in err diff --git a/lib/crewai/src/crewai/flow/flow_definition.py b/lib/crewai/src/crewai/flow/flow_definition.py index 5c277d3ce..6f05853d0 100644 --- a/lib/crewai/src/crewai/flow/flow_definition.py +++ b/lib/crewai/src/crewai/flow/flow_definition.py @@ -1,6 +1,6 @@ -"""Flow Structure: the serializable, language-agnostic Flow contract. +"""Flow Definition: the serializable, declarative Flow contract. -Defines :class:`FlowDefinition` and its sub-models — a static, textual +Defines :class:`FlowDefinition` and its sub-models — a static, declarative (JSON/YAML) representation of a Flow: its methods, trigger conditions, state, and configuration. It is independent of the Python authoring layer that may have produced it and of the engine that runs it (see @@ -11,6 +11,7 @@ from __future__ import annotations import json import logging +from pathlib import Path import re from typing import Annotated, Any, Literal, TypeAlias, cast @@ -18,6 +19,7 @@ from pydantic import ( BaseModel, ConfigDict, Field, + PrivateAttr, field_serializer, model_validator, ) @@ -406,10 +408,19 @@ class FlowCrewActionDefinition(BaseModel): ) call: Literal["crew"] = Field( - description="Action discriminator. Use crew to run an inline Crew definition.", + description=( + "Action discriminator. Use crew to run an inline or referenced Crew " + "definition." + ), examples=["crew"], ) - with_: CrewDefinition = Field( + from_declaration: str | None = Field( + default=None, + description="Path to a JSON/JSONC Crew declaration file or folder.", + examples=["crews/research_crew"], + ) + with_: CrewDefinition | None = Field( + default=None, alias="with", description="Inline Crew definition to load and execute for this action.", examples=[ @@ -430,10 +441,26 @@ class FlowCrewActionDefinition(BaseModel): "agent": "researcher", } ], - "inputs": {"topic": "${state.topic}"}, } ], ) + inputs: dict[str, ExpressionData] | None = Field( + default=None, + description=( + "Input overrides passed to the Crew. String values are evaluated as CEL " + "only when the trimmed value starts with ${ and ends with }; all other " + "values are literal." + ), + examples=[{"topic": "${state.topic}"}], + ) + + @model_validator(mode="after") + def _validate_crew_source(self) -> FlowCrewActionDefinition: + if bool(self.from_declaration) == (self.with_ is not None): + raise ValueError( + "crew action requires exactly one of from_declaration or with" + ) + return self class FlowAgentActionDefinition(BaseModel): @@ -684,10 +711,12 @@ class FlowDefinition(BaseModel): arbitrary_types_allowed=True, ) + _source_path: Path | None = PrivateAttr(default=None) + schema_: Literal["crewai.flow/v1"] = Field( default="crewai.flow/v1", alias="schema", - description="Flow Definition schema identifier and version.", + description="Declarative Flow schema identifier and version.", examples=["crewai.flow/v1"], ) name: str = Field( @@ -764,29 +793,45 @@ class FlowDefinition(BaseModel): allow_unicode=True, ) + @property + def source_path(self) -> Path | None: + """Original definition file path, when loaded from a file.""" + return self._source_path + + @property + def source_dir(self) -> Path | None: + """Directory used to resolve relative paths in the definition.""" + if self._source_path is None: + return None + return self._source_path.parent + @classmethod - def from_dict(cls, data: dict[str, Any]) -> FlowDefinition: + def from_dict( + cls, data: dict[str, Any], *, source_path: Path | None = None + ) -> FlowDefinition: """Load a definition from a dictionary.""" definition = cls.model_validate(data) + if source_path is not None: + definition._source_path = source_path.expanduser().resolve() log_flow_definition_issues(definition) return definition @classmethod - def from_json(cls, data: str) -> FlowDefinition: + def from_json(cls, data: str, *, source_path: Path | None = None) -> FlowDefinition: """Load a definition from JSON.""" - return cls.from_dict(json.loads(data)) + return cls.from_dict(json.loads(data), source_path=source_path) @classmethod - def from_yaml(cls, data: str) -> FlowDefinition: + def from_yaml(cls, data: str, *, source_path: Path | None = None) -> FlowDefinition: """Load a definition from YAML.""" loaded = yaml.safe_load(data) or {} if not isinstance(loaded, dict): raise ValueError("Flow definition YAML must contain a mapping") - return cls.from_dict(loaded) + return cls.from_dict(loaded, source_path=source_path) @classmethod def json_schema(cls) -> dict[str, Any]: - """Return the JSON Schema for the Flow Definition contract.""" + """Return the JSON Schema for the declarative Flow contract.""" return cls.model_json_schema(by_alias=True) @@ -826,10 +871,16 @@ def _validate_action_cel( return if isinstance(action, FlowCrewActionDefinition): - Expression(cast(ExpressionData, action.with_.inputs)).validate_template( - allowed_roots=allowed_roots, - source=f"{path}.with.inputs", - ) + if action.with_ is not None: + Expression(cast(ExpressionData, action.with_.inputs)).validate_template( + allowed_roots=allowed_roots, + source=f"{path}.with.inputs", + ) + if action.inputs is not None: + Expression(cast(ExpressionData, action.inputs)).validate_template( + allowed_roots=allowed_roots, + source=f"{path}.inputs", + ) return if isinstance(action, FlowAgentActionDefinition): diff --git a/lib/crewai/src/crewai/flow/runtime/_actions.py b/lib/crewai/src/crewai/flow/runtime/_actions.py index c437e274b..c8f118775 100644 --- a/lib/crewai/src/crewai/flow/runtime/_actions.py +++ b/lib/crewai/src/crewai/flow/runtime/_actions.py @@ -8,6 +8,7 @@ from collections.abc import Awaitable, Callable import contextvars import inspect import os +from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol, cast from crewai.flow.expressions import Expression, ExpressionData @@ -128,16 +129,34 @@ class CrewAction: self.definition = definition async def run(self, *_args: Any, **kwargs: Any) -> Any: - from crewai.project.crew_loader import load_crew_from_definition + from crewai.project.crew_loader import load_crew, load_crew_from_definition local_context = _pop_local_context(kwargs) - crew_definition = self.definition.with_ + if self.definition.from_declaration is not None: + crew, default_inputs = load_crew( + _resolve_crew_declaration( + self.definition.from_declaration, + base_dir=self.flow._definition.source_dir, + ) + ) + input_template = {**default_inputs, **(self.definition.inputs or {})} + else: + crew_definition = self.definition.with_ + if crew_definition is None: + raise ValueError( + "crew action requires exactly one of from_declaration or with" + ) + input_template = { + **crew_definition.inputs, + **(self.definition.inputs or {}), + } + crew, _ = load_crew_from_definition(crew_definition, source="crew action") + inputs = Expression.from_flow( - cast(ExpressionData, crew_definition.inputs), + cast(ExpressionData, input_template), self.flow, local_context=local_context, ).render_template() - crew, _ = load_crew_from_definition(crew_definition, source="crew action") return await crew.kickoff_async(inputs=inputs) @@ -359,3 +378,29 @@ def _pop_local_context(kwargs: dict[str, Any]) -> LocalContext | None: if not isinstance(local_context, dict): raise TypeError("flow definition local context must be a mapping") return cast(LocalContext, local_context) + + +def _resolve_crew_declaration( + from_declaration: str, *, base_dir: Path | None = None +) -> Path: + path = Path(from_declaration).expanduser() + if base_dir is not None: + resolved_base_dir = base_dir.expanduser().resolve() + if not path.is_absolute(): + path = resolved_base_dir / path + resolved_path = path.resolve() + if not resolved_path.is_relative_to(resolved_base_dir): + raise ValueError( + "crew declaration path must be within the flow definition directory" + ) + path = resolved_path + + if not path.is_dir(): + return path + + for name in ("crew.jsonc", "crew.json"): + candidate = path / name + if candidate.is_file(): + return candidate + + return path / "crew.jsonc" diff --git a/lib/crewai/tests/test_flow_from_definition.py b/lib/crewai/tests/test_flow_from_definition.py index 1ed8dbcf9..693d75ef5 100644 --- a/lib/crewai/tests/test_flow_from_definition.py +++ b/lib/crewai/tests/test_flow_from_definition.py @@ -1005,8 +1005,8 @@ methods: description: Research {topic} expected_output: Findings about {topic} agent: researcher - inputs: - topic: "${state.topic}" + inputs: + topic: "${state.topic}" start: true """ @@ -1020,6 +1020,183 @@ methods: } +def test_crew_action_runs_crew_from_declaration( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +): + from crewai import Crew + + project_root = tmp_path / "project" + crew_root = project_root / "crews" / "research_crew" + agents_root = crew_root / "agents" + agents_root.mkdir(parents=True) + (agents_root / "researcher.jsonc").write_text( + """ +{ + "role": "Researcher", + "goal": "Research {topic}", + "backstory": "Knows things." +} +""", + encoding="utf-8", + ) + (crew_root / "crew.jsonc").write_text( + """ +{ + "name": "referenced_research", + "agents": ["researcher"], + "tasks": [ + { + "name": "research_task", + "description": "Research {topic}", + "expected_output": "Findings about {topic}", + "agent": "researcher" + } + ], + "inputs": { + "topic": "Default topic", + "audience": "developers" + } +} +""", + encoding="utf-8", + ) + + async def fake_kickoff_async( + self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any + ) -> dict[str, Any]: + return { + "crew": self.name, + "tasks": [task.description for task in self.tasks], + "inputs": inputs, + } + + monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async) + monkeypatch.chdir(project_root) + + yaml_str = """ +schema: crewai.flow/v1 +name: CrewFlow +methods: + research: + do: + call: crew + from_declaration: crews/research_crew + inputs: + topic: "${state.topic}" + start: true +""" + + flow = Flow.from_definition(FlowDefinition.from_yaml(yaml_str)) + + assert flow.kickoff(inputs={"topic": "AI"}) == { + "crew": "referenced_research", + "tasks": ["Research {topic}"], + "inputs": {"topic": "AI", "audience": "developers"}, + } + + +def test_crew_action_from_declaration_resolves_relative_to_flow_file( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +): + from crewai import Crew + + project_root = tmp_path / "project" + crew_root = project_root / "crews" / "research_crew" + agents_root = crew_root / "agents" + agents_root.mkdir(parents=True) + (agents_root / "researcher.jsonc").write_text( + """ +{ + "role": "Researcher", + "goal": "Research {topic}", + "backstory": "Knows things." +} +""", + encoding="utf-8", + ) + (crew_root / "crew.jsonc").write_text( + """ +{ + "name": "relative_research", + "agents": ["researcher"], + "tasks": [ + { + "description": "Research {topic}", + "expected_output": "Findings about {topic}", + "agent": "researcher" + } + ], + "inputs": { + "topic": "Default topic" + } +} +""", + encoding="utf-8", + ) + + async def fake_kickoff_async( + self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any + ) -> dict[str, Any]: + return {"crew": self.name, "inputs": inputs} + + monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async) + + flow_path = project_root / "flow.yaml" + yaml_str = """ +schema: crewai.flow/v1 +name: CrewFlow +methods: + research: + do: + call: crew + from_declaration: crews/research_crew + inputs: + topic: "${state.topic}" + start: true +""" + flow_path.write_text(yaml_str, encoding="utf-8") + + other_cwd = tmp_path / "other" + other_cwd.mkdir() + monkeypatch.chdir(other_cwd) + + flow = Flow.from_definition( + FlowDefinition.from_yaml(yaml_str, source_path=flow_path) + ) + + assert flow.kickoff(inputs={"topic": "AI"}) == { + "crew": "relative_research", + "inputs": {"topic": "AI"}, + } + + +def test_crew_action_from_declaration_rejects_paths_outside_flow_file( + tmp_path: Path, +): + flow_path = tmp_path / "project" / "flow.yaml" + flow_path.parent.mkdir() + yaml_str = """ +schema: crewai.flow/v1 +name: CrewFlow +methods: + research: + do: + call: crew + from_declaration: ../outside/crew.jsonc + start: true +""" + + flow = Flow.from_definition( + FlowDefinition.from_yaml(yaml_str, source_path=flow_path) + ) + + with pytest.raises( + ValueError, + match="crew declaration path must be within the flow definition directory", + ): + flow.kickoff() + + def test_crew_action_round_trips_with_inline_definition(): definition = FlowDefinition.from_dict( { @@ -1047,8 +1224,8 @@ def test_crew_action_round_trips_with_inline_definition(): "agent": "researcher", } ], - "inputs": {"topic": "${state.topic}"}, }, + "inputs": {"topic": "${state.topic}"}, }, } }, @@ -1062,6 +1239,9 @@ def test_crew_action_round_trips_with_inline_definition(): ]["role"] == "Researcher" ) + assert definition.to_dict()["methods"]["research"]["do"]["inputs"] == { + "topic": "${state.topic}" + } def test_crew_action_normalizes_named_agent_list_definition(): @@ -1162,7 +1342,7 @@ def test_crew_action_rejects_incomplete_inline_agent_definition(): ) -def test_crew_action_rejects_ref(): +def test_crew_action_rejects_python_ref_field(): with pytest.raises(ValidationError, match="ref"): FlowDefinition.from_dict( { @@ -1174,7 +1354,6 @@ def test_crew_action_rejects_ref(): "do": { "call": "crew", "ref": "project.crew:build_crew", - "with": {"inputs": {"topic": "AI"}}, }, } }, From 720a4c72161623341b78159c14c32d96a1a8a388 Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 22 Jun 2026 20:37:16 -0700 Subject: [PATCH 3/6] Keep flow method progress visible for nested crews (#6295) Inline crews default to `verbose=False`. They set the shared formatter's `verbose` value in `lib/crewai/src/crewai/crew.py`, which could hide flow method status from `lib/crewai/src/crewai/events/utils/ console_formatter.py`. Remove that `verbose` check for flow method status. Flow output is still controlled by `suppress_flow_events`. Normal quiet crews are unchanged because crew, task, and agent logs still use their own `verbose` checks. --- .../src/crewai/events/utils/console_formatter.py | 3 --- .../utilities/test_console_formatter_pause_resume.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/crewai/src/crewai/events/utils/console_formatter.py b/lib/crewai/src/crewai/events/utils/console_formatter.py index fdecf93cf..604c7b051 100644 --- a/lib/crewai/src/crewai/events/utils/console_formatter.py +++ b/lib/crewai/src/crewai/events/utils/console_formatter.py @@ -373,9 +373,6 @@ To enable tracing, do any one of these: status: str = "running", ) -> None: """Show method status panel.""" - if not self.verbose: - return - if status == "running": style = "yellow" panel_title = "🔄 Flow Method Running" diff --git a/lib/crewai/tests/utilities/test_console_formatter_pause_resume.py b/lib/crewai/tests/utilities/test_console_formatter_pause_resume.py index 0adb43d83..1ffbb3850 100644 --- a/lib/crewai/tests/utilities/test_console_formatter_pause_resume.py +++ b/lib/crewai/tests/utilities/test_console_formatter_pause_resume.py @@ -46,6 +46,16 @@ class TestConsoleFormatterPauseResume: formatter.resume_live_updates() + def test_flow_method_status_ignores_formatter_verbose(self): + formatter = ConsoleFormatter(verbose=False) + + with patch.object(formatter, "print_panel") as mock_print_panel: + formatter.handle_method_status("categorize_tickets") + + mock_print_panel.assert_called_once() + _, kwargs = mock_print_panel.call_args + assert kwargs["is_flow"] is True + def test_streaming_after_pause_resume_creates_new_session(self): """Test that streaming after pause/resume creates new Live session.""" formatter = ConsoleFormatter() From 221dfdb08e41dcc24e36bcc9e5a1a2d2cc0722df Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 22 Jun 2026 20:44:08 -0700 Subject: [PATCH 4/6] Consolidate `crewai run` and `crewai flow kickoff` (#6296) Make `crewai run` the single execution path for crews and flows, with `crewai flow kickoff` kept as a deprecated compatibility alias. --- docs/edge/ar/changelog.mdx | 2 +- docs/edge/ar/concepts/flows.mdx | 10 +- docs/edge/ar/guides/flows/first-flow.mdx | 2 +- docs/edge/en/changelog.mdx | 2 +- docs/edge/en/concepts/flows.mdx | 14 +- docs/edge/en/guides/flows/first-flow.mdx | 2 +- docs/edge/ko/changelog.mdx | 2 +- docs/edge/ko/concepts/flows.mdx | 10 +- docs/edge/ko/guides/flows/first-flow.mdx | 2 +- docs/edge/pt-BR/changelog.mdx | 2 +- docs/edge/pt-BR/concepts/flows.mdx | 10 +- docs/edge/pt-BR/guides/flows/first-flow.mdx | 2 +- lib/cli/src/crewai_cli/cli.py | 37 ++--- lib/cli/src/crewai_cli/kickoff_flow.py | 31 ----- lib/cli/src/crewai_cli/run_crew.py | 119 ++++++++++++---- .../src/crewai_cli/run_declarative_flow.py | 8 +- lib/cli/src/crewai_cli/templates/AGENTS.md | 2 +- .../templates/declarative_flow/README.md | 6 +- lib/cli/tests/test_cli.py | 61 +++++++-- lib/cli/tests/test_create_flow.py | 4 +- lib/cli/tests/test_flow_commands.py | 46 ++++--- lib/cli/tests/test_run_crew.py | 128 ++++++++++++++++++ lib/cli/tests/test_run_declarative_flow.py | 2 +- lib/crewai/tests/cli/test_run_crew.py | 19 ++- 24 files changed, 351 insertions(+), 172 deletions(-) delete mode 100644 lib/cli/src/crewai_cli/kickoff_flow.py diff --git a/docs/edge/ar/changelog.mdx b/docs/edge/ar/changelog.mdx index 6f3fc32f5..e70318eff 100644 --- a/docs/edge/ar/changelog.mdx +++ b/docs/edge/ar/changelog.mdx @@ -64,7 +64,7 @@ mode: "wide" - تنفيذ أدوات تشغيل تعريف التدفق بدون كود Python - دفع التغذية الراجعة البشرية من تعريف التدفق - توصيل التكوين والاستمرارية من FlowDefinition إلى وقت التشغيل - - إضافة `crewai run --definition` التجريبية للتدفقات + - إضافة `crewai run --definition` للتدفقات التصريحية - دعم تراجع نشر ZIP وتشغيل مشاريع الطاقم بتنسيق JSON - تقديم الطواقم بتنسيق JSON أولاً diff --git a/docs/edge/ar/concepts/flows.mdx b/docs/edge/ar/concepts/flows.mdx index da162310f..62d34b335 100644 --- a/docs/edge/ar/concepts/flows.mdx +++ b/docs/edge/ar/concepts/flows.mdx @@ -959,7 +959,7 @@ source .venv/bin/activate بعد تفعيل البيئة الافتراضية، يمكنك تشغيل التدفق بتنفيذ أحد الأوامر التالية: ```bash -crewai flow kickoff +crewai run ``` أو @@ -1160,10 +1160,4 @@ crewai run يكتشف هذا الأمر تلقائيًا ما إذا كان مشروعك تدفقًا (بناءً على إعداد `type = "flow"` في pyproject.toml الخاص بك) ويشغّله وفقًا لذلك. هذه هي الطريقة الموصى بها لتشغيل التدفقات من سطر الأوامر. -للتوافق مع الإصدارات السابقة، يمكنك أيضًا استخدام: - -```shell -crewai flow kickoff -``` - -ومع ذلك، فإن أمر `crewai run` هو الطريقة المفضلة الآن لأنه يعمل لكل من فرق Crew والتدفقات. +أمر `crewai flow kickoff` القديم deprecated. استخدم `crewai run` لكل من فرق Crew والتدفقات. diff --git a/docs/edge/ar/guides/flows/first-flow.mdx b/docs/edge/ar/guides/flows/first-flow.mdx index 9ee804653..322e71c2a 100644 --- a/docs/edge/ar/guides/flows/first-flow.mdx +++ b/docs/edge/ar/guides/flows/first-flow.mdx @@ -172,7 +172,7 @@ crewai install ## الخطوة 8: تشغيل Flow ```bash -crewai flow kickoff +crewai run ``` عند تشغيل هذا الأمر، ستشاهد Flow يعمل: diff --git a/docs/edge/en/changelog.mdx b/docs/edge/en/changelog.mdx index 924463ddc..b0a7492c1 100644 --- a/docs/edge/en/changelog.mdx +++ b/docs/edge/en/changelog.mdx @@ -64,7 +64,7 @@ mode: "wide" - Implement Flow definition run tools without Python code - Drive human feedback from the flow definition - Wire config and persistence from FlowDefinition into the runtime - - Add experimental `crewai run --definition` for flows + - Add `crewai run --definition` for declarative flows - Support ZIP deployment fallback and JSON crew project env runs - Introduce JSON first crews diff --git a/docs/edge/en/concepts/flows.mdx b/docs/edge/en/concepts/flows.mdx index 210c573ce..647512545 100644 --- a/docs/edge/en/concepts/flows.mdx +++ b/docs/edge/en/concepts/flows.mdx @@ -956,13 +956,13 @@ Once all of the dependencies are installed, you need to activate the virtual env source .venv/bin/activate ``` -After activating the virtual environment, you can run the flow by executing one of the following commands: +After activating the virtual environment, you can run the flow with the CrewAI CLI: ```bash -crewai flow kickoff +crewai run ``` -or +You can also run the project script directly: ```bash uv run kickoff @@ -1160,10 +1160,4 @@ crewai run This command automatically detects if your project is a flow (based on the `type = "flow"` setting in your pyproject.toml) and runs it accordingly. This is the recommended way to run flows from the command line. -For backward compatibility, you can also use: - -```shell -crewai flow kickoff -``` - -However, the `crewai run` command is now the preferred method as it works for both crews and flows. +The legacy `crewai flow kickoff` command is deprecated. Use `crewai run` for both crews and flows. diff --git a/docs/edge/en/guides/flows/first-flow.mdx b/docs/edge/en/guides/flows/first-flow.mdx index ad6638b77..d26a1eb2d 100644 --- a/docs/edge/en/guides/flows/first-flow.mdx +++ b/docs/edge/en/guides/flows/first-flow.mdx @@ -395,7 +395,7 @@ crewai install Now it's time to see your flow in action! Run it using the CrewAI CLI: ```bash -crewai flow kickoff +crewai run ``` When you run this command, you'll see your flow spring to life: diff --git a/docs/edge/ko/changelog.mdx b/docs/edge/ko/changelog.mdx index aea038515..1fa751baf 100644 --- a/docs/edge/ko/changelog.mdx +++ b/docs/edge/ko/changelog.mdx @@ -64,7 +64,7 @@ mode: "wide" - Python 코드 없이 Flow 정의 실행 도구 구현 - Flow 정의에서 인간 피드백 유도 - FlowDefinition의 구성 및 지속성을 런타임에 연결 - - 흐름을 위한 실험적 `crewai run --definition` 추가 + - 선언적 흐름을 위한 `crewai run --definition` 추가 - ZIP 배포 대체 및 JSON 크루 프로젝트 환경 실행 지원 - JSON 우선 크루 도입 diff --git a/docs/edge/ko/concepts/flows.mdx b/docs/edge/ko/concepts/flows.mdx index 91ca831fb..e168b7e3f 100644 --- a/docs/edge/ko/concepts/flows.mdx +++ b/docs/edge/ko/concepts/flows.mdx @@ -951,7 +951,7 @@ source .venv/bin/activate 가상 환경을 활성화한 후, 아래 명령어 중 하나를 실행하여 플로우를 실행할 수 있습니다: ```bash -crewai flow kickoff +crewai run ``` 또는 @@ -1054,10 +1054,4 @@ crewai run 이 명령어는 프로젝트가 pyproject.toml의 `type = "flow"` 설정을 기반으로 flow인지 자동으로 감지하여 해당 방식으로 실행합니다. 명령줄에서 flow를 실행하는 권장 방법입니다. -하위 호환성을 위해 다음 명령어도 사용할 수 있습니다: - -```shell -crewai flow kickoff -``` - -하지만 `crewai run` 명령어가 이제 crew와 flow 모두에 작동하므로 더욱 선호되는 방법입니다. +레거시 `crewai flow kickoff` 명령어는 deprecated되었습니다. crew와 flow 모두 `crewai run`을 사용하세요. diff --git a/docs/edge/ko/guides/flows/first-flow.mdx b/docs/edge/ko/guides/flows/first-flow.mdx index b8a693086..04d0f3edf 100644 --- a/docs/edge/ko/guides/flows/first-flow.mdx +++ b/docs/edge/ko/guides/flows/first-flow.mdx @@ -393,7 +393,7 @@ crewai install 이제 여러분의 flow가 실제로 작동하는 모습을 볼 차례입니다! CrewAI CLI를 사용하여 flow를 실행하세요: ```bash -crewai flow kickoff +crewai run ``` 이 명령어를 실행하면 flow가 다음과 같이 작동하는 것을 확인할 수 있습니다: diff --git a/docs/edge/pt-BR/changelog.mdx b/docs/edge/pt-BR/changelog.mdx index f611f48aa..2e10fc667 100644 --- a/docs/edge/pt-BR/changelog.mdx +++ b/docs/edge/pt-BR/changelog.mdx @@ -64,7 +64,7 @@ mode: "wide" - Implementar ferramentas de execução de definição de fluxo sem código Python - Conduzir feedback humano a partir da definição de fluxo - Conectar configuração e persistência do FlowDefinition ao tempo de execução - - Adicionar `crewai run --definition` experimental para fluxos + - Adicionar `crewai run --definition` para fluxos declarativos - Suportar fallback de implantação ZIP e execuções de projeto de equipe em JSON - Introduzir equipes em JSON primeiro diff --git a/docs/edge/pt-BR/concepts/flows.mdx b/docs/edge/pt-BR/concepts/flows.mdx index 73ac5019a..8879edca8 100644 --- a/docs/edge/pt-BR/concepts/flows.mdx +++ b/docs/edge/pt-BR/concepts/flows.mdx @@ -948,7 +948,7 @@ source .venv/bin/activate Com o ambiente ativado, execute o flow usando um dos comandos: ```bash -crewai flow kickoff +crewai run ``` ou @@ -1052,10 +1052,4 @@ crewai run O comando detecta automaticamente se seu projeto é um flow (com base na configuração `type = "flow"` no pyproject.toml) e executa conforme o esperado. Esse é o método recomendado para executar flows pelo terminal. -Por compatibilidade retroativa, também é possível usar: - -```shell -crewai flow kickoff -``` - -No entanto, o comando `crewai run` é agora o preferido, pois funciona tanto para crews quanto para flows. +O comando legado `crewai flow kickoff` está deprecated. Use `crewai run` para crews e flows. diff --git a/docs/edge/pt-BR/guides/flows/first-flow.mdx b/docs/edge/pt-BR/guides/flows/first-flow.mdx index 0069cf85f..0ff1ba5d6 100644 --- a/docs/edge/pt-BR/guides/flows/first-flow.mdx +++ b/docs/edge/pt-BR/guides/flows/first-flow.mdx @@ -393,7 +393,7 @@ crewai install Agora é hora de ver seu flow em ação! Execute-o usando a CLI do CrewAI: ```bash -crewai flow kickoff +crewai run ``` Quando você rodar esse comando, verá seu flow ganhando vida: diff --git a/lib/cli/src/crewai_cli/cli.py b/lib/cli/src/crewai_cli/cli.py index 1a64a74f3..b2050bc34 100644 --- a/lib/cli/src/crewai_cli/cli.py +++ b/lib/cli/src/crewai_cli/cli.py @@ -40,14 +40,6 @@ def replay_task_command(*args: Any, **kwargs: Any) -> Any: return _replay_task_command(*args, **kwargs) -def run_declarative_flow(*args: Any, **kwargs: Any) -> Any: - from crewai_cli.run_declarative_flow import ( - run_declarative_flow as _run_declarative_flow, - ) - - return _run_declarative_flow(*args, **kwargs) - - def run_crew(*args: Any, **kwargs: Any) -> Any: from crewai_cli.run_crew import run_crew as _run_crew @@ -476,7 +468,7 @@ def memory( type=str, default=None, help=( - "Path to a trained-agents pickle (produced by `crewai train -f`). " + "Crew-only: path to a trained-agents pickle (produced by `crewai train -f`). " "When set, agents load suggestions from this file instead of the " "default trained_agents_data.pkl. Equivalent to setting " "CREWAI_TRAINED_AGENTS_FILE." @@ -520,13 +512,13 @@ def install(context: click.Context) -> None: "--definition", type=str, default=None, - help="Experimental: path to a declarative Flow YAML/JSON file.", + help="Flow-only: path to a declarative flow definition.", ) @click.option( "--inputs", type=str, default=None, - help='Experimental: JSON object passed to flow.kickoff(), e.g. \'{"topic":"AI"}\'.', + help='Flow-only: JSON object passed to the declarative flow, e.g. \'{"topic":"AI"}\'.', ) def run( trained_agents_file: str | None, @@ -536,16 +528,14 @@ def run( """Run the Crew or Flow.""" if inputs is not None and definition is None: raise click.UsageError("--inputs requires --definition") + if trained_agents_file is not None and definition is not None: + raise click.UsageError("--filename can only be used when running crews") - if definition is not None: - click.secho( - "Warning: `crewai run --definition` is experimental and may change without notice.", - fg="yellow", - ) - run_declarative_flow(definition=definition, inputs=inputs) - return - - run_crew(trained_agents_file=trained_agents_file) + run_crew( + trained_agents_file=trained_agents_file, + definition=definition, + inputs=inputs, + ) @crewai.command() @@ -797,13 +787,10 @@ def flow() -> None: """Flow related commands.""" -@flow.command(name="kickoff") +@flow.command(name="kickoff", deprecated=True) def flow_run() -> None: """Kickoff the Flow.""" - from crewai_cli.kickoff_flow import kickoff_flow - - click.echo("Running the Flow") - kickoff_flow() + run_crew(trained_agents_file=None, definition=None, inputs=None) @flow.command(name="plot") diff --git a/lib/cli/src/crewai_cli/kickoff_flow.py b/lib/cli/src/crewai_cli/kickoff_flow.py deleted file mode 100644 index ff5f317dd..000000000 --- a/lib/cli/src/crewai_cli/kickoff_flow.py +++ /dev/null @@ -1,31 +0,0 @@ -import subprocess - -import click - - -def kickoff_flow() -> None: - """ - Kickoff the flow from declarative config or the Python UV entrypoint. - """ - from crewai_cli.run_declarative_flow import ( - configured_project_declarative_flow, - run_declarative_flow_in_project_env, - ) - - if definition := configured_project_declarative_flow(): - run_declarative_flow_in_project_env(definition=definition) - else: - command = ["uv", "run", "kickoff"] - - try: - subprocess.run( # noqa: S603 - command, capture_output=False, text=True, check=True - ) - - except subprocess.CalledProcessError as e: - click.echo(f"An error occurred while running the flow: {e}", err=True) - raise SystemExit(1) from e - - except Exception as e: - click.echo(f"An unexpected error occurred: {e}", err=True) - raise SystemExit(1) from e diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index cbf2445d1..0fa61dc7a 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable from contextlib import AbstractContextManager, nullcontext -from enum import Enum import os from pathlib import Path import re @@ -27,11 +26,6 @@ if TYPE_CHECKING: from crewai_cli.crew_run_tui import CrewRunApp -class CrewType(Enum): - STANDARD = "standard" - FLOW = "flow" - - # Must accept the same names as the kickoff interpolation pattern in # crewai.utilities.string_utils (_VARIABLE_PATTERN), including hyphens — # otherwise placeholders are interpolated at runtime but never prompted for. @@ -537,7 +531,11 @@ def _print_post_tui_summary(app: CrewRunApp) -> None: ) -def run_crew(trained_agents_file: str | None = None) -> None: +def run_crew( + trained_agents_file: str | None = None, + definition: str | None = None, + inputs: str | None = None, +) -> None: """Run the crew or flow. Args: @@ -545,15 +543,90 @@ def run_crew(trained_agents_file: str | None = None) -> None: by ``crewai train -f``. When set, exported as ``CREWAI_TRAINED_AGENTS_FILE`` so agents load suggestions from this file instead of the default ``trained_agents_data.pkl``. + definition: Optional path to a declarative Flow definition. + inputs: Optional JSON object passed to a declarative Flow. """ - # JSON crew projects take precedence + if inputs is not None and definition is None: + raise click.UsageError("--inputs requires --definition") + + if definition is not None: + _run_explicit_declarative_flow( + definition=definition, + inputs=inputs, + trained_agents_file=trained_agents_file, + ) + return + if _has_json_crew(): _run_json_crew_in_project_env(trained_agents_file=trained_agents_file) return + pyproject_data = read_toml() + _warn_if_old_poetry_project(pyproject_data) + project_type = _get_project_type(pyproject_data) + + if project_type == "flow": + _run_flow_project( + pyproject_data=pyproject_data, + trained_agents_file=trained_agents_file, + ) + return + + _run_classic_crew_project( + pyproject_data=pyproject_data, + trained_agents_file=trained_agents_file, + ) + + +def _run_explicit_declarative_flow( + definition: str, inputs: str | None, trained_agents_file: str | None +) -> None: + if trained_agents_file is not None: + raise click.UsageError("--filename can only be used when running crews") + + from crewai_cli.run_declarative_flow import run_declarative_flow + + run_declarative_flow(definition=definition, inputs=inputs) + + +def _run_flow_project( + pyproject_data: dict[str, Any], trained_agents_file: str | None +) -> None: + if trained_agents_file is not None: + raise click.UsageError("--filename can only be used when running crews") + + click.echo("Running the Flow") + from crewai_cli.run_declarative_flow import ( + configured_project_declarative_flow, + run_declarative_flow_in_project_env, + ) + + if definition := configured_project_declarative_flow(pyproject_data): + run_declarative_flow_in_project_env(definition=definition) + return + + _execute_uv_script("kickoff", entity_type="flow") + + +def _run_classic_crew_project( + pyproject_data: dict[str, Any], trained_agents_file: str | None +) -> None: + click.echo("Running the Crew") + _execute_uv_script( + "run_crew", + entity_type="crew", + trained_agents_file=trained_agents_file, + ) + + +def _get_project_type(pyproject_data: dict[str, Any]) -> str | None: + project_type = pyproject_data.get("tool", {}).get("crewai", {}).get("type") + return project_type if isinstance(project_type, str) else None + + +def _warn_if_old_poetry_project(pyproject_data: dict[str, Any]) -> None: crewai_version = get_crewai_version() min_required_version = "0.71.0" - pyproject_data = read_toml() if pyproject_data.get("tool", {}).get("poetry") and ( version.parse(crewai_version) < version.parse(min_required_version) @@ -564,25 +637,22 @@ def run_crew(trained_agents_file: str | None = None) -> None: fg="red", ) - is_flow = pyproject_data.get("tool", {}).get("crewai", {}).get("type") == "flow" - crew_type = CrewType.FLOW if is_flow else CrewType.STANDARD - click.echo(f"Running the {'Flow' if is_flow else 'Crew'}") - - execute_command(crew_type, trained_agents_file=trained_agents_file) - - -def execute_command( - crew_type: CrewType, trained_agents_file: str | None = None +def _execute_uv_script( + script_name: str, + *, + entity_type: str, + trained_agents_file: str | None = None, ) -> None: - """Execute the appropriate command based on crew type. + """Execute a project script through uv. Args: - crew_type: The type of crew to run. + script_name: The project script to run. + entity_type: The user-facing entity being run. trained_agents_file: Optional trained-agents pickle path forwarded to the subprocess via the ``CREWAI_TRAINED_AGENTS_FILE`` env var. """ - command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"] + command = ["uv", "run", script_name] env = build_env_with_all_tool_credentials() if trained_agents_file: @@ -592,21 +662,20 @@ def execute_command( subprocess.run(command, capture_output=False, text=True, check=True, env=env) # noqa: S603 except subprocess.CalledProcessError as e: - handle_error(e, crew_type) + _handle_run_error(e, entity_type) except Exception as e: click.echo(f"An unexpected error occurred: {e}", err=True) -def handle_error(error: subprocess.CalledProcessError, crew_type: CrewType) -> None: +def _handle_run_error(error: subprocess.CalledProcessError, entity_type: str) -> None: """ Handle subprocess errors with appropriate messaging. Args: error: The subprocess error that occurred - crew_type: The type of crew that was being run + entity_type: The type of entity that was being run """ - entity_type = "flow" if crew_type == CrewType.FLOW else "crew" click.echo(f"An error occurred while running the {entity_type}: {error}", err=True) if error.output: diff --git a/lib/cli/src/crewai_cli/run_declarative_flow.py b/lib/cli/src/crewai_cli/run_declarative_flow.py index af7431b02..c6ff668c4 100644 --- a/lib/cli/src/crewai_cli/run_declarative_flow.py +++ b/lib/cli/src/crewai_cli/run_declarative_flow.py @@ -21,7 +21,7 @@ def run_declarative_flow_in_project_env( if inputs is not None: raise click.UsageError("--inputs is only supported with --definition") - _execute_declarative_flow_command(["uv", "run", "crewai", "flow", "kickoff"]) + _execute_declarative_flow_command(["uv", "run", "crewai", "run"]) def plot_declarative_flow_in_project_env(definition: str) -> None: @@ -34,7 +34,7 @@ def plot_declarative_flow_in_project_env(definition: str) -> None: def run_declarative_flow(definition: str, inputs: str | None = None) -> None: - """Run a declarative flow from a YAML/JSON file path.""" + """Run a declarative flow from a definition path.""" parsed_inputs = _parse_inputs(inputs) try: @@ -50,7 +50,7 @@ def run_declarative_flow(definition: str, inputs: str | None = None) -> None: def plot_declarative_flow(definition: str) -> None: - """Plot a declarative flow from a YAML/JSON file path.""" + """Plot a declarative flow from a definition path.""" try: flow = load_declarative_flow(definition) flow.plot() @@ -62,7 +62,7 @@ def plot_declarative_flow(definition: str) -> None: def load_declarative_flow(definition: str) -> Any: - """Load a declarative Flow instance from a YAML/JSON file path.""" + """Load a declarative Flow instance from a definition path.""" try: from crewai.flow.flow import Flow from crewai.flow.flow_definition import FlowDefinition diff --git a/lib/cli/src/crewai_cli/templates/AGENTS.md b/lib/cli/src/crewai_cli/templates/AGENTS.md index cb9fff256..8f39289e7 100644 --- a/lib/cli/src/crewai_cli/templates/AGENTS.md +++ b/lib/cli/src/crewai_cli/templates/AGENTS.md @@ -62,7 +62,7 @@ crewai create flow --skip_provider # New flow project # Running crewai run # Run crew or flow (auto-detects from pyproject.toml) -crewai flow kickoff # Legacy flow execution +crewai flow kickoff # Deprecated compatibility alias for crewai run # Testing & training crewai test # Test crew (default: 2 iterations, gpt-4o-mini) diff --git a/lib/cli/src/crewai_cli/templates/declarative_flow/README.md b/lib/cli/src/crewai_cli/templates/declarative_flow/README.md index 2de72c4df..697c0aa32 100644 --- a/lib/cli/src/crewai_cli/templates/declarative_flow/README.md +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/README.md @@ -1,6 +1,6 @@ # {{name}} Flow -This project defines a CrewAI Flow in `src/{{folder_name}}/flow.yaml`. +This project defines a declarative CrewAI Flow in `src/{{folder_name}}/flow.yaml`. ## Install @@ -11,7 +11,7 @@ crewai install ## Run ```bash -crewai flow kickoff +crewai run ``` -Edit `src/{{folder_name}}/flow.yaml` to change the flow. Add reusable crews under `src/{{folder_name}}/crews/`, custom Python tools under `src/{{folder_name}}/tools/`, and shared knowledge files under `src/{{folder_name}}/knowledge/`. +Edit the declarative flow definition at `src/{{folder_name}}/flow.yaml` to change the flow. Add reusable crews under `src/{{folder_name}}/crews/`, custom Python tools under `src/{{folder_name}}/tools/`, and shared knowledge files under `src/{{folder_name}}/knowledge/`. diff --git a/lib/cli/tests/test_cli.py b/lib/cli/tests/test_cli.py index 9d8802f27..28f849ce2 100644 --- a/lib/cli/tests/test_cli.py +++ b/lib/cli/tests/test_cli.py @@ -12,6 +12,7 @@ from crewai_cli.cli import ( deploy_remove, deply_status, flow_add_crew, + flow_run, login, reset_memories, run, @@ -126,40 +127,72 @@ def test_run_uses_project_runner_by_default(run_crew, runner): result = runner.invoke(run) assert result.exit_code == 0 - run_crew.assert_called_once_with(trained_agents_file=None) + run_crew.assert_called_once_with( + trained_agents_file=None, + definition=None, + inputs=None, + ) assert "experimental" not in result.output.lower() -@mock.patch("crewai_cli.cli.run_declarative_flow") -def test_run_with_definition_uses_definition_runner(run_declarative_flow, runner): +@mock.patch("crewai_cli.cli.run_crew") +def test_run_with_definition_uses_project_runner(run_crew, runner): result = runner.invoke( run, ["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'], ) assert result.exit_code == 0 - assert ( - "Warning: `crewai run --definition` is experimental and may change without notice." - in result.output - ) - run_declarative_flow.assert_called_once_with( - definition="flow.yaml", inputs='{"topic":"AI"}' + run_crew.assert_called_once_with( + trained_agents_file=None, + definition="flow.yaml", + inputs='{"topic":"AI"}', ) @mock.patch("crewai_cli.cli.run_crew") -@mock.patch("crewai_cli.cli.run_declarative_flow") -def test_run_rejects_inputs_without_definition( - run_declarative_flow, run_crew, runner -): +def test_run_rejects_inputs_without_definition(run_crew, runner): result = runner.invoke(run, ["--inputs", '{"topic":"AI"}']) assert result.exit_code == 2 assert "Error: --inputs requires --definition" in result.output - run_declarative_flow.assert_not_called() run_crew.assert_not_called() +@mock.patch("crewai_cli.cli.run_crew") +def test_run_rejects_filename_with_definition(run_crew, runner): + result = runner.invoke(run, ["--definition", "flow.yaml", "--filename", "x.pkl"]) + + assert result.exit_code == 2 + assert "Error: --filename can only be used when running crews" in result.output + run_crew.assert_not_called() + + +@mock.patch("crewai_cli.cli.run_crew") +def test_run_passes_filename_to_project_runner(run_crew, runner): + result = runner.invoke(run, ["--filename", "trained.pkl"]) + + assert result.exit_code == 0 + run_crew.assert_called_once_with( + trained_agents_file="trained.pkl", + definition=None, + inputs=None, + ) + + +@mock.patch("crewai_cli.cli.run_crew") +def test_flow_kickoff_is_deprecated_and_uses_run_path(run_crew, runner): + result = runner.invoke(flow_run) + + assert result.exit_code == 0 + run_crew.assert_called_once_with( + trained_agents_file=None, + definition=None, + inputs=None, + ) + assert "DeprecationWarning" in result.output + + @mock.patch("crewai_cli.create_json_crew.create_json_crew") def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner): result = runner.invoke(create, ["crew", "DMN Crew"], env={"CREWAI_DMN": "True"}) diff --git a/lib/cli/tests/test_create_flow.py b/lib/cli/tests/test_create_flow.py index 2fa941e58..256ace28c 100644 --- a/lib/cli/tests/test_create_flow.py +++ b/lib/cli/tests/test_create_flow.py @@ -28,9 +28,7 @@ def test_create_flow_declarative_project_can_run( assert (project_root / pyproject["tool"]["crewai"]["definition"]).is_file() monkeypatch.chdir(project_root) - result = CliRunner().invoke( - crewai, ["flow", "kickoff"], env={"UV_RUN_RECURSION_DEPTH": "1"} - ) + result = CliRunner().invoke(crewai, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"}) assert result.exit_code == 0 assert "Running the Flow" in result.output diff --git a/lib/cli/tests/test_flow_commands.py b/lib/cli/tests/test_flow_commands.py index 6154ff642..3158fc9e0 100644 --- a/lib/cli/tests/test_flow_commands.py +++ b/lib/cli/tests/test_flow_commands.py @@ -1,12 +1,12 @@ from __future__ import annotations -from collections.abc import Callable from pathlib import Path import subprocess import pytest +from click.testing import CliRunner -import crewai_cli.kickoff_flow as kickoff_flow_module +from crewai_cli.cli import flow_run import crewai_cli.plot_flow as plot_flow_module @@ -33,18 +33,19 @@ def _write_flow_project(project_root: Path) -> None: ) -def test_kickoff_flow_runs_configured_declarative_definition( +def test_flow_kickoff_runs_configured_declarative_definition( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, - capsys: pytest.CaptureFixture[str], ) -> None: _write_flow_project(tmp_path) monkeypatch.chdir(tmp_path) monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1") - kickoff_flow_module.kickoff_flow() + result = CliRunner().invoke(flow_run) - assert capsys.readouterr().out == "AI\n" + assert result.exit_code == 0 + assert "DeprecationWarning" in result.output + assert "Running the Flow\nAI\n" in result.output def test_plot_flow_runs_configured_declarative_definition( @@ -57,18 +58,27 @@ def test_plot_flow_runs_configured_declarative_definition( plot_flow_module.plot_flow() -@pytest.mark.parametrize( - ("command", "expected"), - [ - pytest.param(kickoff_flow_module.kickoff_flow, ["uv", "run", "kickoff"]), - pytest.param(plot_flow_module.plot_flow, ["uv", "run", "plot"]), - ], -) -def test_flow_commands_keep_python_entrypoint_without_definition( +def test_flow_kickoff_delegates_to_run_crew( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls = [] + + monkeypatch.setattr( + "crewai_cli.cli.run_crew", + lambda **kwargs: calls.append(kwargs), + ) + + result = CliRunner().invoke(flow_run) + + assert result.exit_code == 0 + assert calls == [ + {"trained_agents_file": None, "definition": None, "inputs": None}, + ] + + +def test_plot_flow_keeps_python_entrypoint_without_definition( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, - command: Callable[[], None], - expected: list[str], ) -> None: subprocess_calls = [] @@ -79,11 +89,11 @@ def test_flow_commands_keep_python_entrypoint_without_definition( lambda command, **kwargs: subprocess_calls.append((command, kwargs)), ) - command() + plot_flow_module.plot_flow() assert subprocess_calls == [ ( - expected, + ["uv", "run", "plot"], {"capture_output": False, "text": True, "check": True}, ) ] diff --git a/lib/cli/tests/test_run_crew.py b/lib/cli/tests/test_run_crew.py index ebb48d72b..40255abed 100644 --- a/lib/cli/tests/test_run_crew.py +++ b/lib/cli/tests/test_run_crew.py @@ -568,3 +568,131 @@ def test_has_json_crew_true_without_pyproject(monkeypatch, tmp_path: Path): (tmp_path / "crew.jsonc").write_text("{}") assert run_crew_module._has_json_crew() is True + + +def test_run_crew_rejects_inputs_without_definition(): + with pytest.raises(click.UsageError) as exc_info: + run_crew_module.run_crew(inputs='{"topic":"AI"}') + + assert "--inputs requires --definition" in exc_info.value.message + + +def test_run_crew_rejects_filename_with_explicit_definition(): + with pytest.raises(click.UsageError) as exc_info: + run_crew_module.run_crew( + trained_agents_file="trained.pkl", + definition="flow.yaml", + ) + + assert "--filename can only be used when running crews" in exc_info.value.message + + +def test_run_crew_runs_explicit_declarative_definition(monkeypatch, capsys): + calls = [] + + def fake_run_declarative_flow(definition: str, inputs: str | None = None): + calls.append((definition, inputs)) + + monkeypatch.setattr( + "crewai_cli.run_declarative_flow.run_declarative_flow", + fake_run_declarative_flow, + ) + + run_crew_module.run_crew(definition="flow.yaml", inputs='{"topic":"AI"}') + + captured = capsys.readouterr() + assert "experimental" not in captured.out.lower() + assert calls == [("flow.yaml", '{"topic":"AI"}')] + + +def test_run_crew_runs_classic_crew_project(monkeypatch, capsys): + calls = [] + + monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False) + monkeypatch.setattr( + run_crew_module, + "read_toml", + lambda: {"tool": {"crewai": {"type": "crew"}}}, + ) + monkeypatch.setattr( + run_crew_module, + "_execute_uv_script", + lambda script_name, **kwargs: calls.append((script_name, kwargs)), + ) + + run_crew_module.run_crew(trained_agents_file="trained.pkl") + + assert capsys.readouterr().out == "Running the Crew\n" + assert calls == [ + ( + "run_crew", + {"entity_type": "crew", "trained_agents_file": "trained.pkl"}, + ) + ] + + +def test_run_crew_runs_python_flow_project(monkeypatch, capsys): + calls = [] + + monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False) + monkeypatch.setattr( + run_crew_module, + "read_toml", + lambda: {"tool": {"crewai": {"type": "flow"}}}, + ) + monkeypatch.setattr( + run_crew_module, + "_execute_uv_script", + lambda script_name, **kwargs: calls.append((script_name, kwargs)), + ) + + run_crew_module.run_crew() + + assert capsys.readouterr().out == "Running the Flow\n" + assert calls == [("kickoff", {"entity_type": "flow"})] + + +def test_run_crew_rejects_filename_for_flow_project(monkeypatch): + monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False) + monkeypatch.setattr( + run_crew_module, + "read_toml", + lambda: {"tool": {"crewai": {"type": "flow"}}}, + ) + + with pytest.raises(click.UsageError) as exc_info: + run_crew_module.run_crew(trained_agents_file="trained.pkl") + + assert "--filename can only be used when running crews" in exc_info.value.message + + +def test_run_crew_runs_configured_declarative_flow_project(monkeypatch, capsys): + calls = [] + + monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False) + monkeypatch.setattr( + run_crew_module, + "read_toml", + lambda: { + "tool": { + "crewai": { + "type": "flow", + "definition": "flow.yaml", + } + } + }, + ) + monkeypatch.setattr( + "crewai_cli.run_declarative_flow.run_declarative_flow_in_project_env", + lambda definition, inputs=None: calls.append((definition, inputs)), + ) + monkeypatch.setattr( + run_crew_module, + "_execute_uv_script", + lambda *_args, **_kwargs: pytest.fail("declarative flows must not run kickoff"), + ) + + run_crew_module.run_crew() + + assert capsys.readouterr().out == "Running the Flow\n" + assert calls == [("flow.yaml", None)] diff --git a/lib/cli/tests/test_run_declarative_flow.py b/lib/cli/tests/test_run_declarative_flow.py index 9808d6b17..db1286ee7 100644 --- a/lib/cli/tests/test_run_declarative_flow.py +++ b/lib/cli/tests/test_run_declarative_flow.py @@ -83,7 +83,7 @@ def test_run_declarative_flow_in_project_env_uses_uv( assert subprocess_calls == [ ( - ["uv", "run", "crewai", "flow", "kickoff"], + ["uv", "run", "crewai", "run"], { "capture_output": False, "text": True, diff --git a/lib/crewai/tests/cli/test_run_crew.py b/lib/crewai/tests/cli/test_run_crew.py index 34074e526..c58772083 100644 --- a/lib/crewai/tests/cli/test_run_crew.py +++ b/lib/crewai/tests/cli/test_run_crew.py @@ -11,11 +11,10 @@ import pytest from crewai_cli.cli import run from crewai_cli.run_crew import ( - CrewType, + _execute_uv_script, _load_json_crew_for_tui, _missing_input_names, _prompt_for_missing_inputs, - execute_command, ) @@ -30,6 +29,8 @@ def test_run_passes_filename_to_run_crew(run_crew_mock: mock.Mock, runner: CliRu run_crew_mock.assert_called_once_with( trained_agents_file="my_custom_trained.pkl", + definition=None, + inputs=None, ) assert result.exit_code == 0 @@ -38,7 +39,11 @@ def test_run_passes_filename_to_run_crew(run_crew_mock: mock.Mock, runner: CliRu def test_run_without_filename_passes_none(run_crew_mock: mock.Mock, runner: CliRunner) -> None: result = runner.invoke(run) - run_crew_mock.assert_called_once_with(trained_agents_file=None) + run_crew_mock.assert_called_once_with( + trained_agents_file=None, + definition=None, + inputs=None, + ) assert result.exit_code == 0 @@ -50,7 +55,11 @@ def test_run_without_filename_passes_none(run_crew_mock: mock.Mock, runner: CliR def test_execute_command_sets_env_var_when_filename_provided( _build_env: mock.Mock, subprocess_run: mock.Mock ) -> None: - execute_command(CrewType.STANDARD, trained_agents_file="my_custom_trained.pkl") + _execute_uv_script( + "run_crew", + entity_type="crew", + trained_agents_file="my_custom_trained.pkl", + ) _, kwargs = subprocess_run.call_args assert kwargs["env"]["CREWAI_TRAINED_AGENTS_FILE"] == "my_custom_trained.pkl" @@ -65,7 +74,7 @@ def test_execute_command_sets_env_var_when_filename_provided( def test_execute_command_omits_env_var_when_filename_absent( _build_env: mock.Mock, subprocess_run: mock.Mock ) -> None: - execute_command(CrewType.STANDARD) + _execute_uv_script("run_crew", entity_type="crew") _, kwargs = subprocess_run.call_args assert "CREWAI_TRAINED_AGENTS_FILE" not in kwargs["env"] From 2eb4e3a236bada5432290654b8d442345eafb19e Mon Sep 17 00:00:00 2001 From: Vinicius Brasil Date: Mon, 22 Jun 2026 22:31:39 -0700 Subject: [PATCH 5/6] Improve crewai run startup UX (#6297) Remove redundant startup logs from `crewai run` and make the legacy flow command warning actionable. - Stop printing `Running the Flow` and `Running the Crew` before project execution. - Stop printing the redundant `Flow started with ID: ...` line while preserving flow lifecycle event emission. - Replace Click's generic `kickoff` deprecation warning with a clearer message that tells users to use `crewai run`. --- lib/cli/src/crewai_cli/cli.py | 6 +++++- lib/cli/src/crewai_cli/run_crew.py | 2 -- lib/cli/tests/test_cli.py | 5 ++++- lib/cli/tests/test_create_flow.py | 2 +- lib/cli/tests/test_flow_commands.py | 8 ++++++-- lib/cli/tests/test_run_crew.py | 6 +++--- lib/crewai/src/crewai/flow/runtime/__init__.py | 5 ----- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/cli/src/crewai_cli/cli.py b/lib/cli/src/crewai_cli/cli.py index b2050bc34..30eccdc61 100644 --- a/lib/cli/src/crewai_cli/cli.py +++ b/lib/cli/src/crewai_cli/cli.py @@ -787,9 +787,13 @@ def flow() -> None: """Flow related commands.""" -@flow.command(name="kickoff", deprecated=True) +@flow.command(name="kickoff") def flow_run() -> None: """Kickoff the Flow.""" + click.secho( + "The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead.", + fg="yellow", + ) run_crew(trained_agents_file=None, definition=None, inputs=None) diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index 0fa61dc7a..f9948a297 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -595,7 +595,6 @@ def _run_flow_project( if trained_agents_file is not None: raise click.UsageError("--filename can only be used when running crews") - click.echo("Running the Flow") from crewai_cli.run_declarative_flow import ( configured_project_declarative_flow, run_declarative_flow_in_project_env, @@ -611,7 +610,6 @@ def _run_flow_project( def _run_classic_crew_project( pyproject_data: dict[str, Any], trained_agents_file: str | None ) -> None: - click.echo("Running the Crew") _execute_uv_script( "run_crew", entity_type="crew", diff --git a/lib/cli/tests/test_cli.py b/lib/cli/tests/test_cli.py index 28f849ce2..acdeee7ff 100644 --- a/lib/cli/tests/test_cli.py +++ b/lib/cli/tests/test_cli.py @@ -190,7 +190,10 @@ def test_flow_kickoff_is_deprecated_and_uses_run_path(run_crew, runner): definition=None, inputs=None, ) - assert "DeprecationWarning" in result.output + assert ( + "The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead." + in result.output + ) @mock.patch("crewai_cli.create_json_crew.create_json_crew") diff --git a/lib/cli/tests/test_create_flow.py b/lib/cli/tests/test_create_flow.py index 256ace28c..21487a7a1 100644 --- a/lib/cli/tests/test_create_flow.py +++ b/lib/cli/tests/test_create_flow.py @@ -31,5 +31,5 @@ def test_create_flow_declarative_project_can_run( result = CliRunner().invoke(crewai, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"}) assert result.exit_code == 0 - assert "Running the Flow" in result.output + assert "Running the Flow" not in result.output assert "AI agents" in result.output diff --git a/lib/cli/tests/test_flow_commands.py b/lib/cli/tests/test_flow_commands.py index 3158fc9e0..00e39b6db 100644 --- a/lib/cli/tests/test_flow_commands.py +++ b/lib/cli/tests/test_flow_commands.py @@ -44,8 +44,12 @@ def test_flow_kickoff_runs_configured_declarative_definition( result = CliRunner().invoke(flow_run) assert result.exit_code == 0 - assert "DeprecationWarning" in result.output - assert "Running the Flow\nAI\n" in result.output + assert ( + "The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead." + in result.output + ) + assert "AI\n" in result.output + assert "Running the Flow" not in result.output def test_plot_flow_runs_configured_declarative_definition( diff --git a/lib/cli/tests/test_run_crew.py b/lib/cli/tests/test_run_crew.py index 40255abed..c51fc16c5 100644 --- a/lib/cli/tests/test_run_crew.py +++ b/lib/cli/tests/test_run_crew.py @@ -622,7 +622,7 @@ def test_run_crew_runs_classic_crew_project(monkeypatch, capsys): run_crew_module.run_crew(trained_agents_file="trained.pkl") - assert capsys.readouterr().out == "Running the Crew\n" + assert capsys.readouterr().out == "" assert calls == [ ( "run_crew", @@ -648,7 +648,7 @@ def test_run_crew_runs_python_flow_project(monkeypatch, capsys): run_crew_module.run_crew() - assert capsys.readouterr().out == "Running the Flow\n" + assert capsys.readouterr().out == "" assert calls == [("kickoff", {"entity_type": "flow"})] @@ -694,5 +694,5 @@ def test_run_crew_runs_configured_declarative_flow_project(monkeypatch, capsys): run_crew_module.run_crew() - assert capsys.readouterr().out == "Running the Flow\n" + assert capsys.readouterr().out == "" assert calls == [("flow.yaml", None)] diff --git a/lib/crewai/src/crewai/flow/runtime/__init__.py b/lib/crewai/src/crewai/flow/runtime/__init__.py index fa465cb71..c47526a78 100644 --- a/lib/crewai/src/crewai/flow/runtime/__init__.py +++ b/lib/crewai/src/crewai/flow/runtime/__init__.py @@ -2455,11 +2455,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): object.__setattr__( self, "_deferred_flow_started_event_id", started_event.event_id ) - if not self.suppress_flow_events: - self._log_flow_event( - f"Flow started with ID: {self.flow_id}", color="bold magenta" - ) - # After FlowStarted: env events must not pre-empt trace batch init # with implicit "crew" execution_type. get_env_context() From 793539173d1df02735f711179441cee026f03ef4 Mon Sep 17 00:00:00 2001 From: Lucas Gomide Date: Tue, 23 Jun 2026 15:51:22 -0300 Subject: [PATCH 6/6] fix: pin opentelemetry to ~=1.42.0 (#6292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous `~=1.34.0` pin kept us on the unmaintained 1.34 line — last patched as `1.34.1` in June 2025, eight minor releases behind upstream — and caused `_create_exp_backoff_generator` `ImportError` crashes in factory deployments where the OpenTelemetry Operator's injected init container shadows `opentelemetry.exporter.otlp.proto.common._internal` with >=1.35 while our `opentelemetry-exporter-otlp-proto-grpc==1.34.1` still imports the removed private symbol. Pinning to `~=1.42.0` tracks the current upstream stable line; the resolver now lands on 1.42.1 and our public OTel trace API usage is unaffected. --- lib/crewai-core/pyproject.toml | 6 ++-- lib/crewai/pyproject.toml | 6 ++-- uv.lock | 63 +++++++++++++++++----------------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/lib/crewai-core/pyproject.toml b/lib/crewai-core/pyproject.toml index e641548e4..405dd4d46 100644 --- a/lib/crewai-core/pyproject.toml +++ b/lib/crewai-core/pyproject.toml @@ -16,9 +16,9 @@ dependencies = [ "pyjwt>=2.13.0,<3", "pydantic>=2.11.9,<2.13", "rich>=13.7.1", - "opentelemetry-api~=1.34.0", - "opentelemetry-sdk~=1.34.0", - "opentelemetry-exporter-otlp-proto-http~=1.34.0", + "opentelemetry-api~=1.42.0", + "opentelemetry-sdk~=1.42.0", + "opentelemetry-exporter-otlp-proto-http~=1.42.0", "tomli~=2.0.2", ] diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 95a41f97b..a90d6d7a1 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -18,9 +18,9 @@ dependencies = [ "pdfplumber~=0.11.4", "regex~=2026.1.15", # Telemetry and Monitoring - "opentelemetry-api~=1.34.0", - "opentelemetry-sdk~=1.34.0", - "opentelemetry-exporter-otlp-proto-http~=1.34.0", + "opentelemetry-api~=1.42.0", + "opentelemetry-sdk~=1.42.0", + "opentelemetry-exporter-otlp-proto-http~=1.42.0", # Data Handling "chromadb~=1.1.0", "tokenizers>=0.21,<1", diff --git a/uv.lock b/uv.lock index b623014c8..93e0f8be8 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-06-20T16:46:21.117658Z" exclude-newer-span = "P3D" [options.exclude-newer-package] @@ -1452,9 +1452,9 @@ requires-dist = [ { name = "openai", specifier = ">=2.30.0,<3" }, { name = "openpyxl", specifier = "~=3.1.5" }, { name = "openpyxl", marker = "extra == 'openpyxl'", specifier = "~=3.1.5" }, - { name = "opentelemetry-api", specifier = "~=1.34.0" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" }, - { name = "opentelemetry-sdk", specifier = "~=1.34.0" }, + { name = "opentelemetry-api", specifier = "~=1.42.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.42.0" }, + { name = "opentelemetry-sdk", specifier = "~=1.42.0" }, { name = "pandas", marker = "extra == 'pandas'", specifier = "~=2.2.3" }, { name = "pdfplumber", specifier = "~=0.11.4" }, { name = "portalocker", specifier = "~=2.7.0" }, @@ -1539,9 +1539,9 @@ requires-dist = [ { name = "appdirs", specifier = "~=1.4.4" }, { name = "cryptography", specifier = ">=42.0" }, { name = "httpx", specifier = "~=0.28.1" }, - { name = "opentelemetry-api", specifier = "~=1.34.0" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" }, - { name = "opentelemetry-sdk", specifier = "~=1.34.0" }, + { name = "opentelemetry-api", specifier = "~=1.42.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.42.0" }, + { name = "opentelemetry-sdk", specifier = "~=1.42.0" }, { name = "packaging", specifier = ">=23.0" }, { name = "portalocker", specifier = "~=2.7.0" }, { name = "pydantic", specifier = ">=2.11.9,<2.13" }, @@ -5585,45 +5585,44 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/ba/786b4de7e39d88043622d901b92c4485835f43e0be76c2824d2687911bc2/opentelemetry_exporter_otlp-1.34.1.tar.gz", hash = "sha256:71c9ad342d665d9e4235898d205db17c5764cd7a69acb8a5dcd6d5e04c4c9988", size = 6173, upload-time = "2025-06-10T08:55:21.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/94/8637919a5d01f81dacf510234bc0110b944f4687a6e96b0a02adf2f6bdce/opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9", size = 6086, upload-time = "2026-05-21T16:32:51.963Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c1/259b8d8391c968e8f005d8a0ccefcb41aeef64cf55905cd0c0db4e22aaee/opentelemetry_exporter_otlp-1.34.1-py3-none-any.whl", hash = "sha256:f4a453e9cde7f6362fd4a090d8acf7881d1dc585540c7b65cbd63e36644238d4", size = 7040, upload-time = "2025-06-10T08:54:59.655Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4d/c26080295a36fd22e201fefd7cb9c22cd203189b1af8cd73b158382b7ad8/opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee", size = 6733, upload-time = "2026-05-21T16:32:31.261Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/f0/ff235936ee40db93360233b62da932d4fd9e8d103cd090c6bcb9afaf5f01/opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b", size = 20817, upload-time = "2025-06-10T08:55:22.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/e8/8b292a11cc8d8d87ec0c4089ae21b6a58af49ca2e51fa916435bc922fdc7/opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87", size = 18834, upload-time = "2025-06-10T08:55:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -5634,14 +5633,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/f7/bb63837a3edb9ca857aaf5760796874e7cecddc88a2571b0992865a48fb6/opentelemetry_exporter_otlp_proto_grpc-1.34.1.tar.gz", hash = "sha256:7c841b90caa3aafcfc4fee58487a6c71743c34c6dc1787089d8b0578bbd794dd", size = 22566, upload-time = "2025-06-10T08:55:23.214Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/42/0a4dd47e7ef54edf670c81fc06a83d68ea42727b82126a1df9dd0477695d/opentelemetry_exporter_otlp_proto_grpc-1.34.1-py3-none-any.whl", hash = "sha256:04bb8b732b02295be79f8a86a4ad28fae3d4ddb07307a98c7aa6f331de18cca6", size = 18615, upload-time = "2025-06-10T08:55:02.214Z" }, + { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -5652,48 +5651,48 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/8f/954bc725961cbe425a749d55c0ba1df46832a5999eae764d1a7349ac1c29/opentelemetry_exporter_otlp_proto_http-1.34.1.tar.gz", hash = "sha256:aaac36fdce46a8191e604dcf632e1f9380c7d5b356b27b3e0edb5610d9be28ad", size = 15351, upload-time = "2025-06-10T08:55:24.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/54/b05251c04e30c1ac70cf4a7c5653c085dfcf2c8b98af71661d6a252adc39/opentelemetry_exporter_otlp_proto_http-1.34.1-py3-none-any.whl", hash = "sha256:5251f00ca85872ce50d871f6d3cc89fe203b94c3c14c964bbdc3883366c705d8", size = 17744, upload-time = "2025-06-10T08:55:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/b3/c3158dd012463bb7c0eb7304a85a6f63baeeb5b4c93a53845cf89f848c7e/opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e", size = 34344, upload-time = "2025-06-10T08:55:32.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/ab/4591bfa54e946350ce8b3f28e5c658fe9785e7cd11e9c11b1671a867822b/opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2", size = 55692, upload-time = "2025-06-10T08:55:14.904Z" }, + { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.34.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/41/fe20f9036433da8e0fcef568984da4c1d1c771fa072ecd1a4d98779dccdd/opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d", size = 159441, upload-time = "2025-06-10T08:55:33.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/1b/def4fe6aa73f483cabf4c748f4c25070d5f7604dcc8b52e962983491b29e/opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e", size = 118477, upload-time = "2025-06-10T08:55:16.02Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.55b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/f0/f33458486da911f47c4aa6db9bda308bb80f3236c111bf848bd870c16b16/opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3", size = 119829, upload-time = "2025-06-10T08:55:33.881Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, ] [[package]]