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 b153885f3..30eccdc61 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_flow_definition(*args: Any, **kwargs: Any) -> Any: - from crewai_cli.run_flow_definition import ( - run_flow_definition as _run_flow_definition, - ) - - return _run_flow_definition(*args, **kwargs) - - def run_crew(*args: Any, **kwargs: Any) -> Any: from crewai_cli.run_crew import run_crew as _run_crew @@ -155,12 +147,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 +192,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 +205,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") @@ -468,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." @@ -512,16 +512,13 @@ 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="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, @@ -531,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_flow_definition(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() @@ -795,10 +790,11 @@ def flow() -> None: @flow.command(name="kickoff") def flow_run() -> None: """Kickoff the Flow.""" - from crewai_cli.kickoff_flow import kickoff_flow - - click.echo("Running the Flow") - kickoff_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) @flow.command(name="plot") 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 deleted file mode 100644 index b5bc0d81e..000000000 --- a/lib/cli/src/crewai_cli/kickoff_flow.py +++ /dev/null @@ -1,23 +0,0 @@ -import subprocess - -import click - - -def kickoff_flow() -> None: - """ - Kickoff the flow by running a command in the UV environment. - """ - command = ["uv", "run", "kickoff"] - - try: - result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603 - - if result.stderr: - click.echo(result.stderr, err=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 Exception as e: - click.echo(f"An unexpected error occurred: {e}", err=True) 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_crew.py b/lib/cli/src/crewai_cli/run_crew.py index cbf2445d1..f9948a297 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,88 @@ 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") + + 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: + _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 +635,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 +660,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 new file mode 100644 index 000000000..c6ff668c4 --- /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", "run"]) + + +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 definition 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 definition 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 definition 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/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/.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..697c0aa32 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/declarative_flow/README.md @@ -0,0 +1,17 @@ +# {{name}} Flow + +This project defines a declarative CrewAI Flow in `src/{{folder_name}}/flow.yaml`. + +## Install + +```bash +crewai install +``` + +## Run + +```bash +crewai run +``` + +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/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..acdeee7ff 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,38 +127,75 @@ 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_flow_definition") -def test_run_with_definition_uses_definition_runner(run_flow_definition, 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_flow_definition.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_flow_definition") -def test_run_rejects_inputs_without_definition(run_flow_definition, 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_flow_definition.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 ( + "The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead." + 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"}) @@ -166,6 +204,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..21487a7a1 --- /dev/null +++ b/lib/cli/tests/test_create_flow.py @@ -0,0 +1,35 @@ +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, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"}) + + assert result.exit_code == 0 + 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 new file mode 100644 index 000000000..00e39b6db --- /dev/null +++ b/lib/cli/tests/test_flow_commands.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from pathlib import Path +import subprocess + +import pytest +from click.testing import CliRunner + +from crewai_cli.cli import flow_run +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_flow_kickoff_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") + + result = CliRunner().invoke(flow_run) + + assert result.exit_code == 0 + 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( + 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() + + +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, +) -> None: + subprocess_calls = [] + + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + subprocess, + "run", + lambda command, **kwargs: subprocess_calls.append((command, kwargs)), + ) + + plot_flow_module.plot_flow() + + assert subprocess_calls == [ + ( + ["uv", "run", "plot"], + {"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_crew.py b/lib/cli/tests/test_run_crew.py index ebb48d72b..c51fc16c5 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 == "" + 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 == "" + 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 == "" + 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 new file mode 100644 index 000000000..db1286ee7 --- /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", "run"], + { + "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-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/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/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..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): @@ -870,14 +921,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 dbb547e19..5253d5a33 100644 --- a/lib/crewai/src/crewai/flow/runtime/__init__.py +++ b/lib/crewai/src/crewai/flow/runtime/__init__.py @@ -2472,11 +2472,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() @@ -3043,6 +3038,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 @@ -3080,6 +3076,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] = ( @@ -3100,7 +3101,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/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/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"] 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 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"}}, }, } }, 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() 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]]