Fix JSON crew template rendering (#6359)

JSON crews were not using existing CLI templates.
This commit is contained in:
Vinicius Brasil
2026-06-26 13:48:48 -07:00
committed by GitHub
parent 8eaae40acf
commit a149a30bc0
13 changed files with 465 additions and 310 deletions

View File

@@ -18,6 +18,7 @@ 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
@@ -79,61 +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_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:<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 ────────────────────────────────────
@@ -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_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(
@@ -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/<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 ─────────────────────────────────────────────
@@ -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",
)

View File

@@ -10,12 +10,6 @@ from typing import TYPE_CHECKING, Any
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

View File

@@ -0,0 +1,4 @@
.env
__pycache__/
.DS_Store
report.md

View 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.

View 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}}
}
}

View File

@@ -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

View 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}}
}

View File

@@ -0,0 +1 @@
# Add your knowledge files here

View 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"

View 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
}

View File

@@ -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 @@ __all__ = [
"load_env_vars",
"parse_toml",
"read_toml",
"render_template",
"tree_copy",
"tree_find_and_replace",
"write_env_file",
@@ -42,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:
@@ -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:

View File

@@ -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,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") == (

View File

@@ -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 = {}