mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
* feat(cli): run declarative flows on the TUI with a headless terminal fallback Declarative flows now run on the CrewRunApp TUI when interactive, matching declarative crews and conversational flows. Headless contexts — CREWAI_DMN (deploy), piped output, CI, any non-TTY — fall back to the direct-terminal kickoff, gated by is_interactive() (folds in the CREWAI_DMN check and requires a real TTY). The TUI shows per-method progress: a new STEPS panel driven by flow method events (FlowStarted / MethodExecutionStarted/Finished/Failed), each labeled with its declarative call type (crew/agent/expression/…) read from the flow definition. Crews/agents inside a method keep streaming in the main panel via the existing crew/task/LLM handlers. - crew_run_tui.py: _run_flow_worker (flow.kickoff in a thread worker; reuses _on_crew_done/_on_crew_failed + _stringify_output), _is_flow_run gate so crew rendering is byte-identical, flow-event subscriptions building _flow_steps, and the STEPS sidebar + flow-aware header. - run_declarative_flow.py: is_interactive() branch → _run_declarative_flow_tui (EventListener, method-type map from flow._definition, crew-parity exit codes and deploy chaining) or the existing terminal path. Deviation from the approved plan: gate on is_interactive() rather than is_dmn_mode_enabled() alone, so non-TTY runs (CI/pipes/CliRunner) never launch a TUI — this also keeps existing headless flow tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh * fix(cli): force flow events on for the TUI so STEPS renders under suppress_flow_events Review follow-up: the STEPS panel and header are driven by flow method events (FlowStarted / MethodExecution*), but the declarative runtime skips emitting those when the flow declared config.suppress_flow_events. Interactive TUI runs would then keep STEPS on "waiting…" and the header on "Starting flow…" while nested crews still execute. _run_declarative_flow_tui now forces flow.suppress_flow_events = False for the interactive run (mirroring how the conversational path mutates the flow for the TUI). The headless/terminal path never reaches this and keeps the flow's declared setting. Regression test: test_run_declarative_flow_tui_enables_flow_events. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh * fix(cli): clear flow header's current method when a method ends Review follow-up: the flow header keys off _current_method, which was set on MethodExecutionStarted but never cleared on Finished/Failed. Between steps (or after a failed method before kickoff exits) the header kept spinning the old method name while the STEPS sidebar already showed it done/failed. _clear_current_method now drops the header's active method when it ends, falling back to another still-active step (methods can overlap) or none. The header's idle fallback shows "Working…" once a step has run and "Starting flow…" only before the first method. Tests: test_current_method_clears_and_falls_back_across_overlap, plus a _current_method assertion in test_flow_method_events_build_steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh * fix: suppress flow console panels in TUI mode; clear header agent on method change Two review follow-ups: 1) Method panels break Textual TUI (Cursor): forcing suppress_flow_events off so the STEPS panel receives events also un-gated the EventListener's Rich flow/method panels (ConsoleFormatter.print_panel prints is_flow=True panels regardless of verbose), which interleave with Textual and corrupt the TUI. print_panel now skips is_flow panels when is_tui_mode() is set (the same context the TUI worker already establishes and the tracing listeners already honor). Non-TUI/headless flow runs are unaffected. Test: test_console_formatter_tui_mode. 2) Flow header showed a stale agent (CodeRabbit): _current_agent persisted across methods. It's now cleared when a method starts and when the active method changes, so the header never shows the previous method's agent until a new agent event arrives. Test: test_flow_method_transitions_clear_current_agent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh * fix(cli): keep flow name over nested crews; show paused flow methods Two review follow-ups on the flow TUI: 1) Crew kickoff renamed the flow (Cursor): CrewKickoffStartedEvent overwrote _crew_name / the app title with a nested `call: crew` step's crew name, so the post-run summary could be labeled with a child crew. The rename is now gated on `not _is_flow_run`, preserving the flow's name; crew runs still adopt the crew name. Tests: test_crew_kickoff_does_not_rename_flow_run, test_crew_kickoff_renames_in_crew_mode. 2) Paused methods showed active (Cursor): the TUI didn't handle MethodExecutionPausedEvent, so a @human_feedback pause left the STEPS spinner running (flow status panels are suppressed in TUI mode). It now marks the step "paused" (⏸, teal) and the header shows "waiting for feedback" instead of a spinner. Test: test_method_paused_marks_step_paused. Note: interactively *providing* human feedback from the flow TUI is a separate follow-up; this only makes the pause visible instead of a silent stuck spinner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh * fix(cli): run human-feedback declarative flows on the terminal, not the TUI Two review follow-ups, both rooted in @human_feedback methods: - Paused flow marked complete (Cursor): async human feedback makes kickoff RETURN a HumanFeedbackPending marker (not raise), which _run_flow_worker would stringify and report as a successful completion with exit 0. - Sync feedback breaks TUI (Cursor): default (sync) @human_feedback collects input via the flow runtime's Rich console.print + blocking input(), which interleaves with Textual and leaves the user unable to review output or submit feedback. run_declarative_flow now routes any flow whose declarative definition declares human feedback (_flow_uses_human_feedback) to the terminal path, where blocking input and Rich prompts work natively — regardless of interactivity. Non-feedback flows still get the TUI. Tests: test_flow_uses_human_feedback_detection, test_human_feedback_flow_uses_terminal_even_when_interactive. Fully interactive human feedback inside the TUI remains a separate follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh * refactor(cli): address review — Flow typing, debug logging, flow-vs-crew naming Review follow-ups from @lucasgomide: - Type flow helpers as Flow[Any] (via TYPE_CHECKING import) instead of Any and drop the defensive getattr chains — _definition is a typed PrivateAttr and name/suppress_flow_events are typed fields, so attribute access is safe. - Replace the silent `except Exception: pass` blocks with logger.debug(..., exc_info=True) so unexpected failures are diagnosable in the field (_flow_method_types, _flow_uses_human_feedback, suppress_flow_events toggle). - Flow-vs-crew naming: the flow worker now uses group="flow" (was the misleading "crew"), and the shared completion/failure handlers report the run with an entity-aware noun ("flow" vs "crew") via _run_noun. Deferred (separate PR): the os._exit(130) hard-kill on user quit is kept as-is to match the existing crew convention (run_crew._run_json_crew). Tests: test_flow_done_uses_flow_wording_for_unfinished_tool; existing crew wording tests unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
509 lines
17 KiB
Python
509 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import click
|
|
from crewai_core.project import ProjectDefinitionError, configured_project_definition
|
|
from pydantic import ValidationError
|
|
|
|
from crewai_cli.input_prompt import (
|
|
closest_name,
|
|
is_interactive,
|
|
parse_inputs_json,
|
|
prompt_for_inputs,
|
|
)
|
|
from crewai_cli.utils import build_env_with_all_tool_credentials
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from crewai.flow.flow import Flow
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run_declarative_flow_in_project_env(
|
|
definition: str | Path, 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
|
|
|
|
# Re-run inside the project env (so the flow loads with the project's deps).
|
|
# The configured definition is re-resolved there; forward --inputs so the
|
|
# in-env run kicks off with the same values instead of losing them.
|
|
command = ["uv", "run", "crewai", "run"]
|
|
if inputs is not None:
|
|
command += ["--inputs", inputs]
|
|
_execute_declarative_flow_command(command)
|
|
|
|
|
|
def plot_declarative_flow_in_project_env(definition: str | Path) -> 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 | Path, inputs: str | None = None) -> None:
|
|
"""Run a declarative flow from a definition path.
|
|
|
|
Inputs come from one place: the flow's own state schema. Any ``--inputs``
|
|
JSON is layered on top as an override, missing required fields are prompted
|
|
for interactively, and everything is validated against the schema before
|
|
kickoff — so a bare ``crewai run`` on a configured flow just works.
|
|
"""
|
|
# Load the project's .env before kickoff, mirroring the JSON-crew path
|
|
# (run_crew._run_json_crew) so flow projects pick up API keys/config the
|
|
# same way regardless of where crewai is installed.
|
|
from dotenv import load_dotenv
|
|
|
|
env_file = Path.cwd() / ".env"
|
|
if env_file.exists():
|
|
load_dotenv(env_file, override=True)
|
|
|
|
provided = parse_inputs_json(inputs) or {}
|
|
|
|
flow = load_declarative_flow(definition)
|
|
resolved_inputs = _resolve_flow_inputs(flow, provided)
|
|
|
|
# The TUI is the interactive default. Headless contexts run directly on the
|
|
# terminal: deploy/CREWAI_DMN, piped output, CI — anything without an
|
|
# interactive TTY. is_interactive() already folds in the CREWAI_DMN check.
|
|
# Human-feedback flows also run on the terminal: their methods collect input
|
|
# via the flow runtime's blocking input()/Rich prompts (and async feedback
|
|
# returns a pending marker rather than completing), neither of which the
|
|
# Textual TUI can handle correctly.
|
|
if is_interactive() and not _flow_uses_human_feedback(flow):
|
|
_run_declarative_flow_tui(flow, resolved_inputs or None)
|
|
return
|
|
|
|
try:
|
|
result = flow.kickoff(inputs=resolved_inputs or None)
|
|
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 _run_declarative_flow_tui(
|
|
flow: Flow[Any], resolved_inputs: dict[str, Any] | None
|
|
) -> Any:
|
|
"""Run a declarative flow on the CrewAI TUI (the interactive default).
|
|
|
|
Mirrors the declarative-crew TUI contract (``run_crew._run_json_crew``):
|
|
a failed flow exits non-zero, a user quit ends the process so in-flight LLM
|
|
work stops, and choosing Deploy chains into the deploy command.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
from crewai.events.event_listener import EventListener
|
|
|
|
from crewai_cli.crew_run_tui import CrewRunApp
|
|
|
|
# The flow runtime (unlike a Crew constructor) doesn't create the event
|
|
# listener, and the TUI's trace/telemetry features depend on it.
|
|
EventListener()
|
|
|
|
# The STEPS panel and header are driven by flow method events. A flow may
|
|
# declare ``config.suppress_flow_events`` (a headless/production
|
|
# optimization) which would leave STEPS stuck on "waiting…" here — so force
|
|
# emission on for the interactive TUI run. The headless path never reaches
|
|
# this and keeps the flow's declared setting.
|
|
try:
|
|
flow.suppress_flow_events = False
|
|
except Exception:
|
|
logger.debug(
|
|
"Could not disable suppress_flow_events for the flow TUI", exc_info=True
|
|
)
|
|
|
|
app = CrewRunApp(crew_name=flow.name or type(flow).__name__)
|
|
app._flow = flow
|
|
app._flow_inputs = resolved_inputs
|
|
app._flow_method_types = _flow_method_types(flow)
|
|
|
|
app.run()
|
|
|
|
_print_flow_post_tui_summary(app)
|
|
|
|
if app._status == "failed":
|
|
raise SystemExit(1)
|
|
|
|
if app._status not in ("completed", "failed"):
|
|
# User quit mid-run. kickoff runs in a thread worker that cannot be
|
|
# force-cancelled, so end the process to stop in-flight LLM and tool
|
|
# work instead of letting it burn tokens in the background.
|
|
click.secho("\n Run cancelled.", fg="yellow")
|
|
sys.stdout.flush()
|
|
os._exit(130)
|
|
|
|
if getattr(app, "_want_deploy", False):
|
|
from crewai_cli.run_crew import _chain_deploy
|
|
|
|
_chain_deploy()
|
|
|
|
return app._crew_result
|
|
|
|
|
|
def _flow_uses_human_feedback(flow: Flow[Any]) -> bool:
|
|
"""True if any declarative method declares ``@human_feedback``.
|
|
|
|
Such flows need the flow runtime's interactive stdin / Rich prompts, which
|
|
don't compose with Textual — so they run on the terminal, not the TUI.
|
|
"""
|
|
try:
|
|
return any(
|
|
method.human_feedback is not None
|
|
for method in flow._definition.methods.values()
|
|
)
|
|
except Exception:
|
|
logger.debug("Could not inspect flow for human feedback", exc_info=True)
|
|
return False
|
|
|
|
|
|
def _flow_method_types(flow: Flow[Any]) -> dict[str, str]:
|
|
"""Map each declarative method name to its ``call`` type (crew/agent/…).
|
|
|
|
Best-effort: the STEPS panel shows this as a dim label. Method events don't
|
|
carry the call type, so it's read from the flow definition up front.
|
|
"""
|
|
method_types: dict[str, str] = {}
|
|
try:
|
|
for name, method_definition in flow._definition.methods.items():
|
|
method_types[name] = method_definition.do.call
|
|
except Exception:
|
|
logger.debug("Could not derive flow method types", exc_info=True)
|
|
return method_types
|
|
|
|
|
|
def _print_flow_post_tui_summary(app: Any) -> None:
|
|
"""Print a compact result panel after the flow TUI exits."""
|
|
import time
|
|
|
|
from rich.console import Console
|
|
from rich.markdown import Markdown
|
|
from rich.padding import Padding
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
|
|
console = Console()
|
|
elapsed = (app._elapsed_frozen or (time.time() - app._start_time)) or 0.0
|
|
|
|
out_tokens = app._output_tokens + app._live_out_tokens
|
|
token_parts = []
|
|
if app._input_tokens:
|
|
token_parts.append(f"↑{app._input_tokens:,}")
|
|
if out_tokens:
|
|
token_parts.append(f"↓{out_tokens:,}")
|
|
token_str = " ".join(token_parts)
|
|
if token_str:
|
|
token_str += " tokens"
|
|
|
|
crewai_red = "#FF5A50"
|
|
crewai_teal = "#1F7982"
|
|
|
|
if app._status == "completed":
|
|
summary = Text()
|
|
summary.append(" ✔ Flow complete", style=f"bold {crewai_teal}")
|
|
summary.append(f" in {elapsed:.1f}s", style="dim")
|
|
if token_str:
|
|
summary.append(f" {token_str}", style="dim")
|
|
console.print(
|
|
Panel(
|
|
summary,
|
|
title=f" {app._crew_name} ",
|
|
title_align="left",
|
|
border_style=crewai_teal,
|
|
padding=(0, 1),
|
|
)
|
|
)
|
|
if app._final_output:
|
|
console.print()
|
|
console.print(Text(" Final Result", style=f"bold {crewai_teal}"))
|
|
console.print()
|
|
console.print(Padding(Markdown(app._final_output), (0, 2)))
|
|
elif app._status == "failed":
|
|
content = Text()
|
|
content.append(" ✘ Failed", style=f"bold {crewai_red}")
|
|
content.append(f" after {elapsed:.1f}s\n", style="dim")
|
|
if app._error:
|
|
content.append(f"\n {app._error}\n", style=crewai_red)
|
|
console.print(
|
|
Panel(
|
|
content,
|
|
title=f" {app._crew_name} ",
|
|
title_align="left",
|
|
border_style=crewai_red,
|
|
padding=(0, 1),
|
|
)
|
|
)
|
|
|
|
|
|
def _resolve_flow_inputs(flow: Any, provided: dict[str, Any]) -> dict[str, Any]:
|
|
"""Resolve kickoff inputs from the flow's state schema.
|
|
|
|
Warns on unknown keys, prompts for missing required fields (unless
|
|
non-interactive), and validates types before kickoff. Exits with a pointed
|
|
message when a required input is still missing or an input is invalid.
|
|
"""
|
|
schema = _flow_state_schema(flow)
|
|
if schema is None:
|
|
# dict / unschematized state — nothing to derive; pass inputs through.
|
|
return dict(provided)
|
|
|
|
properties = {
|
|
name: spec
|
|
for name, spec in (schema.get("properties") or {}).items()
|
|
if name != "id"
|
|
}
|
|
state_model = type(flow.state)
|
|
defaults = _flow_state_defaults(flow)
|
|
|
|
# ``id`` signals a persistence restore: kickoff hydrates the full state from
|
|
# storage, so required fields may come from the restored state rather than
|
|
# --inputs. We still filter the rest of the payload below, but skip the
|
|
# required-field prompt and pre-kickoff validation, which would otherwise
|
|
# fail on fields the resume will supply.
|
|
restoring = "id" in provided
|
|
|
|
# Unknown keys are almost always typos — warn and drop them (they'd fail
|
|
# structured-state validation at kickoff anyway). ``id`` is a reserved
|
|
# kickoff key rather than a state field, so forward it untouched.
|
|
collected: dict[str, Any] = {}
|
|
for key, value in provided.items():
|
|
if key == "id":
|
|
collected["id"] = value
|
|
continue
|
|
if key in properties:
|
|
collected[key] = value
|
|
continue
|
|
suggestion = closest_name(key, properties)
|
|
hint = f" Did you mean '{suggestion}'?" if suggestion else ""
|
|
click.secho(
|
|
f" Ignoring unknown input '{key}' — not in the flow's state schema.{hint}",
|
|
fg="yellow",
|
|
err=True,
|
|
)
|
|
|
|
if restoring:
|
|
return collected
|
|
|
|
missing = _missing_required(state_model, {**defaults, **collected})
|
|
if missing and _is_interactive():
|
|
collected.update(
|
|
prompt_for_inputs(
|
|
missing,
|
|
title="Flow inputs",
|
|
subtitle="This flow needs the following to run.",
|
|
describe=lambda name: (properties.get(name) or {}).get("description"),
|
|
coerce=lambda name, raw: _coerce_input(raw, properties.get(name) or {}),
|
|
)
|
|
)
|
|
missing = _missing_required(state_model, {**defaults, **collected})
|
|
|
|
if missing:
|
|
for name in missing:
|
|
description = (properties.get(name) or {}).get("description")
|
|
suffix = f" — {description}" if description else ""
|
|
click.secho(
|
|
f" Missing required input '{name}'{suffix}", fg="red", err=True
|
|
)
|
|
raise SystemExit(1)
|
|
|
|
_validate_flow_inputs(state_model, {**defaults, **collected})
|
|
return collected
|
|
|
|
|
|
def _is_interactive() -> bool:
|
|
"""Prompt only in an interactive terminal, never in non-interactive mode."""
|
|
return is_interactive()
|
|
|
|
|
|
def _flow_state_schema(flow: Any) -> dict[str, Any] | None:
|
|
"""Return the flow's state JSON schema, or ``None`` for dict/plain state."""
|
|
state = getattr(flow, "state", None)
|
|
if state is None or isinstance(state, dict):
|
|
return None
|
|
model_json_schema = getattr(type(state), "model_json_schema", None)
|
|
if not callable(model_json_schema):
|
|
return None
|
|
try:
|
|
schema = model_json_schema()
|
|
except Exception:
|
|
return None
|
|
return schema if isinstance(schema, dict) else None
|
|
|
|
|
|
def _flow_state_defaults(flow: Any) -> dict[str, Any]:
|
|
"""Declared state defaults (``state.default``) from the flow definition."""
|
|
state_definition = getattr(getattr(flow, "_definition", None), "state", None)
|
|
default = getattr(state_definition, "default", None)
|
|
return dict(default) if isinstance(default, dict) else {}
|
|
|
|
|
|
def _missing_required(state_model: Any, values: dict[str, Any]) -> list[str]:
|
|
"""Required state fields not satisfied by ``values`` (defaults + inputs)."""
|
|
try:
|
|
state_model.model_validate(values)
|
|
except ValidationError as exc:
|
|
return [
|
|
str(error["loc"][0])
|
|
for error in exc.errors()
|
|
if error.get("type") == "missing" and error.get("loc")
|
|
]
|
|
return []
|
|
|
|
|
|
def _validate_flow_inputs(state_model: Any, values: dict[str, Any]) -> None:
|
|
"""Validate inputs against the state schema; exit with pointed type errors."""
|
|
try:
|
|
state_model.model_validate(values)
|
|
except ValidationError as exc:
|
|
for error in exc.errors():
|
|
location = ".".join(str(part) for part in error.get("loc", ()))
|
|
click.secho(
|
|
f" Invalid input '{location}': {error.get('msg')}", fg="red", err=True
|
|
)
|
|
raise SystemExit(1) from exc
|
|
|
|
|
|
def _coerce_input(raw: str, spec: dict[str, Any]) -> Any:
|
|
"""Best-effort coerce a prompted string to the field's JSON-schema type."""
|
|
field_type = spec.get("type")
|
|
if field_type == "integer":
|
|
try:
|
|
return int(raw)
|
|
except ValueError:
|
|
return raw
|
|
if field_type == "number":
|
|
try:
|
|
return float(raw)
|
|
except ValueError:
|
|
return raw
|
|
if field_type == "boolean":
|
|
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
return raw
|
|
|
|
|
|
def plot_declarative_flow(definition: str | Path) -> 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 | Path) -> Any:
|
|
"""Load a declarative Flow instance from a definition path."""
|
|
try:
|
|
from crewai.flow.flow import Flow
|
|
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()
|
|
try:
|
|
if not definition_path.is_file():
|
|
if definition_path.exists():
|
|
click.echo(
|
|
f"Invalid --definition path: {definition} is not a file.",
|
|
err=True,
|
|
)
|
|
raise SystemExit(1)
|
|
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
|
|
|
|
try:
|
|
return Flow.from_declaration(path=definition_path)
|
|
except (OSError, UnicodeError, ValueError, ValidationError) as exc:
|
|
click.echo(
|
|
f"Unable to read --definition path {definition_path}: {exc}",
|
|
err=True,
|
|
)
|
|
raise SystemExit(1) from exc
|
|
|
|
|
|
def configured_project_declarative_flow(
|
|
pyproject_data: dict[str, Any] | None = None,
|
|
project_root: Path | None = None,
|
|
) -> Path | None:
|
|
"""Return the configured declarative flow source for flow projects."""
|
|
root = project_root or Path.cwd()
|
|
if pyproject_data is None and not (root / "pyproject.toml").is_file():
|
|
return None
|
|
|
|
try:
|
|
return configured_project_definition(
|
|
"flow",
|
|
pyproject_data=pyproject_data,
|
|
project_root=root,
|
|
)
|
|
except ProjectDefinitionError as exc:
|
|
raise click.UsageError(str(exc)) from exc
|
|
|
|
|
|
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 _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)
|