mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-27 02:58:10 +00:00
Compare commits
8 Commits
fix/codeql
...
1.15.1a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e716f3de8b | ||
|
|
1e2c965a75 | ||
|
|
a149a30bc0 | ||
|
|
8eaae40acf | ||
|
|
596150188b | ||
|
|
e10c17fcf6 | ||
|
|
f364a7d988 | ||
|
|
2771c02f45 |
@@ -4,6 +4,33 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="26 يونيو 2026">
|
||||
## v1.15.1a1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.15.1a1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- تتبع بيانات الزر TUI
|
||||
- يتطلب تعريفات مشروع CrewAI بشكل صريح
|
||||
- فتح صفحة النشر بعد نشر CLI
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح عرض قالب الطاقم بصيغة JSON
|
||||
- إصلاح تثبيت إصدار الطاقم بصيغة JSON
|
||||
- إصلاح تجاوز إعادة التوجيه SSRF في عمليات جلب البيانات
|
||||
|
||||
### الوثائق
|
||||
- تحسين دعوة إعداد وكيل البرمجة
|
||||
- لقطة وتغيير السجل للإصدار v1.15.0
|
||||
|
||||
## المساهمون
|
||||
|
||||
@joaomdmoura, @lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 يونيو 2026">
|
||||
## v1.15.0
|
||||
|
||||
|
||||
@@ -4,6 +4,33 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Jun 26, 2026">
|
||||
## v1.15.1a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.15.1a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Track TUI button telemetry
|
||||
- Require explicit CrewAI project definitions
|
||||
- Open deployment page after CLI deploy
|
||||
|
||||
### Bug Fixes
|
||||
- Fix JSON crew template rendering
|
||||
- Fix JSON crew version pin
|
||||
- Fix SSRF redirect bypass in scraping fetches
|
||||
|
||||
### Documentation
|
||||
- Improve coding agent setup CTA
|
||||
- Snapshot and changelog for v1.15.0
|
||||
|
||||
## Contributors
|
||||
|
||||
@joaomdmoura, @lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Jun 25, 2026">
|
||||
## v1.15.0
|
||||
|
||||
|
||||
@@ -5,15 +5,49 @@ icon: wrench
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 18,
|
||||
padding: "24px",
|
||||
marginBottom: 32,
|
||||
borderRadius: 12,
|
||||
border: "1px solid rgba(235,102,88,0.28)",
|
||||
background: "linear-gradient(180deg, rgba(235,102,88,0.14) 0%, rgba(235,102,88,0.06) 100%)"
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p style={{ margin: 0, color: "#EB6658", fontSize: 13, fontWeight: 700, textTransform: "uppercase" }}>
|
||||
Coding agent setup
|
||||
</p>
|
||||
<h2 style={{ margin: "6px 0 8px" }}>Set up CrewAI in your coding agent</h2>
|
||||
<p style={{ margin: 0, color: "var(--mint-text-2)", maxWidth: 760 }}>
|
||||
Copy a ready-to-paste setup prompt for Claude Code, Codex, Cursor, or any coding agent. It installs the official CrewAI skills, checks the CLI, and points the agent at the right docs before it edits code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="button button-primary"
|
||||
onClick={async (event) => {
|
||||
const prompt = `Set up this environment so I can build with CrewAI.
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 12, alignItems: "center" }}>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid #EB6658",
|
||||
background: "#EB6658",
|
||||
color: "#FFFFFF",
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
cursor: "pointer",
|
||||
boxShadow: "0 10px 24px rgba(235,102,88,0.22)"
|
||||
}}
|
||||
onClick={async (event) => {
|
||||
const prompt = `Set up this environment so I can build with CrewAI.
|
||||
|
||||
First install the official CrewAI coding-agent skills if this environment supports npx:
|
||||
|
||||
@@ -48,21 +82,49 @@ Setup steps:
|
||||
Do not hardcode API keys. Use .env.
|
||||
Do not invent CLI flags. Validate with crewai --help or crewai create --help.
|
||||
If a command fails, show the exact command and error, explain the likely cause, fix what you can safely fix, and retry once.`;
|
||||
const button = event.currentTarget;
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
button.textContent = "Copied";
|
||||
} catch {
|
||||
button.textContent = "Copy failed";
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
button.textContent = "Copy instructions for coding agents";
|
||||
}, 1600);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy instructions for coding agents
|
||||
</button>
|
||||
const button = event.currentTarget;
|
||||
const resetTimeout = button.dataset.resetTimeout;
|
||||
if (resetTimeout) {
|
||||
window.clearTimeout(Number(resetTimeout));
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
button.textContent = "Copied";
|
||||
} catch {
|
||||
button.textContent = "Copy failed";
|
||||
} finally {
|
||||
button.dataset.resetTimeout = String(window.setTimeout(() => {
|
||||
button.textContent = "Copy agent setup prompt";
|
||||
delete button.dataset.resetTimeout;
|
||||
}, 1600));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy agent setup prompt
|
||||
</button>
|
||||
<a
|
||||
href="/en/guides/coding-tools/build-with-ai"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.36)",
|
||||
color: "#EB6658",
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
View coding-agent guide
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
|
||||
@@ -4,6 +4,33 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 6월 26일">
|
||||
## v1.15.1a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.15.1a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- TUI 버튼 텔레메트리 추적
|
||||
- 명시적인 CrewAI 프로젝트 정의 필요
|
||||
- CLI 배포 후 배포 페이지 열기
|
||||
|
||||
### 버그 수정
|
||||
- JSON 크루 템플릿 렌더링 수정
|
||||
- JSON 크루 버전 고정 수정
|
||||
- 스크래핑 페치에서 SSRF 리다이렉트 우회 수정
|
||||
|
||||
### 문서
|
||||
- 코딩 에이전트 설정 CTA 개선
|
||||
- v1.15.0에 대한 스냅샷 및 변경 로그
|
||||
|
||||
## 기여자
|
||||
|
||||
@joaomdmoura, @lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 6월 25일">
|
||||
## v1.15.0
|
||||
|
||||
|
||||
@@ -4,6 +4,33 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="26 jun 2026">
|
||||
## v1.15.1a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.15.1a1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Rastrear a telemetria dos botões TUI
|
||||
- Exigir definições explícitas de projetos CrewAI
|
||||
- Abrir a página de implantação após o deploy via CLI
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir a renderização do template de equipe em JSON
|
||||
- Corrigir o pin de versão da equipe em JSON
|
||||
- Corrigir a bypass de redirecionamento SSRF em fetches de scraping
|
||||
|
||||
### Documentação
|
||||
- Melhorar o CTA de configuração do agente de codificação
|
||||
- Snapshot e changelog para v1.15.0
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@joaomdmoura, @lorenzejay, @theCyberTech, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 jun 2026">
|
||||
## v1.15.0
|
||||
|
||||
|
||||
130
docs/index.mdx
130
docs/index.mdx
@@ -27,9 +27,133 @@ mode: "wide"
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center' }}>
|
||||
<a className="button button-primary" href="/en/quickstart">Get started</a>
|
||||
<a className="button" href="/en/changelog">View changelog</a>
|
||||
<a className="button" href="/en/api-reference/introduction">API Reference</a>
|
||||
<a
|
||||
href="/en/quickstart"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid #EB6658",
|
||||
background: "#EB6658",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.36)",
|
||||
background: "rgba(235,102,88,0.08)",
|
||||
color: "#EB6658",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={async (event) => {
|
||||
const prompt = `Set up this environment so I can build with CrewAI.
|
||||
|
||||
First install the official CrewAI coding-agent skills if this environment supports npx:
|
||||
|
||||
npx skills add crewaiinc/skills
|
||||
|
||||
If npx is missing or the current agent cannot load skills, do not fail the whole setup. Report the exact issue and continue using the CrewAI docs directly.
|
||||
|
||||
Use these CrewAI docs as source of truth before making assumptions:
|
||||
- https://skills.crewai.com
|
||||
- https://docs.crewai.com/llms.txt
|
||||
- https://docs.crewai.com/en/installation
|
||||
- https://docs.crewai.com/en/guides/coding-tools/build-with-ai
|
||||
|
||||
Setup steps:
|
||||
1. Check python3 --version. CrewAI requires Python >=3.10 and <3.14.
|
||||
2. Install uv if missing:
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
3. Source the uv environment if needed:
|
||||
source "$HOME/.local/bin/env"
|
||||
4. Install the CrewAI CLI:
|
||||
uv tool install crewai
|
||||
5. Verify the CLI:
|
||||
crewai version
|
||||
crewai create --help
|
||||
6. Create a project:
|
||||
CREWAI_DMN=true crewai create
|
||||
7. After project creation, inspect the generated files before editing.
|
||||
8. Run:
|
||||
crewai install
|
||||
crewai run
|
||||
|
||||
Do not hardcode API keys. Use .env.
|
||||
Do not invent CLI flags. Validate with crewai --help or crewai create --help.
|
||||
If a command fails, show the exact command and error, explain the likely cause, fix what you can safely fix, and retry once.`;
|
||||
const button = event.currentTarget;
|
||||
const resetTimeout = button.dataset.resetTimeout;
|
||||
if (resetTimeout) {
|
||||
window.clearTimeout(Number(resetTimeout));
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
button.textContent = "Copied";
|
||||
} catch {
|
||||
button.textContent = "Copy failed";
|
||||
} finally {
|
||||
button.dataset.resetTimeout = String(window.setTimeout(() => {
|
||||
button.textContent = "Copy agent setup prompt";
|
||||
delete button.dataset.resetTimeout;
|
||||
}, 1600));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy agent setup prompt
|
||||
</button>
|
||||
<a
|
||||
href="/guides/coding-tools/build-with-ai"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.28)",
|
||||
color: "#EB6658",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
Coding-agent guide
|
||||
</a>
|
||||
<a
|
||||
href="/en/api-reference/introduction"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.18)",
|
||||
color: "var(--mint-text-2)",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
API Reference
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.15.0",
|
||||
"crewai-core==1.15.1a1",
|
||||
"click>=8.1.7,<9",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"pydantic-settings~=2.10.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.15.0"
|
||||
__version__ = "1.15.1a1"
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,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
|
||||
@@ -571,6 +572,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 ──────────────────────────────────────────────
|
||||
|
||||
@@ -1042,10 +1044,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
|
||||
|
||||
@@ -126,6 +128,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
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.15.0"
|
||||
__version__ = "1.15.1a1"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import reduce
|
||||
from pathlib import Path, PureWindowsPath
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
@@ -16,7 +17,11 @@ if sys.version_info >= (3, 11):
|
||||
console = Console()
|
||||
|
||||
|
||||
def read_toml(file_path: str = "pyproject.toml") -> dict[str, Any]:
|
||||
class ProjectDefinitionError(ValueError):
|
||||
"""Invalid ``[tool.crewai].definition`` project configuration."""
|
||||
|
||||
|
||||
def read_toml(file_path: str | Path = "pyproject.toml") -> dict[str, Any]:
|
||||
"""Read a TOML file from disk and return its parsed contents."""
|
||||
with open(file_path, "rb") as f:
|
||||
return tomli.load(f)
|
||||
@@ -29,6 +34,115 @@ def parse_toml(content: str) -> dict[str, Any]:
|
||||
return tomli.loads(content)
|
||||
|
||||
|
||||
def get_crewai_project_config(pyproject_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return the normalized ``[tool.crewai]`` table from pyproject data."""
|
||||
tool_config = pyproject_data.get("tool")
|
||||
if not isinstance(tool_config, dict):
|
||||
return {}
|
||||
crewai_config = tool_config.get("crewai")
|
||||
if not isinstance(crewai_config, dict):
|
||||
return {}
|
||||
return crewai_config
|
||||
|
||||
|
||||
def get_crewai_project_type(pyproject_data: dict[str, Any]) -> str | None:
|
||||
"""Return ``[tool.crewai].type`` when configured."""
|
||||
project_type = get_crewai_project_config(pyproject_data).get("type")
|
||||
return project_type if isinstance(project_type, str) else None
|
||||
|
||||
|
||||
def configured_project_definition(
|
||||
project_type: str,
|
||||
*,
|
||||
pyproject_data: dict[str, Any] | None = None,
|
||||
project_root: Path | str | None = None,
|
||||
) -> Path | None:
|
||||
"""Return a configured CrewAI definition path for a project type.
|
||||
|
||||
``[tool.crewai].type`` must match ``project_type`` and ``definition`` must
|
||||
be a non-empty project-local file path. Missing definitions return ``None``
|
||||
so callers can fall back to legacy entrypoints for that project type.
|
||||
"""
|
||||
root = Path(project_root) if project_root is not None else Path.cwd()
|
||||
if pyproject_data is None:
|
||||
pyproject_data = read_toml(root / "pyproject.toml")
|
||||
|
||||
crewai_config = get_crewai_project_config(pyproject_data)
|
||||
if crewai_config.get("type") != project_type:
|
||||
return None
|
||||
|
||||
if "definition" not in crewai_config:
|
||||
return None
|
||||
raw_definition = crewai_config["definition"]
|
||||
if not isinstance(raw_definition, str):
|
||||
raise ProjectDefinitionError(
|
||||
"[tool.crewai] definition must be a string project-local path; "
|
||||
f"got {raw_definition!r}."
|
||||
)
|
||||
|
||||
definition = raw_definition.strip()
|
||||
if not definition:
|
||||
raise ProjectDefinitionError(
|
||||
"[tool.crewai] definition must be a non-empty project-local path."
|
||||
)
|
||||
|
||||
return resolve_project_definition_path(definition=definition, project_root=root)
|
||||
|
||||
|
||||
def resolve_project_definition_path(definition: str, project_root: Path | str) -> Path:
|
||||
"""Resolve a ``[tool.crewai].definition`` path inside ``project_root``."""
|
||||
root_path = Path(project_root)
|
||||
definition_path = Path(definition)
|
||||
windows_definition_path = PureWindowsPath(definition)
|
||||
|
||||
if definition.startswith("~"):
|
||||
raise ProjectDefinitionError(
|
||||
"[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 ProjectDefinitionError(
|
||||
"[tool.crewai] definition must be relative to the project root; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
try:
|
||||
root = root_path.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise ProjectDefinitionError(
|
||||
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 ProjectDefinitionError(
|
||||
f"Invalid [tool.crewai] definition path {definition!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
if not resolved_candidate.is_relative_to(root):
|
||||
raise ProjectDefinitionError(
|
||||
"[tool.crewai] definition must resolve inside the project root; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if not resolved_candidate.exists():
|
||||
raise ProjectDefinitionError(
|
||||
"[tool.crewai] definition must point to an existing file; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
if not resolved_candidate.is_file():
|
||||
raise ProjectDefinitionError(
|
||||
"[tool.crewai] definition must point to a regular file; "
|
||||
f"got {definition!r}."
|
||||
)
|
||||
|
||||
return resolved_candidate
|
||||
|
||||
|
||||
def _get_nested_value(data: dict[str, Any], keys: list[str]) -> Any:
|
||||
return reduce(dict.__getitem__, keys, data)
|
||||
|
||||
|
||||
@@ -249,6 +249,19 @@ class Telemetry:
|
||||
|
||||
self._safe_telemetry_procedure(_operation)
|
||||
|
||||
def feature_usage_span(self, feature: str) -> None:
|
||||
"""Records that a feature was used. One span = one count."""
|
||||
from crewai_core.version import get_crewai_version
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Feature Usage")
|
||||
self._add_attribute(span, "crewai_version", get_crewai_version())
|
||||
self._add_attribute(span, "feature", feature)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_procedure(_operation)
|
||||
|
||||
def flow_creation_span(self, flow_name: str) -> None:
|
||||
"""Records the creation of a new flow."""
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
from crewai_core import (
|
||||
constants,
|
||||
lock_store,
|
||||
paths,
|
||||
printer,
|
||||
project,
|
||||
user_data,
|
||||
version,
|
||||
)
|
||||
@@ -97,6 +99,83 @@ def test_unused_var_warning_silenced() -> None:
|
||||
assert os.environ is not None
|
||||
|
||||
|
||||
def test_configured_project_definition_resolves_project_local_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
definition = tmp_path / "crew.jsonc"
|
||||
definition.write_text("{}\n")
|
||||
|
||||
resolved = project.configured_project_definition(
|
||||
"crew",
|
||||
pyproject_data={
|
||||
"tool": {
|
||||
"crewai": {
|
||||
"type": "crew",
|
||||
"definition": " crew.jsonc ",
|
||||
}
|
||||
}
|
||||
},
|
||||
project_root=tmp_path,
|
||||
)
|
||||
|
||||
assert resolved == definition.resolve()
|
||||
|
||||
|
||||
def test_configured_project_definition_rejects_project_escape(tmp_path: Path) -> None:
|
||||
outside = tmp_path.parent / f"{tmp_path.name}-outside-crew.jsonc"
|
||||
outside.write_text("{}\n")
|
||||
|
||||
with pytest.raises(project.ProjectDefinitionError):
|
||||
project.configured_project_definition(
|
||||
"crew",
|
||||
pyproject_data={
|
||||
"tool": {
|
||||
"crewai": {
|
||||
"type": "crew",
|
||||
"definition": "../outside-crew.jsonc",
|
||||
}
|
||||
}
|
||||
},
|
||||
project_root=tmp_path,
|
||||
)
|
||||
|
||||
|
||||
def test_configured_project_definition_rejects_non_string_definition(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with pytest.raises(project.ProjectDefinitionError, match="must be a string"):
|
||||
project.configured_project_definition(
|
||||
"crew",
|
||||
pyproject_data={
|
||||
"tool": {
|
||||
"crewai": {
|
||||
"type": "crew",
|
||||
"definition": ["crew.jsonc"],
|
||||
}
|
||||
}
|
||||
},
|
||||
project_root=tmp_path,
|
||||
)
|
||||
|
||||
|
||||
def test_configured_project_definition_rejects_empty_definition(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with pytest.raises(project.ProjectDefinitionError, match="non-empty"):
|
||||
project.configured_project_definition(
|
||||
"crew",
|
||||
pyproject_data={
|
||||
"tool": {
|
||||
"crewai": {
|
||||
"type": "crew",
|
||||
"definition": " ",
|
||||
}
|
||||
}
|
||||
},
|
||||
project_root=tmp_path,
|
||||
)
|
||||
|
||||
|
||||
def test_core_telemetry_skips_duplicate_tracer_provider(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -128,3 +207,29 @@ def test_core_telemetry_skips_duplicate_tracer_provider(
|
||||
|
||||
assert called is False
|
||||
assert telemetry.trace_set is True
|
||||
|
||||
|
||||
def test_core_telemetry_records_feature_usage(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from crewai_core.telemetry import Telemetry
|
||||
|
||||
Telemetry._instance = None
|
||||
monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False)
|
||||
monkeypatch.delenv("CREWAI_DISABLE_TELEMETRY", raising=False)
|
||||
monkeypatch.delenv("CREWAI_DISABLE_TRACKING", raising=False)
|
||||
|
||||
tracer = Mock()
|
||||
span = Mock()
|
||||
tracer.start_span.return_value = span
|
||||
monkeypatch.setattr(
|
||||
"crewai_core.telemetry.trace.get_tracer",
|
||||
lambda _name: tracer,
|
||||
)
|
||||
|
||||
telemetry = Telemetry()
|
||||
telemetry.feature_usage_span("cli_usage:view_traces")
|
||||
|
||||
tracer.start_span.assert_called_once_with("Feature Usage")
|
||||
span.set_attribute.assert_any_call("feature", "cli_usage:view_traces")
|
||||
span.end.assert_called_once()
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.15.0"
|
||||
__version__ = "1.15.1a1"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests>=2.33.0,<3",
|
||||
"crewai==1.15.0",
|
||||
"crewai==1.15.1a1",
|
||||
"tiktoken>=0.8.0,<0.13",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -330,4 +330,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.15.0"
|
||||
__version__ = "1.15.1a1"
|
||||
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.15.0",
|
||||
"crewai-cli==1.15.0",
|
||||
"crewai-core==1.15.1a1",
|
||||
"crewai-cli==1.15.1a1",
|
||||
# Core Dependencies
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"openai>=2.30.0,<3",
|
||||
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.15.0",
|
||||
"crewai-tools==1.15.1a1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken>=0.8.0,<0.13"
|
||||
|
||||
@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.15.0"
|
||||
__version__ = "1.15.1a1"
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
|
||||
@@ -9,7 +9,6 @@ layer that may have produced it and of the engine that runs it (see
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -780,19 +779,6 @@ class FlowDefinition(BaseModel):
|
||||
"""Serialize the definition to a declaration-ready dictionary."""
|
||||
return self.model_dump(by_alias=True, exclude_none=exclude_none, mode="json")
|
||||
|
||||
def to_json(self, *, indent: int | None = 2, exclude_none: bool = True) -> str:
|
||||
"""Serialize the definition to JSON."""
|
||||
data = self.to_dict(exclude_none=exclude_none)
|
||||
return json.dumps(data, indent=indent)
|
||||
|
||||
def to_yaml(self, *, exclude_none: bool = True) -> str:
|
||||
"""Serialize the definition to YAML."""
|
||||
return yaml.safe_dump(
|
||||
self.to_dict(exclude_none=exclude_none),
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def source_path(self) -> Path | None:
|
||||
"""Original definition file path, when loaded from a file."""
|
||||
@@ -805,17 +791,6 @@ class FlowDefinition(BaseModel):
|
||||
return None
|
||||
return self._source_path.parent
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls, data: dict[str, Any], *, source_path: Path | None = None
|
||||
) -> FlowDefinition:
|
||||
"""Load a definition from a dictionary."""
|
||||
definition = cls.model_validate(data)
|
||||
if source_path is not None:
|
||||
definition._source_path = source_path.expanduser().resolve()
|
||||
log_flow_definition_issues(definition)
|
||||
return definition
|
||||
|
||||
@classmethod
|
||||
def from_declaration(
|
||||
cls,
|
||||
@@ -835,7 +810,7 @@ class FlowDefinition(BaseModel):
|
||||
contents = source_path.expanduser().read_text(encoding="utf-8")
|
||||
|
||||
if isinstance(contents, dict):
|
||||
return cls.from_dict(contents)
|
||||
return cls._load_mapping(contents)
|
||||
|
||||
if not isinstance(contents, str):
|
||||
raise TypeError("Flow declaration contents must be a string or dictionary")
|
||||
@@ -848,12 +823,17 @@ class FlowDefinition(BaseModel):
|
||||
loaded = yaml.safe_load(contents)
|
||||
if not isinstance(loaded, dict):
|
||||
raise ValueError("Flow declaration must contain a mapping")
|
||||
return cls.from_dict(loaded, source_path=source_path)
|
||||
return cls._load_mapping(loaded, source_path=source_path)
|
||||
|
||||
@classmethod
|
||||
def json_schema(cls) -> dict[str, Any]:
|
||||
"""Return the JSON Schema for the declarative Flow contract."""
|
||||
return cls.model_json_schema(by_alias=True)
|
||||
def _load_mapping(
|
||||
cls, data: dict[str, Any], *, source_path: Path | None = None
|
||||
) -> FlowDefinition:
|
||||
definition = cls.model_validate(data)
|
||||
if source_path is not None:
|
||||
definition._source_path = source_path.expanduser().resolve()
|
||||
log_flow_definition_issues(definition)
|
||||
return definition
|
||||
|
||||
|
||||
def _validate_step_name(name: str, *, field: str) -> None:
|
||||
|
||||
@@ -480,11 +480,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
cls._flow_definition = flow_definition
|
||||
return flow_definition
|
||||
|
||||
@classmethod
|
||||
def from_definition(cls, definition: FlowDefinition, **kwargs: Any) -> Flow[Any]:
|
||||
"""Build a runnable Flow directly from a definition; no subclass required."""
|
||||
return cls.from_declaration(contents=definition, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_declaration(
|
||||
cls,
|
||||
@@ -604,7 +599,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
config: Checkpoint configuration with ``restore_from`` set to
|
||||
the path of the checkpoint to load.
|
||||
definition: The FlowDefinition to restore a definition-built flow
|
||||
(one created via ``Flow.from_definition``) from; its actions
|
||||
(one created via ``Flow.from_declaration``) from; its actions
|
||||
are re-resolved since checkpoints carry no callables.
|
||||
Subclasses carry their own definition and don't need this.
|
||||
|
||||
@@ -629,7 +624,9 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
entity._restore_from_checkpoint()
|
||||
return entity
|
||||
instance = (
|
||||
cls.from_definition(definition) if definition is not None else cls()
|
||||
cls.from_declaration(contents=definition)
|
||||
if definition is not None
|
||||
else cls()
|
||||
)
|
||||
instance.checkpoint_completed_methods = entity.checkpoint_completed_methods
|
||||
instance.checkpoint_method_outputs = entity.checkpoint_method_outputs
|
||||
@@ -1178,7 +1175,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
registered factory when present, else the built-in SQLite
|
||||
fallback).
|
||||
definition: The FlowDefinition to restore a definition-built flow
|
||||
(one created via ``Flow.from_definition``) from. Subclasses
|
||||
(one created via ``Flow.from_declaration``) from. Subclasses
|
||||
carry their own definition and don't need this.
|
||||
**kwargs: Additional keyword arguments passed to the Flow constructor
|
||||
|
||||
@@ -1212,7 +1209,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
state_data, pending_context = loaded
|
||||
|
||||
instance = (
|
||||
cls.from_definition(definition, persistence=persistence, **kwargs)
|
||||
cls.from_declaration(contents=definition, persistence=persistence, **kwargs)
|
||||
if definition is not None
|
||||
else cls(persistence=persistence, **kwargs)
|
||||
)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""Memory reset utilities for CrewAI crews and flows."""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from crewai_core.project import configured_project_definition, read_toml
|
||||
|
||||
from crewai.flow import Flow
|
||||
from crewai.memory.unified_memory import Memory
|
||||
from crewai.project.crew_loader import load_crew
|
||||
from crewai.project.json_loader import find_crew_json_file
|
||||
from crewai.utilities.project_utils import get_crews, get_flows, read_toml
|
||||
from crewai.utilities.project_utils import get_crews, get_flows
|
||||
|
||||
|
||||
def _reset_flow_memory(flow: Flow[Any]) -> None:
|
||||
@@ -42,35 +43,20 @@ def _reset_flow_memory(flow: Flow[Any]) -> None:
|
||||
click.echo(f"Memory reset skipped: {exc}", err=True)
|
||||
|
||||
|
||||
def _current_project_declares_flow() -> bool:
|
||||
try:
|
||||
pyproject_data = read_toml()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
declared_type: str | None = (
|
||||
pyproject_data.get("tool", {}).get("crewai", {}).get("type")
|
||||
)
|
||||
return declared_type == "flow"
|
||||
def _configured_json_crew_path() -> Path | None:
|
||||
if not Path("pyproject.toml").is_file():
|
||||
return None
|
||||
pyproject_data = read_toml()
|
||||
return configured_project_definition("crew", pyproject_data=pyproject_data)
|
||||
|
||||
|
||||
def _get_json_crew() -> Any | None:
|
||||
"""Load a JSON-first crew from the current project, if present."""
|
||||
if _current_project_declares_flow():
|
||||
return None
|
||||
|
||||
crew_path = find_crew_json_file()
|
||||
crew_path = _configured_json_crew_path()
|
||||
if crew_path is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
crew, _ = load_crew(crew_path)
|
||||
except Exception as exc:
|
||||
click.echo(
|
||||
f"Skipping JSON crew at {crew_path}: failed to load ({exc}).",
|
||||
err=True,
|
||||
)
|
||||
return None
|
||||
crew, _ = load_crew(crew_path)
|
||||
return crew
|
||||
|
||||
|
||||
@@ -151,3 +137,4 @@ def reset_memories_command(
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
raise SystemExit(1) from e
|
||||
|
||||
@@ -240,6 +240,9 @@ def test_reset_no_crew_or_flow_found(runner):
|
||||
def test_reset_json_crew_memory(mock_crew, runner, monkeypatch, tmp_path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"crewai.utilities.reset_memories.get_crews", return_value=[]
|
||||
@@ -251,16 +254,19 @@ def test_reset_json_crew_memory(mock_crew, runner, monkeypatch, tmp_path):
|
||||
) as mock_load_crew:
|
||||
result = runner.invoke(reset_memories, ["-m"])
|
||||
|
||||
mock_load_crew.assert_called_once_with(Path("crew.jsonc"))
|
||||
mock_load_crew.assert_called_once_with((tmp_path / "crew.jsonc").resolve())
|
||||
mock_crew.reset_memories.assert_called_once_with(command_type="memory")
|
||||
assert f"[Crew ({mock_crew.name})] Memory has been reset." in result.output
|
||||
|
||||
|
||||
def test_reset_invalid_json_crew_does_not_block_classic_crew(
|
||||
def test_reset_invalid_json_crew_blocks_reset(
|
||||
mock_crew, runner, monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "crew.jsonc").write_text("{invalid")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"crewai.utilities.reset_memories.get_crews", return_value=[mock_crew]
|
||||
@@ -272,10 +278,10 @@ def test_reset_invalid_json_crew_does_not_block_classic_crew(
|
||||
) as mock_load_crew:
|
||||
result = runner.invoke(reset_memories, ["-m"])
|
||||
|
||||
mock_load_crew.assert_called_once_with(Path("crew.jsonc"))
|
||||
mock_crew.reset_memories.assert_called_once_with(command_type="memory")
|
||||
assert "Skipping JSON crew at crew.jsonc: failed to load (invalid JSON)." in result.output
|
||||
assert f"[Crew ({mock_crew.name})] Memory has been reset." in result.output
|
||||
mock_load_crew.assert_called_once_with((tmp_path / "crew.jsonc").resolve())
|
||||
mock_crew.reset_memories.assert_not_called()
|
||||
assert result.exit_code != 0
|
||||
assert "An unexpected error occurred: invalid JSON" in result.output
|
||||
|
||||
|
||||
def test_reset_json_crew_skipped_for_declared_flow_project(
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_flow_public_exports_are_explicit():
|
||||
|
||||
|
||||
def test_flow_definition_json_schema_carries_reference_descriptions():
|
||||
schema = flow_definition.FlowDefinition.json_schema()
|
||||
schema = flow_definition.FlowDefinition.model_json_schema(by_alias=True)
|
||||
defs = schema["$defs"]
|
||||
|
||||
assert schema["properties"]["schema"]["description"]
|
||||
@@ -120,7 +120,7 @@ def test_flow_definition_json_schema_carries_reference_descriptions():
|
||||
|
||||
|
||||
def test_flow_definition_json_schema_carries_field_examples_only():
|
||||
schema = flow_definition.FlowDefinition.json_schema()
|
||||
schema = flow_definition.FlowDefinition.model_json_schema(by_alias=True)
|
||||
defs = schema["$defs"]
|
||||
|
||||
for model_name in [
|
||||
@@ -437,7 +437,7 @@ def test_flow_definition_uses_collapsed_conversational_router_start():
|
||||
assert methods["route_conversation"].router is True
|
||||
|
||||
|
||||
def test_flow_definition_serializes_human_feedback_metadata(caplog):
|
||||
def test_flow_definition_degrades_human_feedback_metadata(caplog):
|
||||
caplog.set_level(logging.WARNING, logger="crewai.flow.dsl._utils")
|
||||
marker = object()
|
||||
|
||||
@@ -461,7 +461,7 @@ def test_flow_definition_serializes_human_feedback_metadata(caplog):
|
||||
and "not fully serializable" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
definition.to_json()
|
||||
definition.to_dict()
|
||||
|
||||
|
||||
def test_flow_definition_fragments_cover_start_listen_and_condition_sugar():
|
||||
@@ -613,7 +613,7 @@ def test_flow_definition_merges_stacked_listen_router():
|
||||
assert methods["second_router"].emit == ["second_approval", "not_approved"]
|
||||
|
||||
|
||||
def test_flow_definition_round_trips_declaration_serialization():
|
||||
def test_flow_definition_from_declaration_accepts_json_and_yaml_strings():
|
||||
class RoundTripFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
@@ -627,17 +627,67 @@ def test_flow_definition_round_trips_declaration_serialization():
|
||||
def left(self):
|
||||
return "left"
|
||||
|
||||
definition = RoundTripFlow.flow_definition()
|
||||
|
||||
round_trips = [
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_json()),
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_yaml()),
|
||||
expected = RoundTripFlow.flow_definition()
|
||||
declarations = [
|
||||
"""
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "RoundTripFlow",
|
||||
"methods": {
|
||||
"begin": {
|
||||
"start": true,
|
||||
"do": {
|
||||
"call": "code",
|
||||
"ref": "test_flow_definition:RoundTripFlow.begin"
|
||||
}
|
||||
},
|
||||
"decide": {
|
||||
"listen": "begin",
|
||||
"router": true,
|
||||
"do": {
|
||||
"call": "code",
|
||||
"ref": "test_flow_definition:RoundTripFlow.decide"
|
||||
}
|
||||
},
|
||||
"left": {
|
||||
"listen": "left",
|
||||
"do": {
|
||||
"call": "code",
|
||||
"ref": "test_flow_definition:RoundTripFlow.left"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
schema: crewai.flow/v1
|
||||
name: RoundTripFlow
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: code
|
||||
ref: test_flow_definition:RoundTripFlow.begin
|
||||
decide:
|
||||
listen: begin
|
||||
router: true
|
||||
do:
|
||||
call: code
|
||||
ref: test_flow_definition:RoundTripFlow.decide
|
||||
left:
|
||||
listen: left
|
||||
do:
|
||||
call: code
|
||||
ref: test_flow_definition:RoundTripFlow.left
|
||||
""",
|
||||
]
|
||||
|
||||
for round_trip in round_trips:
|
||||
assert round_trip.to_dict() == definition.to_dict()
|
||||
assert round_trip.methods["decide"].router is True
|
||||
assert round_trip.methods["decide"].listen == "begin"
|
||||
for declaration in declarations:
|
||||
loaded = flow_definition.FlowDefinition.from_declaration(contents=declaration)
|
||||
|
||||
assert loaded.name == expected.name
|
||||
assert loaded.methods["decide"].router is True
|
||||
assert loaded.methods["decide"].listen == "begin"
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_accepts_contents():
|
||||
@@ -654,20 +704,41 @@ def test_flow_definition_from_declaration_accepts_contents():
|
||||
},
|
||||
},
|
||||
}
|
||||
definition = flow_definition.FlowDefinition.from_dict(data)
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=data)
|
||||
contents = [
|
||||
definition,
|
||||
data,
|
||||
definition.to_json(),
|
||||
definition.to_yaml(),
|
||||
"""
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "DeclarationFlow",
|
||||
"methods": {
|
||||
"begin": {
|
||||
"start": true,
|
||||
"do": {
|
||||
"call": "expression",
|
||||
"expr": "'started'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
schema: crewai.flow/v1
|
||||
name: DeclarationFlow
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: "'started'"
|
||||
""",
|
||||
]
|
||||
expected = definition.to_dict()
|
||||
|
||||
for content in contents:
|
||||
loaded = flow_definition.FlowDefinition.from_declaration(contents=content)
|
||||
|
||||
assert loaded.to_dict() == expected
|
||||
|
||||
assert loaded.to_dict() == definition.to_dict()
|
||||
|
||||
def test_flow_definition_from_declaration_rejects_empty_file(tmp_path: Path):
|
||||
declaration_path = tmp_path / "flow.crewai"
|
||||
@@ -686,7 +757,7 @@ def test_flow_definition_from_declaration_rejects_falsey_non_mapping_contents(
|
||||
|
||||
|
||||
def test_flow_definition_from_declaration_accepts_paths(tmp_path: Path):
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "DeclarationFlow",
|
||||
@@ -702,7 +773,19 @@ def test_flow_definition_from_declaration_accepts_paths(tmp_path: Path):
|
||||
}
|
||||
)
|
||||
declaration_path = tmp_path / "flow.crewai"
|
||||
declaration_path.write_text(definition.to_yaml(), encoding="utf-8")
|
||||
declaration_path.write_text(
|
||||
"""
|
||||
schema: crewai.flow/v1
|
||||
name: DeclarationFlow
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: "'started'"
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
path_inputs = [
|
||||
declaration_path,
|
||||
str(declaration_path),
|
||||
@@ -711,7 +794,9 @@ def test_flow_definition_from_declaration_accepts_paths(tmp_path: Path):
|
||||
for path_input in path_inputs:
|
||||
loaded = flow_definition.FlowDefinition.from_declaration(path=path_input)
|
||||
|
||||
assert loaded.to_dict() == definition.to_dict()
|
||||
assert loaded.name == definition.name
|
||||
assert loaded.methods["begin"].is_start is True
|
||||
assert loaded.methods["begin"].do.call == "expression"
|
||||
assert loaded.source_path == declaration_path.resolve()
|
||||
|
||||
|
||||
@@ -744,8 +829,8 @@ def test_flow_definition_from_declaration_prefers_contents_over_path(
|
||||
assert loaded.source_path is None
|
||||
|
||||
|
||||
def test_each_action_round_trips_declaration_serialization():
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
def test_each_action_loads_from_declaration():
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "EachFlow",
|
||||
@@ -783,22 +868,13 @@ def test_each_action_round_trips_declaration_serialization():
|
||||
}
|
||||
)
|
||||
|
||||
round_trips = [
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_json()),
|
||||
flow_definition.FlowDefinition.from_declaration(contents=definition.to_yaml()),
|
||||
]
|
||||
|
||||
for round_trip in round_trips:
|
||||
assert round_trip.to_dict() == definition.to_dict()
|
||||
assert round_trip.methods["process_rows"].description == (
|
||||
"Process every loaded row."
|
||||
)
|
||||
assert round_trip.methods["process_rows"].do.call == "each"
|
||||
assert definition.methods["process_rows"].description == "Process every loaded row."
|
||||
assert definition.methods["process_rows"].do.call == "each"
|
||||
|
||||
|
||||
def test_flow_definition_rejects_invalid_method_names():
|
||||
with pytest.raises(ValueError, match="Flow method names must match"):
|
||||
flow_definition.FlowDefinition.from_dict(
|
||||
flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "InvalidMethodNameFlow",
|
||||
@@ -1009,7 +1085,7 @@ def test_flow_definition_accepts_explicit_router_events():
|
||||
|
||||
|
||||
def test_flow_definition_ignores_legacy_diagnostics_loaded_from_contract():
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "LoadedDiagnosticsFlow",
|
||||
@@ -1042,7 +1118,7 @@ def test_flow_definition_ignores_legacy_diagnostics_loaded_from_contract():
|
||||
def test_router_start_false_without_listen_is_allowed(caplog):
|
||||
caplog.set_level(logging.ERROR, logger="crewai.flow.flow_definition")
|
||||
|
||||
flow_definition.FlowDefinition.from_dict(
|
||||
flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "LoadedFlow",
|
||||
@@ -1118,7 +1194,7 @@ def test_dynamic_router_string_listener_is_valid_contract():
|
||||
|
||||
|
||||
def test_static_string_listener_is_allowed_by_contract():
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "TypoFlow",
|
||||
@@ -1138,7 +1214,7 @@ def test_static_string_listener_is_allowed_by_contract():
|
||||
|
||||
|
||||
def test_start_false_not_classified_as_start_method():
|
||||
definition = flow_definition.FlowDefinition.from_dict(
|
||||
definition = flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ExplicitNonStartFlow",
|
||||
@@ -1202,7 +1278,7 @@ def test_flow_definition_cache_is_not_reused_by_subclasses():
|
||||
def test_flow_definition_allows_router_without_trigger(caplog):
|
||||
caplog.set_level(logging.WARNING, logger="crewai.flow.flow_definition")
|
||||
|
||||
flow_definition.FlowDefinition.from_dict(
|
||||
flow_definition.FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "LoadedFlow",
|
||||
|
||||
@@ -477,7 +477,7 @@ def assert_parity(flow_cls, yaml_str, inputs=None, ordered=True):
|
||||
class_result, class_events = _run_with_events(class_flow, inputs)
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=yaml_str)
|
||||
definition_flow = Flow.from_definition(definition)
|
||||
definition_flow = Flow.from_declaration(contents=definition)
|
||||
definition_result, definition_events = _run_with_events(definition_flow, inputs)
|
||||
|
||||
assert definition_result == class_result
|
||||
@@ -537,7 +537,7 @@ def test_cyclic_flow_parity():
|
||||
|
||||
def test_definition_flow_events_use_definition_name():
|
||||
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
|
||||
flow = Flow.from_definition(definition)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
_, events = _run_with_events(flow)
|
||||
assert events
|
||||
assert all(flow_name == "ChainFlow" for _, _, flow_name in events)
|
||||
@@ -545,7 +545,7 @@ def test_definition_flow_events_use_definition_name():
|
||||
|
||||
def test_definition_method_without_action_is_invalid():
|
||||
with pytest.raises(ValidationError, match="do"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "NoActions",
|
||||
@@ -554,8 +554,8 @@ def test_definition_method_without_action_is_invalid():
|
||||
)
|
||||
|
||||
|
||||
def test_from_definition_unresolvable_ref_raises():
|
||||
definition = FlowDefinition.from_dict(
|
||||
def test_from_declaration_unresolvable_ref_raises():
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "BadRefs",
|
||||
@@ -569,11 +569,11 @@ def test_from_definition_unresolvable_ref_raises():
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="unresolvable actions.*begin"):
|
||||
Flow.from_definition(definition)
|
||||
Flow.from_declaration(contents=definition)
|
||||
|
||||
|
||||
def test_from_definition_malformed_ref_raises():
|
||||
definition = FlowDefinition.from_dict(
|
||||
def test_from_declaration_malformed_ref_raises():
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "MalformedRefs",
|
||||
@@ -582,11 +582,11 @@ def test_from_definition_malformed_ref_raises():
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="expected 'module:qualname'"):
|
||||
Flow.from_definition(definition)
|
||||
Flow.from_declaration(contents=definition)
|
||||
|
||||
|
||||
def test_from_definition_local_scope_ref_raises():
|
||||
definition = FlowDefinition.from_dict(
|
||||
def test_from_declaration_local_scope_ref_raises():
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "LocalRefs",
|
||||
@@ -600,7 +600,7 @@ def test_from_definition_local_scope_ref_raises():
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="expected 'module:qualname'"):
|
||||
Flow.from_definition(definition)
|
||||
Flow.from_declaration(contents=definition)
|
||||
|
||||
|
||||
def test_flow_definition_stamps_refs():
|
||||
@@ -610,7 +610,7 @@ def test_flow_definition_stamps_refs():
|
||||
assert definition.methods["shout"].do.ref == f"{__name__}:ChainFlow.shout"
|
||||
|
||||
|
||||
def test_from_definition_runs_tool_action_with_static_inputs():
|
||||
def test_from_declaration_runs_tool_action_with_static_inputs():
|
||||
yaml_str = f"""
|
||||
schema: crewai.flow/v1
|
||||
name: ToolFlow
|
||||
@@ -625,13 +625,13 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff() == "found:ai agents"
|
||||
|
||||
|
||||
def test_tool_action_round_trips_with_inputs():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ToolFlow",
|
||||
@@ -648,12 +648,12 @@ def test_tool_action_round_trips_with_inputs():
|
||||
}
|
||||
)
|
||||
|
||||
assert definition.to_dict()["methods"]["search"]["do"] == {
|
||||
"call": "tool",
|
||||
"ref": f"{__name__}:StaticSearchTool",
|
||||
"with": {"search_query": "ai agents"},
|
||||
}
|
||||
assert Flow.from_definition(definition).kickoff() == "search:ai agents"
|
||||
action = definition.methods["search"].do
|
||||
|
||||
assert action.call == "tool"
|
||||
assert action.ref == f"{__name__}:StaticSearchTool"
|
||||
assert action.with_ == {"search_query": "ai agents"}
|
||||
assert Flow.from_declaration(contents=definition).kickoff() == "search:ai agents"
|
||||
|
||||
|
||||
def test_tool_action_renders_cel_inputs_at_runtime():
|
||||
@@ -676,13 +676,13 @@ methods:
|
||||
listen: begin
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "ai"}) == "found:ai agents"
|
||||
|
||||
|
||||
def test_tool_action_treats_embedded_cel_marker_as_literal():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ToolFlow",
|
||||
@@ -702,11 +702,11 @@ def test_tool_action_treats_embedded_cel_marker_as_literal():
|
||||
}
|
||||
)
|
||||
|
||||
assert Flow.from_definition(definition).kickoff() == "p}x:wrapped ${'a}b'} value"
|
||||
assert Flow.from_declaration(contents=definition).kickoff() == "p}x:wrapped ${'a}b'} value"
|
||||
|
||||
|
||||
def test_tool_action_treats_marker_with_trailing_text_as_literal():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ToolFlow",
|
||||
@@ -726,12 +726,12 @@ def test_tool_action_treats_marker_with_trailing_text_as_literal():
|
||||
}
|
||||
)
|
||||
|
||||
assert Flow.from_definition(definition).kickoff() == "p:${state.topic} extra"
|
||||
assert Flow.from_declaration(contents=definition).kickoff() == "p:${state.topic} extra"
|
||||
|
||||
|
||||
def test_tool_action_rejects_adjacent_markers_as_invalid_cel():
|
||||
with pytest.raises(ValidationError, match="invalid CEL expression"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ToolFlow",
|
||||
@@ -753,7 +753,7 @@ def test_tool_action_rejects_adjacent_markers_as_invalid_cel():
|
||||
|
||||
|
||||
def test_tool_action_accepts_braces_in_full_cel_marker():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ToolFlow",
|
||||
@@ -773,7 +773,7 @@ def test_tool_action_accepts_braces_in_full_cel_marker():
|
||||
}
|
||||
)
|
||||
|
||||
assert Flow.from_definition(definition).kickoff() == "p}x:ai agents"
|
||||
assert Flow.from_declaration(contents=definition).kickoff() == "p}x:ai agents"
|
||||
|
||||
|
||||
def test_tool_action_renders_latest_output_by_method_name():
|
||||
@@ -795,7 +795,7 @@ methods:
|
||||
listen: begin
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff() == "search:hello agents"
|
||||
|
||||
@@ -820,7 +820,7 @@ methods:
|
||||
listen: build_query
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff() == "found:ai agents news"
|
||||
|
||||
@@ -840,7 +840,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert (
|
||||
flow.kickoff(inputs={"limit": 2, "domains": ["crewai.com", "example.com"]})
|
||||
@@ -873,7 +873,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"question": "What is CrewAI?"}) == {
|
||||
"agent": "Analyst",
|
||||
@@ -911,7 +911,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"questions": ["one", "two"]}) == [
|
||||
"Analyst:one",
|
||||
@@ -920,7 +920,7 @@ methods:
|
||||
|
||||
|
||||
def test_agent_action_round_trips_with_inline_definition():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "AgentFlow",
|
||||
@@ -942,17 +942,16 @@ def test_agent_action_round_trips_with_inline_definition():
|
||||
}
|
||||
)
|
||||
|
||||
round_trip = FlowDefinition.from_declaration(contents=definition.to_yaml())
|
||||
action = round_trip.to_dict()["methods"]["answer"]["do"]
|
||||
action = definition.methods["answer"].do
|
||||
|
||||
assert action["call"] == "agent"
|
||||
assert action["with"]["role"] == "Analyst"
|
||||
assert action["with"]["input"] == "${state.question}"
|
||||
assert action["with"]["settings"] == {"verbose": True}
|
||||
assert action.call == "agent"
|
||||
assert action.with_.role == "Analyst"
|
||||
assert action.with_.input == "${state.question}"
|
||||
assert action.with_.settings == {"verbose": True}
|
||||
|
||||
|
||||
def test_agent_action_json_schema_describes_inline_agent_definitions():
|
||||
schema_defs = FlowDefinition.json_schema()["$defs"]
|
||||
schema_defs = FlowDefinition.model_json_schema(by_alias=True)["$defs"]
|
||||
|
||||
assert set(schema_defs["AgentDefinition"]["properties"]) >= {
|
||||
"role",
|
||||
@@ -966,7 +965,7 @@ def test_agent_action_json_schema_describes_inline_agent_definitions():
|
||||
|
||||
def test_agent_action_rejects_non_string_input_in_definition():
|
||||
with pytest.raises(ValidationError, match="agent.input must be a string"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "AgentFlow",
|
||||
@@ -1047,7 +1046,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "inline_research",
|
||||
@@ -1123,7 +1122,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "referenced_research",
|
||||
@@ -1197,7 +1196,7 @@ methods:
|
||||
other_cwd.mkdir()
|
||||
monkeypatch.chdir(other_cwd)
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(path=flow_path))
|
||||
flow = Flow.from_declaration(path=flow_path)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "relative_research",
|
||||
@@ -1222,7 +1221,7 @@ methods:
|
||||
"""
|
||||
flow_path.write_text(yaml_str, encoding="utf-8")
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(path=flow_path))
|
||||
flow = Flow.from_declaration(path=flow_path)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
@@ -1232,7 +1231,7 @@ methods:
|
||||
|
||||
|
||||
def test_crew_action_round_trips_with_inline_definition():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
@@ -1266,20 +1265,16 @@ def test_crew_action_round_trips_with_inline_definition():
|
||||
}
|
||||
)
|
||||
|
||||
assert definition.to_dict()["methods"]["research"]["do"]["call"] == "crew"
|
||||
assert (
|
||||
definition.to_dict()["methods"]["research"]["do"]["with"]["agents"][
|
||||
"researcher"
|
||||
]["role"]
|
||||
== "Researcher"
|
||||
)
|
||||
assert definition.to_dict()["methods"]["research"]["do"]["inputs"] == {
|
||||
"topic": "${state.topic}"
|
||||
}
|
||||
action = definition.methods["research"].do
|
||||
|
||||
assert action.call == "crew"
|
||||
assert action.with_ is not None
|
||||
assert action.with_.agents["researcher"].role == "Researcher"
|
||||
assert action.inputs == {"topic": "${state.topic}"}
|
||||
|
||||
|
||||
def test_crew_action_normalizes_named_agent_list_definition():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
@@ -1311,16 +1306,15 @@ def test_crew_action_normalizes_named_agent_list_definition():
|
||||
}
|
||||
)
|
||||
|
||||
assert (
|
||||
definition.to_dict()["methods"]["research"]["do"]["with"]["agents"][
|
||||
"researcher"
|
||||
]["role"]
|
||||
== "Researcher"
|
||||
)
|
||||
action = definition.methods["research"].do
|
||||
|
||||
assert action.call == "crew"
|
||||
assert action.with_ is not None
|
||||
assert action.with_.agents["researcher"].role == "Researcher"
|
||||
|
||||
|
||||
def test_crew_action_json_schema_describes_inline_crew_definitions():
|
||||
schema_defs = FlowDefinition.json_schema()["$defs"]
|
||||
schema_defs = FlowDefinition.model_json_schema(by_alias=True)["$defs"]
|
||||
agents_schema = schema_defs["CrewDefinition"]["properties"]["agents"]
|
||||
|
||||
assert set(schema_defs["CrewDefinition"]["properties"]) >= {
|
||||
@@ -1345,7 +1339,7 @@ def test_crew_action_json_schema_describes_inline_crew_definitions():
|
||||
|
||||
def test_crew_action_rejects_incomplete_inline_agent_definition():
|
||||
with pytest.raises(ValidationError, match="goal"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
@@ -1378,7 +1372,7 @@ def test_crew_action_rejects_incomplete_inline_agent_definition():
|
||||
|
||||
def test_crew_action_rejects_python_ref_field():
|
||||
with pytest.raises(ValidationError, match="ref"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
@@ -1397,7 +1391,7 @@ def test_crew_action_rejects_python_ref_field():
|
||||
|
||||
def test_crew_action_rejects_non_mapping_inputs_in_definition():
|
||||
with pytest.raises(ValidationError, match="crew.inputs must be a mapping"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
@@ -1463,7 +1457,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"name": "hello"}) == "hello!"
|
||||
|
||||
@@ -1482,7 +1476,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"value": "ok"}) == "callable:ok"
|
||||
|
||||
@@ -1506,7 +1500,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
|
||||
"normalized:a",
|
||||
@@ -1533,7 +1527,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
caller_thread_id = threading.get_ident()
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a"]}) == ["process_rows:a"]
|
||||
@@ -1560,7 +1554,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == ["async:a", "async:b"]
|
||||
|
||||
@@ -1582,7 +1576,7 @@ methods:
|
||||
FlowScriptExecutionDisabledError,
|
||||
match="CREWAI_ALLOW_FLOW_SCRIPT_EXECUTION=1",
|
||||
) as exc_info:
|
||||
Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
Flow.from_declaration(contents=yaml_str)
|
||||
assert "methods with unresolvable actions" not in str(exc_info.value)
|
||||
|
||||
|
||||
@@ -1606,7 +1600,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"raw_score": 3.2}) == "rounded:4"
|
||||
assert flow.state["rounded"] == 4
|
||||
@@ -1635,7 +1629,7 @@ methods:
|
||||
listen: seed
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff() == "alpha:alpha"
|
||||
assert flow.state["input_matches_output"] is True
|
||||
@@ -1673,7 +1667,7 @@ methods:
|
||||
listen: seed
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": [" a ", " b "]}) == ["global:a", "global:b"]
|
||||
|
||||
@@ -1705,7 +1699,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
|
||||
{"row": "a", "normalized": "saved:a"},
|
||||
@@ -1734,7 +1728,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == ["a", "b"]
|
||||
assert flow._method_outputs == [
|
||||
@@ -1772,7 +1766,7 @@ methods:
|
||||
listen: seed
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": ["a", "b"]}) == [
|
||||
"local:a",
|
||||
@@ -1811,7 +1805,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(
|
||||
inputs={
|
||||
@@ -1845,7 +1839,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"rows": [{"kind": "keep", "value": "a"}]}) == ["a"]
|
||||
|
||||
@@ -1872,7 +1866,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(
|
||||
inputs={
|
||||
@@ -1902,7 +1896,7 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
with pytest.raises(ValueError, match="if expression must evaluate to a boolean"):
|
||||
flow.kickoff(inputs={"rows": [{"value": "truthy"}]})
|
||||
@@ -1932,7 +1926,7 @@ methods:
|
||||
listen: process_rows
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
events = []
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@@ -1958,7 +1952,7 @@ methods:
|
||||
],
|
||||
)
|
||||
def test_each_action_rejects_non_list_inputs(expr, inputs):
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "EachFlow",
|
||||
@@ -1979,7 +1973,7 @@ def test_each_action_rejects_non_list_inputs(expr, inputs):
|
||||
},
|
||||
}
|
||||
)
|
||||
flow = Flow.from_definition(definition)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
|
||||
with pytest.raises(ValueError, match="each.in must evaluate to an array"):
|
||||
flow.kickoff(inputs=inputs)
|
||||
@@ -2009,7 +2003,7 @@ def test_each_action_rejects_non_list_inputs(expr, inputs):
|
||||
)
|
||||
def test_each_action_validates_step_shape(action_do):
|
||||
with pytest.raises(ValidationError):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "EachFlow",
|
||||
@@ -2029,7 +2023,7 @@ def test_each_action_validates_step_shape(action_do):
|
||||
|
||||
def test_if_clauses_are_rejected_at_method_level():
|
||||
with pytest.raises(ValidationError):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "TopLevelIfFlow",
|
||||
@@ -2049,7 +2043,7 @@ def test_if_clauses_are_rejected_at_method_level():
|
||||
|
||||
def test_each_action_rejects_nested_each_actions():
|
||||
with pytest.raises(ValidationError):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "EachFlow",
|
||||
@@ -2103,14 +2097,14 @@ methods:
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
with pytest.raises(RuntimeError, match="bad row"):
|
||||
flow.kickoff(inputs={"rows": ["ok", "bad"]})
|
||||
|
||||
|
||||
def test_expression_action_round_trips():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ExpressionFlow",
|
||||
@@ -2126,15 +2120,15 @@ def test_expression_action_round_trips():
|
||||
}
|
||||
)
|
||||
|
||||
assert definition.to_dict()["methods"]["classify"]["do"] == {
|
||||
"call": "expression",
|
||||
"expr": "state.score >= 80 ? 'qualified' : 'nurture'",
|
||||
}
|
||||
assert Flow.from_definition(definition).kickoff(inputs={"score": 90}) == "qualified"
|
||||
action = definition.methods["classify"].do
|
||||
|
||||
assert action.call == "expression"
|
||||
assert action.expr == "state.score >= 80 ? 'qualified' : 'nurture'"
|
||||
assert Flow.from_declaration(contents=definition).kickoff(inputs={"score": 90}) == "qualified"
|
||||
|
||||
|
||||
def test_explicit_cel_fields_accept_expression_markers():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ExpressionFlow",
|
||||
@@ -2150,7 +2144,7 @@ def test_explicit_cel_fields_accept_expression_markers():
|
||||
}
|
||||
)
|
||||
|
||||
assert Flow.from_definition(definition).kickoff(inputs={"score": 90}) == "qualified"
|
||||
assert Flow.from_declaration(contents=definition).kickoff(inputs={"score": 90}) == "qualified"
|
||||
|
||||
|
||||
def test_expression_local_context_recurses_into_dataclass_values():
|
||||
@@ -2226,10 +2220,10 @@ methods:
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=yaml_str)
|
||||
|
||||
assert Flow.from_definition(definition).kickoff(
|
||||
assert Flow.from_declaration(contents=definition).kickoff(
|
||||
inputs={"direction": "left"}
|
||||
) == "took-left"
|
||||
assert Flow.from_definition(definition).kickoff(
|
||||
assert Flow.from_declaration(contents=definition).kickoff(
|
||||
inputs={"direction": "right"}
|
||||
) == "took-right"
|
||||
|
||||
@@ -2267,7 +2261,7 @@ methods:
|
||||
|
||||
|
||||
def test_tool_action_requires_module_qualname_ref():
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "ToolFlow",
|
||||
@@ -2285,7 +2279,7 @@ def test_tool_action_requires_module_qualname_ref():
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="expected 'module:qualname'"):
|
||||
Flow.from_definition(definition)
|
||||
Flow.from_declaration(contents=definition)
|
||||
|
||||
|
||||
def test_pydantic_state_from_ref_parity():
|
||||
@@ -2297,7 +2291,7 @@ def test_pydantic_state_from_ref_parity():
|
||||
|
||||
|
||||
def test_pydantic_state_default_overlay():
|
||||
flow = Flow.from_definition(
|
||||
flow = Flow.from_declaration(contents=
|
||||
FlowDefinition.from_declaration(contents=PYDANTIC_STATE_OVERLAY_YAML)
|
||||
)
|
||||
result = flow.kickoff()
|
||||
@@ -2306,7 +2300,7 @@ def test_pydantic_state_default_overlay():
|
||||
|
||||
|
||||
def test_json_schema_state():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=JSON_SCHEMA_STATE_YAML))
|
||||
flow = Flow.from_declaration(contents=JSON_SCHEMA_STATE_YAML)
|
||||
result = flow.kickoff()
|
||||
assert result == "count=1"
|
||||
assert flow.state.count == 1
|
||||
@@ -2315,13 +2309,13 @@ def test_json_schema_state():
|
||||
|
||||
|
||||
def test_json_schema_state_validates_inputs():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=JSON_SCHEMA_STATE_YAML))
|
||||
flow = Flow.from_declaration(contents=JSON_SCHEMA_STATE_YAML)
|
||||
with pytest.raises(ValueError, match="Invalid inputs"):
|
||||
flow.kickoff(inputs={"count": "not-a-number"})
|
||||
|
||||
|
||||
def test_json_schema_state_required_fields_can_come_from_kickoff_inputs():
|
||||
flow = Flow.from_definition(
|
||||
flow = Flow.from_declaration(contents=
|
||||
FlowDefinition.from_declaration(contents=JSON_SCHEMA_REQUIRED_INPUT_STATE_YAML)
|
||||
)
|
||||
|
||||
@@ -2333,7 +2327,7 @@ def test_json_schema_state_required_fields_can_come_from_kickoff_inputs():
|
||||
|
||||
|
||||
def test_pydantic_state_falls_back_to_json_schema_when_ref_unimportable():
|
||||
flow = Flow.from_definition(
|
||||
flow = Flow.from_declaration(contents=
|
||||
FlowDefinition.from_declaration(contents=PYDANTIC_REF_WITH_SCHEMA_FALLBACK_YAML)
|
||||
)
|
||||
result = flow.kickoff()
|
||||
@@ -2343,7 +2337,7 @@ def test_pydantic_state_falls_back_to_json_schema_when_ref_unimportable():
|
||||
|
||||
def test_pydantic_state_without_ref_or_schema_falls_back_to_dict(caplog):
|
||||
with caplog.at_level("ERROR"):
|
||||
flow = Flow.from_definition(
|
||||
flow = Flow.from_declaration(contents=
|
||||
FlowDefinition.from_declaration(contents=UNRESOLVABLE_STATE_YAML)
|
||||
)
|
||||
assert "falling back to dict state" in caplog.text
|
||||
@@ -2357,13 +2351,13 @@ def test_pydantic_state_without_ref_or_schema_falls_back_to_dict(caplog):
|
||||
def test_dict_state_is_a_copy_of_default_plus_id():
|
||||
definition = FlowDefinition.from_declaration(contents=DICT_STATE_YAML)
|
||||
|
||||
flow = Flow.from_definition(definition)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
assert flow.state["count"] == 5
|
||||
assert flow.state["id"]
|
||||
flow.kickoff()
|
||||
assert flow.state["begin_ran"] is True
|
||||
|
||||
second = Flow.from_definition(definition)
|
||||
second = Flow.from_declaration(contents=definition)
|
||||
assert second.state["count"] == 5
|
||||
assert "begin_ran" not in second.state
|
||||
assert second.state["id"] != flow.state["id"]
|
||||
@@ -2372,7 +2366,7 @@ def test_dict_state_is_a_copy_of_default_plus_id():
|
||||
|
||||
def test_unknown_state_type_falls_back_to_dict(caplog):
|
||||
with caplog.at_level("WARNING"):
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=UNKNOWN_STATE_YAML))
|
||||
flow = Flow.from_declaration(contents=UNKNOWN_STATE_YAML)
|
||||
assert "falling back to dict state" in caplog.text
|
||||
|
||||
result = flow.kickoff()
|
||||
@@ -2445,7 +2439,7 @@ def _run_capturing_flow_lifecycle(yaml_str, event_types):
|
||||
def capture(source, event):
|
||||
events.append(event)
|
||||
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
result = flow.kickoff()
|
||||
return flow, result, events
|
||||
|
||||
@@ -2483,13 +2477,13 @@ def test_config_suppress_flow_events_from_declaration():
|
||||
|
||||
|
||||
def test_config_max_method_calls_from_declaration():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=CAPPED_LOOP_YAML))
|
||||
flow = Flow.from_declaration(contents=CAPPED_LOOP_YAML)
|
||||
with pytest.raises(RecursionError, match="has been called 2 times"):
|
||||
flow.kickoff()
|
||||
|
||||
|
||||
def test_config_stream_from_declaration():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=STREAMING_CHAIN_YAML))
|
||||
flow = Flow.from_declaration(contents=STREAMING_CHAIN_YAML)
|
||||
streaming = flow.kickoff()
|
||||
assert isinstance(streaming, FlowStreamingOutput)
|
||||
for _ in streaming:
|
||||
@@ -2521,24 +2515,24 @@ config:
|
||||
location: {tmp_path}
|
||||
"""
|
||||
)
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
assert isinstance(flow.checkpoint, CheckpointConfig)
|
||||
assert flow.checkpoint.location == str(tmp_path)
|
||||
|
||||
|
||||
def test_config_input_provider_from_declaration():
|
||||
flow = Flow.from_definition(
|
||||
flow = Flow.from_declaration(contents=
|
||||
FlowDefinition.from_declaration(contents=INPUT_PROVIDER_CHAIN_YAML)
|
||||
)
|
||||
assert isinstance(flow.input_provider, StubInputProvider)
|
||||
|
||||
|
||||
def test_round_trip_config_equivalence():
|
||||
def test_definition_config_equivalence():
|
||||
class_flow = ConfiguredFlow()
|
||||
definition = FlowDefinition.from_declaration(
|
||||
contents=ConfiguredFlow.flow_definition().to_yaml()
|
||||
contents=ConfiguredFlow.flow_definition()
|
||||
)
|
||||
definition_flow = Flow.from_definition(definition)
|
||||
definition_flow = Flow.from_declaration(contents=definition)
|
||||
|
||||
assert definition.config.suppress_flow_events is True
|
||||
assert definition.config.max_method_calls == 5
|
||||
@@ -2555,7 +2549,7 @@ def test_round_trip_config_equivalence():
|
||||
|
||||
def test_unknown_schema_rejected():
|
||||
with pytest.raises(ValidationError, match="schema"):
|
||||
FlowDefinition.from_dict(
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v2",
|
||||
"name": "FutureSchema",
|
||||
@@ -2709,7 +2703,7 @@ class MethodPersistedFlow(Flow):
|
||||
|
||||
def test_flow_level_persist_from_declaration_saves_once_per_method():
|
||||
yaml_str = _flow_level_persist_yaml("yaml-flow-level")
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "two"
|
||||
@@ -2721,7 +2715,7 @@ def test_flow_level_persist_from_declaration_saves_once_per_method():
|
||||
|
||||
def test_method_level_persist_from_declaration_saves_only_that_method():
|
||||
yaml_str = _method_level_persist_yaml("yaml-method-level")
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("yaml-method-level") == ["first"]
|
||||
@@ -2750,7 +2744,7 @@ methods:
|
||||
persist:
|
||||
enabled: false
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("yaml-opt-out") == ["first"]
|
||||
@@ -2759,11 +2753,11 @@ methods:
|
||||
def test_persist_restore_by_id_from_declaration():
|
||||
yaml_str = _flow_level_persist_yaml("yaml-restore")
|
||||
|
||||
flow1 = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow1 = Flow.from_declaration(contents=yaml_str)
|
||||
flow1.kickoff()
|
||||
assert flow1.state["count"] == 2
|
||||
|
||||
flow2 = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow2 = Flow.from_declaration(contents=yaml_str)
|
||||
flow2.kickoff(inputs={"id": flow1.state["id"]})
|
||||
assert flow2.state["count"] == 4
|
||||
|
||||
@@ -2782,13 +2776,13 @@ def test_method_level_persist_decorator_saves_only_that_method():
|
||||
assert _saved_methods("method-decorator")[before:] == ["first"]
|
||||
|
||||
|
||||
def test_round_trip_persist_equivalence():
|
||||
def test_definition_persist_equivalence():
|
||||
definition = FlowDefinition.from_declaration(
|
||||
contents=ClassPersistedFlow.flow_definition().to_yaml()
|
||||
contents=ClassPersistedFlow.flow_definition()
|
||||
)
|
||||
|
||||
before = len(DefinitionStoreBackend.saves["class-decorator"])
|
||||
flow = Flow.from_definition(definition)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("class-decorator")[before:] == ["first", "second"]
|
||||
@@ -2818,7 +2812,7 @@ methods:
|
||||
persistence_type: DefinitionStoreBackend
|
||||
store: yaml-mixed-method
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
flow.kickoff()
|
||||
|
||||
assert _saved_methods("yaml-mixed-flow") == ["first"]
|
||||
@@ -2967,7 +2961,7 @@ methods:
|
||||
|
||||
|
||||
def test_human_feedback_from_declaration_default_outcome_routes():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=REVIEW_YAML))
|
||||
flow = Flow.from_declaration(contents=REVIEW_YAML)
|
||||
|
||||
with patch.object(flow, "_request_human_feedback", return_value="") as request:
|
||||
result = flow.kickoff()
|
||||
@@ -2979,7 +2973,7 @@ def test_human_feedback_from_declaration_default_outcome_routes():
|
||||
|
||||
|
||||
def test_human_feedback_from_declaration_collapses_and_routes():
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=REVIEW_YAML))
|
||||
flow = Flow.from_declaration(contents=REVIEW_YAML)
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="ship it"),
|
||||
@@ -2991,13 +2985,13 @@ def test_human_feedback_from_declaration_collapses_and_routes():
|
||||
assert [r.outcome for r in flow.human_feedback_history] == ["approved"]
|
||||
|
||||
|
||||
def test_round_trip_human_feedback_equivalence():
|
||||
def test_definition_human_feedback_equivalence():
|
||||
class_flow = ReviewFlow()
|
||||
with patch.object(class_flow, "_request_human_feedback", return_value=""):
|
||||
class_result = class_flow.kickoff()
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=ReviewFlow.flow_definition().to_yaml())
|
||||
twin = Flow.from_definition(definition)
|
||||
definition = FlowDefinition.from_declaration(contents=ReviewFlow.flow_definition())
|
||||
twin = Flow.from_declaration(contents=definition)
|
||||
with patch.object(twin, "_request_human_feedback", return_value=""):
|
||||
twin_result = twin.kickoff()
|
||||
|
||||
@@ -3012,7 +3006,7 @@ def test_round_trip_human_feedback_equivalence():
|
||||
def test_human_feedback_pending_and_resume_from_declaration():
|
||||
definition = FlowDefinition.from_declaration(contents=PENDING_REVIEW_YAML)
|
||||
|
||||
flow = Flow.from_definition(definition)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
pending = flow.kickoff()
|
||||
|
||||
assert isinstance(pending, HumanFeedbackPending)
|
||||
@@ -3057,7 +3051,7 @@ methods:
|
||||
return "from-config"
|
||||
|
||||
provider = RecordingProvider()
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
previous = flow_config.hitl_provider
|
||||
flow_config.hitl_provider = provider
|
||||
@@ -3160,7 +3154,7 @@ methods:
|
||||
message: "Review:"
|
||||
provider: {__name__}:_NeedsArgsProvider
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="cannot instantiate human_feedback.provider ref"
|
||||
@@ -3181,7 +3175,7 @@ methods:
|
||||
message: "Review:"
|
||||
provider: missing_module_xyz:Provider
|
||||
"""
|
||||
flow = Flow.from_definition(FlowDefinition.from_declaration(contents=yaml_str))
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="unresolvable human_feedback.provider ref"
|
||||
@@ -3194,7 +3188,7 @@ def _checkpoint_chain_flow(tmp_path):
|
||||
from crewai.state.runtime import RuntimeState
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=CHAIN_YAML)
|
||||
flow = Flow.from_definition(definition)
|
||||
flow = Flow.from_declaration(contents=definition)
|
||||
result = flow.kickoff()
|
||||
assert result == "confirmed:True"
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ class ComplexFlow(Flow):
|
||||
def _attach_flow_definition(
|
||||
flow_class: type[Flow], methods: dict[str, dict[str, object]]
|
||||
) -> None:
|
||||
flow_class._flow_definition = FlowDefinition.from_dict(
|
||||
flow_class._flow_definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": flow_class.__name__,
|
||||
@@ -130,7 +130,7 @@ def test_build_flow_structure_from_flow_class():
|
||||
|
||||
def test_build_flow_structure_from_flow_definition():
|
||||
"""Test building visualization directly from a FlowDefinition."""
|
||||
definition = FlowDefinition.from_dict(
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "DefinedFlow",
|
||||
@@ -374,7 +374,7 @@ def test_topological_path_counting():
|
||||
assert len(structure["edges"]) > 0
|
||||
|
||||
|
||||
def test_class_metadata_comes_from_definition():
|
||||
def test_class_metadata_comes_from_declaration():
|
||||
"""Test that nodes include only definition-derived class metadata."""
|
||||
flow = SimpleFlow()
|
||||
structure = build_flow_structure(flow)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.15.0"
|
||||
__version__ = "1.15.1a1"
|
||||
|
||||
Reference in New Issue
Block a user