diff --git a/lib/cli/src/crewai_cli/create_json_crew.py b/lib/cli/src/crewai_cli/create_json_crew.py index f1e1336377..61a29b874b 100644 --- a/lib/cli/src/crewai_cli/create_json_crew.py +++ b/lib/cli/src/crewai_cli/create_json_crew.py @@ -18,6 +18,7 @@ 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 @@ -79,61 +80,7 @@ ], } - -# ── 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_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" -""" - -_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:` tool references execute `tools/.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 ──────────────────────────────────── @@ -694,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_lines.append(f' "allow_delegation": {delegation_val}{delegation_comma}') - settings_lines.append("") - settings_lines.append( - " // Maximum reasoning iterations per task (prevents infinite loops)" + 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(' // "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" + + 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(' // "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" },' + has_context = bool(task.get("context")) + has_output_file = bool(task.get("output_file")) + context_block = "" + output_file_block = "" + + 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 has_output_file: + output_file_block = ( + "\n\n" + " // Save output to a file\n" + f' "output_file": {json.dumps(task["output_file"])}' + ) + + 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, + }, ) - 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"])}') - - 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 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"])}') - - lines.append("") - lines.append(' // "tools": [],') - lines.append(' // "human_input": false,') - lines.append(' // "async_execution": false') - lines.append(" }") - return "\n".join(lines) def _crew_to_jsonc( @@ -894,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/.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/.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 ───────────────────────────────────────────── @@ -1031,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") @@ -1136,26 +917,32 @@ def create_json_crew( # Write pyproject.toml (folder_path / "pyproject.toml").write_text( - _PYPROJECT_TOML.format( - folder_name=folder_name, - name=name, - crewai_tools_dependency=get_crewai_tools_dependency(), + _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", ) diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index 7601e2d76b..8fde69ee82 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -10,12 +10,6 @@ import click from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV -from crewai_core.project import ( - ProjectDefinitionError, - configured_project_definition, - get_crewai_project_type, - read_toml, -) from packaging import version from crewai_cli.utils import ( @@ -102,11 +96,28 @@ def _full_crewai_install_error() -> click.ClickException: return click.ClickException(_FULL_CREWAI_INSTALL_MESSAGE) +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 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 + + 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, + ) + root = project_root or Path.cwd() if pyproject_data is None and not (root / "pyproject.toml").is_file(): return None diff --git a/lib/cli/src/crewai_cli/templates/json_crew/.gitignore b/lib/cli/src/crewai_cli/templates/json_crew/.gitignore new file mode 100644 index 0000000000..012e444894 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/.gitignore @@ -0,0 +1,4 @@ +.env +__pycache__/ +.DS_Store +report.md diff --git a/lib/cli/src/crewai_cli/templates/json_crew/README.md b/lib/cli/src/crewai_cli/templates/json_crew/README.md new file mode 100644 index 0000000000..1208b2a403 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/README.md @@ -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:` tool references execute `tools/.py` as local +> Python code when the crew loads. Only run crew projects from sources you +> trust. diff --git a/lib/cli/src/crewai_cli/templates/json_crew/agent.jsonc b/lib/cli/src/crewai_cli/templates/json_crew/agent.jsonc new file mode 100644 index 0000000000..d89e59c1cb --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/agent.jsonc @@ -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}} + } +} diff --git a/lib/cli/src/crewai_cli/templates/json_crew/agent_settings.jsonc b/lib/cli/src/crewai_cli/templates/json_crew/agent_settings.jsonc new file mode 100644 index 0000000000..331af82ac7 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/agent_settings.jsonc @@ -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 diff --git a/lib/cli/src/crewai_cli/templates/json_crew/crew.jsonc b/lib/cli/src/crewai_cli/templates/json_crew/crew.jsonc new file mode 100644 index 0000000000..b4a936b05d --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/crew.jsonc @@ -0,0 +1,58 @@ +{ + // Display name for this crew + "name": {{name_json}}, + + // Agents to include — each must have a matching agents/.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/.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}} +} diff --git a/lib/cli/src/crewai_cli/templates/json_crew/knowledge/user_preference.txt b/lib/cli/src/crewai_cli/templates/json_crew/knowledge/user_preference.txt new file mode 100644 index 0000000000..ae0f1e7a72 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/knowledge/user_preference.txt @@ -0,0 +1 @@ +# Add your knowledge files here diff --git a/lib/cli/src/crewai_cli/templates/json_crew/pyproject.toml b/lib/cli/src/crewai_cli/templates/json_crew/pyproject.toml new file mode 100644 index 0000000000..5cc3da30a5 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "{{folder_name}}" +version = "0.1.0" +description = "{{name}} using crewAI" +authors = [{ name = "Your Name", email = "you@example.com" }] +requires-python = ">=3.10,<3.14" +dependencies = [ + "{{crewai_tools_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" diff --git a/lib/cli/src/crewai_cli/templates/json_crew/task.jsonc b/lib/cli/src/crewai_cli/templates/json_crew/task.jsonc new file mode 100644 index 0000000000..796a746e94 --- /dev/null +++ b/lib/cli/src/crewai_cli/templates/json_crew/task.jsonc @@ -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 + } diff --git a/lib/cli/src/crewai_cli/utils.py b/lib/cli/src/crewai_cli/utils.py index ee3a255a96..e20bcfea17 100644 --- a/lib/cli/src/crewai_cli/utils.py +++ b/lib/cli/src/crewai_cli/utils.py @@ -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 @@ -35,6 +37,7 @@ "load_env_vars", "parse_toml", "read_toml", + "render_template", "tree_copy", "tree_find_and_replace", "write_env_file", @@ -42,6 +45,7 @@ console = Console() +_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}") def is_dmn_mode_enabled() -> bool: @@ -69,14 +73,14 @@ 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 = content.replace( - "{{crewai_tools_dependency}}", get_crewai_tools_dependency() + 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: @@ -85,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: diff --git a/lib/cli/tests/test_create_crew.py b/lib/cli/tests/test_create_crew.py index 61311ef4d5..d9ed7a24bc 100644 --- a/lib/cli/tests/test_create_crew.py +++ b/lib/cli/tests/test_create_crew.py @@ -12,6 +12,8 @@ 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,7 +737,7 @@ 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.15.0,<2.0.0" + 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"][ @@ -816,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") == ( diff --git a/lib/cli/tests/test_run_crew.py b/lib/cli/tests/test_run_crew.py index 2d0a23a6de..a1b4c73bc1 100644 --- a/lib/cli/tests/test_run_crew.py +++ b/lib/cli/tests/test_run_crew.py @@ -227,6 +227,80 @@ 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 = {}