Compare commits

...

11 Commits
1.15.0 ... main

Author SHA1 Message Date
Vinicius Brasil
e1ddb32e56 Initialize Git repositories for generated projects (#6364)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Check Documentation Broken Links / Check broken links (push) Waiting to run
Vulnerability Scan / pip-audit (push) Waiting to run
2026-06-26 16:29:49 -07:00
Ossama Alami
05ab1ece8e docs(readme): improve open source positioning (#6363) 2026-06-26 16:06:16 -07:00
Lorenze Jay
e716f3de8b docs: snapshot and changelog for v1.15.1a1 (#6362) 2026-06-26 15:16:43 -07:00
Lorenze Jay
1e2c965a75 feat: bump versions to 1.15.1a1 (#6361) 2026-06-26 15:12:37 -07:00
Vinicius Brasil
a149a30bc0 Fix JSON crew template rendering (#6359)
JSON crews were not using existing CLI templates.
2026-06-26 13:48:48 -07:00
João Moura
8eaae40acf Track TUI button telemetry (#6346)
* feat(cli): track TUI button telemetry

* fix(cli): use feature usage telemetry for TUI buttons

---------

Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
2026-06-26 12:28:47 -07:00
Vinicius Brasil
596150188b Require explicit CrewAI project definitions (#6358)
* Require explicit CrewAI project definitions

JSON crews and declarative flows now resolve from `[tool.crewai]`
metadata instead of implicit filename discovery. This makes project type
selection deterministic, prevents stray `crew.json(c)` files from changing
CLI behavior, and centralizes definition path validation for run, install,
deploy validation, plotting, and memory reset paths.

`[tool.crewai].definition` must be a project-local file path. Absolute
paths, `~`, missing files, directories, and paths escaping the project root
are rejected so deploy and runtime commands use the same contract.

Breaking changes and migration paths:

* JSON crew projects are no longer discovered from `crew.json` or
  `crew.jsonc` alone. Add explicit metadata:

  ```toml
  [tool.crewai]
  type = "crew"
  definition = "crew.jsonc"
  ```

* Declarative flow projects must use a valid project-local definition path:

  ```toml
  [tool.crewai]
  type = "flow"
  definition = "flows/research.yaml"
  ```

* `Flow.from_definition(definition)` is removed. Use:

  ```python
  Flow.from_declaration(contents=definition)
  ```

* `FlowDefinition.to_json()` and `FlowDefinition.to_yaml()` are removed.
  Use `FlowDefinition.to_dict()` and serialize with the caller's JSON or
  YAML library.

* `FlowDefinition.from_dict()` is removed. Use:

  ```python
  FlowDefinition.from_declaration(contents=data)
  ```

* `FlowDefinition.json_schema()` is removed. Use Pydantic's schema API only
  where schema generation is intentionally needed:

  ```python
  FlowDefinition.model_json_schema(by_alias=True)
  ```

* `crewai_cli.run_crew.find_crew_json_file()` and `_has_json_crew()` are
  removed. Use `configured_project_json_crew()` or the shared
  `crewai_core.project.configured_project_definition("crew")` helper.

* `crewai reset-memories` now only loads JSON crews declared through
  `[tool.crewai].definition`, and invalid declared JSON crew definitions
  fail instead of silently falling back to classic crew discovery.

* Address code review comments
2026-06-26 12:07:03 -07:00
João Moura
e10c17fcf6 Open deployment page after CLI deploy (#6343)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
* Open deployment page after CLI deploy

* Format deploy browser URL helper

* Handle browser launch failures

* Prefer nested deployment identifiers
2026-06-26 14:34:07 -03:00
João Moura
f364a7d988 Fix JSON crew version pin (#6342)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
* Fix JSON crew version pin

* Use bounded CrewAI dependency range
2026-06-26 05:19:14 -03:00
João Moura
2771c02f45 docs: improve coding agent setup CTA (#6344)
* docs: improve coding agent setup CTA

* docs: move home CTA to published index

* docs: address CTA review feedback
2026-06-26 05:10:55 -03:00
Rip&Tear
5d4851eac7 Fix SSRF redirect bypass in scraping fetches (#6331)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
* Validate redirects for scraping URL fetches

* Prevent credential forwarding across redirects
2026-06-25 17:42:49 -07:00
70 changed files with 2178 additions and 878 deletions

View File

@@ -12,6 +12,8 @@
<p align="center">
<a href="https://crewai.com">Homepage</a>
·
<a href="https://crewai.com/open-source">Open Source</a>
·
<a href="https://docs.crewai.com">Docs</a>
·
<a href="https://app.crewai.com">Start Cloud Trial</a>
@@ -53,20 +55,20 @@
### Fast and Flexible Multi-Agent Automation Framework
> CrewAI is a lean, lightning-fast Python framework built entirely from scratch—completely **independent of LangChain or other agent frameworks**.
> It empowers developers with both high-level simplicity and precise low-level control, ideal for creating autonomous AI agents tailored to any scenario.
> CrewAI is an open-source Python framework with high-level abstractions and low-level APIs for building production-ready multi-agent workflows.
> It gives developers autonomous agent collaboration through Crews and precise, event-driven control through Flows.
- **CrewAI Crews**: Optimize for autonomy and collaborative intelligence.
- **CrewAI Flows**: The **enterprise and production architecture** for building and deploying multi-agent systems. Enable granular, event-driven control, single LLM calls for precise task orchestration and supports Crews natively
- **CrewAI Crews**: Optimize for autonomy and collaborative intelligence with role-based AI agents.
- **CrewAI Flows**: Build event-driven automations that combine precise workflow control, single LLM calls, and native support for Crews.
With over 100,000 developers certified through our community courses at [learn.crewai.com](https://learn.crewai.com), CrewAI is rapidly becoming the
standard for enterprise-ready AI automation.
standard for production-ready agentic automation.
# CrewAI AMP Suite
CrewAI AMP Suite is a comprehensive bundle tailored for organizations that require secure, scalable, and easy-to-manage agent-driven automation.
For organizations that need a commercial control plane around CrewAI, [CrewAI AMP Suite](https://www.crewai.com/enterprise) adds managed deployment, observability, governance, security, and enterprise support.
You can try one part of the suite the [Crew Control Plane for free](https://app.crewai.com)
You can try one part of the suite, the [Crew Control Plane, for free](https://app.crewai.com).
## Crew Control Plane Key Features:
@@ -88,7 +90,6 @@ intelligent automations.
- [Getting Started](#getting-started)
- [Key Features](#key-features)
- [Understanding Flows and Crews](#understanding-flows-and-crews)
- [CrewAI vs LangGraph](#how-crewai-compares)
- [Examples](#examples)
- [Quick Tutorial](#quick-tutorial)
- [Write Job Descriptions](#write-job-descriptions)
@@ -96,11 +97,11 @@ intelligent automations.
- [Stock Analysis](#stock-analysis)
- [Using Crews and Flows Together](#using-crews-and-flows-together)
- [Connecting Your Crew to a Model](#connecting-your-crew-to-a-model)
- [How CrewAI Compares](#how-crewai-compares)
- [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq)
- [When to Use CrewAI](#when-to-use-crewai)
- [Contribution](#contribution)
- [Telemetry](#telemetry)
- [License](#license)
- [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq)
## Build with AI
@@ -134,15 +135,15 @@ This installs the official [CrewAI Skills](https://github.com/crewAIInc/skills)
<img src="docs/images/asset.png" alt="CrewAI Logo" width="100%">
</div>
CrewAI unlocks the true potential of multi-agent automation, delivering the best-in-class combination of speed, flexibility, and control with either Crews of AI Agents or Flows of Events:
CrewAI unlocks the true potential of multi-agent automation, delivering speed, flexibility, and control through Crews of AI agents and event-driven Flows:
- **Standalone Framework**: Built from scratch, independent of LangChain or any other agent framework.
- **Purpose-built architecture**: Designed specifically for agent orchestration, with a lightweight Python core and clean primitives for real-world automation.
- **High Performance**: Optimized for speed and minimal resource usage, enabling faster execution.
- **Flexible Low Level Customization**: Complete freedom to customize at both high and low levels - from overall workflows and system architecture to granular agent behaviors, internal prompts, and execution logic.
- **Ideal for Every Use Case**: Proven effective for both simple tasks and highly complex, real-world, enterprise-grade scenarios.
- **Flexible Low-Level Customization**: Complete freedom to customize everything from workflows and system architecture to agent behaviors, internal prompts, and execution logic.
- **Ideal for Every Use Case**: Proven effective for simple tasks, complex workflows, and production-grade automation.
- **Robust Community**: Backed by a rapidly growing community of over **100,000 certified** developers offering comprehensive support and resources.
CrewAI empowers developers and enterprises to confidently build intelligent automations, bridging the gap between simplicity, flexibility, and performance.
CrewAI empowers developers and teams to build intelligent automations that balance simplicity, flexibility, and production-grade control.
## Getting Started
@@ -433,16 +434,17 @@ In addition to the sequential process, you can use the hierarchical process, whi
## Key Features
CrewAI stands apart as a lean, standalone, high-performance multi-AI Agent framework delivering simplicity, flexibility, and precise control—free from the complexity and limitations found in other agent frameworks.
CrewAI gives developers a practical foundation for building agentic systems that move from prototype to production: autonomous collaboration where it helps, explicit workflow control where it matters, and Python-native customization throughout.
- **Standalone & Lean**: Completely independent from other frameworks like LangChain, offering faster execution and lighter resource demands.
- **Flexible & Precise**: Easily orchestrate autonomous agents through intuitive [Crews](https://docs.crewai.com/concepts/crews) or precise [Flows](https://docs.crewai.com/concepts/flows), achieving perfect balance for your needs.
- **Seamless Integration**: Effortlessly combine Crews (autonomy) and Flows (precision) to create complex, real-world automations.
- **Deep Customization**: Tailor every aspect—from high-level workflows down to low-level internal prompts and agent behaviors.
- **Reliable Performance**: Consistent results across simple tasks and complex, enterprise-level automations.
- **Thriving Community**: Backed by robust documentation and over 100,000 certified developers, providing exceptional support and guidance.
- **Crews for autonomy**: Model teams of specialized AI agents with roles, goals, tools, and tasks.
- **Flows for control**: Build event-driven workflows with state, branching, routing, and production logic.
- **Seamless integration**: Combine Crews and Flows to create complex, real-world automations.
- **Python-native customization**: Customize prompts, tools, execution paths, state, and integrations without fighting the framework.
- **Agent-ready capabilities**: Use tools, memory, knowledge, checkpointing, async execution, and MCP/A2A support for more capable production agents.
- **Production-ready patterns**: Add deterministic steps, human input, structured outputs, and checkpointing as your system grows.
- **Thriving community**: Backed by robust documentation and over 100,000 certified developers, providing exceptional support and guidance.
Choose CrewAI to easily build powerful, adaptable, and production-ready AI automations.
Choose CrewAI to build powerful, adaptable, and production-ready AI automations.
## Examples
@@ -580,16 +582,17 @@ CrewAI supports using various LLMs through a variety of connection options. By d
Please refer to the [Connect CrewAI to LLMs](https://docs.crewai.com/how-to/LLM-Connections/) page for details on configuring your agents' connections to models.
## How CrewAI Compares
## When to Use CrewAI
**CrewAI's Advantage**: CrewAI combines autonomous agent intelligence with precise workflow control through its unique Crews and Flows architecture. The framework excels at both high-level orchestration and low-level customization, enabling complex, production-grade systems with granular control.
Use CrewAI when you need more than a single prompt or chatbot: multi-step work, specialized agents, tool use, structured outputs, human review, or workflows that combine autonomous reasoning with explicit business logic.
- **LangGraph**: While LangGraph provides a foundation for building agent workflows, its approach requires significant boilerplate code and complex state management patterns. The framework's tight coupling with LangChain can limit flexibility when implementing custom agent behaviors or integrating with external systems.
CrewAI is especially useful when you want to:
_P.S. CrewAI demonstrates significant performance advantages over LangGraph, executing 5.76x faster in certain cases like this QA task example ([see comparison](https://github.com/crewAIInc/crewAI-examples/tree/main/Notebooks/CrewAI%20Flows%20%26%20Langgraph/QA%20Agent)) while achieving higher evaluation scores with faster completion times in certain coding tasks, like in this example ([detailed analysis](https://github.com/crewAIInc/crewAI-examples/blob/main/Notebooks/CrewAI%20Flows%20%26%20Langgraph/Coding%20Assistant/coding_assistant_eval.ipynb))._
- **Autogen**: While Autogen excels at creating conversational agents capable of working together, it lacks an inherent concept of process. In Autogen, orchestrating agents' interactions requires additional programming, which can become complex and cumbersome as the scale of tasks grows.
- **ChatDev**: ChatDev introduced the idea of processes into the realm of AI agents, but its implementation is quite rigid. Customizations in ChatDev are limited and not geared towards production environments, which can hinder scalability and flexibility in real-world applications.
- Coordinate multiple agents with clear roles and tasks.
- Wrap agent work in deterministic, event-driven workflows.
- Keep application logic in regular Python.
- Move from experiment to production without changing frameworks.
- Add tools, memory, checkpointing, and async execution as your system grows.
## Contribution
@@ -698,7 +701,7 @@ CrewAI is released under the [MIT License](https://github.com/crewAIInc/crewAI/b
- [What exactly is CrewAI?](#q-what-exactly-is-crewai)
- [How do I install CrewAI?](#q-how-do-i-install-crewai)
- [Does CrewAI depend on LangChain?](#q-does-crewai-depend-on-langchain)
- [Is CrewAI a standalone framework?](#q-is-crewai-a-standalone-framework)
- [Is CrewAI open-source?](#q-is-crewai-open-source)
- [Does CrewAI collect data from users?](#q-does-crewai-collect-data-from-users)
@@ -707,7 +710,6 @@ CrewAI is released under the [MIT License](https://github.com/crewAIInc/crewAI/b
- [Can CrewAI handle complex use cases?](#q-can-crewai-handle-complex-use-cases)
- [Can I use CrewAI with local AI models?](#q-can-i-use-crewai-with-local-ai-models)
- [What makes Crews different from Flows?](#q-what-makes-crews-different-from-flows)
- [How is CrewAI better than LangChain?](#q-how-is-crewai-better-than-langchain)
- [Does CrewAI support fine-tuning or training custom models?](#q-does-crewai-support-fine-tuning-or-training-custom-models)
### Resources and Community
@@ -723,7 +725,7 @@ CrewAI is released under the [MIT License](https://github.com/crewAIInc/crewAI/b
### Q: What exactly is CrewAI?
A: CrewAI is a standalone, lean, and fast Python framework built specifically for orchestrating autonomous AI agents. Unlike frameworks like LangChain, CrewAI does not rely on external dependencies, making it leaner, faster, and simpler.
A: CrewAI is a lean, fast Python framework built specifically for orchestrating autonomous AI agents and production-ready agentic workflows.
### Q: How do I install CrewAI?
@@ -739,9 +741,9 @@ For additional tools, use:
uv pip install 'crewai[tools]'
```
### Q: Does CrewAI depend on LangChain?
### Q: Is CrewAI a standalone framework?
A: No. CrewAI is built entirely from the ground up, with no dependencies on LangChain or other agent frameworks. This ensures a lean, fast, and flexible experience.
A: Yes. CrewAI is a standalone Python framework with its own primitives for agents, tasks, crews, flows, tools, and orchestration.
### Q: Can CrewAI handle complex use cases?
@@ -755,10 +757,6 @@ A: Absolutely! CrewAI supports various language models, including local ones. To
A: Crews provide autonomous agent collaboration, ideal for tasks requiring flexible decision-making and dynamic interaction. Flows offer precise, event-driven control, ideal for managing detailed execution paths and secure state management. You can seamlessly combine both for maximum effectiveness.
### Q: How is CrewAI better than LangChain?
A: CrewAI provides simpler, more intuitive APIs, faster execution speeds, more reliable and consistent results, robust documentation, and an active community—addressing common criticisms and limitations associated with LangChain.
### Q: Is CrewAI open-source?
A: Yes, CrewAI is open-source and actively encourages community contributions and collaboration.
@@ -797,11 +795,11 @@ A: Absolutely! CrewAI agents can easily integrate with external tools, APIs, and
### Q: Is CrewAI suitable for production environments?
A: Yes, CrewAI is explicitly designed with production-grade standards, ensuring reliability, stability, and scalability for enterprise deployments.
A: Yes, CrewAI is designed with production-grade patterns that support reliable, stable, and scalable agentic workflows.
### Q: How scalable is CrewAI?
A: CrewAI is highly scalable, supporting simple automations and large-scale enterprise workflows involving numerous agents and complex tasks simultaneously.
A: CrewAI is highly scalable, supporting simple automations and large-scale workflows involving numerous agents and complex tasks simultaneously.
### Q: Does CrewAI offer debugging and monitoring tools?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
__version__ = "1.15.0"
__version__ = "1.15.1a1"

View File

@@ -6,6 +6,7 @@ import click
import tomli
from crewai_cli.constants import ENV_VARS, MODELS
from crewai_cli.git import initialize_if_git_available
from crewai_cli.provider import (
get_provider_data,
select_model,
@@ -318,4 +319,7 @@ def create_crew(
dst_file = src_folder / file_name
copy_template(src_file, dst_file, name, class_name, folder_name)
if not parent_folder:
initialize_if_git_available(folder_path)
click.secho(f"Crew {name} created successfully!", fg="green", bold=True)

View File

@@ -4,6 +4,9 @@ import shutil
import click
from crewai_core.telemetry import Telemetry
from crewai_cli.git import initialize_if_git_available
from crewai_cli.version import get_crewai_tools_dependency
DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills")
@@ -28,6 +31,8 @@ def create_flow(name: str, *, declarative: bool = False) -> None:
else:
_create_python_flow(name, class_name, folder_name, project_root)
initialize_if_git_available(project_root)
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
@@ -71,6 +76,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 +146,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")

View File

@@ -13,13 +13,16 @@ from rich.console import Console
from rich.text import Text
from crewai_cli.constants import ENV_VARS
from crewai_cli.git import initialize_if_git_available
from crewai_cli.tui_picker import pick_many, pick_one
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 +81,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 +642,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 +719,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 +807,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 +918,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",
)
@@ -1162,6 +956,8 @@ def create_json_crew(
for model in models:
_setup_env(folder_path, model)
initialize_if_git_available(folder_path)
click.echo()
click.secho(f" ✔ Crew {name} created successfully!", fg="green", bold=True)
click.echo()

View File

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

View File

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

View File

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

View File

@@ -201,3 +201,12 @@ class Repository:
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
def initialize_if_git_available(path: Path) -> bool:
"""Initialize a Git repository when Git is available."""
if not Repository.is_git_installed():
return False
subprocess.run(["git", "init"], cwd=path, check=True) # noqa: S607
return True

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
# {{name}}
A crewAI project using JSON-first configuration.
## Running
```bash
crewai run
```
## Project Structure
- `agents/` - Agent definitions (JSONC)
- `crew.jsonc` - Crew definition with tasks and configuration
- `tools/` - Custom tools (Python)
- `knowledge/` - Knowledge files for agents
> **Note:** `custom:<name>` tool references execute `tools/<name>.py` as local
> Python code when the crew loads. Only run crew projects from sources you
> trust.

View File

@@ -0,0 +1,59 @@
{
// Agent's role title — appears in prompts and logs.
// You can use {placeholder} inputs in role, goal, or backstory.
// Example: "role": "Senior {industry} Researcher"
"role": {{role_json}},
// Optional custom Agent subclass
// "type": {"python": "my_project.agents.CustomAgent"},
// The agent's primary objective
"goal": {{goal_json}},
// Background story that shapes the agent's personality and approach
"backstory": {{backstory_json}},
// LLM model in provider/model format
// Examples: "openai/gpt-4o", "anthropic/claude-sonnet-4-6", "ollama/llama3.3"
// For custom endpoints or deployment-based providers, replace with:
// "llm": {"model": "llama3", "provider": "ollama", "base_url": "http://localhost:11434"},
// "llm": {"deployment_name": "my-deployment", "provider": "azure", "api_version": "2024-10-21"},
"llm": {{llm_json}},
// Override LLM used specifically for tool/function calling
// "function_calling_llm": "openai/gpt-5.4-mini",
// Tools available to this agent
// Built-in: "SerperDevTool", "ScrapeWebsiteTool", "FileReadTool", etc.
// Custom: "custom:my_tool" loads from tools/my_tool.py
"tools": {{tools_json}},
// Optional agent-level guardrail — validates this agent's final output.
// String guardrails are checked by an LLM and can reject/retry output.
// Python refs must point to module-level functions/classes in trusted code.
// "guardrail": "Only answer with information supported by retrieved evidence.",
// "step_callback": {"python": "my_project.callbacks.on_agent_step"},
// "guardrail_max_retries": 2,
// Advanced agent options:
// Docs: https://docs.crewai.com/concepts/agents
// "reasoning": true,
// "max_reasoning_attempts": 3,
// "planning_config": {
// "reasoning_effort": "medium",
// "llm": {"model": "deepseek-chat", "provider": "deepseek"}
// },
// "multimodal": false,
// "allow_code_execution": false,
// "code_execution_mode": "safe",
// "knowledge_sources": [],
// "knowledge_config": {},
// "inject_date": true,
// "date_format": "%Y-%m-%d",
// "security_config": {},
// Agent behavior settings
"settings": {
{{settings_block}}
}
}

View File

@@ -0,0 +1,35 @@
// Show detailed execution logs
"verbose": false,
// Allow this agent to delegate tasks to other agents in the crew
"allow_delegation": {{allow_delegation}}{{delegation_comma}}
// Maximum reasoning iterations per task (prevents infinite loops)
// "max_iter": 25,
// Maximum tokens for agent's response generation
// "max_tokens": null,
// Maximum execution time in seconds
// "max_execution_time": null,
// Maximum LLM requests per minute (rate limiting)
// "max_rpm": null,
// Enable agent-level memory (persists across tasks)
// "memory": false,
// Cache tool results to avoid duplicate calls
// "cache": true,
// Auto-summarize context when it exceeds the LLM's context window
// "respect_context_window": true,
// Maximum retries on execution errors
// "max_retry_limit": 2,
// Enable step-by-step planning before task execution
{{planning_line}}
// Include system prompt in LLM calls
// "use_system_prompt": true

View File

@@ -0,0 +1,58 @@
{
// Display name for this crew
"name": {{name_json}},
// Agents to include — each must have a matching agents/<name>.jsonc file
"agents": {{agent_names_json}},
// Task definitions — executed in order for sequential process
"tasks": [
{{tasks_fragments}}
],
// Execution process
// "sequential" — tasks run in order, each receiving prior task outputs
// "hierarchical" — a manager agent delegates tasks (requires manager_llm)
"process": {{process_json}},
// Enable verbose logging during execution
"verbose": true,
// Enable crew memory — persists context and learnings across tasks
"memory": {{memory}},
// Automatically plan the execution strategy before running tasks
// "planning": false,
// LLM for the planning step (used when planning is true)
// "planning_llm": "openai/gpt-4o",
// LLM for the manager agent (required when process is "hierarchical")
// "manager_llm": "openai/gpt-4o",
// Crew-level LLM fields also accept object form for custom endpoints
// "chat_llm": {"model": "llama3", "provider": "ollama", "base_url": "http://localhost:11434"},
// Advanced crew options:
// Docs: https://docs.crewai.com/concepts/crews
// For hierarchical crews, manager_agent can reference an agents/<name>.jsonc file
// that is not included in the "agents" list.
// "manager_agent": "{{manager_agent_name}}",
// "before_kickoff_callbacks": [{"python": "my_project.callbacks.before_kickoff"}],
// "after_kickoff_callbacks": [{"python": "my_project.callbacks.after_kickoff"}],
// "function_calling_llm": "openai/gpt-4o-mini",
// "max_rpm": null,
// "cache": true,
// "knowledge_sources": [],
// "embedder": {},
// "output_log_file": "crew.log",
// "stream": false,
// "tracing": false,
// "security_config": {},
// Optional runtime input defaults.
// Use {placeholder} in agent or task text, for example:
// "description": "Research {topic} and write a brief"
// `crewai run` prompts for any placeholders missing from this object.
"inputs": {{inputs_json}}
}

View File

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

View File

@@ -0,0 +1,20 @@
[project]
name = "{{folder_name}}"
version = "0.1.0"
description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"{{crewai_tools_dependency}}"
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
only-include = ["agents", "crew.jsonc", "tools", "knowledge", "skills"]
[tool.crewai]
type = "crew"
definition = "crew.jsonc"

View File

@@ -0,0 +1,40 @@
{
// Task identifier
"name": {{name_json}},
// What the task should accomplish
// Use {placeholder} inputs here; crewai run prompts for missing values
"description": {{description_json}},
// Clear definition of what the output should look like
"expected_output": {{expected_output_json}},
// Optional task guardrail(s) validate output before completion
// Use "guardrail" for one rule or "guardrails" for many
// Failed guardrails retry up to guardrail_max_retries times
// "guardrail": "Every factual claim needs context support.",
// "guardrails": [
// "Every factual claim must be supported by context.",
// "The answer must match the expected output format."
// ],
// "guardrail_max_retries": 2,
// Advanced task options:
// Docs: https://docs.crewai.com/concepts/tasks
// "type": "ConditionalTask",
// "condition": { "python": "my_project.conditions.should_run" },
// "output_json": { "python": "my_project.models.ReportOutput" },
// "output_pydantic": null,
// "response_model": null,
// "converter_cls": { "python": "my_project.converters.CustomConverter" },
// "markdown": false,
// "input_files": { "brief": "data/brief.txt" },
// "security_config": {},
// Which agent handles this task
"agent": {{agent_json}}{{agent_comma}}{{context_block}}{{output_file_block}}
// "tools": [],
// "human_input": false,
// "async_execution": false
}

View File

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

View File

@@ -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():

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from collections.abc import Mapping
import os
from pathlib import Path
import re
import shutil
from typing import Any
@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,10 @@ from packaging.requirements import Requirement
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.cli import crewai
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
@@ -107,6 +110,7 @@ def test_create_crew_with_trailing_slash_creates_valid_project(
"crewai_cli.create_crew.create_folder_structure"
) as mock_create_folder:
mock_folder_path = Path(work_dir) / "test_project"
mock_folder_path.mkdir()
mock_create_folder.return_value = (
mock_folder_path,
"test_project",
@@ -141,6 +145,7 @@ def test_create_crew_with_multiple_trailing_slashes(
"crewai_cli.create_crew.create_folder_structure"
) as mock_create_folder:
mock_folder_path = Path(work_dir) / "test_project"
mock_folder_path.mkdir()
mock_create_folder.return_value = (
mock_folder_path,
"test_project",
@@ -165,6 +170,7 @@ def test_create_crew_normal_name_still_works(
"crewai_cli.create_crew.create_folder_structure"
) as mock_create_folder:
mock_folder_path = Path(work_dir) / "normal_project"
mock_folder_path.mkdir()
mock_create_folder.return_value = (
mock_folder_path,
"normal_project",
@@ -176,6 +182,26 @@ def test_create_crew_normal_name_still_works(
mock_create_folder.assert_called_once_with("normal-project", None)
@pytest.mark.skipif(shutil.which("git") is None, reason="git is not installed")
@pytest.mark.parametrize(
("args", "project_root"),
[
(["create", "crew", "Git Crew"], "git_crew"),
(["create", "flow", "Git Flow"], "git_flow"),
],
)
def test_create_initializes_git_repo_when_git_is_available(
args, project_root, tmp_path, monkeypatch, runner
):
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CREWAI_DMN", "True")
result = runner.invoke(crewai, args)
assert result.exit_code == 0, result.output
assert (tmp_path / project_root / ".git").is_dir()
def test_create_folder_structure_handles_spaces_and_dashes_with_slash():
with tempfile.TemporaryDirectory() as temp_dir:
folder_path, folder_name, class_name = create_folder_structure(
@@ -735,11 +761,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 +842,37 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
assert '"knowledge_sources": []' in agent_template
def test_json_crew_uses_template_files():
template_names = {
"pyproject.toml",
"README.md",
".gitignore",
"agent.jsonc",
"agent_settings.jsonc",
"task.jsonc",
"crew.jsonc",
"knowledge/user_preference.txt",
}
for template_name in template_names:
assert (json_crew._TEMPLATES_DIR / template_name).is_file()
def test_render_template_does_not_replace_tokens_inside_replacement_values(tmp_path):
template = tmp_path / "template.txt"
template.write_text("{{first}} {{second}}", encoding="utf-8")
rendered = render_template(
template,
{
"first": "{{second}}",
"second": "done",
},
)
assert rendered == "{{second}} done"
def test_json_provider_default_model_helper():
assert json_crew._default_model_for_provider("openai") == "openai/gpt-5.5"
assert json_crew._default_model_for_provider("anthropic/claude-custom") == (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
__version__ = "1.15.0"
__version__ = "1.15.1a1"

View File

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

View File

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

View File

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

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.15.0"
__version__ = "1.15.1a1"

View File

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

View File

@@ -330,4 +330,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.15.0"
__version__ = "1.15.1a1"

View File

@@ -8,6 +8,7 @@ import requests
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
from crewai_tools.rag.source_content import SourceContent
from crewai_tools.security.safe_requests import safe_get
class DocsSiteLoader(BaseLoader):
@@ -26,7 +27,7 @@ class DocsSiteLoader(BaseLoader):
docs_url = source.source
try:
response = requests.get(docs_url, timeout=30)
response = safe_get(docs_url, timeout=30)
response.raise_for_status()
except requests.RequestException as e:
raise ValueError(

View File

@@ -2,10 +2,9 @@ import os
import tempfile
from typing import Any
import requests
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
from crewai_tools.rag.source_content import SourceContent
from crewai_tools.security.safe_requests import safe_get
class DOCXLoader(BaseLoader):
@@ -43,7 +42,7 @@ class DOCXLoader(BaseLoader):
)
try:
response = requests.get(url, headers=headers, timeout=30)
response = safe_get(url, headers=headers, timeout=30)
response.raise_for_status()
# Create temporary file to save the DOCX content

View File

@@ -6,10 +6,9 @@ import tempfile
from typing import Any
from urllib.parse import urlparse
import requests
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
from crewai_tools.rag.source_content import SourceContent
from crewai_tools.security.safe_requests import safe_get
class PDFLoader(BaseLoader):
@@ -47,7 +46,7 @@ class PDFLoader(BaseLoader):
)
try:
response = requests.get(url, headers=headers, timeout=30)
response = safe_get(url, headers=headers, timeout=30)
response.raise_for_status()
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:

View File

@@ -23,7 +23,7 @@ def load_from_url(
Raises:
ValueError: If there's an error fetching the URL
"""
import requests
from crewai_tools.security.safe_requests import safe_get
headers = kwargs.get(
"headers",
@@ -34,7 +34,7 @@ def load_from_url(
)
try:
response = requests.get(url, headers=headers, timeout=30)
response = safe_get(url, headers=headers, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:

View File

@@ -2,10 +2,10 @@ import re
from typing import Any, Final
from bs4 import BeautifulSoup
import requests
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
from crewai_tools.rag.source_content import SourceContent
from crewai_tools.security.safe_requests import safe_get
_SPACES_PATTERN: Final[re.Pattern[str]] = re.compile(r"[ \t]+")
@@ -25,7 +25,7 @@ class WebPageLoader(BaseLoader):
)
try:
response = requests.get(url, timeout=15, headers=headers)
response = safe_get(url, timeout=15, headers=headers)
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, "html.parser")

View File

@@ -0,0 +1,88 @@
"""HTTP helpers that preserve crewai-tools URL safety checks."""
from __future__ import annotations
from typing import Any
from urllib.parse import urljoin, urlparse
import requests
from crewai_tools.security.safe_path import validate_url
_REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
_SENSITIVE_HEADER_NAMES = {
"authorization",
"cookie",
"proxy-authorization",
"x-api-key",
}
_SENSITIVE_HEADER_FRAGMENTS = ("api-key", "apikey", "secret", "token")
def _same_origin(previous_url: str, next_url: str) -> bool:
previous = urlparse(previous_url)
next_ = urlparse(next_url)
return (previous.scheme, previous.netloc) == (next_.scheme, next_.netloc)
def _is_sensitive_header(header: str) -> bool:
normalized = header.lower()
return (
normalized in _SENSITIVE_HEADER_NAMES
or normalized.startswith("authorization-")
or any(fragment in normalized for fragment in _SENSITIVE_HEADER_FRAGMENTS)
)
def _strip_cross_origin_credentials(request_kwargs: dict[str, Any]) -> dict[str, Any]:
sanitized = {**request_kwargs}
headers = sanitized.get("headers")
if headers:
sanitized["headers"] = {
key: value
for key, value in headers.items()
if not _is_sensitive_header(str(key))
}
sanitized.pop("cookies", None)
return sanitized
def safe_get(url: str, *, max_redirects: int = 10, **kwargs: Any) -> requests.Response:
"""GET a URL while validating each redirect target before following it."""
current_url = validate_url(url)
request_kwargs = {**kwargs, "allow_redirects": False}
timeout = request_kwargs.pop("timeout", 30)
history: list[requests.Response] = []
redirects_followed = 0
while True:
response = requests.get(current_url, timeout=timeout, **request_kwargs)
if (
response.status_code not in _REDIRECT_STATUS_CODES
or "Location" not in response.headers
):
response.history = history
return response
if redirects_followed >= max_redirects:
response.close()
raise ValueError(f"Too many redirects while fetching URL: {url}")
location = response.headers.get("Location")
if not location:
response.history = history
return response
try:
redirect_url = validate_url(urljoin(response.url, location))
except ValueError:
response.close()
raise
if not _same_origin(current_url, redirect_url):
request_kwargs = _strip_cross_origin_credentials(request_kwargs)
history.append(response)
current_url = redirect_url
redirects_followed += 1

View File

@@ -3,9 +3,8 @@ from typing import Any
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
import requests
from crewai_tools.security.safe_path import validate_url
from crewai_tools.security.safe_requests import safe_get
try:
@@ -83,8 +82,7 @@ class ScrapeElementFromWebsiteTool(BaseTool):
if website_url is None or css_element is None:
raise ValueError("Both website_url and css_element must be provided.")
website_url = validate_url(website_url)
page = requests.get(
page = safe_get(
website_url,
headers=self.headers,
cookies=self.cookies if self.cookies else {},

View File

@@ -3,9 +3,8 @@ import re
from typing import Any
from pydantic import Field
import requests
from crewai_tools.security.safe_path import validate_url
from crewai_tools.security.safe_requests import safe_get
try:
@@ -75,8 +74,7 @@ class ScrapeWebsiteTool(BaseTool):
if website_url is None:
raise ValueError("Website URL must be provided.")
website_url = validate_url(website_url)
page = requests.get(
page = safe_get(
website_url,
timeout=15,
headers=self.headers,

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
import socket
from typing import Any
import pytest
@pytest.fixture(autouse=True)
def public_example_dns(monkeypatch: pytest.MonkeyPatch) -> None:
original_getaddrinfo = socket.getaddrinfo
def fake_getaddrinfo(
host: str, port: int, *args: Any, **kwargs: Any
) -> list[tuple[Any, ...]]:
if host in {"example.com", "api.example.com"}:
return [
(
socket.AF_INET,
socket.SOCK_STREAM,
6,
"",
("93.184.216.34", port),
)
]
return original_getaddrinfo(host, port, *args, **kwargs)
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)

View File

@@ -0,0 +1,177 @@
"""Tests for redirect-aware safe HTTP helpers."""
from __future__ import annotations
import socket
from io import BytesIO
from typing import Any
import pytest
import requests
from crewai_tools.security.safe_requests import safe_get
def _response(url: str, status_code: int, *, location: str | None = None) -> requests.Response:
response = requests.Response()
response.status_code = status_code
response.url = url
response._content = b"ok"
response.raw = BytesIO()
if location is not None:
response.headers["Location"] = location
return response
@pytest.fixture
def public_dns(monkeypatch: pytest.MonkeyPatch) -> None:
original_getaddrinfo = socket.getaddrinfo
def fake_getaddrinfo(
host: str, port: int, *args: Any, **kwargs: Any
) -> list[tuple[Any, ...]]:
if host in {"public.example", "safe.example"}:
return [
(
socket.AF_INET,
socket.SOCK_STREAM,
6,
"",
("93.184.216.34", port),
)
]
return original_getaddrinfo(host, port, *args, **kwargs)
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
def test_safe_get_blocks_direct_internal_url() -> None:
with pytest.raises(ValueError, match="private/reserved IP"):
safe_get("http://127.0.0.1/admin", timeout=15)
def _mock_get(monkeypatch: pytest.MonkeyPatch, get_response: Any) -> None:
monkeypatch.setattr(
"crewai_tools.security.safe_requests.requests.get",
get_response,
)
def test_safe_get_blocks_redirect_to_internal_url(
monkeypatch: pytest.MonkeyPatch, public_dns: None
) -> None:
requested_urls: list[str] = []
def fake_get(url: str, **kwargs: Any) -> requests.Response:
requested_urls.append(url)
assert kwargs["allow_redirects"] is False
return _response(url, 302, location="http://127.0.0.1/admin")
_mock_get(monkeypatch, fake_get)
with pytest.raises(ValueError, match="private/reserved IP"):
safe_get("http://public.example/start", timeout=15)
assert requested_urls == ["http://public.example/start"]
def test_safe_get_follows_safe_relative_redirect(
monkeypatch: pytest.MonkeyPatch, public_dns: None
) -> None:
requested_urls: list[str] = []
def fake_get(url: str, **kwargs: Any) -> requests.Response:
requested_urls.append(url)
assert kwargs["allow_redirects"] is False
if url == "http://public.example/start":
return _response(url, 302, location="/final")
return _response(url, 200)
_mock_get(monkeypatch, fake_get)
response = safe_get("http://public.example/start", timeout=15)
assert response.status_code == 200
assert response.url == "http://public.example/final"
assert requested_urls == [
"http://public.example/start",
"http://public.example/final",
]
assert len(response.history) == 1
def test_safe_get_fails_closed_after_too_many_redirects(
monkeypatch: pytest.MonkeyPatch, public_dns: None
) -> None:
def fake_get(url: str, **kwargs: Any) -> requests.Response:
return _response(url, 302, location="http://safe.example/again")
_mock_get(monkeypatch, fake_get)
with pytest.raises(ValueError, match="Too many redirects"):
safe_get("http://public.example/start", max_redirects=1, timeout=15)
def test_safe_get_strips_credentials_on_cross_origin_redirect(
monkeypatch: pytest.MonkeyPatch, public_dns: None
) -> None:
requests_made: list[tuple[str, dict[str, Any]]] = []
def fake_get(url: str, **kwargs: Any) -> requests.Response:
requests_made.append((url, kwargs))
if url == "http://public.example/start":
return _response(url, 302, location="http://safe.example/final")
return _response(url, 200)
_mock_get(monkeypatch, fake_get)
response = safe_get(
"http://public.example/start",
timeout=15,
headers={
"Authorization": "Bearer token",
"Authorization-Custom": "secret token",
"Cookie": "session=abc",
"X-API-Key": "api key",
"X-CrewAI-Token": "crewai token",
"User-Agent": "crewai-test",
},
cookies={"session": "abc"},
)
assert response.status_code == 200
assert requests_made[0][1]["headers"] == {
"Authorization": "Bearer token",
"Authorization-Custom": "secret token",
"Cookie": "session=abc",
"X-API-Key": "api key",
"X-CrewAI-Token": "crewai token",
"User-Agent": "crewai-test",
}
assert requests_made[0][1]["cookies"] == {"session": "abc"}
assert requests_made[1][1]["headers"] == {"User-Agent": "crewai-test"}
assert "cookies" not in requests_made[1][1]
def test_safe_get_preserves_credentials_on_same_origin_redirect(
monkeypatch: pytest.MonkeyPatch, public_dns: None
) -> None:
requests_made: list[tuple[str, dict[str, Any]]] = []
def fake_get(url: str, **kwargs: Any) -> requests.Response:
requests_made.append((url, kwargs))
if url == "http://public.example/start":
return _response(url, 302, location="/final")
return _response(url, 200)
_mock_get(monkeypatch, fake_get)
safe_get(
"http://public.example/start",
timeout=15,
headers={"Authorization": "Bearer token"},
cookies={"session": "abc"},
)
assert requests_made[1][1]["headers"] == {"Authorization": "Bearer token"}
assert requests_made[1][1]["cookies"] == {"session": "abc"}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.15.0"
__version__ = "1.15.1a1"