mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 22:19:27 +00:00
Merge branch 'main' into fix-skill-archive-symlink-traversal
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "1.14.7"
|
||||
__version__ = "1.14.8a3"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -89,13 +89,16 @@ description = "{name} using crewAI"
|
||||
authors = [{{ name = "Your Name", email = "you@example.com" }}]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=1.14.7"
|
||||
"crewai[tools]==1.14.8a1"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
only-include = ["agents", "crew.jsonc", "tools", "knowledge", "skills"]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
"""
|
||||
@@ -677,7 +680,7 @@ def _default_agents_and_tasks(
|
||||
]
|
||||
crew_settings = {
|
||||
"process": "sequential",
|
||||
"memory": False,
|
||||
"memory": True,
|
||||
"inputs": {},
|
||||
}
|
||||
return agents, tasks, crew_settings
|
||||
|
||||
@@ -17,7 +17,7 @@ from textual.binding import Binding, BindingType
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.css.query import NoMatches
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Footer, Header, Static
|
||||
from textual.widgets import Button, Footer, Header, Input, Static
|
||||
|
||||
|
||||
_SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
@@ -34,6 +34,25 @@ _C_MUTED = "#666666" # dimmer than _C_DIM for past timeline
|
||||
_STEP_NUMBER_RE = re.compile(r"\bstep\s+(\d+)\b", re.IGNORECASE)
|
||||
_REFINEMENT_RE = re.compile(r"^\s*step\s+(\d+)\s*:\s*(.+)\s*$", re.IGNORECASE)
|
||||
_INTERNAL_TOOL_NAMES = {"create_reasoning_plan"}
|
||||
_LOG_ARGS_TEXT_LIMIT = 3_000
|
||||
_LOG_RESULT_TEXT_LIMIT = 5_000
|
||||
_LOG_TRUNCATION_SUFFIX = "... [truncated]"
|
||||
# Background memory saves can emit their start event just after kickoff returns.
|
||||
_MEMORY_SAVE_DRAIN_GRACE_SECONDS = 2.0
|
||||
|
||||
|
||||
def _is_save_to_memory_tool(tool_name: str | None) -> bool:
|
||||
return (tool_name or "").replace(" ", "_").lower() == "save_to_memory"
|
||||
|
||||
|
||||
def _truncate_log_text(value: Any, limit: int) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value)
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
suffix = _LOG_TRUNCATION_SUFFIX
|
||||
return f"{text[: max(0, limit - len(suffix))]}{suffix}"
|
||||
|
||||
|
||||
def _enable_tracing_in_dotenv() -> None:
|
||||
@@ -363,6 +382,18 @@ Screen {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#conversation-input {
|
||||
display: none;
|
||||
height: 3;
|
||||
border-top: hkey #333333;
|
||||
background: #1c1c1c;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#conversation-input:focus {
|
||||
border-top: hkey #1F7982;
|
||||
}
|
||||
|
||||
Header {
|
||||
background: #1c1c1c;
|
||||
color: #FF5A50;
|
||||
@@ -464,6 +495,7 @@ FooterKey .footer-key--key {
|
||||
total_tasks: int = 0,
|
||||
agent_names: list[str] | None = None,
|
||||
task_names: list[str] | None = None,
|
||||
conversational: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.title = f"CrewAI — {crew_name}"
|
||||
@@ -519,10 +551,19 @@ FooterKey .footer-key--key {
|
||||
self._log_expanded: set[int] = set()
|
||||
self._log_scroll_needed: bool = False
|
||||
self._log_line_map: list[tuple[int, int, int]] = []
|
||||
self._suppressed_memory_save_event_ids: set[str] = set()
|
||||
self._memory_save_drain_timer: Any = None
|
||||
|
||||
self._event_handlers: list[tuple[type, Any]] = []
|
||||
|
||||
self._crew: Any = None
|
||||
self._flow: Any = None
|
||||
self._is_conversational = conversational
|
||||
self._conversation_messages: list[tuple[str, str]] = []
|
||||
self._conversation_turns = 0
|
||||
self._conversation_turn_in_progress = False
|
||||
self._conversation_previous_defer_trace_finalization: bool | None = None
|
||||
self._conversation_exit_commands = {"exit", "quit"}
|
||||
self._default_inputs: dict[str, Any] | None = None
|
||||
self._crew_result: Any = None
|
||||
self._crew_json_path: Any = None
|
||||
@@ -545,6 +586,10 @@ FooterKey .footer-key--key {
|
||||
yield Static(id="task-header")
|
||||
with VerticalScroll(id="scroll-area"):
|
||||
yield Static(id="main-content")
|
||||
yield Input(
|
||||
placeholder="Message the flow...",
|
||||
id="conversation-input",
|
||||
)
|
||||
with VerticalScroll(id="log-panel"):
|
||||
yield Static(id="log-content")
|
||||
yield Footer()
|
||||
@@ -553,7 +598,9 @@ FooterKey .footer-key--key {
|
||||
self._start_time = time.time()
|
||||
self._subscribe()
|
||||
self._tick_timer = self.set_interval(1 / 8, self._tick)
|
||||
if self._crew:
|
||||
if self._is_conversational and self._flow:
|
||||
self._start_conversational_session()
|
||||
elif self._crew:
|
||||
self._run_crew_worker()
|
||||
elif self._crew_json_path:
|
||||
self._load_and_run_worker()
|
||||
@@ -633,7 +680,6 @@ FooterKey .footer-key--key {
|
||||
self.call_from_thread(self._on_crew_failed, str(e))
|
||||
|
||||
def _on_crew_done(self, output: str | None) -> None:
|
||||
self._unsubscribe()
|
||||
with self._lock:
|
||||
self._status = "completed"
|
||||
self._final_output = output
|
||||
@@ -649,6 +695,8 @@ FooterKey .footer-key--key {
|
||||
now = time.time()
|
||||
for entry in self._log_entries:
|
||||
if entry["status"] == "running":
|
||||
if entry["tool_name"] == "memory_save":
|
||||
continue
|
||||
entry["status"] = "timeout"
|
||||
entry["error"] = "No result received before crew completed"
|
||||
entry["duration"] = now - entry["start_time"]
|
||||
@@ -680,9 +728,9 @@ FooterKey .footer-key--key {
|
||||
self.call_later(self._focus_activity_log)
|
||||
self._tick_timer.stop()
|
||||
self._tick_timer = self.set_interval(1 / 2, self._tick)
|
||||
self._unsubscribe_if_no_running_memory_save(wait_for_queued=True)
|
||||
|
||||
def _on_crew_failed(self, error: str) -> None:
|
||||
self._unsubscribe()
|
||||
with self._lock:
|
||||
self._status = "failed"
|
||||
self._error = error
|
||||
@@ -692,12 +740,150 @@ FooterKey .footer-key--key {
|
||||
now = time.time()
|
||||
for entry in self._log_entries:
|
||||
if entry["status"] == "running":
|
||||
if entry["tool_name"] == "memory_save":
|
||||
continue
|
||||
entry["status"] = "error"
|
||||
entry["error"] = "No result received before crew failed"
|
||||
entry["duration"] = now - entry["start_time"]
|
||||
self._tick()
|
||||
self.call_later(self._focus_activity_log)
|
||||
self._tick_timer.stop()
|
||||
self._tick_timer = self.set_interval(1 / 2, self._tick)
|
||||
self._unsubscribe_if_no_running_memory_save(wait_for_queued=True)
|
||||
|
||||
# ── Conversational flow execution ───────────────────────
|
||||
|
||||
def _start_conversational_session(self) -> None:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
set_suppress_tracing_messages,
|
||||
set_tui_mode,
|
||||
)
|
||||
|
||||
set_tui_mode(True)
|
||||
set_suppress_tracing_messages(True)
|
||||
with self._lock:
|
||||
self._status = "chatting"
|
||||
self._current_step = None
|
||||
self._elapsed_frozen = None
|
||||
self._conversation_previous_defer_trace_finalization = getattr(
|
||||
self._flow, "defer_trace_finalization", False
|
||||
)
|
||||
self._flow.defer_trace_finalization = True
|
||||
|
||||
try:
|
||||
input_widget = self.query_one("#conversation-input", Input)
|
||||
input_widget.display = True
|
||||
input_widget.focus()
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _finalize_conversational_session(self) -> None:
|
||||
if not (self._is_conversational and self._flow):
|
||||
return
|
||||
try:
|
||||
self._flow.finalize_session_traces()
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
previous = self._conversation_previous_defer_trace_finalization
|
||||
if previous is not None:
|
||||
try:
|
||||
self._flow.defer_trace_finalization = previous
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
if event.input.id != "conversation-input":
|
||||
return
|
||||
if not self._is_conversational:
|
||||
return
|
||||
|
||||
message = event.value.strip()
|
||||
event.input.value = ""
|
||||
if not message:
|
||||
return
|
||||
if message.lower() in self._conversation_exit_commands:
|
||||
self._finalize_conversational_session()
|
||||
self._unsubscribe()
|
||||
self.exit(self._crew_result)
|
||||
return
|
||||
if self._conversation_turn_in_progress:
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
self._conversation_messages.append(("user", message))
|
||||
self._conversation_turn_in_progress = True
|
||||
self._conversation_turns += 1
|
||||
self._status = "working"
|
||||
self._current_step = ("yellow", "Thinking…", "")
|
||||
self._is_streaming = False
|
||||
self._streaming_text = ""
|
||||
self._task_full_output = ""
|
||||
self._current_llm_text = ""
|
||||
|
||||
event.input.disabled = True
|
||||
self._run_conversation_turn_worker(message)
|
||||
|
||||
@work(thread=True, exclusive=True, group="conversation")
|
||||
def _run_conversation_turn_worker(self, message: str) -> None:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
set_suppress_tracing_messages,
|
||||
set_tui_mode,
|
||||
)
|
||||
|
||||
set_tui_mode(True)
|
||||
set_suppress_tracing_messages(True)
|
||||
try:
|
||||
result = self._flow.handle_turn(message)
|
||||
if hasattr(result, "get_full_text") and hasattr(result, "result"):
|
||||
for _chunk in result:
|
||||
pass
|
||||
result = result.result
|
||||
self.call_from_thread(self._on_conversation_turn_done, result)
|
||||
except Exception as e:
|
||||
self.call_from_thread(self._on_conversation_turn_failed, str(e))
|
||||
|
||||
def _on_conversation_turn_done(self, result: Any) -> None:
|
||||
with self._lock:
|
||||
output = self._stringify_output(result)
|
||||
self._conversation_messages.append(("assistant", output))
|
||||
self._crew_result = result
|
||||
self._conversation_turn_in_progress = False
|
||||
self._status = "chatting"
|
||||
self._is_streaming = False
|
||||
self._streaming_text = ""
|
||||
self._current_step = None
|
||||
self._enable_conversation_input()
|
||||
self._tick()
|
||||
self._scroll_to_result()
|
||||
|
||||
def _on_conversation_turn_failed(self, error: str) -> None:
|
||||
with self._lock:
|
||||
self._status = "failed"
|
||||
self._error = error
|
||||
self._conversation_turn_in_progress = False
|
||||
self._is_streaming = False
|
||||
self._current_step = None
|
||||
self._enable_conversation_input()
|
||||
self._tick()
|
||||
|
||||
def _enable_conversation_input(self) -> None:
|
||||
try:
|
||||
input_widget = self.query_one("#conversation-input", Input)
|
||||
input_widget.disabled = False
|
||||
input_widget.focus()
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _stringify_output(self, result: Any) -> str:
|
||||
raw_result = getattr(result, "raw", result)
|
||||
if raw_result is None:
|
||||
return ""
|
||||
if isinstance(raw_result, str):
|
||||
return raw_result
|
||||
try:
|
||||
return _json.dumps(raw_result, default=str, ensure_ascii=False)
|
||||
except TypeError:
|
||||
return str(raw_result)
|
||||
|
||||
# ── Actions ─────────────────────────────────────────────
|
||||
|
||||
@@ -757,6 +943,7 @@ FooterKey .footer-key--key {
|
||||
self._refresh_log_panel()
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
self._finalize_conversational_session()
|
||||
self._unsubscribe()
|
||||
self.exit(self._crew_result)
|
||||
|
||||
@@ -932,6 +1119,30 @@ FooterKey .footer-key--key {
|
||||
t = Text()
|
||||
sidebar_width = 30
|
||||
|
||||
if self._is_conversational:
|
||||
t.append(" CONVERSATION\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
if self._conversation_turn_in_progress:
|
||||
t.append(f" {self._spinner()} ", style=_C_PRIMARY)
|
||||
t.append("Working\n", style=f"bold {_C_TEXT}")
|
||||
elif self._status == "failed":
|
||||
t.append(" ✘ Failed\n", style=_C_RED)
|
||||
else:
|
||||
t.append(" ● Ready\n", style=_C_GREEN)
|
||||
t.append(f" Turns {self._conversation_turns}\n", style=_C_DIM)
|
||||
t.append("\n")
|
||||
t.append(" TOKENS\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
out = self._output_tokens + self._live_out_tokens
|
||||
t.append(f" ↑ {self._input_tokens:,}\n", style=_C_DIM)
|
||||
t.append(f" ↓ {out:,}\n", style=_C_DIM)
|
||||
t.append("\n")
|
||||
t.append(" COMMANDS\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
t.append(" quit / exit\n", style=_C_DIM)
|
||||
widget.update(t)
|
||||
return
|
||||
|
||||
t.append(" TASKS\n", style=f"bold {_C_PRIMARY}")
|
||||
t.append("\n")
|
||||
|
||||
@@ -985,6 +1196,22 @@ FooterKey .footer-key--key {
|
||||
widget = self.query_one("#task-header", Static)
|
||||
t = Text()
|
||||
|
||||
if self._is_conversational:
|
||||
if self._status == "failed":
|
||||
t.append("✘ ", style=f"bold {_C_RED}")
|
||||
t.append("Failed", style=f"bold {_C_RED}")
|
||||
if self._error:
|
||||
t.append(f"\n{self._error[:120]}", style=_C_RED)
|
||||
elif self._conversation_turn_in_progress:
|
||||
t.append(f"{self._spinner()} ", style=_C_PRIMARY)
|
||||
t.append("Flow is responding", style=f"bold {_C_PRIMARY}")
|
||||
else:
|
||||
t.append("● ", style=f"bold {_C_GREEN}")
|
||||
t.append("Conversational flow ready", style=f"bold {_C_GREEN}")
|
||||
t.append(" Type a message below", style=_C_DIM)
|
||||
widget.update(t)
|
||||
return
|
||||
|
||||
if self._status == "completed":
|
||||
elapsed = self._elapsed_frozen or (time.time() - self._start_time)
|
||||
t.append("✔ ", style=f"bold {_C_GREEN}")
|
||||
@@ -1036,6 +1263,41 @@ FooterKey .footer-key--key {
|
||||
t = Text()
|
||||
should_scroll = False
|
||||
|
||||
if self._is_conversational:
|
||||
if not self._conversation_messages and not self._is_streaming:
|
||||
t.append(" Start the conversation below.\n", style=_C_MUTED)
|
||||
for role, content in self._conversation_messages:
|
||||
if role == "user":
|
||||
t.append("\n You\n", style=f"bold {_C_TEAL}")
|
||||
else:
|
||||
t.append("\n Assistant\n", style=f"bold {_C_PRIMARY}")
|
||||
rendered = _format_json_in_text(_unescape_text(content))
|
||||
for line in rendered.split("\n"):
|
||||
style = _C_TEXT if role == "assistant" else _C_DIM
|
||||
t.append(f" {line}\n", style=style)
|
||||
|
||||
if self._is_streaming and self._streaming_text:
|
||||
text = _unescape_text(self._filtered_streaming_text())
|
||||
if text.strip():
|
||||
t.append("\n Assistant\n", style=f"bold {_C_PRIMARY}")
|
||||
for line in text.rstrip().split("\n")[-40:]:
|
||||
t.append(f" {line}\n", style=_C_TEXT)
|
||||
should_scroll = True
|
||||
|
||||
if self._status == "failed" and self._error:
|
||||
t.append("\n Error\n", style=f"bold {_C_RED}")
|
||||
t.append(f" {self._error}\n", style=_C_RED)
|
||||
|
||||
widget.update(t)
|
||||
if should_scroll:
|
||||
try:
|
||||
self.query_one("#scroll-area", VerticalScroll).scroll_end(
|
||||
animate=False
|
||||
)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
return
|
||||
|
||||
# Plan section
|
||||
if self._plan and self._plan.get("steps"):
|
||||
plan_title = self._plan.get("plan", "Plan")
|
||||
@@ -1514,6 +1776,53 @@ FooterKey .footer-key--key {
|
||||
pass
|
||||
self._event_handlers.clear()
|
||||
|
||||
def _has_running_memory_save_locked(self) -> bool:
|
||||
return any(
|
||||
entry["tool_name"] == "memory_save" and entry["status"] == "running"
|
||||
for entry in self._log_entries
|
||||
)
|
||||
|
||||
def _on_memory_save_drain_elapsed(self) -> None:
|
||||
self._memory_save_drain_timer = None
|
||||
self._unsubscribe_if_no_running_memory_save()
|
||||
|
||||
def _schedule_memory_save_drain_unsubscribe(self) -> bool:
|
||||
loop = getattr(self, "_loop", None)
|
||||
if loop is None:
|
||||
return False
|
||||
if getattr(self, "_thread_id", None) != threading.get_ident():
|
||||
try:
|
||||
loop.call_soon_threadsafe(self._schedule_memory_save_drain_unsubscribe)
|
||||
except RuntimeError:
|
||||
return False
|
||||
return True
|
||||
if self._memory_save_drain_timer is not None:
|
||||
self._memory_save_drain_timer.stop()
|
||||
self._memory_save_drain_timer = self.set_timer(
|
||||
_MEMORY_SAVE_DRAIN_GRACE_SECONDS,
|
||||
self._on_memory_save_drain_elapsed,
|
||||
name="memory-save-drain",
|
||||
)
|
||||
return True
|
||||
|
||||
def _unsubscribe_if_no_running_memory_save(
|
||||
self, *, wait_for_queued: bool = False
|
||||
) -> None:
|
||||
with self._lock:
|
||||
should_unsubscribe = (
|
||||
self._status
|
||||
in {
|
||||
"completed",
|
||||
"failed",
|
||||
}
|
||||
and not self._has_running_memory_save_locked()
|
||||
)
|
||||
|
||||
if should_unsubscribe:
|
||||
if wait_for_queued and self._schedule_memory_save_drain_unsubscribe():
|
||||
return
|
||||
self._unsubscribe()
|
||||
|
||||
def _subscribe(self) -> None:
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.crew_events import CrewKickoffStartedEvent
|
||||
@@ -1802,6 +2111,8 @@ FooterKey .footer-key--key {
|
||||
entry["status"] == "running"
|
||||
and entry["tool_name"] != event.tool_name
|
||||
):
|
||||
if entry["tool_name"] == "memory_save":
|
||||
continue
|
||||
entry["status"] = "timeout"
|
||||
entry["error"] = (
|
||||
"No result received before the next tool started"
|
||||
@@ -1830,6 +2141,7 @@ FooterKey .footer-key--key {
|
||||
"duration": None,
|
||||
"task_idx": self._current_task_idx,
|
||||
"plan_step_number": plan_step_number,
|
||||
"event_id": event.event_id,
|
||||
}
|
||||
)
|
||||
self._complete_step("teal", f"⚡ {event.tool_name}…")
|
||||
@@ -1923,8 +2235,178 @@ FooterKey .footer-key--key {
|
||||
MemoryRetrievalCompletedEvent,
|
||||
MemoryRetrievalFailedEvent,
|
||||
MemoryRetrievalStartedEvent,
|
||||
MemorySaveCompletedEvent,
|
||||
MemorySaveFailedEvent,
|
||||
MemorySaveStartedEvent,
|
||||
)
|
||||
|
||||
def is_nested_save_to_memory_event(event: Any) -> bool:
|
||||
if event.parent_event_id is None:
|
||||
return False
|
||||
state = crewai_event_bus.runtime_state
|
||||
if state is None:
|
||||
return False
|
||||
parent_node = state.event_record.nodes.get(event.parent_event_id)
|
||||
parent_event = getattr(parent_node, "event", None)
|
||||
return getattr(
|
||||
parent_event, "type", None
|
||||
) == "tool_usage_started" and _is_save_to_memory_tool(
|
||||
getattr(parent_event, "tool_name", None)
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MemorySaveStartedEvent)
|
||||
def on_memory_save_started(source: Any, event: MemorySaveStartedEvent) -> None:
|
||||
with self._lock:
|
||||
if is_nested_save_to_memory_event(event):
|
||||
self._suppressed_memory_save_event_ids.add(event.event_id)
|
||||
return
|
||||
for entry in reversed(self._log_entries):
|
||||
if (
|
||||
_is_save_to_memory_tool(entry["tool_name"])
|
||||
and entry.get("event_id") == event.parent_event_id
|
||||
):
|
||||
self._suppressed_memory_save_event_ids.add(event.event_id)
|
||||
return
|
||||
for entry in reversed(self._log_entries):
|
||||
if (
|
||||
entry["tool_name"] == "memory_save"
|
||||
and entry.get("started_event_id") == event.event_id
|
||||
):
|
||||
entry["args"] = _truncate_log_text(
|
||||
event.value, _LOG_ARGS_TEXT_LIMIT
|
||||
)
|
||||
return
|
||||
self._log_entries.append(
|
||||
{
|
||||
"tool_name": "memory_save",
|
||||
"status": "running",
|
||||
"args": _truncate_log_text(event.value, _LOG_ARGS_TEXT_LIMIT),
|
||||
"result": None,
|
||||
"error": None,
|
||||
"start_time": time.time(),
|
||||
"duration": None,
|
||||
"task_idx": self._current_task_idx,
|
||||
"event_id": event.event_id,
|
||||
}
|
||||
)
|
||||
|
||||
self._register_handler(MemorySaveStartedEvent, on_memory_save_started)
|
||||
|
||||
@crewai_event_bus.on(MemorySaveCompletedEvent)
|
||||
def on_memory_save_completed(
|
||||
source: Any, event: MemorySaveCompletedEvent
|
||||
) -> None:
|
||||
with self._lock:
|
||||
if (
|
||||
event.started_event_id in self._suppressed_memory_save_event_ids
|
||||
or is_nested_save_to_memory_event(event)
|
||||
):
|
||||
if event.started_event_id is not None:
|
||||
self._suppressed_memory_save_event_ids.discard(
|
||||
event.started_event_id
|
||||
)
|
||||
else:
|
||||
for entry in reversed(self._log_entries):
|
||||
has_started_event_match = (
|
||||
event.started_event_id is not None
|
||||
and (
|
||||
entry.get("event_id") == event.started_event_id
|
||||
or entry.get("started_event_id")
|
||||
== event.started_event_id
|
||||
)
|
||||
)
|
||||
has_running_event_without_id = (
|
||||
event.started_event_id is None
|
||||
and entry["status"] == "running"
|
||||
)
|
||||
if entry["tool_name"] == "memory_save" and (
|
||||
has_running_event_without_id or has_started_event_match
|
||||
):
|
||||
entry["status"] = "success"
|
||||
entry["duration"] = event.save_time_ms / 1000
|
||||
entry["result"] = _truncate_log_text(
|
||||
event.value, _LOG_RESULT_TEXT_LIMIT
|
||||
)
|
||||
entry["error"] = None
|
||||
entry["started_event_id"] = event.started_event_id
|
||||
break
|
||||
else:
|
||||
self._log_entries.append(
|
||||
{
|
||||
"tool_name": "memory_save",
|
||||
"status": "success",
|
||||
"args": None,
|
||||
"result": _truncate_log_text(
|
||||
event.value, _LOG_RESULT_TEXT_LIMIT
|
||||
),
|
||||
"error": None,
|
||||
"start_time": time.time(),
|
||||
"duration": event.save_time_ms / 1000,
|
||||
"task_idx": self._current_task_idx,
|
||||
"started_event_id": event.started_event_id,
|
||||
}
|
||||
)
|
||||
|
||||
self._unsubscribe_if_no_running_memory_save(wait_for_queued=True)
|
||||
|
||||
self._register_handler(MemorySaveCompletedEvent, on_memory_save_completed)
|
||||
|
||||
@crewai_event_bus.on(MemorySaveFailedEvent)
|
||||
def on_memory_save_failed(source: Any, event: MemorySaveFailedEvent) -> None:
|
||||
with self._lock:
|
||||
if (
|
||||
event.started_event_id in self._suppressed_memory_save_event_ids
|
||||
or is_nested_save_to_memory_event(event)
|
||||
):
|
||||
if event.started_event_id is not None:
|
||||
self._suppressed_memory_save_event_ids.discard(
|
||||
event.started_event_id
|
||||
)
|
||||
else:
|
||||
for idx, entry in reversed(list(enumerate(self._log_entries))):
|
||||
has_started_event_match = (
|
||||
event.started_event_id is not None
|
||||
and (
|
||||
entry.get("event_id") == event.started_event_id
|
||||
or entry.get("started_event_id")
|
||||
== event.started_event_id
|
||||
)
|
||||
)
|
||||
has_running_event_without_id = (
|
||||
event.started_event_id is None
|
||||
and entry["status"] == "running"
|
||||
)
|
||||
if entry["tool_name"] == "memory_save" and (
|
||||
has_running_event_without_id or has_started_event_match
|
||||
):
|
||||
entry["status"] = "error"
|
||||
entry["error"] = event.error
|
||||
entry["duration"] = time.time() - entry["start_time"]
|
||||
entry["started_event_id"] = event.started_event_id
|
||||
self._log_expanded.add(idx)
|
||||
break
|
||||
else:
|
||||
self._log_entries.append(
|
||||
{
|
||||
"tool_name": "memory_save",
|
||||
"status": "error",
|
||||
"args": _truncate_log_text(
|
||||
event.value, _LOG_ARGS_TEXT_LIMIT
|
||||
),
|
||||
"result": None,
|
||||
"error": event.error,
|
||||
"start_time": time.time(),
|
||||
"duration": 0,
|
||||
"task_idx": self._current_task_idx,
|
||||
"started_event_id": event.started_event_id,
|
||||
}
|
||||
)
|
||||
self._log_expanded.add(len(self._log_entries) - 1)
|
||||
|
||||
self._unsubscribe_if_no_running_memory_save(wait_for_queued=True)
|
||||
|
||||
self._register_handler(MemorySaveFailedEvent, on_memory_save_failed)
|
||||
|
||||
@crewai_event_bus.on(MemoryRetrievalStartedEvent)
|
||||
def on_memory_retrieval_started(
|
||||
source: Any, event: MemoryRetrievalStartedEvent
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import Any
|
||||
import zipfile
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.deploy.validate import normalize_package_name
|
||||
from crewai_cli.utils import parse_toml
|
||||
|
||||
|
||||
_EXCLUDED_DIRS = {
|
||||
@@ -38,8 +34,6 @@ _EXCLUDED_SUFFIXES = {
|
||||
".pyc",
|
||||
".pyo",
|
||||
}
|
||||
_SCRIPT_KEY_PATTERN = re.compile(r"^\s*(?P<key>[A-Za-z0-9_.-]+|\"[^\"]+\"|'[^']+')\s*=")
|
||||
_SECTION_PATTERN = re.compile(r"^\s*\[[^\]]+\]\s*(?:#.*)?$")
|
||||
|
||||
|
||||
def create_project_zip(
|
||||
@@ -143,267 +137,7 @@ def _stage_project(root: Path, files: list[Path]) -> Path:
|
||||
destination = staging_root / relative_path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
if _is_json_crew_project(staging_root):
|
||||
_add_json_crew_deploy_wrapper(staging_root)
|
||||
except Exception:
|
||||
shutil.rmtree(staging_root, ignore_errors=True)
|
||||
raise
|
||||
return staging_root
|
||||
|
||||
|
||||
def _is_json_crew_project(root: Path) -> bool:
|
||||
"""Return True for JSON crew projects that need a Python deploy wrapper."""
|
||||
if not ((root / "crew.jsonc").is_file() or (root / "crew.json").is_file()):
|
||||
return False
|
||||
|
||||
project = _read_pyproject(root)
|
||||
tool_config = project.get("tool") or {}
|
||||
crewai_config = tool_config.get("crewai") if isinstance(tool_config, dict) else None
|
||||
declared_type = (
|
||||
crewai_config.get("type") if isinstance(crewai_config, dict) else None
|
||||
)
|
||||
if declared_type == "flow":
|
||||
return False
|
||||
|
||||
package_name = _package_name(root)
|
||||
if package_name is None:
|
||||
raise ValueError(
|
||||
"Could not derive a valid Python package name from [project].name."
|
||||
)
|
||||
|
||||
return not (root / "src" / package_name / "crew.py").is_file()
|
||||
|
||||
|
||||
def _read_pyproject(root: Path) -> dict[str, Any]:
|
||||
"""Read pyproject.toml, returning an empty mapping on missing or invalid data."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
if not pyproject_path.is_file():
|
||||
return {}
|
||||
try:
|
||||
pyproject = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
return pyproject if isinstance(pyproject, dict) else {}
|
||||
|
||||
|
||||
def _package_name(root: Path) -> str | None:
|
||||
"""Return the normalized Python package name for the project."""
|
||||
project = _read_pyproject(root).get("project")
|
||||
if not isinstance(project, dict):
|
||||
return None
|
||||
|
||||
name = project.get("name")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
return None
|
||||
|
||||
package_name = normalize_package_name(name)
|
||||
return package_name or None
|
||||
|
||||
|
||||
def _class_name(package_name: str) -> str:
|
||||
"""Return the generated wrapper class name for a package."""
|
||||
parts = [part for part in re.split(r"[^a-zA-Z0-9]+", package_name) if part]
|
||||
class_name = "".join(part[:1].upper() + part[1:] for part in parts)
|
||||
if not class_name:
|
||||
return "JsonCrew"
|
||||
if class_name[0].isdigit():
|
||||
return f"Crew{class_name}"
|
||||
return class_name
|
||||
|
||||
|
||||
def _add_json_crew_deploy_wrapper(root: Path) -> None:
|
||||
"""Add Python wrapper files required to deploy a JSON crew project."""
|
||||
package_name = _package_name(root)
|
||||
if package_name is None:
|
||||
raise ValueError(
|
||||
"Could not derive a valid Python package name from [project].name."
|
||||
)
|
||||
|
||||
package_dir = root / "src" / package_name
|
||||
config_dir = package_dir / "config"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class_name = _class_name(package_name)
|
||||
crew_filename = "crew.jsonc" if (root / "crew.jsonc").is_file() else "crew.json"
|
||||
|
||||
(package_dir / "__init__.py").write_text("", encoding="utf-8")
|
||||
(config_dir / "agents.yaml").write_text("{}\n", encoding="utf-8")
|
||||
(config_dir / "tasks.yaml").write_text("{}\n", encoding="utf-8")
|
||||
(package_dir / "crew.py").write_text(
|
||||
_json_crew_py(class_name, crew_filename),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(package_dir / "main.py").write_text(
|
||||
_json_main_py(package_name, class_name),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_ensure_project_scripts(root, package_name)
|
||||
|
||||
|
||||
def _json_crew_py(class_name: str, crew_filename: str) -> str:
|
||||
"""Render the generated crew.py module for a JSON crew."""
|
||||
return f'''from pathlib import Path
|
||||
|
||||
from crewai import Crew
|
||||
from crewai.project import CrewBase, crew
|
||||
from crewai.project.crew_loader import load_crew
|
||||
|
||||
|
||||
def _crew_path() -> Path:
|
||||
return Path(__file__).resolve().parents[2] / "{crew_filename}"
|
||||
|
||||
|
||||
@CrewBase
|
||||
class {class_name}:
|
||||
"""Compatibility wrapper for a JSON-defined CrewAI project."""
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
crew_instance, default_inputs = load_crew(_crew_path())
|
||||
self.default_inputs = default_inputs
|
||||
return crew_instance
|
||||
'''
|
||||
|
||||
|
||||
def _json_main_py(package_name: str, class_name: str) -> str:
|
||||
"""Render the generated main.py entrypoints for a JSON crew."""
|
||||
return f"""#!/usr/bin/env python
|
||||
import json
|
||||
import sys
|
||||
|
||||
from {package_name}.crew import {class_name}
|
||||
|
||||
|
||||
def _load():
|
||||
wrapper = {class_name}()
|
||||
crew = wrapper.crew()
|
||||
return crew, getattr(wrapper, "default_inputs", {{}})
|
||||
|
||||
|
||||
def run():
|
||||
crew, inputs = _load()
|
||||
return crew.kickoff(inputs=inputs)
|
||||
|
||||
|
||||
def train():
|
||||
crew, inputs = _load()
|
||||
return crew.train(
|
||||
n_iterations=int(sys.argv[1]),
|
||||
filename=sys.argv[2],
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
|
||||
def replay():
|
||||
crew, _ = _load()
|
||||
return crew.replay(task_id=sys.argv[1])
|
||||
|
||||
|
||||
def test():
|
||||
crew, inputs = _load()
|
||||
return crew.test(
|
||||
n_iterations=int(sys.argv[1]),
|
||||
eval_llm=sys.argv[2],
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
|
||||
def run_with_trigger():
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError("No trigger payload provided.")
|
||||
|
||||
crew, inputs = _load()
|
||||
trigger_payload = json.loads(sys.argv[1])
|
||||
return crew.kickoff(
|
||||
inputs={{**inputs, "crewai_trigger_payload": trigger_payload}}
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _ensure_project_scripts(root: Path, package_name: str) -> None:
|
||||
"""Ensure generated wrappers have project script entrypoints."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
if not pyproject_path.is_file():
|
||||
return
|
||||
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
entries = _project_script_entries(package_name)
|
||||
pyproject_path.write_text(
|
||||
_update_project_scripts(content, entries),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _project_script_entries(package_name: str) -> dict[str, str]:
|
||||
"""Return script entrypoints required by the generated JSON wrapper."""
|
||||
return {
|
||||
package_name: f"{package_name}.main:run",
|
||||
"run_crew": f"{package_name}.main:run",
|
||||
"train": f"{package_name}.main:train",
|
||||
"replay": f"{package_name}.main:replay",
|
||||
"test": f"{package_name}.main:test",
|
||||
"run_with_trigger": f"{package_name}.main:run_with_trigger",
|
||||
}
|
||||
|
||||
|
||||
def _update_project_scripts(content: str, entries: dict[str, str]) -> str:
|
||||
"""Add or replace generated script entries in pyproject.toml content."""
|
||||
lines = content.rstrip().splitlines()
|
||||
header_index = _project_scripts_header_index(lines)
|
||||
if header_index is None:
|
||||
return content.rstrip() + _project_scripts_block(entries)
|
||||
|
||||
end_index = _section_end_index(lines, header_index + 1)
|
||||
seen: set[str] = set()
|
||||
for index in range(header_index + 1, end_index):
|
||||
key = _script_key(lines[index])
|
||||
if key in entries:
|
||||
lines[index] = _script_line(key, entries[key])
|
||||
seen.add(key)
|
||||
|
||||
missing_lines = [
|
||||
_script_line(key, value) for key, value in entries.items() if key not in seen
|
||||
]
|
||||
lines[end_index:end_index] = missing_lines
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def _project_scripts_header_index(lines: list[str]) -> int | None:
|
||||
"""Return the line index of the project scripts table, if present."""
|
||||
for index, line in enumerate(lines):
|
||||
if line.strip() == "[project.scripts]":
|
||||
return index
|
||||
return None
|
||||
|
||||
|
||||
def _section_end_index(lines: list[str], start_index: int) -> int:
|
||||
"""Return the exclusive end index for a TOML table section."""
|
||||
for index in range(start_index, len(lines)):
|
||||
if _SECTION_PATTERN.match(lines[index]):
|
||||
return index
|
||||
return len(lines)
|
||||
|
||||
|
||||
def _script_key(line: str) -> str | None:
|
||||
"""Return the script key for a pyproject script line."""
|
||||
match = _SCRIPT_KEY_PATTERN.match(line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
key = match.group("key")
|
||||
if key.startswith(("'", '"')) and key.endswith(("'", '"')):
|
||||
return key[1:-1]
|
||||
return key
|
||||
|
||||
|
||||
def _script_line(key: str, value: str) -> str:
|
||||
"""Render a project script TOML entry."""
|
||||
return f'{key} = "{value}"'
|
||||
|
||||
|
||||
def _project_scripts_block(entries: dict[str, str]) -> str:
|
||||
"""Render a project scripts TOML table."""
|
||||
lines = ["", "", "[project.scripts]"]
|
||||
lines.extend(_script_line(key, value) for key, value in entries.items())
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
@@ -212,8 +212,16 @@ class DeployValidator:
|
||||
if crew_path is None:
|
||||
return self.results
|
||||
|
||||
agents_dir = self.project_root / "agents"
|
||||
|
||||
self._check_pyproject()
|
||||
self._check_lockfile()
|
||||
agents_dir_ok = self._check_json_agents_dir(agents_dir)
|
||||
|
||||
project = None
|
||||
try:
|
||||
project = validate_crew_project(crew_path, self.project_root / "agents")
|
||||
if agents_dir_ok:
|
||||
project = validate_crew_project(crew_path, agents_dir)
|
||||
except JSONProjectValidationError as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
@@ -232,15 +240,27 @@ class DeployValidator:
|
||||
)
|
||||
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)
|
||||
if project is not None:
|
||||
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
|
||||
self._check_version_vs_lockfile()
|
||||
|
||||
return self.results
|
||||
|
||||
def _check_json_agents_dir(self, agents_dir: Path) -> bool:
|
||||
if agents_dir.is_dir():
|
||||
return True
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"missing_agents_dir",
|
||||
"Cannot find agents/ directory",
|
||||
detail=(
|
||||
"JSON crew projects load agent definitions from "
|
||||
f"{agents_dir.relative_to(self.project_root)}/*.jsonc or *.json."
|
||||
),
|
||||
hint="Create agents/ and add one JSON or JSONC file per agent.",
|
||||
)
|
||||
return False
|
||||
|
||||
def _check_env_vars_json(
|
||||
self, crew_path: Path, agents_dir: Path, agent_names: list[str]
|
||||
) -> None:
|
||||
|
||||
@@ -1,12 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def _project_script_target(script_name: str) -> str | None:
|
||||
try:
|
||||
from crewai_cli.utils import read_toml
|
||||
|
||||
pyproject = read_toml()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
target = pyproject.get("project", {}).get("scripts", {}).get(script_name)
|
||||
return target if isinstance(target, str) else None
|
||||
|
||||
|
||||
def _prepare_project_import_path() -> None:
|
||||
cwd = Path.cwd()
|
||||
for path in (cwd / "src", cwd):
|
||||
path_str = str(path)
|
||||
if path.exists() and path_str not in sys.path:
|
||||
sys.path.insert(0, path_str)
|
||||
|
||||
|
||||
def _load_conversational_flow_from_kickoff_script() -> Any | None:
|
||||
target = _project_script_target("kickoff")
|
||||
if not target or ":" not in target:
|
||||
return None
|
||||
|
||||
module_name, _callable_name = target.split(":", 1)
|
||||
_prepare_project_import_path()
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
from crewai.flow.flow import Flow
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for value in vars(module).values():
|
||||
if (
|
||||
inspect.isclass(value)
|
||||
and value is not Flow
|
||||
and issubclass(value, Flow)
|
||||
and getattr(value, "conversational", False)
|
||||
):
|
||||
return value()
|
||||
|
||||
for value in vars(module).values():
|
||||
if (
|
||||
isinstance(value, Flow)
|
||||
and getattr(value, "conversational", False)
|
||||
and callable(getattr(value, "handle_turn", None))
|
||||
):
|
||||
return value
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _run_conversational_flow_tui(flow: Any) -> Any:
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
app = CrewRunApp(
|
||||
crew_name=getattr(flow, "name", None) or type(flow).__name__,
|
||||
conversational=True,
|
||||
)
|
||||
app._flow = flow
|
||||
app.run()
|
||||
|
||||
if app._status == "failed":
|
||||
raise SystemExit(1)
|
||||
|
||||
return app._crew_result
|
||||
|
||||
|
||||
def kickoff_flow() -> None:
|
||||
"""
|
||||
Kickoff the flow by running a command in the UV environment.
|
||||
"""
|
||||
flow = _load_conversational_flow_from_kickoff_script()
|
||||
if flow is not None:
|
||||
_run_conversational_flow_tui(flow)
|
||||
return
|
||||
|
||||
command = ["uv", "run", "kickoff"]
|
||||
|
||||
try:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
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
|
||||
|
||||
@@ -27,17 +26,21 @@ 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.
|
||||
_INPUT_PLACEHOLDER_RE = re.compile(r"(?<!{){([A-Za-z_][A-Za-z0-9_\-]*)}(?!})")
|
||||
_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV = "CREWAI_CLI_RUNNER_PACKAGE_DIR"
|
||||
_CREWAI_RUNNER_SOURCE_DIR_ENV = "CREWAI_RUNNER_SOURCE_DIR"
|
||||
_FULL_CREWAI_INSTALL_MESSAGE = """\
|
||||
CrewAI CLI is installed without the `crewai` package required to run crews.
|
||||
|
||||
Install the full CrewAI prerelease package:
|
||||
|
||||
uv tool install --force --prerelease=allow 'crewai[tools]==1.14.8a1'
|
||||
|
||||
The quotes are required in zsh so `crewai[tools]` is not treated as a glob.
|
||||
"""
|
||||
_JSON_CREW_RUNNER_CODE = """
|
||||
import importlib.util
|
||||
import os
|
||||
@@ -72,12 +75,39 @@ module_spec.loader.exec_module(module)
|
||||
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
|
||||
module._run_json_crew(
|
||||
trained_agents_file=os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV)
|
||||
)
|
||||
try:
|
||||
module._run_json_crew(
|
||||
trained_agents_file=os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV)
|
||||
)
|
||||
except module.click.ClickException as exc:
|
||||
exc.show()
|
||||
raise SystemExit(exc.exit_code)
|
||||
""".strip()
|
||||
|
||||
|
||||
def _import_find_crew_json_file() -> Callable[[], Path | None]:
|
||||
from crewai.project.json_loader import find_crew_json_file as _find_crew_json_file
|
||||
|
||||
return cast("Callable[[], Path | None]", _find_crew_json_file)
|
||||
|
||||
|
||||
def _is_missing_crewai_package(exc: ModuleNotFoundError) -> bool:
|
||||
return bool(exc.name and exc.name.startswith("crewai"))
|
||||
|
||||
|
||||
def _full_crewai_install_error() -> click.ClickException:
|
||||
return click.ClickException(_FULL_CREWAI_INSTALL_MESSAGE)
|
||||
|
||||
|
||||
def find_crew_json_file() -> Path | None:
|
||||
try:
|
||||
return _import_find_crew_json_file()()
|
||||
except ModuleNotFoundError as exc:
|
||||
if _is_missing_crewai_package(exc):
|
||||
raise _full_crewai_install_error() from exc
|
||||
raise
|
||||
|
||||
|
||||
def _has_json_crew() -> bool:
|
||||
"""Check if this is a JSON-defined crew project.
|
||||
|
||||
@@ -501,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:
|
||||
@@ -509,15 +543,98 @@ 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
|
||||
|
||||
from crewai_cli.kickoff_flow import (
|
||||
_load_conversational_flow_from_kickoff_script,
|
||||
_run_conversational_flow_tui,
|
||||
)
|
||||
|
||||
flow = _load_conversational_flow_from_kickoff_script()
|
||||
if flow is not None:
|
||||
_run_conversational_flow_tui(flow)
|
||||
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)
|
||||
@@ -528,25 +645,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:
|
||||
@@ -556,21 +670,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:
|
||||
|
||||
241
lib/cli/src/crewai_cli/run_declarative_flow.py
Normal file
241
lib/cli/src/crewai_cli/run_declarative_flow.py
Normal file
@@ -0,0 +1,241 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path, PureWindowsPath
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
|
||||
|
||||
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
|
||||
|
||||
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 | 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."""
|
||||
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 | 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."""
|
||||
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
|
||||
definition = definition.strip()
|
||||
if not definition:
|
||||
return None
|
||||
|
||||
return _resolve_project_definition_path(
|
||||
definition=definition,
|
||||
project_root=project_root or Path.cwd(),
|
||||
)
|
||||
|
||||
|
||||
def _resolve_project_definition_path(definition: str, project_root: Path) -> Path:
|
||||
definition_path = Path(definition)
|
||||
windows_definition_path = PureWindowsPath(definition)
|
||||
|
||||
if definition.startswith("~"):
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must be a project-local path; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if definition_path.is_absolute() or windows_definition_path.is_absolute():
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must be relative to the project root; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
try:
|
||||
root = project_root.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise click.UsageError(
|
||||
f"Invalid project root for [tool.crewai] definition: {exc}"
|
||||
) from exc
|
||||
|
||||
candidate = root / definition_path
|
||||
try:
|
||||
resolved_candidate = candidate.resolve(strict=False)
|
||||
except OSError as exc:
|
||||
raise click.UsageError(
|
||||
f"Invalid [tool.crewai] definition path {definition!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
if not resolved_candidate.is_relative_to(root):
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must resolve inside the project root; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if not resolved_candidate.exists():
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must point to an existing file; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if not resolved_candidate.is_file():
|
||||
raise click.UsageError(
|
||||
"[tool.crewai] definition must point to a regular file; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
return resolved_candidate
|
||||
|
||||
|
||||
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 _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)
|
||||
@@ -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)
|
||||
@@ -62,7 +62,7 @@ crewai create flow <name> --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)
|
||||
@@ -767,10 +767,11 @@ class CustomSearchTool(BaseTool):
|
||||
```python
|
||||
from crewai.tools import tool
|
||||
|
||||
@tool("Calculator")
|
||||
def calculator(expression: str) -> str:
|
||||
"""Evaluates a mathematical expression and returns the result."""
|
||||
return str(eval(expression))
|
||||
@tool("WordCount")
|
||||
def word_count(text: str) -> str:
|
||||
"""Counts the number of words in the given text."""
|
||||
count = len(text.split())
|
||||
return f"Word count: {count}"
|
||||
```
|
||||
|
||||
### Built-in Tools (install with `uv add crewai-tools`)
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.7"
|
||||
"crewai[tools]==1.14.8a3"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
5
lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore
vendored
Normal file
5
lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
.crewai/
|
||||
output/
|
||||
17
lib/cli/src/crewai_cli/templates/declarative_flow/README.md
Normal file
17
lib/cli/src/crewai_cli/templates/declarative_flow/README.md
Normal file
@@ -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/`.
|
||||
15
lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml
Normal file
15
lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml
Normal file
@@ -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
|
||||
@@ -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.8a3"
|
||||
]
|
||||
|
||||
[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"
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.7"
|
||||
"crewai[tools]==1.14.8a3"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.7"
|
||||
"crewai[tools]==1.14.8a3"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
Reference in New Issue
Block a user