mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 06:08:15 +00:00
Merge branch 'main' of github.com:crewAIInc/crewAI into lorenze/imp/streaming
This commit is contained in:
@@ -4,6 +4,8 @@ import shutil
|
||||
import click
|
||||
from crewai_core.telemetry import Telemetry
|
||||
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills")
|
||||
|
||||
@@ -71,6 +73,9 @@ def _create_python_flow(
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{flow_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
content = content.replace(
|
||||
"{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
|
||||
with open(dst_file, "w") as file:
|
||||
file.write(content)
|
||||
@@ -138,6 +143,9 @@ def _create_declarative_flow(
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{flow_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
content = content.replace(
|
||||
"{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
dst_file.write_text(content, encoding="utf-8")
|
||||
|
||||
(project_root / ".env").write_text("OPENAI_API_KEY=YOUR_API_KEY", encoding="utf-8")
|
||||
|
||||
@@ -18,8 +18,10 @@ from crewai_cli.utils import (
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
load_env_vars,
|
||||
render_template,
|
||||
write_env_file,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
# ── Provider / model data ───────────────────────────────────────
|
||||
@@ -78,60 +80,7 @@ _PROVIDER_MODELS: dict[str, list[tuple[str, str]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ── Static project files ───────────────────────────────────────
|
||||
|
||||
_PYPROJECT_TOML = """\
|
||||
[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.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"
|
||||
"""
|
||||
|
||||
_GITIGNORE = """\
|
||||
.env
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
report.md
|
||||
"""
|
||||
|
||||
_README = """\
|
||||
# {name}
|
||||
|
||||
A crewAI project using JSON-first configuration.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `agents/` - Agent definitions (JSONC)
|
||||
- `crew.jsonc` - Crew definition with tasks and configuration
|
||||
- `tools/` - Custom tools (Python)
|
||||
- `knowledge/` - Knowledge files for agents
|
||||
|
||||
> **Note:** `custom:<name>` tool references execute `tools/<name>.py` as local
|
||||
> Python code when the crew loads. Only run crew projects from sources you
|
||||
> trust.
|
||||
"""
|
||||
_TEMPLATES_DIR = Path(__file__).parent / "templates" / "json_crew"
|
||||
|
||||
|
||||
# ── Common tools for picker ────────────────────────────────────
|
||||
@@ -692,187 +641,64 @@ def _default_agents_and_tasks(
|
||||
def _agent_to_jsonc(agent: dict[str, Any]) -> str:
|
||||
"""Convert agent wizard data to JSONC string with comments."""
|
||||
has_planning = agent["planning"]
|
||||
delegation_val = "true" if agent["allow_delegation"] else "false"
|
||||
delegation_comma = "," if has_planning else ""
|
||||
|
||||
settings_lines = []
|
||||
settings_lines.append(" // Show detailed execution logs")
|
||||
settings_lines.append(' "verbose": false,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(
|
||||
" // Allow this agent to delegate tasks to other agents in the crew"
|
||||
settings_block = _render_json_crew_template(
|
||||
"agent_settings.jsonc",
|
||||
{
|
||||
"allow_delegation": "true" if agent["allow_delegation"] else "false",
|
||||
"delegation_comma": "," if has_planning else "",
|
||||
"planning_line": '"planning": true'
|
||||
if has_planning
|
||||
else '// "planning": false',
|
||||
},
|
||||
)
|
||||
settings_lines.append(f' "allow_delegation": {delegation_val}{delegation_comma}')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(
|
||||
" // Maximum reasoning iterations per task (prevents infinite loops)"
|
||||
|
||||
return _render_json_crew_template(
|
||||
"agent.jsonc",
|
||||
{
|
||||
"role_json": json.dumps(agent["role"]),
|
||||
"goal_json": json.dumps(agent["goal"]),
|
||||
"backstory_json": json.dumps(agent["backstory"]),
|
||||
"llm_json": json.dumps(agent["llm"]),
|
||||
"tools_json": json.dumps(agent["tools"]),
|
||||
"settings_block": settings_block,
|
||||
},
|
||||
)
|
||||
settings_lines.append(' // "max_iter": 25,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Maximum tokens for agent's response generation")
|
||||
settings_lines.append(' // "max_tokens": null,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Maximum execution time in seconds")
|
||||
settings_lines.append(' // "max_execution_time": null,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Maximum LLM requests per minute (rate limiting)")
|
||||
settings_lines.append(' // "max_rpm": null,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Enable agent-level memory (persists across tasks)")
|
||||
settings_lines.append(' // "memory": false,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Cache tool results to avoid duplicate calls")
|
||||
settings_lines.append(' // "cache": true,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(
|
||||
" // Auto-summarize context when it exceeds the LLM's context window"
|
||||
)
|
||||
settings_lines.append(' // "respect_context_window": true,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Maximum retries on execution errors")
|
||||
settings_lines.append(' // "max_retry_limit": 2,')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Enable step-by-step planning before task execution")
|
||||
if has_planning:
|
||||
settings_lines.append(' "planning": true')
|
||||
else:
|
||||
settings_lines.append(' // "planning": false')
|
||||
settings_lines.append("")
|
||||
settings_lines.append(" // Include system prompt in LLM calls")
|
||||
settings_lines.append(' // "use_system_prompt": true')
|
||||
|
||||
settings_block = "\n".join(settings_lines)
|
||||
|
||||
return f"""\
|
||||
{{
|
||||
// Agent's role title — appears in prompts and logs.
|
||||
// You can use {{placeholder}} inputs in role, goal, or backstory.
|
||||
// Example: "role": "Senior {{industry}} Researcher"
|
||||
"role": {json.dumps(agent["role"])},
|
||||
|
||||
// Optional custom Agent subclass
|
||||
// "type": {{"python": "my_project.agents.CustomAgent"}},
|
||||
|
||||
// The agent's primary objective
|
||||
"goal": {json.dumps(agent["goal"])},
|
||||
|
||||
// Background story that shapes the agent's personality and approach
|
||||
"backstory": {json.dumps(agent["backstory"])},
|
||||
|
||||
// LLM model in provider/model format
|
||||
// Examples: "openai/gpt-4o", "anthropic/claude-sonnet-4-6", "ollama/llama3.3"
|
||||
// For custom endpoints or deployment-based providers, replace with:
|
||||
// "llm": {{"model": "llama3", "provider": "ollama", "base_url": "http://localhost:11434"}},
|
||||
// "llm": {{"deployment_name": "my-deployment", "provider": "azure", "api_version": "2024-10-21"}},
|
||||
"llm": {json.dumps(agent["llm"])},
|
||||
|
||||
// Override LLM used specifically for tool/function calling
|
||||
// "function_calling_llm": "openai/gpt-5.4-mini",
|
||||
|
||||
// Tools available to this agent
|
||||
// Built-in: "SerperDevTool", "ScrapeWebsiteTool", "FileReadTool", etc.
|
||||
// Custom: "custom:my_tool" loads from tools/my_tool.py
|
||||
"tools": {json.dumps(agent["tools"])},
|
||||
|
||||
// Optional agent-level guardrail — validates this agent's final output.
|
||||
// String guardrails are checked by an LLM and can reject/retry output.
|
||||
// Python refs must point to module-level functions/classes in trusted code.
|
||||
// "guardrail": "Only answer with information supported by retrieved evidence.",
|
||||
// "step_callback": {{"python": "my_project.callbacks.on_agent_step"}},
|
||||
// "guardrail_max_retries": 2,
|
||||
|
||||
// Advanced agent options:
|
||||
// Docs: https://docs.crewai.com/concepts/agents
|
||||
// "reasoning": true,
|
||||
// "max_reasoning_attempts": 3,
|
||||
// "planning_config": {{
|
||||
// "reasoning_effort": "medium",
|
||||
// "llm": {{"model": "deepseek-chat", "provider": "deepseek"}}
|
||||
// }},
|
||||
// "multimodal": false,
|
||||
// "allow_code_execution": false,
|
||||
// "code_execution_mode": "safe",
|
||||
// "knowledge_sources": [],
|
||||
// "knowledge_config": {{}},
|
||||
// "inject_date": true,
|
||||
// "date_format": "%Y-%m-%d",
|
||||
// "security_config": {{}},
|
||||
|
||||
// Agent behavior settings
|
||||
"settings": {{
|
||||
{settings_block}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def _task_to_json_fragment(task: dict[str, Any]) -> str:
|
||||
"""Convert task wizard data to a JSON-like fragment for embedding in crew JSONC."""
|
||||
lines = []
|
||||
lines.append(" {")
|
||||
lines.append(" // Task identifier")
|
||||
lines.append(f' "name": {json.dumps(task["name"])},')
|
||||
lines.append("")
|
||||
lines.append(" // What the task should accomplish")
|
||||
lines.append(
|
||||
" // Use {placeholder} inputs here; crewai run prompts for missing values"
|
||||
)
|
||||
lines.append(f' "description": {json.dumps(task["description"])},')
|
||||
lines.append("")
|
||||
lines.append(" // Clear definition of what the output should look like")
|
||||
lines.append(f' "expected_output": {json.dumps(task["expected_output"])},')
|
||||
lines.append("")
|
||||
lines.append(
|
||||
" // Optional task guardrail(s) validate output before completion"
|
||||
)
|
||||
lines.append(' // Use "guardrail" for one rule or "guardrails" for many')
|
||||
lines.append(" // Failed guardrails retry up to guardrail_max_retries times")
|
||||
lines.append(' // "guardrail": "Every factual claim needs context support.",')
|
||||
lines.append(' // "guardrails": [')
|
||||
lines.append(' // "Every factual claim must be supported by context.",')
|
||||
lines.append(' // "The answer must match the expected output format."')
|
||||
lines.append(" // ],")
|
||||
lines.append(' // "guardrail_max_retries": 2,')
|
||||
lines.append("")
|
||||
lines.append(" // Advanced task options:")
|
||||
lines.append(" // Docs: https://docs.crewai.com/concepts/tasks")
|
||||
lines.append(' // "type": "ConditionalTask",')
|
||||
lines.append(
|
||||
' // "condition": { "python": "my_project.conditions.should_run" },'
|
||||
)
|
||||
lines.append(
|
||||
' // "output_json": { "python": "my_project.models.ReportOutput" },'
|
||||
)
|
||||
lines.append(' // "output_pydantic": null,')
|
||||
lines.append(' // "response_model": null,')
|
||||
lines.append(
|
||||
' // "converter_cls": { "python": "my_project.converters.CustomConverter" },'
|
||||
)
|
||||
lines.append(' // "markdown": false,')
|
||||
lines.append(' // "input_files": { "brief": "data/brief.txt" },')
|
||||
lines.append(' // "security_config": {},')
|
||||
lines.append("")
|
||||
lines.append(" // Which agent handles this task")
|
||||
lines.append(f' "agent": {json.dumps(task["agent"])}')
|
||||
has_context = bool(task.get("context"))
|
||||
has_output_file = bool(task.get("output_file"))
|
||||
context_block = ""
|
||||
output_file_block = ""
|
||||
|
||||
if task.get("context"):
|
||||
lines[-1] += "," # add comma to agent line
|
||||
lines.append("")
|
||||
lines.append(" // Task outputs used as context")
|
||||
lines.append(f' "context": {json.dumps(task["context"])}')
|
||||
if has_context:
|
||||
context_block = (
|
||||
"\n\n"
|
||||
" // Task outputs used as context\n"
|
||||
f' "context": {json.dumps(task["context"])}'
|
||||
f"{',' if has_output_file else ''}"
|
||||
)
|
||||
|
||||
if task.get("output_file"):
|
||||
lines[-1] += ","
|
||||
lines.append("")
|
||||
lines.append(" // Save output to a file")
|
||||
lines.append(f' "output_file": {json.dumps(task["output_file"])}')
|
||||
if has_output_file:
|
||||
output_file_block = (
|
||||
"\n\n"
|
||||
" // Save output to a file\n"
|
||||
f' "output_file": {json.dumps(task["output_file"])}'
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(' // "tools": [],')
|
||||
lines.append(' // "human_input": false,')
|
||||
lines.append(' // "async_execution": false')
|
||||
lines.append(" }")
|
||||
return "\n".join(lines)
|
||||
return _render_json_crew_template(
|
||||
"task.jsonc",
|
||||
{
|
||||
"name_json": json.dumps(task["name"]),
|
||||
"description_json": json.dumps(task["description"]),
|
||||
"expected_output_json": json.dumps(task["expected_output"]),
|
||||
"agent_json": json.dumps(task["agent"]),
|
||||
"agent_comma": "," if has_context or has_output_file else "",
|
||||
"context_block": context_block,
|
||||
"output_file_block": output_file_block,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _crew_to_jsonc(
|
||||
@@ -892,69 +718,20 @@ def _crew_to_jsonc(
|
||||
inputs_lines[0] + "\n" + "\n".join(" " + line for line in inputs_lines[1:])
|
||||
)
|
||||
|
||||
process = settings.get("process", "sequential")
|
||||
memory = "true" if settings.get("memory") else "false"
|
||||
|
||||
return f"""\
|
||||
{{
|
||||
// Display name for this crew
|
||||
"name": {json.dumps(name)},
|
||||
|
||||
// Agents to include — each must have a matching agents/<name>.jsonc file
|
||||
"agents": {agent_names_json},
|
||||
|
||||
// Task definitions — executed in order for sequential process
|
||||
"tasks": [
|
||||
{tasks_fragments}
|
||||
],
|
||||
|
||||
// Execution process
|
||||
// "sequential" — tasks run in order, each receiving prior task outputs
|
||||
// "hierarchical" — a manager agent delegates tasks (requires manager_llm)
|
||||
"process": "{process}",
|
||||
|
||||
// Enable verbose logging during execution
|
||||
"verbose": true,
|
||||
|
||||
// Enable crew memory — persists context and learnings across tasks
|
||||
"memory": {memory},
|
||||
|
||||
// Automatically plan the execution strategy before running tasks
|
||||
// "planning": false,
|
||||
|
||||
// LLM for the planning step (used when planning is true)
|
||||
// "planning_llm": "openai/gpt-4o",
|
||||
|
||||
// LLM for the manager agent (required when process is "hierarchical")
|
||||
// "manager_llm": "openai/gpt-4o",
|
||||
|
||||
// Crew-level LLM fields also accept object form for custom endpoints
|
||||
// "chat_llm": {{"model": "llama3", "provider": "ollama", "base_url": "http://localhost:11434"}},
|
||||
|
||||
// Advanced crew options:
|
||||
// Docs: https://docs.crewai.com/concepts/crews
|
||||
// For hierarchical crews, manager_agent can reference an agents/<name>.jsonc file
|
||||
// that is not included in the "agents" list.
|
||||
// "manager_agent": "{agents[0]["name"]}",
|
||||
// "before_kickoff_callbacks": [{{"python": "my_project.callbacks.before_kickoff"}}],
|
||||
// "after_kickoff_callbacks": [{{"python": "my_project.callbacks.after_kickoff"}}],
|
||||
// "function_calling_llm": "openai/gpt-4o-mini",
|
||||
// "max_rpm": null,
|
||||
// "cache": true,
|
||||
// "knowledge_sources": [],
|
||||
// "embedder": {{}},
|
||||
// "output_log_file": "crew.log",
|
||||
// "stream": false,
|
||||
// "tracing": false,
|
||||
// "security_config": {{}},
|
||||
|
||||
// Optional runtime input defaults.
|
||||
// Use {{placeholder}} in agent or task text, for example:
|
||||
// "description": "Research {{topic}} and write a brief"
|
||||
// `crewai run` prompts for any placeholders missing from this object.
|
||||
"inputs": {inputs_json}
|
||||
}}
|
||||
"""
|
||||
return _render_json_crew_template(
|
||||
"crew.jsonc",
|
||||
{
|
||||
"name_json": json.dumps(name),
|
||||
"agent_names_json": agent_names_json,
|
||||
"tasks_fragments": tasks_fragments,
|
||||
"process_json": json.dumps(settings.get("process", "sequential")),
|
||||
"memory": memory,
|
||||
"manager_agent_name": agents[0]["name"],
|
||||
"inputs_json": inputs_json,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Model selection ─────────────────────────────────────────────
|
||||
@@ -1029,6 +806,12 @@ def _default_model_for_provider(provider: str | None) -> str | None:
|
||||
# ── Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _render_json_crew_template(
|
||||
template_name: str, replacements: dict[str, str] | None = None
|
||||
) -> str:
|
||||
return render_template(_TEMPLATES_DIR / template_name, replacements or {})
|
||||
|
||||
|
||||
def _write_jsonc(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
@@ -1134,22 +917,32 @@ def create_json_crew(
|
||||
|
||||
# Write pyproject.toml
|
||||
(folder_path / "pyproject.toml").write_text(
|
||||
_PYPROJECT_TOML.format(folder_name=folder_name, name=name),
|
||||
_render_json_crew_template(
|
||||
"pyproject.toml",
|
||||
{
|
||||
"folder_name": folder_name,
|
||||
"name": name,
|
||||
"crewai_tools_dependency": get_crewai_tools_dependency(),
|
||||
},
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Write .gitignore
|
||||
(folder_path / ".gitignore").write_text(_GITIGNORE, encoding="utf-8")
|
||||
(folder_path / ".gitignore").write_text(
|
||||
_render_json_crew_template(".gitignore"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Write README
|
||||
(folder_path / "README.md").write_text(
|
||||
_README.format(name=name),
|
||||
_render_json_crew_template("README.md", {"name": name}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Write knowledge placeholder
|
||||
(folder_path / "knowledge" / "user_preference.txt").write_text(
|
||||
"# Add your knowledge files here\n",
|
||||
_render_json_crew_template("knowledge/user_preference.txt"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import threading
|
||||
import time
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
from crewai_core.telemetry import Telemetry
|
||||
from rich.text import Text
|
||||
from textual import work
|
||||
from textual.app import App, ComposeResult
|
||||
@@ -581,6 +582,7 @@ FooterKey .footer-key--key {
|
||||
self._want_deploy: bool = False
|
||||
self._trace_url: str | None = None
|
||||
self._consent_screen: TraceConsentScreen | None = None
|
||||
self._telemetry: Telemetry | None = None
|
||||
|
||||
# ── Layout ──────────────────────────────────────────────
|
||||
|
||||
@@ -1056,10 +1058,21 @@ FooterKey .footer-key--key {
|
||||
self._unsubscribe()
|
||||
self.exit(self._crew_result)
|
||||
|
||||
def _record_tui_button_click(self, button_name: str) -> None:
|
||||
try:
|
||||
if self._telemetry is None:
|
||||
self._telemetry = Telemetry()
|
||||
self._telemetry.set_tracer()
|
||||
self._telemetry.feature_usage_span(f"cli_usage:{button_name}")
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id in ("btn-traces", "btn-traces-done"):
|
||||
self._record_tui_button_click("view_traces")
|
||||
self.action_view_traces()
|
||||
elif event.button.id == "btn-deploy":
|
||||
self._record_tui_button_click("deploy")
|
||||
self.action_deploy_crew()
|
||||
|
||||
def _scroll_to_result(self) -> None:
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
import webbrowser
|
||||
|
||||
from crewai_core.plus_api import CreateCrewPayload
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.command import BaseCommand, PlusAPIMixin
|
||||
from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai_cli.deploy.archive import create_project_zip
|
||||
from crewai_cli.deploy.validate import DeployValidator, Severity, render_report
|
||||
from crewai_cli.utils import fetch_and_json_env_file, get_project_name
|
||||
@@ -14,6 +17,8 @@ from crewai_cli.utils import fetch_and_json_env_file, get_project_name
|
||||
|
||||
console = Console()
|
||||
_MISSING_LOCKFILE_ERROR_CODES = {"missing_lockfile"}
|
||||
_DEPLOYMENT_ID_KEYS = ("deployment_id", "deploymentId")
|
||||
_DEPLOYMENT_FALLBACK_IDENTIFIER_KEYS = ("id", "uuid")
|
||||
|
||||
|
||||
def _run_predeploy_validation(
|
||||
@@ -79,6 +84,39 @@ def _env_summary(env_vars: dict[str, str]) -> str:
|
||||
return f"{len(env_vars)} env vars: {keys}"
|
||||
|
||||
|
||||
def _deployment_identifier(json_response: dict[str, Any]) -> str | None:
|
||||
"""Return the best available identifier for a deployment show URL."""
|
||||
deployment = json_response.get("deployment")
|
||||
|
||||
for key in _DEPLOYMENT_ID_KEYS:
|
||||
value = json_response.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
if isinstance(deployment, dict):
|
||||
for key in _DEPLOYMENT_ID_KEYS + _DEPLOYMENT_FALLBACK_IDENTIFIER_KEYS:
|
||||
value = deployment.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
for key in _DEPLOYMENT_FALLBACK_IDENTIFIER_KEYS:
|
||||
value = json_response.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _deployment_page_url(base_url: str, json_response: dict[str, Any]) -> str | None:
|
||||
"""Build the CrewAI deployment show URL for a response payload."""
|
||||
identifier = _deployment_identifier(json_response)
|
||||
if not identifier:
|
||||
return None
|
||||
return (
|
||||
f"{base_url.rstrip('/')}/crewai_plus/deployments/{quote(identifier, safe='')}"
|
||||
)
|
||||
|
||||
|
||||
def _needs_lockfile_for_deploy(project_root: Path | None = None) -> bool:
|
||||
"""Return True when deploy should create the project's first lockfile."""
|
||||
root = project_root or Path.cwd()
|
||||
@@ -165,6 +203,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
console.print("crewai deploy status")
|
||||
console.print(" or")
|
||||
console.print(f'crewai deploy status --uuid "{json_response["uuid"]}"')
|
||||
self._open_deployment_page(json_response)
|
||||
|
||||
def _display_logs(self, log_messages: list[dict[str, Any]]) -> None:
|
||||
"""
|
||||
@@ -178,6 +217,28 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}"
|
||||
)
|
||||
|
||||
def _open_deployment_page(self, json_response: dict[str, Any]) -> None:
|
||||
"""Open the deployment show page in the user's browser when possible."""
|
||||
base_url = str(
|
||||
getattr(self.plus_api_client, "base_url", None)
|
||||
or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
)
|
||||
deployment_url = _deployment_page_url(base_url, json_response)
|
||||
if not deployment_url:
|
||||
return
|
||||
|
||||
console.print(f"\nOpening deployment page: [blue]{deployment_url}[/blue]")
|
||||
try:
|
||||
opened = webbrowser.open(deployment_url)
|
||||
except Exception:
|
||||
opened = False
|
||||
|
||||
if not opened:
|
||||
console.print(
|
||||
"Could not open the deployment page automatically.",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
def deploy(self, uuid: str | None = None, skip_validate: bool = False) -> None:
|
||||
"""
|
||||
Deploy a crew using either UUID or project name.
|
||||
@@ -438,6 +499,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
console.print("crewai deploy push")
|
||||
console.print(" or")
|
||||
console.print(f"crewai deploy push --uuid {json_response['uuid']}")
|
||||
self._open_deployment_page(json_response)
|
||||
|
||||
def list_crews(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -40,14 +40,18 @@ from typing import Any
|
||||
|
||||
from crewai.project.json_loader import (
|
||||
JSONProjectValidationError,
|
||||
find_crew_json_file,
|
||||
find_json_project_file,
|
||||
validate_crew_project,
|
||||
)
|
||||
from crewai_core.project import (
|
||||
ProjectDefinitionError,
|
||||
configured_project_definition,
|
||||
get_crewai_project_config,
|
||||
get_crewai_project_type,
|
||||
read_toml,
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli.utils import parse_toml
|
||||
|
||||
|
||||
console = Console()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -159,24 +163,16 @@ class DeployValidator:
|
||||
|
||||
@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
|
||||
"""True for JSON crew projects with configured crew definitions."""
|
||||
pyproject_path = self.project_root / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return True
|
||||
return False
|
||||
try:
|
||||
data = parse_toml(pyproject_path.read_text())
|
||||
data = read_toml(pyproject_path)
|
||||
except Exception:
|
||||
return True
|
||||
declared_type: str | None = (
|
||||
(data.get("tool") or {}).get("crewai", {}).get("type")
|
||||
)
|
||||
return declared_type != "flow"
|
||||
return False
|
||||
crewai_config = get_crewai_project_config(data)
|
||||
return crewai_config.get("type") == "crew" and "definition" in crewai_config
|
||||
|
||||
def run(self) -> list[ValidationResult]:
|
||||
"""Run all checks. Later checks are skipped when earlier ones make
|
||||
@@ -208,14 +204,32 @@ class DeployValidator:
|
||||
|
||||
def _run_json_checks(self) -> list[ValidationResult]:
|
||||
"""Validation suite for JSON-defined crew projects."""
|
||||
crew_path = find_crew_json_file(self.project_root)
|
||||
self._check_pyproject()
|
||||
self._check_lockfile()
|
||||
|
||||
try:
|
||||
crew_path = configured_project_definition(
|
||||
"crew",
|
||||
pyproject_data=self._pyproject,
|
||||
project_root=self.project_root,
|
||||
)
|
||||
except ProjectDefinitionError as exc:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"invalid_crew_definition",
|
||||
"[tool.crewai] definition is invalid",
|
||||
detail=str(exc),
|
||||
hint=(
|
||||
"Set `[tool.crewai] definition` to a project-local JSON "
|
||||
"or JSONC crew file."
|
||||
),
|
||||
)
|
||||
return self.results
|
||||
|
||||
if crew_path is None:
|
||||
return self.results
|
||||
|
||||
agents_dir = self.project_root / "agents"
|
||||
|
||||
self._check_pyproject()
|
||||
self._check_lockfile()
|
||||
agents_dir = crew_path.parent / "agents"
|
||||
agents_dir_ok = self._check_json_agents_dir(agents_dir)
|
||||
|
||||
project = None
|
||||
@@ -346,7 +360,7 @@ class DeployValidator:
|
||||
return False
|
||||
|
||||
try:
|
||||
self._pyproject = parse_toml(pyproject_path.read_text())
|
||||
self._pyproject = read_toml(pyproject_path)
|
||||
except Exception as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
@@ -374,9 +388,7 @@ class DeployValidator:
|
||||
|
||||
self._project_name = name
|
||||
self._package_name = normalize_package_name(name)
|
||||
self._is_flow = (self._pyproject.get("tool") or {}).get("crewai", {}).get(
|
||||
"type"
|
||||
) == "flow"
|
||||
self._is_flow = get_crewai_project_type(self._pyproject) == "flow"
|
||||
return True
|
||||
|
||||
def _check_lockfile(self) -> None:
|
||||
|
||||
@@ -2,53 +2,35 @@ from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
from crewai_core.project import configured_project_definition, read_toml
|
||||
|
||||
from crewai_cli.deploy.validate import normalize_package_name
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, parse_toml
|
||||
|
||||
|
||||
def _find_json_crew_file(project_root: Path | None = None) -> Path | None:
|
||||
"""Return the JSON crew definition path when present."""
|
||||
root = project_root or Path.cwd()
|
||||
for filename in ("crew.jsonc", "crew.json"):
|
||||
crew_path = root / filename
|
||||
if crew_path.is_file():
|
||||
return crew_path
|
||||
return None
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
|
||||
|
||||
def _is_json_crew_project(project_root: Path | None = None) -> bool:
|
||||
"""Return True for JSON crew projects that do not need package install."""
|
||||
root = project_root or Path.cwd()
|
||||
if _find_json_crew_file(root) is None:
|
||||
return False
|
||||
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
if not pyproject_path.is_file():
|
||||
return True
|
||||
return False
|
||||
|
||||
try:
|
||||
pyproject = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return True
|
||||
if not isinstance(pyproject, dict):
|
||||
return True
|
||||
pyproject = read_toml(pyproject_path)
|
||||
|
||||
tool_config = pyproject.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
|
||||
)
|
||||
project_config = pyproject.get("project") or {}
|
||||
project_name = (
|
||||
project_config.get("name") if isinstance(project_config, dict) else None
|
||||
)
|
||||
if isinstance(project_name, str):
|
||||
package_name = normalize_package_name(project_name)
|
||||
if package_name and (root / "src" / package_name / "crew.py").is_file():
|
||||
return False
|
||||
if (
|
||||
configured_project_definition(
|
||||
"crew", pyproject_data=pyproject, project_root=root
|
||||
)
|
||||
is None
|
||||
):
|
||||
return False
|
||||
|
||||
return declared_type != "flow"
|
||||
project_name = pyproject.get("project", {}).get("name", "")
|
||||
package_name = normalize_package_name(project_name)
|
||||
if package_name and (root / "src" / package_name / "crew.py").is_file():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Be mindful about changing this.
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
@@ -17,9 +16,8 @@ from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
read_toml,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_version
|
||||
from crewai_cli.version import get_crewai_tools_dependency, get_crewai_version
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -32,12 +30,13 @@ if TYPE_CHECKING:
|
||||
_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_JSON_CREW_DEFINITION_ENV = "CREWAI_JSON_CREW_DEFINITION"
|
||||
_FULL_CREWAI_INSTALL_MESSAGE = f"""\
|
||||
CrewAI CLI is installed without the `crewai` package required to run crews.
|
||||
|
||||
Install the full CrewAI prerelease package:
|
||||
Install the full CrewAI package:
|
||||
|
||||
uv tool install --force --prerelease=allow 'crewai[tools]==1.14.8a1'
|
||||
uv tool install --force '{get_crewai_tools_dependency()}'
|
||||
|
||||
The quotes are required in zsh so `crewai[tools]` is not treated as a glob.
|
||||
"""
|
||||
@@ -75,22 +74,20 @@ module_spec.loader.exec_module(module)
|
||||
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
|
||||
kwargs = {
|
||||
"trained_agents_file": os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV),
|
||||
}
|
||||
if crew_definition := os.getenv("CREWAI_JSON_CREW_DEFINITION"):
|
||||
kwargs["crew_path"] = crew_definition
|
||||
|
||||
try:
|
||||
module._run_json_crew(
|
||||
trained_agents_file=os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV)
|
||||
)
|
||||
module._run_json_crew(**kwargs)
|
||||
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"))
|
||||
|
||||
@@ -99,32 +96,40 @@ 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 read_toml(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
from crewai_core.project import read_toml as _read_toml
|
||||
|
||||
return _read_toml(*args, **kwargs)
|
||||
|
||||
|
||||
def _has_json_crew() -> bool:
|
||||
"""Check if this is a JSON-defined crew project.
|
||||
def get_crewai_project_type(pyproject_data: dict[str, Any]) -> str | None:
|
||||
from crewai_core.project import get_crewai_project_type as _get_crewai_project_type
|
||||
|
||||
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 _get_crewai_project_type(pyproject_data)
|
||||
|
||||
|
||||
def configured_project_json_crew(
|
||||
pyproject_data: dict[str, Any] | None = None,
|
||||
project_root: Path | None = None,
|
||||
) -> Path | None:
|
||||
"""Return the configured JSON crew definition for crew projects."""
|
||||
from crewai_core.project import (
|
||||
ProjectDefinitionError,
|
||||
configured_project_definition,
|
||||
)
|
||||
return declared_type != "flow"
|
||||
|
||||
root = project_root or Path.cwd()
|
||||
if pyproject_data is None and not (root / "pyproject.toml").is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
return configured_project_definition(
|
||||
"crew",
|
||||
pyproject_data=pyproject_data,
|
||||
project_root=root,
|
||||
)
|
||||
except ProjectDefinitionError as exc:
|
||||
raise click.UsageError(str(exc)) from exc
|
||||
|
||||
|
||||
def _extract_input_placeholders(text: str | None) -> set[str]:
|
||||
@@ -199,7 +204,12 @@ def _json_loading_status(message: str) -> AbstractContextManager[Any]:
|
||||
|
||||
|
||||
def _load_json_crew(crew_path: Path) -> tuple[Any, dict[str, Any]]:
|
||||
from crewai.project.crew_loader import load_crew
|
||||
try:
|
||||
from crewai.project.crew_loader import load_crew
|
||||
except ModuleNotFoundError as exc:
|
||||
if _is_missing_crewai_package(exc):
|
||||
raise _full_crewai_install_error() from exc
|
||||
raise
|
||||
|
||||
return load_crew(crew_path)
|
||||
|
||||
@@ -262,7 +272,10 @@ def _run_json_crew_without_tui(crew_path: Path) -> Any:
|
||||
return result
|
||||
|
||||
|
||||
def _run_json_crew(trained_agents_file: str | None = None) -> Any:
|
||||
def _run_json_crew(
|
||||
trained_agents_file: str | None = None,
|
||||
crew_path: str | Path | None = None,
|
||||
) -> Any:
|
||||
"""Load and run a JSON-defined crew."""
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -275,9 +288,13 @@ def _run_json_crew(trained_agents_file: str | None = None) -> Any:
|
||||
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_path = configured_project_json_crew()
|
||||
if crew_path is None:
|
||||
raise FileNotFoundError(
|
||||
"No JSON crew definition configured in [tool.crewai].definition"
|
||||
)
|
||||
crew_path = Path(crew_path)
|
||||
|
||||
if is_dmn_mode_enabled():
|
||||
return _run_json_crew_without_tui(crew_path)
|
||||
@@ -391,10 +408,16 @@ def _json_crew_run_command(project_root: Path | None = None) -> list[str]:
|
||||
return ["uv", "run", "--no-sync", "python", "-c", _JSON_CREW_RUNNER_CODE]
|
||||
|
||||
|
||||
def _run_json_crew_in_project_env(trained_agents_file: str | None = None) -> Any:
|
||||
def _run_json_crew_in_project_env(
|
||||
trained_agents_file: str | None = None,
|
||||
crew_path: str | Path | None = None,
|
||||
) -> Any:
|
||||
"""Run JSON crews from the project's uv-managed environment."""
|
||||
if not (Path.cwd() / "pyproject.toml").is_file():
|
||||
return _run_json_crew(trained_agents_file=trained_agents_file)
|
||||
return _run_json_crew(
|
||||
trained_agents_file=trained_agents_file,
|
||||
crew_path=crew_path,
|
||||
)
|
||||
|
||||
_install_json_crew_dependencies_if_needed()
|
||||
|
||||
@@ -405,6 +428,8 @@ def _run_json_crew_in_project_env(trained_agents_file: str | None = None) -> Any
|
||||
env[_CREWAI_RUNNER_SOURCE_DIR_ENV] = str(local_crewai_source_dir)
|
||||
if trained_agents_file:
|
||||
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file
|
||||
if crew_path is not None:
|
||||
env[_CREWAI_JSON_CREW_DEFINITION_ENV] = str(crew_path)
|
||||
|
||||
try:
|
||||
subprocess.run( # noqa: S603
|
||||
@@ -557,13 +582,16 @@ def run_crew(
|
||||
)
|
||||
return
|
||||
|
||||
if _has_json_crew():
|
||||
_run_json_crew_in_project_env(trained_agents_file=trained_agents_file)
|
||||
pyproject_data = read_toml()
|
||||
if json_crew_definition := configured_project_json_crew(pyproject_data):
|
||||
_run_json_crew_in_project_env(
|
||||
trained_agents_file=trained_agents_file,
|
||||
crew_path=json_crew_definition,
|
||||
)
|
||||
return
|
||||
|
||||
pyproject_data = read_toml()
|
||||
_warn_if_old_poetry_project(pyproject_data)
|
||||
project_type = _get_project_type(pyproject_data)
|
||||
project_type = get_crewai_project_type(pyproject_data)
|
||||
|
||||
if project_type == "flow":
|
||||
_run_flow_project(
|
||||
@@ -627,11 +655,6 @@ def _run_classic_crew_project(
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from crewai_core.project import ProjectDefinitionError, configured_project_definition
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
@@ -105,80 +106,18 @@ def configured_project_declarative_flow(
|
||||
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":
|
||||
root = project_root or Path.cwd()
|
||||
if pyproject_data is None and not (root / "pyproject.toml").is_file():
|
||||
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}."
|
||||
return configured_project_definition(
|
||||
"flow",
|
||||
pyproject_data=pyproject_data,
|
||||
project_root=root,
|
||||
)
|
||||
|
||||
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
|
||||
except ProjectDefinitionError as exc:
|
||||
raise click.UsageError(str(exc)) from exc
|
||||
|
||||
|
||||
def _execute_declarative_flow_command(command: list[str]) -> None:
|
||||
|
||||
@@ -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.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -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.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -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.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
4
lib/cli/src/crewai_cli/templates/json_crew/.gitignore
vendored
Normal file
4
lib/cli/src/crewai_cli/templates/json_crew/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
report.md
|
||||
20
lib/cli/src/crewai_cli/templates/json_crew/README.md
Normal file
20
lib/cli/src/crewai_cli/templates/json_crew/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# {{name}}
|
||||
|
||||
A crewAI project using JSON-first configuration.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `agents/` - Agent definitions (JSONC)
|
||||
- `crew.jsonc` - Crew definition with tasks and configuration
|
||||
- `tools/` - Custom tools (Python)
|
||||
- `knowledge/` - Knowledge files for agents
|
||||
|
||||
> **Note:** `custom:<name>` tool references execute `tools/<name>.py` as local
|
||||
> Python code when the crew loads. Only run crew projects from sources you
|
||||
> trust.
|
||||
59
lib/cli/src/crewai_cli/templates/json_crew/agent.jsonc
Normal file
59
lib/cli/src/crewai_cli/templates/json_crew/agent.jsonc
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
// Agent's role title — appears in prompts and logs.
|
||||
// You can use {placeholder} inputs in role, goal, or backstory.
|
||||
// Example: "role": "Senior {industry} Researcher"
|
||||
"role": {{role_json}},
|
||||
|
||||
// Optional custom Agent subclass
|
||||
// "type": {"python": "my_project.agents.CustomAgent"},
|
||||
|
||||
// The agent's primary objective
|
||||
"goal": {{goal_json}},
|
||||
|
||||
// Background story that shapes the agent's personality and approach
|
||||
"backstory": {{backstory_json}},
|
||||
|
||||
// LLM model in provider/model format
|
||||
// Examples: "openai/gpt-4o", "anthropic/claude-sonnet-4-6", "ollama/llama3.3"
|
||||
// For custom endpoints or deployment-based providers, replace with:
|
||||
// "llm": {"model": "llama3", "provider": "ollama", "base_url": "http://localhost:11434"},
|
||||
// "llm": {"deployment_name": "my-deployment", "provider": "azure", "api_version": "2024-10-21"},
|
||||
"llm": {{llm_json}},
|
||||
|
||||
// Override LLM used specifically for tool/function calling
|
||||
// "function_calling_llm": "openai/gpt-5.4-mini",
|
||||
|
||||
// Tools available to this agent
|
||||
// Built-in: "SerperDevTool", "ScrapeWebsiteTool", "FileReadTool", etc.
|
||||
// Custom: "custom:my_tool" loads from tools/my_tool.py
|
||||
"tools": {{tools_json}},
|
||||
|
||||
// Optional agent-level guardrail — validates this agent's final output.
|
||||
// String guardrails are checked by an LLM and can reject/retry output.
|
||||
// Python refs must point to module-level functions/classes in trusted code.
|
||||
// "guardrail": "Only answer with information supported by retrieved evidence.",
|
||||
// "step_callback": {"python": "my_project.callbacks.on_agent_step"},
|
||||
// "guardrail_max_retries": 2,
|
||||
|
||||
// Advanced agent options:
|
||||
// Docs: https://docs.crewai.com/concepts/agents
|
||||
// "reasoning": true,
|
||||
// "max_reasoning_attempts": 3,
|
||||
// "planning_config": {
|
||||
// "reasoning_effort": "medium",
|
||||
// "llm": {"model": "deepseek-chat", "provider": "deepseek"}
|
||||
// },
|
||||
// "multimodal": false,
|
||||
// "allow_code_execution": false,
|
||||
// "code_execution_mode": "safe",
|
||||
// "knowledge_sources": [],
|
||||
// "knowledge_config": {},
|
||||
// "inject_date": true,
|
||||
// "date_format": "%Y-%m-%d",
|
||||
// "security_config": {},
|
||||
|
||||
// Agent behavior settings
|
||||
"settings": {
|
||||
{{settings_block}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Show detailed execution logs
|
||||
"verbose": false,
|
||||
|
||||
// Allow this agent to delegate tasks to other agents in the crew
|
||||
"allow_delegation": {{allow_delegation}}{{delegation_comma}}
|
||||
|
||||
// Maximum reasoning iterations per task (prevents infinite loops)
|
||||
// "max_iter": 25,
|
||||
|
||||
// Maximum tokens for agent's response generation
|
||||
// "max_tokens": null,
|
||||
|
||||
// Maximum execution time in seconds
|
||||
// "max_execution_time": null,
|
||||
|
||||
// Maximum LLM requests per minute (rate limiting)
|
||||
// "max_rpm": null,
|
||||
|
||||
// Enable agent-level memory (persists across tasks)
|
||||
// "memory": false,
|
||||
|
||||
// Cache tool results to avoid duplicate calls
|
||||
// "cache": true,
|
||||
|
||||
// Auto-summarize context when it exceeds the LLM's context window
|
||||
// "respect_context_window": true,
|
||||
|
||||
// Maximum retries on execution errors
|
||||
// "max_retry_limit": 2,
|
||||
|
||||
// Enable step-by-step planning before task execution
|
||||
{{planning_line}}
|
||||
|
||||
// Include system prompt in LLM calls
|
||||
// "use_system_prompt": true
|
||||
58
lib/cli/src/crewai_cli/templates/json_crew/crew.jsonc
Normal file
58
lib/cli/src/crewai_cli/templates/json_crew/crew.jsonc
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
// Display name for this crew
|
||||
"name": {{name_json}},
|
||||
|
||||
// Agents to include — each must have a matching agents/<name>.jsonc file
|
||||
"agents": {{agent_names_json}},
|
||||
|
||||
// Task definitions — executed in order for sequential process
|
||||
"tasks": [
|
||||
{{tasks_fragments}}
|
||||
],
|
||||
|
||||
// Execution process
|
||||
// "sequential" — tasks run in order, each receiving prior task outputs
|
||||
// "hierarchical" — a manager agent delegates tasks (requires manager_llm)
|
||||
"process": {{process_json}},
|
||||
|
||||
// Enable verbose logging during execution
|
||||
"verbose": true,
|
||||
|
||||
// Enable crew memory — persists context and learnings across tasks
|
||||
"memory": {{memory}},
|
||||
|
||||
// Automatically plan the execution strategy before running tasks
|
||||
// "planning": false,
|
||||
|
||||
// LLM for the planning step (used when planning is true)
|
||||
// "planning_llm": "openai/gpt-4o",
|
||||
|
||||
// LLM for the manager agent (required when process is "hierarchical")
|
||||
// "manager_llm": "openai/gpt-4o",
|
||||
|
||||
// Crew-level LLM fields also accept object form for custom endpoints
|
||||
// "chat_llm": {"model": "llama3", "provider": "ollama", "base_url": "http://localhost:11434"},
|
||||
|
||||
// Advanced crew options:
|
||||
// Docs: https://docs.crewai.com/concepts/crews
|
||||
// For hierarchical crews, manager_agent can reference an agents/<name>.jsonc file
|
||||
// that is not included in the "agents" list.
|
||||
// "manager_agent": "{{manager_agent_name}}",
|
||||
// "before_kickoff_callbacks": [{"python": "my_project.callbacks.before_kickoff"}],
|
||||
// "after_kickoff_callbacks": [{"python": "my_project.callbacks.after_kickoff"}],
|
||||
// "function_calling_llm": "openai/gpt-4o-mini",
|
||||
// "max_rpm": null,
|
||||
// "cache": true,
|
||||
// "knowledge_sources": [],
|
||||
// "embedder": {},
|
||||
// "output_log_file": "crew.log",
|
||||
// "stream": false,
|
||||
// "tracing": false,
|
||||
// "security_config": {},
|
||||
|
||||
// Optional runtime input defaults.
|
||||
// Use {placeholder} in agent or task text, for example:
|
||||
// "description": "Research {topic} and write a brief"
|
||||
// `crewai run` prompts for any placeholders missing from this object.
|
||||
"inputs": {{inputs_json}}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Add your knowledge files here
|
||||
20
lib/cli/src/crewai_cli/templates/json_crew/pyproject.toml
Normal file
20
lib/cli/src/crewai_cli/templates/json_crew/pyproject.toml
Normal file
@@ -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_dependency}}"
|
||||
]
|
||||
|
||||
[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"
|
||||
definition = "crew.jsonc"
|
||||
40
lib/cli/src/crewai_cli/templates/json_crew/task.jsonc
Normal file
40
lib/cli/src/crewai_cli/templates/json_crew/task.jsonc
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
// Task identifier
|
||||
"name": {{name_json}},
|
||||
|
||||
// What the task should accomplish
|
||||
// Use {placeholder} inputs here; crewai run prompts for missing values
|
||||
"description": {{description_json}},
|
||||
|
||||
// Clear definition of what the output should look like
|
||||
"expected_output": {{expected_output_json}},
|
||||
|
||||
// Optional task guardrail(s) validate output before completion
|
||||
// Use "guardrail" for one rule or "guardrails" for many
|
||||
// Failed guardrails retry up to guardrail_max_retries times
|
||||
// "guardrail": "Every factual claim needs context support.",
|
||||
// "guardrails": [
|
||||
// "Every factual claim must be supported by context.",
|
||||
// "The answer must match the expected output format."
|
||||
// ],
|
||||
// "guardrail_max_retries": 2,
|
||||
|
||||
// Advanced task options:
|
||||
// Docs: https://docs.crewai.com/concepts/tasks
|
||||
// "type": "ConditionalTask",
|
||||
// "condition": { "python": "my_project.conditions.should_run" },
|
||||
// "output_json": { "python": "my_project.models.ReportOutput" },
|
||||
// "output_pydantic": null,
|
||||
// "response_model": null,
|
||||
// "converter_cls": { "python": "my_project.converters.CustomConverter" },
|
||||
// "markdown": false,
|
||||
// "input_files": { "brief": "data/brief.txt" },
|
||||
// "security_config": {},
|
||||
|
||||
// Which agent handles this task
|
||||
"agent": {{agent_json}}{{agent_comma}}{{context_block}}{{output_file_block}}
|
||||
|
||||
// "tools": [],
|
||||
// "human_input": false,
|
||||
// "async_execution": false
|
||||
}
|
||||
@@ -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.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -23,6 +23,7 @@ from crewai_cli.utils import (
|
||||
tree_copy,
|
||||
tree_find_and_replace,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
console = Console()
|
||||
@@ -81,6 +82,9 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
tree_copy(template_dir, project_root)
|
||||
tree_find_and_replace(project_root, "{{folder_name}}", folder_name)
|
||||
tree_find_and_replace(project_root, "{{class_name}}", class_name)
|
||||
tree_find_and_replace(
|
||||
project_root, "{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
|
||||
agents_md_src = Path(__file__).parent.parent / "templates" / "AGENTS.md"
|
||||
if agents_md_src.exists():
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
@@ -19,6 +21,8 @@ from crewai_core.tool_credentials import (
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
__all__ = [
|
||||
"build_env_with_all_tool_credentials",
|
||||
@@ -33,6 +37,7 @@ __all__ = [
|
||||
"load_env_vars",
|
||||
"parse_toml",
|
||||
"read_toml",
|
||||
"render_template",
|
||||
"tree_copy",
|
||||
"tree_find_and_replace",
|
||||
"write_env_file",
|
||||
@@ -40,6 +45,7 @@ __all__ = [
|
||||
|
||||
|
||||
console = Console()
|
||||
_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}")
|
||||
|
||||
|
||||
def is_dmn_mode_enabled() -> bool:
|
||||
@@ -67,12 +73,15 @@ def copy_template(
|
||||
src: Path, dst: Path, name: str, class_name: str, folder_name: str
|
||||
) -> None:
|
||||
"""Copy a file from src to dst."""
|
||||
with open(src, "r") as file:
|
||||
content = file.read()
|
||||
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{crew_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
content = render_template(
|
||||
src,
|
||||
{
|
||||
"name": name,
|
||||
"crew_name": class_name,
|
||||
"folder_name": folder_name,
|
||||
"crewai_tools_dependency": get_crewai_tools_dependency(),
|
||||
},
|
||||
)
|
||||
|
||||
with open(dst, "w") as file:
|
||||
file.write(content)
|
||||
@@ -80,6 +89,15 @@ def copy_template(
|
||||
click.secho(f" - Created {dst}", fg="green")
|
||||
|
||||
|
||||
def render_template(src: Path, replacements: Mapping[str, str]) -> str:
|
||||
"""Render a template file using ``{{placeholder}}`` replacements."""
|
||||
content = src.read_text(encoding="utf-8")
|
||||
return _TEMPLATE_TOKEN_RE.sub(
|
||||
lambda match: replacements.get(match.group(1), match.group(0)),
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict[str, Any]:
|
||||
"""Fetch the environment variables from a .env file and return them as a dictionary."""
|
||||
try:
|
||||
|
||||
@@ -13,10 +13,26 @@ from crewai_core.version import (
|
||||
is_current_version_yanked as is_current_version_yanked,
|
||||
is_newer_version_available as is_newer_version_available,
|
||||
)
|
||||
from packaging.version import Version
|
||||
|
||||
from crewai_cli import __version__ as _crewai_cli_version
|
||||
|
||||
|
||||
def get_crewai_dependency_range(current_version: str | None = None) -> str:
|
||||
"""Return the supported CrewAI dependency range for generated projects."""
|
||||
parsed_version = Version(current_version or _crewai_cli_version)
|
||||
return f">={parsed_version},<{parsed_version.major + 1}.0.0"
|
||||
|
||||
|
||||
def get_crewai_tools_dependency(current_version: str | None = None) -> str:
|
||||
"""Return the generated-project dependency for CrewAI with tools."""
|
||||
return f"crewai[tools]{get_crewai_dependency_range(current_version)}"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"check_version",
|
||||
"get_crewai_dependency_range",
|
||||
"get_crewai_tools_dependency",
|
||||
"get_crewai_version",
|
||||
"get_latest_version_from_pypi",
|
||||
"is_current_version_yanked",
|
||||
|
||||
@@ -146,6 +146,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
definition = "crew.jsonc"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
@@ -176,10 +177,11 @@ def test_create_project_zip_keeps_json_project_root_shape(tmp_path: Path):
|
||||
[project]
|
||||
name = "json_crew"
|
||||
version = "0.1.0"
|
||||
dependencies = ["crewai[tools]==1.14.8a1"]
|
||||
dependencies = ["crewai[tools]>=1.15.0,<2.0.0"]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
definition = "crew.jsonc"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
@@ -221,6 +223,7 @@ custom = "custom.module:main"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
definition = "crew.jsonc"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
@@ -167,6 +167,36 @@ def test_prepare_project_for_deploy_creates_missing_lock_after_validation(
|
||||
assert validators == []
|
||||
|
||||
|
||||
def test_deployment_page_url_prefers_deployment_id():
|
||||
assert (
|
||||
deploy_main._deployment_page_url(
|
||||
"https://app.crewai.com",
|
||||
{"uuid": "crew-uuid", "deployment_id": 128687},
|
||||
)
|
||||
== "https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_page_url_prefers_nested_deployment_id_over_crew_uuid():
|
||||
assert (
|
||||
deploy_main._deployment_page_url(
|
||||
"https://app.crewai.com",
|
||||
{"uuid": "crew-uuid", "deployment": {"deployment_id": 128687}},
|
||||
)
|
||||
== "https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_page_url_falls_back_to_nested_uuid():
|
||||
assert (
|
||||
deploy_main._deployment_page_url(
|
||||
"https://app.crewai.com/",
|
||||
{"deployment": {"uuid": "deployment-uuid"}},
|
||||
)
|
||||
== "https://app.crewai.com/crewai_plus/deployments/deployment-uuid"
|
||||
)
|
||||
|
||||
|
||||
class TestDeployCommand(unittest.TestCase):
|
||||
@patch("crewai_cli.command.get_auth_token")
|
||||
@patch("crewai_cli.deploy.main.get_project_name")
|
||||
@@ -186,6 +216,12 @@ class TestDeployCommand(unittest.TestCase):
|
||||
|
||||
self.deploy_command = deploy_main.DeployCommand()
|
||||
self.mock_client = self.deploy_command.plus_api_client
|
||||
self.mock_client.base_url = "https://app.crewai.com"
|
||||
self.mock_browser_open_patcher = patch(
|
||||
"crewai_cli.deploy.main.webbrowser.open"
|
||||
)
|
||||
self.mock_browser_open = self.mock_browser_open_patcher.start()
|
||||
self.addCleanup(self.mock_browser_open_patcher.stop)
|
||||
|
||||
def test_init_success(self):
|
||||
self.assertEqual(self.deploy_command.project_name, "test_project")
|
||||
@@ -272,11 +308,50 @@ class TestDeployCommand(unittest.TestCase):
|
||||
def test_display_deployment_info(self):
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command._display_deployment_info(
|
||||
{"uuid": "test-uuid", "status": "deployed"}
|
||||
{"uuid": "test-uuid", "id": 128687, "status": "deployed"}
|
||||
)
|
||||
self.assertIn("Deploying the crew...", fake_out.getvalue())
|
||||
self.assertIn("test-uuid", fake_out.getvalue())
|
||||
self.assertIn("deployed", fake_out.getvalue())
|
||||
self.assertIn(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687",
|
||||
fake_out.getvalue(),
|
||||
)
|
||||
self.mock_browser_open.assert_called_once_with(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
def test_display_deployment_info_warns_when_browser_open_returns_false(self):
|
||||
self.mock_browser_open.return_value = False
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command._display_deployment_info(
|
||||
{"uuid": "test-uuid", "id": 128687, "status": "deployed"}
|
||||
)
|
||||
self.assertIn(
|
||||
"Could not open the deployment page automatically.",
|
||||
fake_out.getvalue(),
|
||||
)
|
||||
|
||||
self.mock_browser_open.assert_called_once_with(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
def test_display_deployment_info_warns_when_browser_open_raises(self):
|
||||
self.mock_browser_open.side_effect = RuntimeError("no browser")
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command._display_deployment_info(
|
||||
{"uuid": "test-uuid", "id": 128687, "status": "deployed"}
|
||||
)
|
||||
self.assertIn(
|
||||
"Could not open the deployment page automatically.",
|
||||
fake_out.getvalue(),
|
||||
)
|
||||
|
||||
self.mock_browser_open.assert_called_once_with(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
def test_display_logs(self):
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
|
||||
@@ -111,7 +111,12 @@ def _run_without_import_check(root: Path) -> DeployValidator:
|
||||
|
||||
|
||||
def _scaffold_json_crew(root: Path, *, task_agent: str = "researcher") -> None:
|
||||
(root / "pyproject.toml").write_text(_make_pyproject(name="json_crew"))
|
||||
(root / "pyproject.toml").write_text(
|
||||
_make_pyproject(
|
||||
name="json_crew",
|
||||
extra='[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"',
|
||||
)
|
||||
)
|
||||
(root / "uv.lock").write_text("# dummy uv lockfile\n")
|
||||
agents_dir = root / "agents"
|
||||
agents_dir.mkdir()
|
||||
@@ -221,7 +226,6 @@ def test_json_crew_reports_project_metadata_before_invalid_json(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_scaffold_json_crew(tmp_path)
|
||||
(tmp_path / "pyproject.toml").unlink()
|
||||
(tmp_path / "uv.lock").unlink()
|
||||
(tmp_path / "crew.jsonc").write_text('{"agents": ["researcher"], "tasks": []}\n')
|
||||
|
||||
@@ -229,7 +233,6 @@ def test_json_crew_reports_project_metadata_before_invalid_json(
|
||||
v.run()
|
||||
|
||||
codes = _codes(v)
|
||||
assert "missing_pyproject" in codes
|
||||
assert "missing_lockfile" in codes
|
||||
assert "invalid_crew_json" in codes
|
||||
assert "missing_src_dir" not in codes
|
||||
@@ -546,17 +549,43 @@ def test_is_json_crew_defers_to_declared_flow_type(tmp_path):
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is False
|
||||
|
||||
|
||||
def test_is_json_crew_true_for_declared_crew_type(tmp_path):
|
||||
def test_is_json_crew_true_for_declared_crew_definition(tmp_path):
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "demo"\nversion = "0.1.0"\n\n'
|
||||
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
|
||||
)
|
||||
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is True
|
||||
|
||||
|
||||
def test_is_json_crew_false_for_declared_crew_without_definition(tmp_path):
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "demo"\nversion = "0.1.0"\n\n'
|
||||
'[tool.crewai]\ntype = "crew"\n'
|
||||
)
|
||||
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is True
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is False
|
||||
|
||||
|
||||
def test_is_json_crew_true_without_pyproject(tmp_path):
|
||||
def test_json_crew_non_string_definition_reports_invalid_definition(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "demo"\nversion = "0.1.0"\n\n'
|
||||
'[tool.crewai]\ntype = "crew"\ndefinition = ["crew.jsonc"]\n'
|
||||
)
|
||||
|
||||
v = DeployValidator(project_root=tmp_path)
|
||||
v.run()
|
||||
|
||||
finding = next(r for r in v.results if r.code == "invalid_crew_definition")
|
||||
assert finding.severity is Severity.ERROR
|
||||
assert "must be a string" in finding.detail
|
||||
|
||||
|
||||
def test_is_json_crew_false_without_pyproject(tmp_path):
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is True
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is False
|
||||
|
||||
@@ -12,6 +12,8 @@ from packaging.version import Version
|
||||
import crewai_cli.create_json_crew as json_crew
|
||||
import crewai_cli.tui_picker as tui_picker
|
||||
from crewai_cli.create_crew import create_crew, create_folder_structure
|
||||
from crewai_cli.utils import render_template
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -735,11 +737,16 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
|
||||
|
||||
pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text())
|
||||
dependency = pyproject["project"]["dependencies"][0]
|
||||
assert dependency == "crewai[tools]==1.14.8a1"
|
||||
assert Version("1.14.8a1") in Requirement(dependency).specifier
|
||||
assert dependency == get_crewai_tools_dependency()
|
||||
assert Version("1.15.0") in Requirement(dependency).specifier
|
||||
assert Version("2.0.0") not in Requirement(dependency).specifier
|
||||
assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"][
|
||||
"only-include"
|
||||
] == ["agents", "crew.jsonc", "tools", "knowledge", "skills"]
|
||||
assert pyproject["tool"]["crewai"] == {
|
||||
"type": "crew",
|
||||
"definition": "crew.jsonc",
|
||||
}
|
||||
|
||||
crew_template = (tmp_path / "json_crew" / "crew.jsonc").read_text()
|
||||
assert (
|
||||
@@ -811,6 +818,37 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
|
||||
assert '"knowledge_sources": []' in agent_template
|
||||
|
||||
|
||||
def test_json_crew_uses_template_files():
|
||||
template_names = {
|
||||
"pyproject.toml",
|
||||
"README.md",
|
||||
".gitignore",
|
||||
"agent.jsonc",
|
||||
"agent_settings.jsonc",
|
||||
"task.jsonc",
|
||||
"crew.jsonc",
|
||||
"knowledge/user_preference.txt",
|
||||
}
|
||||
|
||||
for template_name in template_names:
|
||||
assert (json_crew._TEMPLATES_DIR / template_name).is_file()
|
||||
|
||||
|
||||
def test_render_template_does_not_replace_tokens_inside_replacement_values(tmp_path):
|
||||
template = tmp_path / "template.txt"
|
||||
template.write_text("{{first}} {{second}}", encoding="utf-8")
|
||||
|
||||
rendered = render_template(
|
||||
template,
|
||||
{
|
||||
"first": "{{second}}",
|
||||
"second": "done",
|
||||
},
|
||||
)
|
||||
|
||||
assert rendered == "{{second}} done"
|
||||
|
||||
|
||||
def test_json_provider_default_model_helper():
|
||||
assert json_crew._default_model_for_provider("openai") == "openai/gpt-5.5"
|
||||
assert json_crew._default_model_for_provider("anthropic/claude-custom") == (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from datetime import datetime
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -127,6 +129,37 @@ def test_chain_deploy_does_not_login_for_deploy_exit(monkeypatch, capsys) -> Non
|
||||
assert "Deploy failed with exit code 42" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_view_traces_button_click_records_telemetry(monkeypatch) -> None:
|
||||
app = CrewRunApp()
|
||||
app._status = "completed"
|
||||
app._trace_url = "https://app.crewai.com/traces/test"
|
||||
app._telemetry = Mock()
|
||||
opened_urls: list[str] = []
|
||||
|
||||
monkeypatch.setattr("webbrowser.open", lambda url: opened_urls.append(url))
|
||||
|
||||
app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces")))
|
||||
|
||||
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces")
|
||||
assert opened_urls == ["https://app.crewai.com/traces/test"]
|
||||
|
||||
|
||||
def test_deploy_button_click_records_telemetry() -> None:
|
||||
app = CrewRunApp()
|
||||
app._status = "completed"
|
||||
app._crew_result = object()
|
||||
app._telemetry = Mock()
|
||||
app._unsubscribe = lambda: None # type: ignore[method-assign]
|
||||
exits: list[object] = []
|
||||
app.exit = lambda result: exits.append(result) # type: ignore[method-assign]
|
||||
|
||||
app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-deploy")))
|
||||
|
||||
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:deploy")
|
||||
assert app._want_deploy is True
|
||||
assert exits == [app._crew_result]
|
||||
|
||||
|
||||
def test_conversation_turn_done_records_assistant_message() -> None:
|
||||
class RawResult:
|
||||
raw = "hello from the flow"
|
||||
|
||||
@@ -26,6 +26,7 @@ name = "json_crew"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
definition = "crew.jsonc"
|
||||
""".strip()
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
@@ -45,6 +46,7 @@ name = "hybrid-crew"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
definition = "crew.jsonc"
|
||||
""".strip()
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
|
||||
@@ -16,29 +16,37 @@ def test_missing_crewai_package_shows_full_install_hint(monkeypatch):
|
||||
def missing_crewai_package():
|
||||
raise ModuleNotFoundError("No module named 'crewai'", name="crewai")
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_import_find_crew_json_file", missing_crewai_package
|
||||
)
|
||||
real_import = __import__
|
||||
|
||||
def fake_import(name, *args, **kwargs):
|
||||
if name == "crewai.project.crew_loader":
|
||||
missing_crewai_package()
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||
|
||||
with pytest.raises(click.ClickException) as exc_info:
|
||||
run_crew_module.find_crew_json_file()
|
||||
run_crew_module._load_json_crew(Path("crew.jsonc"))
|
||||
|
||||
message = exc_info.value.message
|
||||
assert "CrewAI CLI is installed without the `crewai` package" in message
|
||||
assert (
|
||||
"uv tool install --force --prerelease=allow 'crewai[tools]==1.14.8a1'"
|
||||
in message
|
||||
)
|
||||
assert "uv tool install --force 'crewai[tools]>=1.15.0,<2.0.0'" in message
|
||||
assert "quotes are required in zsh" in message
|
||||
|
||||
|
||||
def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch):
|
||||
"""crewai run -f must reach JSON crews, not only classic subprocess crews."""
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: True)
|
||||
monkeypatch.setattr(run_crew_module, "read_toml", lambda: {})
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"configured_project_json_crew",
|
||||
lambda pyproject_data=None, project_root=None: Path("crew.jsonc"),
|
||||
)
|
||||
called: dict = {}
|
||||
|
||||
def fake_run_json_crew_in_project_env(trained_agents_file=None):
|
||||
def fake_run_json_crew_in_project_env(trained_agents_file=None, crew_path=None):
|
||||
called["trained_agents_file"] = trained_agents_file
|
||||
called["crew_path"] = crew_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
@@ -48,7 +56,10 @@ def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch):
|
||||
|
||||
run_crew_module.run_crew(trained_agents_file="some.pkl")
|
||||
|
||||
assert called == {"trained_agents_file": "some.pkl"}
|
||||
assert called == {
|
||||
"trained_agents_file": "some.pkl",
|
||||
"crew_path": Path("crew.jsonc"),
|
||||
}
|
||||
|
||||
|
||||
def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path: Path):
|
||||
@@ -74,8 +85,10 @@ def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path:
|
||||
|
||||
monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run)
|
||||
|
||||
crew_path = tmp_path / "crew.jsonc"
|
||||
run_crew_module._run_json_crew_in_project_env(
|
||||
trained_agents_file="trained.pkl"
|
||||
trained_agents_file="trained.pkl",
|
||||
crew_path=crew_path,
|
||||
)
|
||||
|
||||
expected_env = {
|
||||
@@ -84,6 +97,7 @@ def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path:
|
||||
Path(run_crew_module.__file__).resolve().parent
|
||||
),
|
||||
CREWAI_TRAINED_AGENTS_FILE_ENV: "trained.pkl",
|
||||
run_crew_module._CREWAI_JSON_CREW_DEFINITION_ENV: str(crew_path),
|
||||
}
|
||||
if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir():
|
||||
expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str(
|
||||
@@ -213,12 +227,87 @@ def test_json_runner_code_loads_current_cli_package_over_project_env(tmp_path: P
|
||||
assert marker.read_text() == "current:trained.pkl"
|
||||
|
||||
|
||||
def test_json_runner_imports_with_older_project_env_crewai_core(tmp_path: Path):
|
||||
old_parent = tmp_path / "old_env"
|
||||
old_crewai_core = old_parent / "crewai_core"
|
||||
old_crewai_core.mkdir(parents=True)
|
||||
(old_crewai_core / "__init__.py").write_text("")
|
||||
(old_crewai_core / "constants.py").write_text(
|
||||
"CREWAI_TRAINED_AGENTS_FILE_ENV = 'CREWAI_TRAINED_AGENTS_FILE'\n"
|
||||
)
|
||||
(old_crewai_core / "project.py").write_text(
|
||||
"def read_toml(*args, **kwargs):\n"
|
||||
" return {}\n"
|
||||
"def parse_toml(*args, **kwargs):\n"
|
||||
" return {}\n"
|
||||
"def get_project_description(*args, **kwargs):\n"
|
||||
" return None\n"
|
||||
"def get_project_name(*args, **kwargs):\n"
|
||||
" return None\n"
|
||||
"def get_project_version(*args, **kwargs):\n"
|
||||
" return None\n"
|
||||
)
|
||||
(old_crewai_core / "tool_credentials.py").write_text(
|
||||
"def build_env_with_all_tool_credentials(*args, **kwargs):\n"
|
||||
" return {}\n"
|
||||
"def build_env_with_tool_repository_credentials(*args, **kwargs):\n"
|
||||
" return {}\n"
|
||||
)
|
||||
(old_crewai_core / "version.py").write_text(
|
||||
"def check_version(*args, **kwargs):\n"
|
||||
" return None\n"
|
||||
"def get_crewai_version(*args, **kwargs):\n"
|
||||
" return '1.0.0'\n"
|
||||
"def get_latest_version_from_pypi(*args, **kwargs):\n"
|
||||
" return None\n"
|
||||
"def is_current_version_yanked(*args, **kwargs):\n"
|
||||
" return False\n"
|
||||
"def is_newer_version_available(*args, **kwargs):\n"
|
||||
" return False\n"
|
||||
)
|
||||
|
||||
marker = tmp_path / "marker.txt"
|
||||
old_crewai_project = old_parent / "crewai" / "project"
|
||||
old_crewai_project.mkdir(parents=True)
|
||||
(old_parent / "crewai" / "__init__.py").write_text("")
|
||||
(old_crewai_project / "__init__.py").write_text("")
|
||||
(old_crewai_project / "crew_loader.py").write_text(
|
||||
"from pathlib import Path\n"
|
||||
"class Crew:\n"
|
||||
" agents = []\n"
|
||||
" tasks = []\n"
|
||||
" def kickoff(self, inputs):\n"
|
||||
f" Path({str(marker)!r}).write_text('ran')\n"
|
||||
" return 'done'\n"
|
||||
"def load_crew(path):\n"
|
||||
" return Crew(), {}\n"
|
||||
)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = str(old_parent)
|
||||
env["CREWAI_DMN"] = "true"
|
||||
env[run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(
|
||||
Path(run_crew_module.__file__).resolve().parent
|
||||
)
|
||||
env[run_crew_module._CREWAI_JSON_CREW_DEFINITION_ENV] = "crew.jsonc"
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", run_crew_module._JSON_CREW_RUNNER_CODE],
|
||||
check=True,
|
||||
env=env,
|
||||
cwd=tmp_path,
|
||||
)
|
||||
|
||||
assert marker.read_text() == "ran"
|
||||
|
||||
|
||||
def test_json_run_without_pyproject_runs_in_process(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
called: dict = {}
|
||||
|
||||
def fake_run_json_crew(trained_agents_file=None):
|
||||
def fake_run_json_crew(trained_agents_file=None, crew_path=None):
|
||||
called["trained_agents_file"] = trained_agents_file
|
||||
called["crew_path"] = crew_path
|
||||
return "result"
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_run_json_crew", fake_run_json_crew)
|
||||
@@ -229,7 +318,7 @@ def test_json_run_without_pyproject_runs_in_process(monkeypatch, tmp_path: Path)
|
||||
)
|
||||
== "result"
|
||||
)
|
||||
assert called == {"trained_agents_file": "trained.pkl"}
|
||||
assert called == {"trained_agents_file": "trained.pkl", "crew_path": None}
|
||||
|
||||
|
||||
def test_json_project_env_run_failure_exits_nonzero(monkeypatch, tmp_path: Path):
|
||||
@@ -438,7 +527,7 @@ def _patch_tui_run(monkeypatch, status: str):
|
||||
|
||||
crew = SimpleNamespace(name="Demo", tasks=[], agents=[])
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "find_crew_json_file", lambda: Path("crew.jsonc")
|
||||
run_crew_module, "configured_project_json_crew", lambda: Path("crew.jsonc")
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
@@ -492,7 +581,9 @@ def test_run_json_crew_dmn_mode_bypasses_tui(monkeypatch, tmp_path: Path, capsys
|
||||
kickoff_calls.append(inputs)
|
||||
return "plain result"
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "find_crew_json_file", lambda: crew_path)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "configured_project_json_crew", lambda: crew_path
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_load_json_crew",
|
||||
@@ -531,7 +622,9 @@ def test_run_json_crew_dmn_mode_exits_on_missing_inputs(
|
||||
tasks=[],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "find_crew_json_file", lambda: crew_path)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "configured_project_json_crew", lambda: crew_path
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_load_json_crew",
|
||||
@@ -546,28 +639,47 @@ def test_run_json_crew_dmn_mode_exits_on_missing_inputs(
|
||||
assert "Missing runtime inputs for CREWAI_DMN mode: topic" in captured.err
|
||||
|
||||
|
||||
def test_has_json_crew_defers_to_declared_flow_type(monkeypatch, tmp_path: Path):
|
||||
def test_configured_project_json_crew_defers_to_declared_flow_type(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
"""A flow project containing a stray crew.jsonc must still run as a flow."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text('[tool.crewai]\ntype = "flow"\n')
|
||||
|
||||
assert run_crew_module._has_json_crew() is False
|
||||
assert run_crew_module.configured_project_json_crew() is None
|
||||
|
||||
|
||||
def test_has_json_crew_true_for_declared_crew_type(monkeypatch, tmp_path: Path):
|
||||
def test_configured_project_json_crew_returns_declared_crew_definition(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
crew_path = tmp_path / "crew.jsonc"
|
||||
crew_path.write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
|
||||
)
|
||||
|
||||
assert run_crew_module.configured_project_json_crew() == crew_path.resolve()
|
||||
|
||||
|
||||
def test_configured_project_json_crew_ignores_declared_crew_without_definition(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text('[tool.crewai]\ntype = "crew"\n')
|
||||
|
||||
assert run_crew_module._has_json_crew() is True
|
||||
assert run_crew_module.configured_project_json_crew() is None
|
||||
|
||||
|
||||
def test_has_json_crew_true_without_pyproject(monkeypatch, tmp_path: Path):
|
||||
def test_configured_project_json_crew_ignores_missing_pyproject(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
|
||||
assert run_crew_module._has_json_crew() is True
|
||||
assert run_crew_module.configured_project_json_crew() is None
|
||||
|
||||
|
||||
def test_run_crew_rejects_inputs_without_definition():
|
||||
@@ -608,7 +720,6 @@ def test_run_crew_runs_explicit_declarative_definition(monkeypatch, capsys):
|
||||
def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
@@ -634,7 +745,6 @@ def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
|
||||
def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
@@ -663,7 +773,6 @@ def test_run_crew_runs_conversational_flow_tui(monkeypatch, capsys):
|
||||
flow = Flow()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
@@ -692,7 +801,6 @@ def test_run_crew_runs_conversational_flow_tui(monkeypatch, capsys):
|
||||
|
||||
|
||||
def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
@@ -713,7 +821,6 @@ def test_run_crew_runs_configured_declarative_flow_project(
|
||||
monkeypatch.chdir(tmp_path)
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\n", encoding="utf-8")
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
|
||||
@@ -7,6 +7,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai_cli.version import get_crewai_version as _get_ver
|
||||
from crewai_cli.version import (
|
||||
get_crewai_dependency_range,
|
||||
get_crewai_tools_dependency,
|
||||
get_crewai_version,
|
||||
get_latest_version_from_pypi,
|
||||
is_current_version_yanked,
|
||||
@@ -31,6 +33,11 @@ def test_dynamic_versioning_consistency() -> None:
|
||||
assert len(package_version.strip()) > 0
|
||||
|
||||
|
||||
def test_generated_project_dependency_uses_next_major_upper_bound() -> None:
|
||||
assert get_crewai_dependency_range("1.15.0") == ">=1.15.0,<2.0.0"
|
||||
assert get_crewai_tools_dependency("1.15.0") == "crewai[tools]>=1.15.0,<2.0.0"
|
||||
|
||||
|
||||
class TestVersionChecking:
|
||||
"""Test version checking utilities."""
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ def test_create_success(mock_subprocess, capsys, tool_command):
|
||||
)
|
||||
assert os.path.isfile(os.path.join("test_tool", "src", "test_tool", "tool.py"))
|
||||
|
||||
with open(os.path.join("test_tool", "pyproject.toml"), "r") as f:
|
||||
content = f.read()
|
||||
assert '"crewai[tools]>=1.15.0,<2.0.0"' in content
|
||||
|
||||
with open(os.path.join("test_tool", "src", "test_tool", "tool.py"), "r") as f:
|
||||
content = f.read()
|
||||
assert "class TestTool" in content
|
||||
|
||||
Reference in New Issue
Block a user