mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-01 21:28:10 +00:00
JSON first crews (#6131)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
* feat(cli): introduce JSON crew project support and TUI enhancements - Added support for creating and running JSON-defined crew projects, allowing users to scaffold projects with a new `create_json_crew.py` file. - Implemented a full-screen Textual TUI for crew execution in `crew_run_tui.py`, enhancing user interaction with a two-column layout. - Updated `run_crew.py` to prioritize JSON crew projects and added daemon mode for running without TUI. - Introduced interactive pickers in `tui_picker.py` for improved CLI prompts. - Enhanced validation for JSON crew files in `validate.py` to ensure proper structure and agent definitions. - Updated `.gitignore` to exclude demo and crewai directories. * feat: update LLM model references to gpt-5.4-mini - Changed default LLM model from gpt-4o-mini to gpt-5.4-mini across various files, including CLI options, JSON crew configurations, and agent definitions. - Enhanced benchmark and human feedback functionalities to utilize the new model. - Improved user interface elements in the TUI for better interaction and feedback during execution. - Added support for new skills directory in JSON crew project creation. * feat(benchmark): add crew-level benchmarking functionality - Introduced a new `benchmark` command in the CLI for crew-level benchmarking, allowing users to specify agents, models, and timeout settings. - Implemented `CrewBenchmarkCase` to handle crew-level benchmark cases with inputs and criteria. - Enhanced the benchmark runner to support progress tracking and detailed reporting of results for multiple models. - Added tests for loading crew benchmark cases and validating their structure. - Updated existing benchmark functions to accommodate the new crew-level execution model. * feat(cli): enhance JSON crew project functionality and TUI improvements - Added optional agent-level guardrails and advanced options in JSON crew configurations to improve output validation and flexibility. - Updated the TUI to better handle plan step statuses, including visual indicators for task completion and failure. - Introduced methods for parsing and managing step observation events, ensuring accurate updates to task statuses during execution. - Enhanced validation for JSON crew projects, ensuring proper structure and error handling for agent and task definitions. - Added comprehensive tests for new features and validation logic, ensuring robustness in JSON crew project handling. * refactor(cli): streamline JSON crew project handling and improve validation - Refactored JSON crew project loading and validation logic to enhance clarity and maintainability. - Introduced utility functions for finding JSON crew files, improving code reuse across modules. - Removed deprecated benchmark functionality and associated tests to simplify the codebase. - Updated CLI commands to utilize the new JSON project structure, ensuring compatibility with recent changes. - Enhanced test coverage for JSON crew project features, ensuring robust validation and error handling. * feat(cli): enhance activity log navigation and focus management - Added functionality to focus on the activity log when navigating through log entries. - Implemented refresh logic for the log panel to ensure updates are displayed correctly during navigation. - Improved keyboard navigation for log entries, allowing users to expand and scroll through logs seamlessly. - Added tests to verify the correct behavior of log navigation and focus management in the TUI. * feat(cli): enhance JSON crew project interaction and input handling - Introduced a new function to enable prompt line editing for better user experience during input prompts. - Updated the JSON crew project wizards to show interpolation hints for dynamic values, improving user guidance. - Enhanced the handling of missing input placeholders by prompting users for required values during crew setup. - Refactored the crew run logic to ensure proper loading and preparation of JSON-defined crews, including runtime input management. - Added tests to verify the correct behavior of new input handling features and JSON crew project interactions. * feat(cli): improve crew project input prompts and event handling - Enhanced the `_prompt_text` function to allow for configurable spacing before prompts, improving user experience during input collection. - Updated the wizards for agent and task creation to utilize the new prompt configuration, ensuring a more compact and streamlined interaction. - Introduced new plan step lifecycle events (`PlanStepStartedEvent`, `PlanStepCompletedEvent`) to better track the execution status of plan steps. - Refactored the step executor to emit these events during the execution of tasks, improving observability and debugging capabilities. - Added tests to verify the correct behavior of new prompt handling and event emissions during crew project execution. * fix: refine json-first crew interactions * fix: prioritize common json crew tools * fix: make json crew more tools expandable * fix: show json crew tools by category * feat(memory): update default embedder to OpenAI text-embedding-3-large and enhance memory compatibility - Changed the default embedding model for Memory to OpenAI text-embedding-3-large, which uses 3072-dimensional vectors. - Added warnings regarding compatibility issues with existing local memory stores created with 1536-dimensional embeddings. - Updated documentation to reflect the new default embedder and its configuration options. - Enhanced the CLI and codebase to support the new embedding model across various components, ensuring a seamless transition for users. * fix: address PR review feedback for JSON-first crews Review blockers: - Forward trained_agents_file to JSON crews: crewai run -f now exports CREWAI_TRAINED_AGENTS_FILE for the in-process JSON crew path - Wizard agent picker: Esc/cancel now reprompts instead of silently assigning the first agent - JSON tool resolution hard-fails: unknown tool names, missing custom tool files, and invalid custom tool modules raise JSONProjectError with actionable messages instead of warn-and-continue - Embedding dimension mismatch: LanceDB and Qdrant Edge storages raise EmbeddingDimensionMismatchError with reset/pin guidance instead of silently zero-filling vectors or returning empty search results - Custom tool code execution documented in loader docstring and the scaffolded project README CI fixes: - ruff format across lib/ - All 133 PR-introduced mypy errors fixed (llm.py lazy-litellm and cli.py lazy command shims now use TYPE_CHECKING imports; textual is_mounted misuse fixed; pick_many overloads; misc annotations) Bot review comments: - Empty except blocks now have explanatory comments or debug logging - Removed unused _C_BG/_C_PANEL/_C_BORDER globals and redundant import re; tests use a single import style for create_json_crew Tests: trained-agents propagation, wizard cancel, tool resolution failures, and dimension mismatch guidance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address second round of PR review comments Cursor Bugbot: - Wizard agent slugs: strip to [a-z0-9_] and fall back to agent_<n> so symbol-only roles can't produce an empty agents/.jsonc filename - Wizard task names: dedupe against prior task names and fall back to task_<n> for symbol-only descriptions CodeRabbit: - Agent.message(): import Task explicitly at runtime instead of relying on the namespace injection done by crewai/__init__ - Async executor: move the native-tools-unsupported fallback from _ainvoke_loop_react (self-recursion) to _ainvoke_loop_native_tools, mirroring the sync implementation - StepExecutor downgrade: keep the in-step conversation and append the text-tooling instructions instead of rebuilding messages, so completed native tool calls are not re-executed - crewai-files: extension-based MIME lookup now runs before byte sniffing so csv/xml types are not degraded to text/plain - Memory storages: validate every record in a save() batch against a consistent embedding dimension (LanceDB previously checked only the first record); added mixed-batch tests - _print_post_tui_summary now typed against CrewRunApp - Docs: Azure OpenAI default embedder change called out in the memory migration warning and provider table Code quality bots: - Removed unused _C_YELLOW/_C_CYAN (crew_run_tui) and _GREEN (tui_picker) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cli): accordion tool picker in JSON crew wizard The flat tool list had grown to ~90 rows. The picker now shows: - Common tools always visible at the top - Every other category as a single expandable row with tool and selection counts (e.g. "Search & Research (27 tools, 2 selected)") - Expanding a category collapses the previously expanded one - Selections persist across expand/collapse via new preselected support in pick_many; cursor follows the toggled category row tui_picker gains preselected + initial_cursor options on pick_many, and Esc in multi-select now confirms the current selection instead of discarding it (required so collapsing can't silently drop choices). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(cli): remove --daemon flag from crewai run The flag only affected JSON crew projects — classic and flow projects ignored it entirely, which made the behavior inconsistent. Removed the option, the daemon code path (_run_json_crew_daemon), and its helper (_load_json_crew_with_inputs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: update run command tests after --daemon removal lib/crewai/tests/cli/test_run_crew.py still asserted the old run_crew(trained_agents_file=..., daemon=False) call signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): exit codes, mid-run quit, async statuses, hyphen placeholders Addresses the latest Bugbot review round: - Failed JSON crew runs now exit non-zero (SystemExit(1)) so scripts and CI don't treat failures as success, mirroring the classic path - Quitting the TUI mid-run now ends the process (os._exit(130)); kickoff runs in a thread worker that cannot be force-cancelled, so letting the CLI return would leave LLM/tool work burning tokens in the background - Sidebar task statuses are now async-safe: completion/failure events resolve the task's own row via identity instead of assuming the most recently started task, and starting a task no longer blanket-marks earlier active rows as done - The runtime-input prompt regex now accepts hyphenated placeholder names ({my-topic}), matching kickoff's interpolation pattern Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: validation safety, custom tool sandboxing, TUI log integrity, memory error surfacing - Deploy validation no longer executes project code: validation mode checks tool declarations structurally (well-formed entries, custom tool file exists) without importing or instantiating anything. custom:<name> resolution only happens on the actual run path. - custom:<name> is constrained to [A-Za-z_][A-Za-z0-9_]* and the resolved path must stay inside the project's tools/ directory, so custom:../foo or absolute-path names cannot execute code outside it. Tool paths resolve relative to the crew project root, not cwd. - TUI task logs are built from per-task state captured at task start (idx, description, agent, start time); an out-of-order completion takes its output from the event and no longer steals or resets the current task's streamed steps/output. - EmbeddingDimensionMismatchError now inherits ValueError instead of RuntimeError so background saves surface it through MemorySaveFailedEvent instead of silently dropping the save; the shutdown catch in _background_encode_batch is narrowed to the "cannot schedule new futures" case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): declared project type wins over crew.json presence A flow project that also contains a crew.json(c) file now runs and validates as the flow it declares in pyproject.toml instead of being hijacked by the JSON crew path. Both crewai run (_has_json_crew) and deploy validation (_is_json_crew) check tool.crewai.type; a missing or unreadable pyproject still means a bare JSON crew project. Also documents why StepObservationFailedEvent intentionally marks the plan step "done": the event signals an observer failure, not a step failure, and the executor continues past it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): type the declared_type locals so mypy stays clean Comparing an Any-typed .get() chain returns Any, which tripped no-any-return on the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,42 +3,94 @@ from __future__ import annotations
|
||||
from importlib.metadata import version as get_version
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
from crewai_cli.add_crew_to_flow import add_crew_to_flow
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
from crewai_cli.config import Settings
|
||||
from crewai_cli.create_crew import create_crew
|
||||
from crewai_cli.create_flow import create_flow
|
||||
from crewai_cli.crew_chat import run_chat
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
from crewai_cli.enterprise.main import EnterpriseConfigureCommand
|
||||
from crewai_cli.evaluate_crew import evaluate_crew
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
from crewai_cli.install_crew import install_crew
|
||||
from crewai_cli.kickoff_flow import kickoff_flow
|
||||
from crewai_cli.organization.main import OrganizationCommand
|
||||
from crewai_cli.plot_flow import plot_flow
|
||||
from crewai_cli.remote_template.main import TemplateCommand
|
||||
from crewai_cli.replay_from_task import replay_task_command
|
||||
from crewai_cli.reset_memories_command import reset_memories_command
|
||||
from crewai_cli.run_crew import run_crew
|
||||
from crewai_cli.run_flow_definition import run_flow_definition
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
from crewai_cli.task_outputs import load_task_outputs
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
from crewai_cli.train_crew import train_crew
|
||||
from crewai_cli.triggers.main import TriggersCommand
|
||||
from crewai_cli.update_crew import update_crew
|
||||
from crewai_cli.user_data import (
|
||||
_load_user_data,
|
||||
is_tracing_enabled,
|
||||
update_user_data,
|
||||
)
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
read_toml,
|
||||
)
|
||||
|
||||
|
||||
def train_crew(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.train_crew import train_crew as _train_crew
|
||||
|
||||
return _train_crew(*args, **kwargs)
|
||||
|
||||
|
||||
def evaluate_crew(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.evaluate_crew import evaluate_crew as _evaluate_crew
|
||||
|
||||
return _evaluate_crew(*args, **kwargs)
|
||||
|
||||
|
||||
def replay_task_command(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.replay_from_task import replay_task_command as _replay_task_command
|
||||
|
||||
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
|
||||
|
||||
return _run_crew(*args, **kwargs)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# mypy sees the real classes; at runtime the shims below defer the
|
||||
# heavy imports until a command actually instantiates them.
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
from crewai_cli.organization.main import OrganizationCommand
|
||||
from crewai_cli.remote_template.main import TemplateCommand
|
||||
else:
|
||||
|
||||
class AuthenticationCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.authentication.main import (
|
||||
AuthenticationCommand as _AuthenticationCommand,
|
||||
)
|
||||
|
||||
return _AuthenticationCommand(*args, **kwargs)
|
||||
|
||||
class DeployCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.deploy.main import DeployCommand as _DeployCommand
|
||||
|
||||
return _DeployCommand(*args, **kwargs)
|
||||
|
||||
class TemplateCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.remote_template.main import (
|
||||
TemplateCommand as _TemplateCommand,
|
||||
)
|
||||
|
||||
return _TemplateCommand(*args, **kwargs)
|
||||
|
||||
class OrganizationCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.organization.main import (
|
||||
OrganizationCommand as _OrganizationCommand,
|
||||
)
|
||||
|
||||
return _OrganizationCommand(*args, **kwargs)
|
||||
|
||||
|
||||
def _get_cli_version() -> str:
|
||||
@@ -91,17 +143,57 @@ def uv(uv_args: tuple[str, ...]) -> None:
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@click.argument("type", type=click.Choice(["crew", "flow"]))
|
||||
@click.argument("name")
|
||||
@click.argument(
|
||||
"type", required=False, default=None, type=click.Choice(["crew", "flow"])
|
||||
)
|
||||
@click.argument("name", required=False, default=None)
|
||||
@click.option("--provider", type=str, help="The provider to use for the crew")
|
||||
@click.option("--skip_provider", is_flag=True, help="Skip provider validation")
|
||||
@click.option(
|
||||
"--classic",
|
||||
is_flag=True,
|
||||
help="Use classic Python/YAML project structure instead of JSON",
|
||||
)
|
||||
def create(
|
||||
type: str, name: str, provider: str | None, skip_provider: bool = False
|
||||
type: str | None,
|
||||
name: str | None,
|
||||
provider: str | None,
|
||||
skip_provider: bool = False,
|
||||
classic: bool = False,
|
||||
) -> None:
|
||||
"""Create a new crew, or flow."""
|
||||
if not type:
|
||||
from crewai_cli.tui_picker import pick
|
||||
|
||||
options = [
|
||||
("crew", "A team of AI agents working together"),
|
||||
(
|
||||
"flow",
|
||||
"A deterministic workflow with full control over agents and crews",
|
||||
),
|
||||
]
|
||||
type = pick("What would you like to create?", options)
|
||||
if type is None:
|
||||
raise SystemExit(0)
|
||||
click.echo()
|
||||
if not name:
|
||||
enable_prompt_line_editing()
|
||||
name = click.prompt(
|
||||
click.style(f" Name of your {type}", fg="cyan", bold=True),
|
||||
prompt_suffix=click.style(" › ", fg="bright_white"), # noqa: RUF001
|
||||
)
|
||||
if type == "crew":
|
||||
create_crew(name, provider, skip_provider)
|
||||
if classic:
|
||||
from crewai_cli.create_crew import create_crew
|
||||
|
||||
create_crew(name, provider, skip_provider)
|
||||
else:
|
||||
from crewai_cli.create_json_crew import create_json_crew
|
||||
|
||||
create_json_crew(name, provider, skip_provider)
|
||||
elif type == "flow":
|
||||
from crewai_cli.create_flow import create_flow
|
||||
|
||||
create_flow(name)
|
||||
else:
|
||||
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
|
||||
@@ -186,6 +278,8 @@ def replay(task_id: str, trained_agents_file: str | None) -> None:
|
||||
def log_tasks_outputs() -> None:
|
||||
"""Retrieve your latest crew.kickoff() task outputs."""
|
||||
try:
|
||||
from crewai_cli.task_outputs import load_task_outputs
|
||||
|
||||
tasks = load_task_outputs()
|
||||
|
||||
if not tasks:
|
||||
@@ -274,6 +368,8 @@ def reset_memories(
|
||||
"Please specify at least one memory type to reset using the appropriate flags."
|
||||
)
|
||||
return
|
||||
from crewai_cli.reset_memories_command import reset_memories_command
|
||||
|
||||
reset_memories_command(memory, knowledge, agent_knowledge, kickoff_outputs, all)
|
||||
except Exception as e:
|
||||
click.echo(f"An error occurred while resetting memories: {e}", err=True)
|
||||
@@ -296,7 +392,7 @@ def reset_memories(
|
||||
"--embedder-model",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Embedder model name (e.g. text-embedding-3-small, gemini-embedding-001).",
|
||||
help="Embedder model name (e.g. text-embedding-3-large, gemini-embedding-001).",
|
||||
)
|
||||
@click.option(
|
||||
"--embedder-config",
|
||||
@@ -351,7 +447,7 @@ def memory(
|
||||
"-m",
|
||||
"--model",
|
||||
type=str,
|
||||
default="gpt-4o-mini",
|
||||
default="gpt-5.4-mini",
|
||||
help="LLM Model to run the tests on the Crew. For now only accepting only OpenAI models.",
|
||||
)
|
||||
@click.option(
|
||||
@@ -382,6 +478,8 @@ def test(n_iterations: int, model: str, trained_agents_file: str | None) -> None
|
||||
@click.pass_context
|
||||
def install(context: click.Context) -> None:
|
||||
"""Install the Crew."""
|
||||
from crewai_cli.install_crew import install_crew
|
||||
|
||||
install_crew(context.args)
|
||||
|
||||
|
||||
@@ -415,7 +513,9 @@ def install(context: click.Context) -> None:
|
||||
help='Experimental: JSON object passed to flow.kickoff(), e.g. \'{"topic":"AI"}\'.',
|
||||
)
|
||||
def run(
|
||||
trained_agents_file: str | None, definition: str | None, inputs: str | None
|
||||
trained_agents_file: str | None,
|
||||
definition: str | None,
|
||||
inputs: str | None,
|
||||
) -> None:
|
||||
"""Run the Crew or Flow."""
|
||||
if inputs is not None and definition is None:
|
||||
@@ -435,6 +535,8 @@ def run(
|
||||
@crewai.command()
|
||||
def update() -> None:
|
||||
"""Update the pyproject.toml of the Crew project to use uv."""
|
||||
from crewai_cli.update_crew import update_crew
|
||||
|
||||
update_crew()
|
||||
|
||||
|
||||
@@ -544,6 +646,8 @@ def tool() -> None:
|
||||
@tool.command(name="create")
|
||||
@click.argument("handle")
|
||||
def tool_create(handle: str) -> None:
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.create(handle)
|
||||
|
||||
@@ -551,6 +655,8 @@ def tool_create(handle: str) -> None:
|
||||
@tool.command(name="install")
|
||||
@click.argument("handle")
|
||||
def tool_install(handle: str) -> None:
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.login()
|
||||
tool_cmd.install(handle)
|
||||
@@ -567,6 +673,8 @@ def tool_install(handle: str) -> None:
|
||||
@click.option("--public", "is_public", flag_value=True, default=False)
|
||||
@click.option("--private", "is_public", flag_value=False)
|
||||
def tool_publish(is_public: bool, force: bool) -> None:
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.login()
|
||||
tool_cmd.publish(is_public, force)
|
||||
@@ -599,6 +707,8 @@ def skill() -> None:
|
||||
help="Create skill in current dir instead of ./skills/",
|
||||
)
|
||||
def skill_create(name: str, in_project: bool) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.create(name, in_project=in_project)
|
||||
|
||||
@@ -606,6 +716,8 @@ def skill_create(name: str, in_project: bool) -> None:
|
||||
@skill.command(name="install")
|
||||
@click.argument("ref")
|
||||
def skill_install(ref: str) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.install(ref)
|
||||
|
||||
@@ -622,6 +734,8 @@ def skill_install(ref: str) -> None:
|
||||
@click.option("--private", "is_public", flag_value=False)
|
||||
@click.option("--org", default=None, help="Organisation slug (overrides settings).")
|
||||
def skill_publish(is_public: bool, org: str | None, force: bool) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.publish(is_public, org=org, force=force)
|
||||
|
||||
@@ -629,6 +743,8 @@ def skill_publish(is_public: bool, org: str | None, force: bool) -> None:
|
||||
@skill.command(name="list")
|
||||
def skill_list() -> None:
|
||||
"""List locally installed skills."""
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.list_cached()
|
||||
|
||||
@@ -668,6 +784,8 @@ 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()
|
||||
|
||||
@@ -675,6 +793,8 @@ def flow_run() -> None:
|
||||
@flow.command(name="plot")
|
||||
def flow_plot() -> None:
|
||||
"""Plot the Flow."""
|
||||
from crewai_cli.plot_flow import plot_flow
|
||||
|
||||
click.echo("Plotting the Flow")
|
||||
plot_flow()
|
||||
|
||||
@@ -683,6 +803,8 @@ def flow_plot() -> None:
|
||||
@click.argument("crew_name")
|
||||
def flow_add_crew(crew_name: str) -> None:
|
||||
"""Add a crew to an existing flow."""
|
||||
from crewai_cli.add_crew_to_flow import add_crew_to_flow
|
||||
|
||||
click.echo(f"Adding crew {crew_name} to the flow")
|
||||
add_crew_to_flow(crew_name)
|
||||
|
||||
@@ -695,6 +817,8 @@ def triggers() -> None:
|
||||
@triggers.command(name="list")
|
||||
def triggers_list() -> None:
|
||||
"""List all available triggers from integrations."""
|
||||
from crewai_cli.triggers.main import TriggersCommand
|
||||
|
||||
triggers_cmd = TriggersCommand()
|
||||
triggers_cmd.list_triggers()
|
||||
|
||||
@@ -703,6 +827,8 @@ def triggers_list() -> None:
|
||||
@click.argument("trigger_path")
|
||||
def triggers_run(trigger_path: str) -> None:
|
||||
"""Execute crew with trigger payload. Format: app_slug/trigger_slug"""
|
||||
from crewai_cli.triggers.main import TriggersCommand
|
||||
|
||||
triggers_cmd = TriggersCommand()
|
||||
triggers_cmd.execute_with_trigger(trigger_path)
|
||||
|
||||
@@ -715,6 +841,8 @@ def chat() -> None:
|
||||
click.secho(
|
||||
"\nStarting a conversation with the Crew\nType 'exit' or Ctrl+C to quit.\n",
|
||||
)
|
||||
from crewai_cli.crew_chat import run_chat
|
||||
|
||||
run_chat()
|
||||
|
||||
|
||||
@@ -754,6 +882,8 @@ def enterprise() -> None:
|
||||
@click.argument("enterprise_url")
|
||||
def enterprise_configure(enterprise_url: str) -> None:
|
||||
"""Configure CrewAI AMP OAuth2 settings from the provided Enterprise URL."""
|
||||
from crewai_cli.enterprise.main import EnterpriseConfigureCommand
|
||||
|
||||
enterprise_command = EnterpriseConfigureCommand()
|
||||
enterprise_command.configure(enterprise_url)
|
||||
|
||||
@@ -766,6 +896,8 @@ def config() -> None:
|
||||
@config.command("list")
|
||||
def config_list() -> None:
|
||||
"""List all CLI configuration parameters."""
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
|
||||
config_command = SettingsCommand()
|
||||
config_command.list()
|
||||
|
||||
@@ -775,6 +907,8 @@ def config_list() -> None:
|
||||
@click.argument("value")
|
||||
def config_set(key: str, value: str) -> None:
|
||||
"""Set a CLI configuration parameter."""
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
|
||||
config_command = SettingsCommand()
|
||||
config_command.set(key, value)
|
||||
|
||||
@@ -782,6 +916,8 @@ def config_set(key: str, value: str) -> None:
|
||||
@config.command("reset")
|
||||
def config_reset() -> None:
|
||||
"""Reset all CLI configuration parameters to default values."""
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
|
||||
config_command = SettingsCommand()
|
||||
config_command.reset_all_settings()
|
||||
|
||||
|
||||
1108
lib/cli/src/crewai_cli/create_json_crew.py
Normal file
1108
lib/cli/src/crewai_cli/create_json_crew.py
Normal file
File diff suppressed because it is too large
Load Diff
2098
lib/cli/src/crewai_cli/crew_run_tui.py
Normal file
2098
lib/cli/src/crewai_cli/crew_run_tui.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,39 @@ def _run_predeploy_validation(skip_validate: bool) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _display_git_repository_help() -> None:
|
||||
"""Explain how to prepare a new project for deployment."""
|
||||
console.print(
|
||||
"Deployment requires a Git repository with an origin remote.",
|
||||
style="bold red",
|
||||
)
|
||||
console.print(
|
||||
"CrewAI AMP deploys from the remote repository URL, so commit and push "
|
||||
"this project first, then run deploy again.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print("\nSuggested setup:")
|
||||
console.print(" git init")
|
||||
console.print(" git add .")
|
||||
console.print(' git commit -m "Initial crew"')
|
||||
console.print(" git branch -M main")
|
||||
console.print(" git remote add origin <your-repo-url>")
|
||||
console.print(" git push -u origin main")
|
||||
|
||||
|
||||
def _display_git_remote_help() -> None:
|
||||
"""Explain how to add a remote to an existing Git repository."""
|
||||
console.print("No remote repository URL found.", style="bold red")
|
||||
console.print(
|
||||
"CrewAI AMP deploys from the origin remote. Add a remote, push your "
|
||||
"latest commit, then run deploy again.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print("\nSuggested setup:")
|
||||
console.print(" git remote add origin <your-repo-url>")
|
||||
console.print(" git push -u origin HEAD")
|
||||
|
||||
|
||||
class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
"""
|
||||
A class to handle deployment-related operations for CrewAI projects.
|
||||
@@ -124,14 +157,11 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
try:
|
||||
remote_repo_url = git.Repository().origin_url()
|
||||
except ValueError:
|
||||
remote_repo_url = None
|
||||
_display_git_repository_help()
|
||||
return
|
||||
|
||||
if remote_repo_url is None:
|
||||
console.print("No remote repository URL found.", style="bold red")
|
||||
console.print(
|
||||
"Please ensure your project has a valid remote repository.",
|
||||
style="yellow",
|
||||
)
|
||||
_display_git_remote_help()
|
||||
return
|
||||
|
||||
self._confirm_input(env_vars, remote_repo_url, confirm)
|
||||
|
||||
@@ -38,6 +38,12 @@ import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from crewai.project.json_loader import (
|
||||
JSONProjectValidationError,
|
||||
find_crew_json_file,
|
||||
find_json_project_file,
|
||||
validate_crew_project,
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli.utils import parse_toml
|
||||
@@ -151,9 +157,33 @@ class DeployValidator:
|
||||
def ok(self) -> bool:
|
||||
return not self.errors
|
||||
|
||||
@property
|
||||
def _is_json_crew(self) -> bool:
|
||||
"""True for JSON crew projects, deferring to the declared type.
|
||||
|
||||
A flow project that also contains a crew.json(c) file validates as
|
||||
the flow it declares in pyproject.toml, not as a JSON crew.
|
||||
"""
|
||||
if find_crew_json_file(self.project_root) is None:
|
||||
return False
|
||||
pyproject_path = self.project_root / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return True
|
||||
try:
|
||||
data = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return True
|
||||
declared_type: str | None = (
|
||||
(data.get("tool") or {}).get("crewai", {}).get("type")
|
||||
)
|
||||
return declared_type != "flow"
|
||||
|
||||
def run(self) -> list[ValidationResult]:
|
||||
"""Run all checks. Later checks are skipped when earlier ones make
|
||||
them impossible (e.g. no pyproject.toml → no lockfile check)."""
|
||||
if self._is_json_crew:
|
||||
return self._run_json_checks()
|
||||
|
||||
if not self._check_pyproject():
|
||||
return self.results
|
||||
|
||||
@@ -176,6 +206,110 @@ class DeployValidator:
|
||||
|
||||
return self.results
|
||||
|
||||
def _run_json_checks(self) -> list[ValidationResult]:
|
||||
"""Validation suite for JSON-defined crew projects."""
|
||||
crew_path = find_crew_json_file(self.project_root)
|
||||
if crew_path is None:
|
||||
return self.results
|
||||
|
||||
try:
|
||||
project = validate_crew_project(crew_path, self.project_root / "agents")
|
||||
except JSONProjectValidationError as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"invalid_crew_json",
|
||||
f"{crew_path.name} has invalid JSON crew configuration",
|
||||
detail="\n".join(e.errors),
|
||||
hint="Fix the JSON crew, agent, and task references before deploying.",
|
||||
)
|
||||
return self.results
|
||||
except Exception as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"invalid_crew_json",
|
||||
f"Cannot parse {crew_path.name}",
|
||||
detail=str(e),
|
||||
)
|
||||
return self.results
|
||||
|
||||
agents_dir = self.project_root / "agents"
|
||||
|
||||
self._check_pyproject()
|
||||
self._check_lockfile()
|
||||
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
|
||||
self._check_version_vs_lockfile()
|
||||
|
||||
return self.results
|
||||
|
||||
def _check_env_vars_json(
|
||||
self, crew_path: Path, agents_dir: Path, agent_names: list[str]
|
||||
) -> None:
|
||||
"""Check for env var references in JSON crew files."""
|
||||
referenced: set[str] = set()
|
||||
pattern = re.compile(r"\$\{?([A-Z][A-Z0-9_]+)\}?")
|
||||
|
||||
try:
|
||||
referenced.update(pattern.findall(crew_path.read_text(errors="ignore")))
|
||||
except OSError as exc:
|
||||
logger.debug("Skipping unreadable crew file %s: %s", crew_path, exc)
|
||||
|
||||
for name in agent_names:
|
||||
agent_path = find_json_project_file(agents_dir, name)
|
||||
if agent_path is None:
|
||||
continue
|
||||
try:
|
||||
referenced.update(
|
||||
pattern.findall(agent_path.read_text(errors="ignore"))
|
||||
)
|
||||
except OSError as exc:
|
||||
logger.debug("Skipping unreadable agent file %s: %s", agent_path, exc)
|
||||
|
||||
for py_path in self.project_root.rglob("*.py"):
|
||||
if ".venv" in py_path.parts:
|
||||
continue
|
||||
try:
|
||||
text = py_path.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
env_pattern = re.compile(
|
||||
r"""(?x)
|
||||
(?:os\.environ\s*(?:\[\s*|\.get\s*\(\s*)
|
||||
|os\.getenv\s*\(\s*
|
||||
|getenv\s*\(\s*)
|
||||
['"]([A-Z][A-Z0-9_]*)['"]
|
||||
"""
|
||||
)
|
||||
referenced.update(env_pattern.findall(text))
|
||||
|
||||
env_file = self.project_root / ".env"
|
||||
env_keys: set[str] = set()
|
||||
if env_file.exists():
|
||||
for line in env_file.read_text(errors="ignore").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
env_keys.add(line.split("=", 1)[0].strip())
|
||||
|
||||
missing_known = sorted(
|
||||
var
|
||||
for var in referenced
|
||||
if var in _KNOWN_API_KEY_HINTS
|
||||
and var not in env_keys
|
||||
and var not in os.environ
|
||||
)
|
||||
if missing_known:
|
||||
self._add(
|
||||
Severity.WARNING,
|
||||
"env_vars_not_in_dotenv",
|
||||
f"{len(missing_known)} referenced API key(s) not in .env",
|
||||
detail=(
|
||||
"These env vars are referenced in your project but not set "
|
||||
f"locally: {', '.join(missing_known)}. Deploys will fail "
|
||||
"unless they are added to the deployment's Environment "
|
||||
"Variables in the CrewAI dashboard."
|
||||
),
|
||||
)
|
||||
|
||||
def _check_pyproject(self) -> bool:
|
||||
pyproject_path = self.project_root / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
|
||||
@@ -48,6 +48,7 @@ class Repository:
|
||||
["git", "rev-parse", "--is-inside-work-tree"], # noqa: S607
|
||||
cwd=self.path,
|
||||
encoding="utf-8",
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
|
||||
@@ -1,25 +1,311 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from enum import Enum
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
from crewai.project.json_loader import find_crew_json_file
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
from packaging import version
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
read_toml,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_version
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
|
||||
class CrewType(Enum):
|
||||
STANDARD = "standard"
|
||||
FLOW = "flow"
|
||||
|
||||
|
||||
def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
"""Run the crew or flow by running a command in the UV environment.
|
||||
# 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.
|
||||
_INPUT_PLACEHOLDER_RE = re.compile(r"(?<!{){([A-Za-z_][A-Za-z0-9_\-]*)}(?!})")
|
||||
|
||||
Starting from version 0.103.0, this command can be used to run both
|
||||
standard crews and flows. For flows, it detects the type from pyproject.toml
|
||||
and automatically runs the appropriate command.
|
||||
|
||||
def _has_json_crew() -> bool:
|
||||
"""Check if this is a JSON-defined crew project.
|
||||
|
||||
The project type declared in pyproject.toml wins: a flow project that
|
||||
happens to contain a crew.json(c) file still runs as a flow. A missing
|
||||
or unreadable pyproject means a bare JSON crew project.
|
||||
"""
|
||||
if find_crew_json_file() is None:
|
||||
return False
|
||||
try:
|
||||
pyproject_data = read_toml()
|
||||
except Exception:
|
||||
return True
|
||||
declared_type: str | None = (
|
||||
pyproject_data.get("tool", {}).get("crewai", {}).get("type")
|
||||
)
|
||||
return declared_type != "flow"
|
||||
|
||||
|
||||
def _extract_input_placeholders(text: str | None) -> set[str]:
|
||||
if not text:
|
||||
return set()
|
||||
return set(_INPUT_PLACEHOLDER_RE.findall(text))
|
||||
|
||||
|
||||
def _missing_input_names(crew: Any, inputs: dict[str, Any]) -> list[str]:
|
||||
"""Return input placeholders used by a crew but not provided as defaults."""
|
||||
placeholders: set[str] = set()
|
||||
|
||||
for agent in getattr(crew, "agents", []) or []:
|
||||
placeholders.update(_extract_input_placeholders(getattr(agent, "role", None)))
|
||||
placeholders.update(_extract_input_placeholders(getattr(agent, "goal", None)))
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(agent, "backstory", None))
|
||||
)
|
||||
|
||||
for task in getattr(crew, "tasks", []) or []:
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(task, "description", None))
|
||||
)
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(task, "expected_output", None))
|
||||
)
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(task, "output_file", None))
|
||||
)
|
||||
|
||||
return sorted(name for name in placeholders if name not in inputs)
|
||||
|
||||
|
||||
def _prompt_for_missing_inputs(
|
||||
crew: Any, default_inputs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Ask for runtime values for placeholders that lack default inputs."""
|
||||
inputs = dict(default_inputs or {})
|
||||
missing = _missing_input_names(crew, inputs)
|
||||
if not missing:
|
||||
return inputs
|
||||
|
||||
enable_prompt_line_editing()
|
||||
|
||||
click.echo()
|
||||
click.secho(" Runtime inputs", fg="cyan", bold=True)
|
||||
click.secho(
|
||||
" Values for {placeholder} references in your agents and tasks.",
|
||||
dim=True,
|
||||
)
|
||||
|
||||
for name in missing:
|
||||
inputs[name] = click.prompt(
|
||||
click.style(f" {name}", fg="cyan"),
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
def _json_loading_status(message: str) -> AbstractContextManager[Any]:
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
if not console.is_terminal:
|
||||
return nullcontext()
|
||||
return console.status(
|
||||
Text(f" {message}", style="bold #1F7982"),
|
||||
spinner="dots",
|
||||
)
|
||||
|
||||
|
||||
def _load_json_crew(crew_path: Path) -> tuple[Any, dict[str, Any]]:
|
||||
from crewai.project.crew_loader import load_crew
|
||||
|
||||
return load_crew(crew_path)
|
||||
|
||||
|
||||
def _load_json_crew_for_tui(
|
||||
crew_path: Path,
|
||||
) -> tuple[type[Any], Any, dict[str, Any], list[str], list[str]]:
|
||||
with _json_loading_status("Preparing crew..."):
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
crew, default_inputs = _load_json_crew(crew_path)
|
||||
_prepare_json_crew_for_tui(crew)
|
||||
task_names = [
|
||||
getattr(task, "name", "") or getattr(task, "description", "")[:40] or "Task"
|
||||
for task in crew.tasks
|
||||
]
|
||||
agent_names = [
|
||||
getattr(agent, "role", "") or getattr(agent, "name", "") or "Agent"
|
||||
for agent in crew.agents
|
||||
]
|
||||
|
||||
return CrewRunApp, crew, default_inputs, task_names, agent_names
|
||||
|
||||
|
||||
def _prepare_json_crew_for_tui(crew: Any) -> None:
|
||||
"""Apply the same quiet/streaming setup used by the TUI JSON loader."""
|
||||
crew.verbose = False
|
||||
for agent in crew.agents:
|
||||
agent.verbose = False
|
||||
if hasattr(agent, "llm") and hasattr(agent.llm, "stream"):
|
||||
agent.llm.stream = True
|
||||
|
||||
|
||||
def _run_json_crew(trained_agents_file: str | None = None) -> Any:
|
||||
"""Load and run a JSON-defined crew."""
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_file = Path.cwd() / ".env"
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
# JSON crews run in-process, so export the trained-agents file directly
|
||||
# instead of forwarding it to a subprocess like classic crews do.
|
||||
if trained_agents_file:
|
||||
os.environ[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file
|
||||
|
||||
crew_path = find_crew_json_file()
|
||||
if crew_path is None:
|
||||
raise FileNotFoundError("No crew.jsonc or crew.json found")
|
||||
|
||||
crew_run_app_cls, crew, default_inputs, task_names, agent_names = (
|
||||
_load_json_crew_for_tui(crew_path)
|
||||
)
|
||||
runtime_inputs = _prompt_for_missing_inputs(crew, default_inputs)
|
||||
|
||||
app = crew_run_app_cls(
|
||||
crew_name=crew.name or "Crew",
|
||||
total_tasks=len(crew.tasks),
|
||||
agent_names=agent_names,
|
||||
task_names=task_names,
|
||||
)
|
||||
app._crew = crew
|
||||
app._default_inputs = runtime_inputs
|
||||
|
||||
app.run()
|
||||
|
||||
_print_post_tui_summary(app)
|
||||
|
||||
if app._status == "failed":
|
||||
# Mirror the classic subprocess path: a failed crew must produce a
|
||||
# non-zero exit code so scripts and CI don't treat it as success.
|
||||
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):
|
||||
_chain_deploy()
|
||||
|
||||
return app._crew_result
|
||||
|
||||
|
||||
def _chain_deploy() -> None:
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
try:
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
|
||||
console.print("\nStarting deployment…\n", style="bold #FF5A50")
|
||||
DeployCommand().create_crew(confirm=False, skip_validate=True)
|
||||
except SystemExit:
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
|
||||
console.print()
|
||||
AuthenticationCommand().login()
|
||||
try:
|
||||
DeployCommand().create_crew(confirm=False, skip_validate=True)
|
||||
except Exception as e:
|
||||
console.print(f"\nDeploy failed: {e}\n", style="bold red")
|
||||
except Exception as e:
|
||||
console.print(f"\nDeploy failed: {e}\n", style="bold red")
|
||||
|
||||
|
||||
def _print_post_tui_summary(app: CrewRunApp) -> None:
|
||||
"""Print a summary to the terminal after the Textual 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 = time.time() - app._start_time
|
||||
|
||||
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(
|
||||
f" ✔ Completed {app._total_tasks} tasks",
|
||||
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 run_crew(trained_agents_file: str | None = None) -> None:
|
||||
"""Run the crew or flow.
|
||||
|
||||
Args:
|
||||
trained_agents_file: Optional path to a trained-agents pickle produced
|
||||
@@ -27,6 +313,11 @@ def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
``CREWAI_TRAINED_AGENTS_FILE`` so agents load suggestions from this
|
||||
file instead of the default ``trained_agents_data.pkl``.
|
||||
"""
|
||||
# JSON crew projects take precedence
|
||||
if _has_json_crew():
|
||||
_run_json_crew(trained_agents_file=trained_agents_file)
|
||||
return
|
||||
|
||||
crewai_version = get_crewai_version()
|
||||
min_required_version = "0.71.0"
|
||||
pyproject_data = read_toml()
|
||||
|
||||
419
lib/cli/src/crewai_cli/tui_picker.py
Normal file
419
lib/cli/src/crewai_cli/tui_picker.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""Arrow-key interactive pickers for CLI prompts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
import sys
|
||||
from typing import overload
|
||||
|
||||
import click
|
||||
|
||||
|
||||
# CrewAI brand: primary=#FF5A50 (coral), teal=#1F7982
|
||||
_CORAL = "\033[38;2;255;90;80m" # #FF5A50
|
||||
_TEAL = "\033[38;2;31;121;130m" # #1F7982
|
||||
_BOLD = "\033[1m"
|
||||
_DIM = "\033[2m"
|
||||
_RESET = "\033[0m"
|
||||
_HIDE_CURSOR = "\033[?25l"
|
||||
_SHOW_CURSOR = "\033[?25h"
|
||||
|
||||
|
||||
def _is_interactive() -> bool:
|
||||
try:
|
||||
return sys.stdin.isatty() and sys.stdout.isatty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _read_key() -> str:
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
ch = msvcrt.getwch()
|
||||
if ch in ("\x00", "\xe0"):
|
||||
ch2 = msvcrt.getwch()
|
||||
return {"H": "up", "P": "down"}.get(ch2, "")
|
||||
if ch == "\r":
|
||||
return "enter"
|
||||
if ch == " ":
|
||||
return "space"
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch
|
||||
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setcbreak(fd)
|
||||
ch = sys.stdin.read(1)
|
||||
if ch == "\x1b":
|
||||
seq = sys.stdin.read(2)
|
||||
if seq == "[A":
|
||||
return "up"
|
||||
if seq == "[B":
|
||||
return "down"
|
||||
return "esc"
|
||||
if ch in ("\r", "\n"):
|
||||
return "enter"
|
||||
if ch == " ":
|
||||
return "space"
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def _clear_lines(n: int) -> None:
|
||||
sys.stdout.write(f"\033[{n}A")
|
||||
for _ in range(n):
|
||||
sys.stdout.write("\033[2K\n")
|
||||
sys.stdout.write(f"\033[{n}A")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _draw_single(labels: list[str], cursor: int, *, clear: bool = False) -> None:
|
||||
total = len(labels)
|
||||
if clear:
|
||||
sys.stdout.write(f"\033[{total}A")
|
||||
for i, label in enumerate(labels):
|
||||
if i == cursor:
|
||||
sys.stdout.write(f"\033[2K {_CORAL}→{_RESET} {_BOLD}{label}{_RESET}\n")
|
||||
else:
|
||||
sys.stdout.write(f"\033[2K {label}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _draw_multi(
|
||||
labels: list[str],
|
||||
cursor: int,
|
||||
selected: set[int],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
clear: bool = False,
|
||||
) -> None:
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
hint_text = "↑↓ navigate, space toggle, enter confirm"
|
||||
if action_indices:
|
||||
hint_text = "↑↓ navigate, space toggle, enter confirm, ▸ rows expand/collapse"
|
||||
hint = f" {_DIM}{hint_text}{_RESET}"
|
||||
total = len(labels) + 1
|
||||
if clear:
|
||||
sys.stdout.write(f"\033[{total}A")
|
||||
sys.stdout.write(f"\033[2K{hint}\n")
|
||||
for i, label in enumerate(labels):
|
||||
if i in separator_indices:
|
||||
sys.stdout.write(f"\033[2K {_TEAL}{label}{_RESET}\n")
|
||||
continue
|
||||
if i in action_indices:
|
||||
check = " "
|
||||
elif i in selected:
|
||||
check = f"{_CORAL}[x]{_RESET}"
|
||||
else:
|
||||
check = "[ ]"
|
||||
arrow = f"{_CORAL}→{_RESET} " if i == cursor else " "
|
||||
bold = f"{_BOLD}{label}{_RESET}" if i == cursor else label
|
||||
sys.stdout.write(f"\033[2K {arrow}{check} {bold}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _arrow_select_one(labels: list[str]) -> int:
|
||||
cursor = 0
|
||||
total = len(labels)
|
||||
sys.stdout.write(_HIDE_CURSOR)
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
_draw_single(labels, cursor)
|
||||
while True:
|
||||
key = _read_key()
|
||||
if key == "up" and cursor > 0:
|
||||
cursor -= 1
|
||||
_draw_single(labels, cursor, clear=True)
|
||||
elif key == "down" and cursor < total - 1:
|
||||
cursor += 1
|
||||
_draw_single(labels, cursor, clear=True)
|
||||
elif key == "enter":
|
||||
_clear_lines(total)
|
||||
return cursor
|
||||
elif key in ("esc", "q"):
|
||||
_clear_lines(total)
|
||||
return -1
|
||||
finally:
|
||||
sys.stdout.write(_SHOW_CURSOR)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _arrow_select_multi(
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> tuple[list[int], int | None]:
|
||||
total = len(labels)
|
||||
selected: set[int] = set(preselected or ())
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
if initial_cursor is not None and 0 <= initial_cursor < total:
|
||||
cursor = initial_cursor
|
||||
else:
|
||||
cursor = _first_selectable_index(total, separator_indices)
|
||||
sys.stdout.write(_HIDE_CURSOR)
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
)
|
||||
while True:
|
||||
key = _read_key()
|
||||
if key == "up":
|
||||
cursor = _next_selectable_index(cursor, -1, total, separator_indices)
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
)
|
||||
elif key == "down":
|
||||
cursor = _next_selectable_index(cursor, 1, total, separator_indices)
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
)
|
||||
elif key == "space":
|
||||
if cursor in action_indices:
|
||||
_clear_lines(total + 1)
|
||||
return sorted(selected), cursor
|
||||
selected ^= {cursor}
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
)
|
||||
elif key == "enter":
|
||||
_clear_lines(total + 1)
|
||||
if cursor in action_indices:
|
||||
return sorted(selected), cursor
|
||||
return sorted(selected), None
|
||||
elif key in ("esc", "q"):
|
||||
_clear_lines(total + 1)
|
||||
return sorted(selected), None
|
||||
finally:
|
||||
sys.stdout.write(_SHOW_CURSOR)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _numbered_select(labels: list[str]) -> int:
|
||||
for idx, label in enumerate(labels, 1):
|
||||
click.echo(f" {idx}. {label}")
|
||||
click.echo()
|
||||
while True:
|
||||
choice = click.prompt(" Select", type=str, default="1")
|
||||
if choice.lower() == "q":
|
||||
return -1
|
||||
try:
|
||||
num = int(choice)
|
||||
if 1 <= num <= len(labels):
|
||||
return num - 1
|
||||
except ValueError:
|
||||
# Non-numeric input falls through to the shared error message.
|
||||
pass
|
||||
click.secho(f" Invalid choice. Enter 1-{len(labels)}.", fg="red")
|
||||
|
||||
|
||||
def _numbered_select_multi(
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
) -> tuple[list[int], int | None]:
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
numbered_indices: list[int] = []
|
||||
for idx, label in enumerate(labels):
|
||||
if idx in separator_indices:
|
||||
click.secho(f" {label}", fg="cyan")
|
||||
continue
|
||||
numbered_indices.append(idx)
|
||||
click.echo(f" {len(numbered_indices)}. {label}")
|
||||
click.echo()
|
||||
raw = click.prompt(
|
||||
" Select (comma-separated numbers, or empty to skip)",
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
if not raw.strip():
|
||||
return sorted(preselected or ()), None
|
||||
indices: list[int] = list(preselected or ())
|
||||
for part in raw.split(","):
|
||||
with suppress(ValueError):
|
||||
num = int(part.strip())
|
||||
if 1 <= num <= len(numbered_indices):
|
||||
idx = numbered_indices[num - 1]
|
||||
if idx in action_indices:
|
||||
return sorted(set(indices)), idx
|
||||
indices.append(idx)
|
||||
return sorted(set(indices)), None
|
||||
|
||||
|
||||
def _first_selectable_index(total: int, separator_indices: set[int]) -> int:
|
||||
for idx in range(total):
|
||||
if idx not in separator_indices:
|
||||
return idx
|
||||
return 0
|
||||
|
||||
|
||||
def _next_selectable_index(
|
||||
cursor: int,
|
||||
direction: int,
|
||||
total: int,
|
||||
separator_indices: set[int],
|
||||
) -> int:
|
||||
next_cursor = cursor + direction
|
||||
while 0 <= next_cursor < total:
|
||||
if next_cursor not in separator_indices:
|
||||
return next_cursor
|
||||
next_cursor += direction
|
||||
return cursor
|
||||
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def pick(title: str, options: list[tuple[str, str]]) -> str | None:
|
||||
"""Arrow-key single-select picker.
|
||||
|
||||
Args:
|
||||
title: Header text.
|
||||
options: List of ``(value, description)`` tuples.
|
||||
|
||||
Returns:
|
||||
The *value* of the selected option, or ``None`` if cancelled.
|
||||
"""
|
||||
labels = [f"{value:<12s} {desc}" for value, desc in options]
|
||||
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan", bold=True)
|
||||
click.echo()
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
idx = _arrow_select_one(labels)
|
||||
except Exception:
|
||||
idx = _numbered_select(labels)
|
||||
else:
|
||||
idx = _numbered_select(labels)
|
||||
|
||||
if idx < 0:
|
||||
return None
|
||||
|
||||
value, _desc = options[idx]
|
||||
click.secho(f" ✔ {value}", fg="green")
|
||||
return value
|
||||
|
||||
|
||||
def pick_one(title: str, labels: list[str]) -> int:
|
||||
"""Arrow-key single-select from plain labels.
|
||||
|
||||
Returns:
|
||||
Selected index, or ``-1`` if cancelled.
|
||||
"""
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan")
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
return _arrow_select_one(labels)
|
||||
except Exception:
|
||||
return _numbered_select(labels)
|
||||
return _numbered_select(labels)
|
||||
|
||||
|
||||
@overload
|
||||
def pick_many(
|
||||
title: str,
|
||||
labels: list[str],
|
||||
*,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> list[int]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def pick_many(
|
||||
title: str,
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int],
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> tuple[list[int], int | None]: ...
|
||||
|
||||
|
||||
def pick_many(
|
||||
title: str,
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> list[int] | tuple[list[int], int | None]:
|
||||
"""Arrow-key multi-select with checkboxes.
|
||||
|
||||
Returns:
|
||||
Sorted list of selected indices, or ``(indices, action_index)`` when
|
||||
``action_indices`` is provided.
|
||||
"""
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan")
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
selected, action = _arrow_select_multi(
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
initial_cursor=initial_cursor,
|
||||
)
|
||||
except Exception:
|
||||
selected, action = _numbered_select_multi(
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
)
|
||||
else:
|
||||
selected, action = _numbered_select_multi(
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
)
|
||||
if action_indices is None:
|
||||
return selected
|
||||
return selected, action
|
||||
@@ -24,6 +24,7 @@ __all__ = [
|
||||
"build_env_with_all_tool_credentials",
|
||||
"build_env_with_tool_repository_credentials",
|
||||
"copy_template",
|
||||
"enable_prompt_line_editing",
|
||||
"fetch_and_json_env_file",
|
||||
"get_project_description",
|
||||
"get_project_name",
|
||||
@@ -40,6 +41,19 @@ __all__ = [
|
||||
console = Console()
|
||||
|
||||
|
||||
def enable_prompt_line_editing() -> None:
|
||||
"""Enable cursor movement/history editing for Click text prompts when available."""
|
||||
try:
|
||||
import readline
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
try:
|
||||
readline.parse_and_bind("set editing-mode emacs")
|
||||
except Exception: # pragma: no cover - readline backends vary by platform
|
||||
return
|
||||
|
||||
|
||||
def copy_template(
|
||||
src: Path, dst: Path, name: str, class_name: str, folder_name: str
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user