mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-16 21:58:16 +00:00
Compare commits
20 Commits
1.14.7
...
joaomdmour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0b1b29ed | ||
|
|
06ada68083 | ||
|
|
4eb90ffbf3 | ||
|
|
a6cf52ec7e | ||
|
|
9d44d0a5e5 | ||
|
|
49cb1cd2a2 | ||
|
|
5b766f999f | ||
|
|
e9d568dc69 | ||
|
|
fe2c236601 | ||
|
|
53c2284484 | ||
|
|
a5cc6f6d0e | ||
|
|
bb477f8a91 | ||
|
|
d80719df81 | ||
|
|
6ad821b157 | ||
|
|
2444895ca4 | ||
|
|
bf291a7a55 | ||
|
|
64438cba37 | ||
|
|
887adafd2c | ||
|
|
d3fc0d31f8 | ||
|
|
373dca3d04 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,3 +31,5 @@ chromadb-*.lock
|
||||
blogs/*
|
||||
secrets/*
|
||||
UNKNOWN.egg-info/
|
||||
demos/*
|
||||
.crewai/*
|
||||
|
||||
@@ -70,13 +70,39 @@ mode: "wide"
|
||||
|
||||
## إنشاء الوكلاء
|
||||
|
||||
هناك طريقتان لإنشاء الوكلاء في CrewAI: باستخدام **تهيئة YAML (موصى بها)** أو تعريفهم **مباشرة في الكود**.
|
||||
هناك طريقتان شائعتان لإنشاء الوكلاء في CrewAI: باستخدام **تهيئة JSONC (الموصى بها للـ crews الجديدة)** أو تعريفهم **مباشرة في الكود**.
|
||||
|
||||
### تهيئة YAML (موصى بها)
|
||||
### تهيئة JSONC (موصى بها)
|
||||
|
||||
توفر تهيئة YAML طريقة أنظف وأكثر قابلية للصيانة لتعريف الوكلاء. نوصي بشدة باستخدام هذا النهج في مشاريع CrewAI.
|
||||
المشاريع الجديدة التي تُنشأ عبر `crewai create crew <name>` تستخدم تهيئة JSON-first. يُعرّف كل Agent في `agents/<agent_name>.jsonc`، ويحدد `crew.jsonc` أي Agents تدخل في الـ crew.
|
||||
|
||||
بعد إنشاء مشروع CrewAI كما هو موضح في قسم [التثبيت](/ar/installation)، انتقل إلى ملف `src/latest_ai_development/config/agents.yaml` وعدّل القالب ليتوافق مع متطلباتك.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} Senior Data Researcher",
|
||||
"goal": "Uncover cutting-edge developments in {topic}",
|
||||
"backstory": "You find the most relevant information and present it clearly.",
|
||||
"llm": "openai/gpt-4o",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
استخدم `{placeholder}` داخل `role` أو `goal` أو `backstory`. ضع القيم الافتراضية في `inputs` داخل `crew.jsonc`؛ وسيطلب `crewai run` أي قيم ناقصة. يمكن وضع حقول السلوك مثل `verbose` و `allow_delegation` و `max_iter` و `memory` و `cache` و `planning` في المستوى الأعلى أو داخل `settings`.
|
||||
|
||||
<Note>
|
||||
يدعم JSONC التعليقات والفواصل النهائية. إذا وُجد `agents/<name>.jsonc` و `agents/<name>.json` معًا، يستخدم CrewAI ملف JSONC.
|
||||
</Note>
|
||||
|
||||
### تهيئة YAML الكلاسيكية
|
||||
|
||||
المشاريع الكلاسيكية التي تُنشأ عبر `crewai create crew <name> --classic` تستخدم `config/agents.yaml` وفئة `@CrewBase` في `crew.py`.
|
||||
|
||||
تظل تهيئة YAML مدعومة للمشاريع الحالية المبنية بـ Python/YAML وللفِرق التي تفضل تعريف الوكلاء من خلال فئة `@CrewBase`.
|
||||
|
||||
بعد إنشاء مشروع كلاسيكي، انتقل إلى ملف `src/latest_ai_development/config/agents.yaml` وعدّل القالب ليتوافق مع متطلباتك.
|
||||
|
||||
<Note>
|
||||
ستُستبدل المتغيرات في ملفات YAML (مثل `{topic}`) بقيم من مدخلاتك عند تشغيل الطاقم:
|
||||
|
||||
@@ -52,6 +52,8 @@ crewai create crew my_new_crew
|
||||
crewai create flow my_new_flow
|
||||
```
|
||||
|
||||
افتراضيًا، ينشئ `crewai create crew` مشروعًا JSON-first يحتوي على `crew.jsonc` و `agents/*.jsonc`. استخدم `crewai create crew my_new_crew --classic` فقط إذا أردت البنية القديمة Python/YAML مع `crew.py` و `config/agents.yaml` و `config/tasks.yaml`.
|
||||
|
||||
### 2. الإصدار
|
||||
|
||||
عرض الإصدار المثبت من CrewAI.
|
||||
@@ -142,7 +144,20 @@ crewai chat
|
||||
```
|
||||
|
||||
<Note>
|
||||
مهم: عيّن خاصية `chat_llm` في ملف `crew.py` لتفعيل هذا الأمر.
|
||||
مهم: عيّن خاصية `chat_llm` في تعريف الـ crew لتفعيل هذا الأمر.
|
||||
|
||||
للـ crews بنمط JSON-first، أضفها إلى `crew.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "My Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [],
|
||||
"chat_llm": "openai/gpt-4o"
|
||||
}
|
||||
```
|
||||
|
||||
للـ crews الكلاسيكية Python/YAML، عيّنها في `crew.py`:
|
||||
|
||||
```python
|
||||
@crew
|
||||
|
||||
@@ -40,11 +40,52 @@ mode: "wide"
|
||||
|
||||
## إنشاء الأطقم
|
||||
|
||||
هناك طريقتان لإنشاء الأطقم في CrewAI: باستخدام **تهيئة YAML (موصى بها)** أو تعريفها **مباشرة في الكود**.
|
||||
هناك طريقتان رئيسيتان لإنشاء الأطقم في CrewAI: باستخدام **تهيئة JSONC (الموصى بها للـ crews الجديدة)** أو تعريفها **مباشرة في الكود** للمشاريع الكلاسيكية والحالات المتقدمة.
|
||||
|
||||
### تهيئة YAML (موصى بها)
|
||||
### تهيئة JSONC (موصى بها)
|
||||
|
||||
توفر تهيئة YAML طريقة أنظف وأكثر قابلية للصيانة لتعريف الأطقم وتتسق مع كيفية تعريف الوكلاء والمهام في مشاريع CrewAI.
|
||||
المشاريع الجديدة التي تُنشأ عبر `crewai create crew <name>` تستخدم `crew.jsonc` لإعدادات الـ crew والمهام، وملفًا منفصلًا لكل Agent داخل `agents/`. يكتشف `crewai run` ملف `crew.jsonc` أو `crew.json`، ويحمّل الـ Agents المشار إليها، ويطلب قيم placeholders الناقصة، ثم يبدأ الـ crew.
|
||||
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Market Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research",
|
||||
"description": "Research {topic} and collect the most relevant facts.",
|
||||
"expected_output": "Structured research notes about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis",
|
||||
"description": "Analyze the research and write a concise report.",
|
||||
"expected_output": "A markdown report with findings and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research"],
|
||||
"output_file": "output/report.md"
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
كل عنصر في `agents` يُحل أولًا إلى `agents/<name>.jsonc` ثم إلى `agents/<name>.json`. للـ crews الهرمية، استخدم `"process": "hierarchical"` مع `manager_llm` أو `manager_agent`.
|
||||
|
||||
<Warning>
|
||||
شغّل مشاريع JSON crew من مصادر تثق بها فقط. أدوات `custom:<name>` ومراجع `{"python": "module.attribute"}` تنفذ كود Python محليًا عند تحميل الـ crew.
|
||||
</Warning>
|
||||
|
||||
### تهيئة YAML الكلاسيكية
|
||||
|
||||
المشاريع الكلاسيكية التي تُنشأ عبر `crewai create crew <name> --classic` تستخدم `crew.py` و `config/agents.yaml` و `config/tasks.yaml` والمزيّنات `@CrewBase` و `@agent` و `@task` و `@crew`.
|
||||
|
||||
تظل هذه الطريقة مدعومة للمشاريع الحالية المبنية بـ Python/YAML وللفِرق التي تحتاج تحكمًا صريحًا عبر decorators.
|
||||
|
||||
```python code
|
||||
from crewai import Agent, Crew, Task, Process
|
||||
|
||||
@@ -226,6 +226,48 @@ counter=2 message='Hello from first_method - updated by second_method'
|
||||
من خلال ضمان إعادة مخرجات الدالة الأخيرة وتوفير الوصول إلى الحالة، تجعل تدفقات CrewAI من السهل دمج نتائج سير عمل الذكاء الاصطناعي في التطبيقات أو الأنظمة الأكبر،
|
||||
مع الحفاظ على الوصول إلى الحالة طوال تنفيذ التدفق.
|
||||
|
||||
## مقاييس استخدام التدفق
|
||||
|
||||
بعد اكتمال تنفيذ التدفق، يمكنك الوصول إلى الخاصية `usage_metrics` لعرض إجمالي استخدام التوكنات عبر **كل استدعاء لنموذج اللغة** يتم خلال التشغيل — بما في ذلك الاستدعاءات من كل فريق (Crew) ينظمه التدفق، والاستدعاءات داخل أدوات الـ Agents، والاستدعاءات المباشرة لـ `LLM.call(...)` من دوال التدفق. هذا هو المكافئ على جانب الـ SDK للإجماليات المعروضة في واجهة CrewAI Enterprise.
|
||||
|
||||
```python Code
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
|
||||
class UsageMetricsFlow(Flow):
|
||||
@start()
|
||||
def run_first_crew(self):
|
||||
self.state.first_result = FirstCrew().crew().kickoff()
|
||||
|
||||
@listen(run_first_crew)
|
||||
def call_llm_directly(self):
|
||||
# استدعاء مباشر لنموذج اللغة — يُحسب أيضًا ضمن flow.usage_metrics
|
||||
llm = LLM(model="openai/gpt-4o-mini")
|
||||
self.state.summary = llm.call("لخّص النقاط الرئيسية.")
|
||||
|
||||
@listen(call_llm_directly)
|
||||
def run_second_crew(self):
|
||||
self.state.second_result = SecondCrew().crew().kickoff()
|
||||
|
||||
flow = UsageMetricsFlow()
|
||||
flow.kickoff()
|
||||
|
||||
print(flow.usage_metrics)
|
||||
# UsageMetrics(total_tokens=8579, prompt_tokens=6210, completion_tokens=2369,
|
||||
# cached_prompt_tokens=0, reasoning_tokens=0,
|
||||
# cache_creation_tokens=0, successful_requests=5)
|
||||
```
|
||||
|
||||
<Note>
|
||||
`flow.usage_metrics` **ليست** نفس `flow.kickoff().token_usage`. هذه الأخيرة
|
||||
ترجع فقط `CrewOutput.token_usage` لـ **آخر** دالة `@listen` أعادت
|
||||
`CrewOutput`، مما يعني أنها تعكس فقط الفريق الأخير وتتجاهل الفرق السابقة
|
||||
وكذلك أي استدعاءات مباشرة لـ `LLM.call(...)`. استخدم `flow.usage_metrics`
|
||||
كلما احتجت إلى الإجمالي **الكامل** للتوكنات لتنفيذ التدفق.
|
||||
</Note>
|
||||
|
||||
كل حقل في [`UsageMetrics`](https://github.com/crewAIInc/crewAI/blob/main/lib/crewai/src/crewai/types/usage_metrics.py) المُعاد هو مجموع جميع استدعاءات نموذج اللغة التي حدثت خلال استدعاء واحد لـ `flow.kickoff()`. تتم إعادة تعيين العدادات عند الاستدعاء التالي لـ `kickoff()` (وفي كل تكرار من `kickoff_for_each`)، لذلك لن تتكرر العدّات عبر التشغيلات المتتالية. يمكن قراءة هذه الخاصية بأمان في أي وقت بعد اكتمال `kickoff()`؛ قراءتها أثناء التنفيذ تُرجع المجموع الجزئي المتراكم حتى تلك اللحظة.
|
||||
|
||||
## إدارة حالة التدفق
|
||||
|
||||
إدارة الحالة بفعالية أمر بالغ الأهمية لبناء سير عمل ذكاء اصطناعي موثوق وقابل للصيانة. توفر تدفقات CrewAI آليات قوية لإدارة الحالة غير المهيكلة والمهيكلة،
|
||||
@@ -788,7 +830,7 @@ if __name__ == "__main__":
|
||||
crewai create flow name_of_flow
|
||||
```
|
||||
|
||||
سيولّد هذا الأمر مشروع CrewAI جديد مع هيكل المجلدات اللازم. يتضمن المشروع المولّد فريق Crew مُعد مسبقًا يُسمى `poem_crew` ويعمل بالفعل. يمكنك استخدام هذا الفريق كقالب بنسخه ولصقه وتعديله لإنشاء فرق أخرى.
|
||||
سيولّد هذا الأمر مشروع CrewAI جديد مع هيكل المجلدات اللازم. يتضمن المشروع المولّد فريق Crew مُعد مسبقًا يُسمى `poem_crew` ويعمل بالفعل. يستخدم الـ embedded crew الابتدائي بنية Python/YAML الكلاسيكية؛ أما crews المستقلة الجديدة التي تُنشأ عبر `crewai create crew` فتستخدم بنية JSON-first.
|
||||
|
||||
### هيكل المجلدات
|
||||
|
||||
@@ -818,7 +860,29 @@ crewai create flow name_of_flow
|
||||
- `config/tasks.yaml`: يحدد المهام للفريق.
|
||||
- `poem_crew.py`: يحتوي على تعريف الفريق، بما في ذلك الـ Agents والمهام والفريق نفسه.
|
||||
|
||||
يمكنك نسخ ولصق وتعديل `poem_crew` لإنشاء فرق أخرى.
|
||||
يمكنك نسخ ولصق وتعديل `poem_crew` لإنشاء crews كلاسيكية مضمّنة أخرى.
|
||||
|
||||
للـ crews المضمّنة بنمط JSON-first، استخدم مجلدًا يحتوي على `crew.jsonc` و `agents/*.jsonc`:
|
||||
|
||||
```text
|
||||
crews/
|
||||
└── research_crew/
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
└── crew.jsonc
|
||||
```
|
||||
|
||||
ثم حمّلها من خطوة في Flow:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.project import load_crew
|
||||
|
||||
crew, default_inputs = load_crew(
|
||||
Path(__file__).parent / "crews" / "research_crew" / "crew.jsonc"
|
||||
)
|
||||
result = crew.kickoff(inputs={**default_inputs, "topic": "AI Agents"})
|
||||
```
|
||||
|
||||
### ربط فرق Crew في `main.py`
|
||||
|
||||
|
||||
@@ -73,13 +73,48 @@ crew = Crew(
|
||||
|
||||
## إنشاء المهام
|
||||
|
||||
هناك طريقتان لإنشاء المهام في CrewAI: باستخدام **إعداد YAML (موصى به)** أو تعريفها **مباشرة في الكود**.
|
||||
هناك طريقتان شائعتان لإنشاء المهام في CrewAI: باستخدام **تهيئة JSONC (الموصى بها للـ crews الجديدة)** أو تعريفها **مباشرة في الكود**.
|
||||
|
||||
### إعداد YAML (موصى به)
|
||||
### تهيئة JSONC (موصى بها)
|
||||
|
||||
يوفر استخدام إعداد YAML طريقة أنظف وأكثر قابلية للصيانة لتعريف المهام. نوصي بشدة باستخدام هذا النهج لتعريف المهام في مشاريع CrewAI.
|
||||
المشاريع الجديدة التي تُنشأ عبر `crewai create crew <name>` تعرّف المهام في `crew.jsonc`.
|
||||
|
||||
بعد إنشاء مشروع CrewAI كما هو موضح في قسم [التثبيت](/ar/installation)، انتقل إلى ملف `src/latest_ai_development/config/tasks.yaml` وعدّل القالب ليتوافق مع متطلبات مهامك المحددة.
|
||||
````jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "reporting_analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research about {topic}.",
|
||||
"expected_output": "A list of the most relevant information about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "reporting_task",
|
||||
"description": "Review the research and expand it into a detailed report.",
|
||||
"expected_output": "A polished markdown report.",
|
||||
"agent": "reporting_analyst",
|
||||
"context": ["research_task"],
|
||||
"markdown": true,
|
||||
"output_file": "report.md"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
كل مهمة تحتاج إلى `description` و `expected_output`. يجب أن يطابق `agent` اسم Agent مذكورًا في `agents`. يشير `context` إلى أسماء مهام سابقة فقط؛ وترفض الإشارات إلى مهام لاحقة.
|
||||
|
||||
### إعداد YAML الكلاسيكي
|
||||
|
||||
المشاريع الكلاسيكية التي تُنشأ عبر `crewai create crew <name> --classic` تستخدم `config/tasks.yaml` وفئة `@CrewBase` في `crew.py`.
|
||||
|
||||
يظل إعداد YAML مدعومًا للمشاريع الحالية المبنية بـ Python/YAML وللفِرق التي تفضل تعريف المهام من خلال فئة `@CrewBase`.
|
||||
|
||||
بعد إنشاء مشروع كلاسيكي، انتقل إلى ملف `src/latest_ai_development/config/tasks.yaml` وعدّل القالب ليتوافق مع متطلبات مهامك المحددة.
|
||||
|
||||
<Note>
|
||||
المتغيرات في ملفات YAML (مثل `{topic}`) سيتم استبدالها بالقيم من مدخلاتك عند تشغيل الفريق:
|
||||
|
||||
@@ -26,10 +26,10 @@ icon: "arrows-rotate"
|
||||
|
||||
## الخطوة 1 — هيكلة طاقم التحقق
|
||||
|
||||
أنشئ مشروع طاقم جديد. يُهيكل CrewAI CLI البنية:
|
||||
أنشئ مشروع crew كلاسيكيًا لأن هذا المثال يربط أداة Python عبر `crew.py`:
|
||||
|
||||
```bash
|
||||
crewai create crew rotation_verifier --skip_provider
|
||||
crewai create crew rotation_verifier --classic --skip_provider
|
||||
cd rotation_verifier
|
||||
```
|
||||
|
||||
|
||||
@@ -374,17 +374,17 @@ git push
|
||||
|
||||
**الحل**: تحقق من أن مشروعك يتطابق مع البنية المتوقعة:
|
||||
|
||||
- **كل من الطواقم والتدفقات**: يجب أن تكون نقطة الدخول في `src/project_name/main.py`
|
||||
- **الطواقم**: تستخدم دالة `run()` كنقطة دخول
|
||||
- **التدفقات**: تستخدم دالة `kickoff()` كنقطة دخول
|
||||
- **JSON-first Crews**: أبقِ `crew.jsonc` أو `crew.json` و `agents/` في جذر المشروع
|
||||
- **Crews كلاسيكية**: استخدم `src/project_name/main.py` مع دالة دخول `run()`
|
||||
- **Flows**: استخدم `src/project_name/main.py` مع دالة دخول `kickoff()`
|
||||
|
||||
راجع [التحضير للنشر](/ar/enterprise/guides/prepare-for-deployment) لمخططات البنية المفصلة.
|
||||
|
||||
#### مُزخرف CrewBase مفقود
|
||||
#### مُزخرف CrewBase مفقود في crew كلاسيكية
|
||||
|
||||
**العرض**: أخطاء "Crew not found" أو "Config not found" أو أخطاء تهيئة الوكيل/المهمة
|
||||
|
||||
**الحل**: تأكد من أن **جميع** فئات الطاقم تستخدم مُزخرف `@CrewBase`:
|
||||
**الحل**: في crews الكلاسيكية Python/YAML، تأكد من أن جميع فئات الـ crew تستخدم مُزخرف `@CrewBase`. لا تحتاج crews بنمط JSON-first إلى هذا المزخرف.
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
@@ -404,8 +404,8 @@ class YourCrew():
|
||||
```
|
||||
|
||||
<Info>
|
||||
ينطبق هذا على الطواقم المستقلة والطواقم المضمنة داخل مشاريع التدفق.
|
||||
كل فئة طاقم تحتاج المُزخرف.
|
||||
ينطبق هذا على فئات crew الكلاسيكية المكتوبة في Python، بما في ذلك crews الكلاسيكية المضمنة داخل مشاريع Flow.
|
||||
يتم التحقق من crews بنمط JSON-first من `crew.jsonc` و `agents/` بدلاً من ذلك.
|
||||
</Info>
|
||||
|
||||
#### نوع pyproject.toml غير صحيح
|
||||
@@ -442,8 +442,8 @@ type = "flow"
|
||||
**الحل**:
|
||||
1. تحقق من سجلات التنفيذ في لوحة تحكم AMP (علامة تبويب Traces)
|
||||
2. تحقق من أن جميع الأدوات لديها مفاتيح API المطلوبة مُهيأة
|
||||
3. تأكد من صحة تهيئات الوكلاء في `agents.yaml`
|
||||
4. تحقق من تهيئات المهام في `tasks.yaml` بحثاً عن أخطاء الصياغة
|
||||
3. في crews بنمط JSON-first، تحقق من `crew.jsonc` والملفات المشار إليها داخل `agents/`
|
||||
4. في crews الكلاسيكية، تأكد من صحة `agents.yaml` و `tasks.yaml`
|
||||
|
||||
<Card title="تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
|
||||
تواصل مع فريق الدعم للمساعدة في مشاكل النشر أو أسئلة حول
|
||||
|
||||
@@ -23,10 +23,9 @@ company-ai/
|
||||
`-- crews/
|
||||
|-- support_agent/
|
||||
| |-- pyproject.toml
|
||||
| `-- src/
|
||||
| `-- support_agent/
|
||||
| |-- main.py
|
||||
| `-- crew.py
|
||||
| |-- crew.jsonc
|
||||
| `-- agents/
|
||||
| `-- support_agent.jsonc
|
||||
`-- research_flow/
|
||||
|-- pyproject.toml
|
||||
`-- src/
|
||||
@@ -47,7 +46,7 @@ crews/support_agent
|
||||
|
||||
عند تعيين دليل عمل، يستخدم AMP ذلك المجلد من أجل:
|
||||
|
||||
- التحقق من المشروع، بما في ذلك `pyproject.toml` و`src/` ونقطة دخول Crew أو Flow
|
||||
- التحقق من المشروع، بما في ذلك `pyproject.toml` وملفات crew JSON وأي نقطة دخول كلاسيكية لـ Crew أو Flow
|
||||
- تثبيت الاعتماديات باستخدام `uv`
|
||||
- دليل العمل للعملية قيد التشغيل
|
||||
- متغير البيئة `CREW_ROOT_DIR`
|
||||
|
||||
@@ -24,7 +24,7 @@ mode: "wide"
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="مشاريع الطاقم" icon="users">
|
||||
فرق وكلاء ذكاء اصطناعي مستقلة مع `crew.py` يحدد الوكلاء والمهام. الأفضل للمهام المركزة والتعاونية.
|
||||
فرق وكلاء ذكاء اصطناعي مستقلة. الـ crews الجديدة تستخدم بنية JSON-first مع `crew.jsonc` و `agents/`؛ ويمكن للـ crews الكلاسيكية الاستمرار في استخدام `crew.py`.
|
||||
</Card>
|
||||
<Card title="مشاريع التدفق" icon="diagram-project">
|
||||
سير عمل مُنسّق مع طواقم مضمنة في مجلد `crews/`. الأفضل للعمليات المعقدة متعددة المراحل.
|
||||
@@ -33,19 +33,19 @@ mode: "wide"
|
||||
|
||||
| الجانب | الطاقم | التدفق |
|
||||
|--------|--------|--------|
|
||||
| **بنية المشروع** | `src/project_name/` مع `crew.py` | `src/project_name/` مع مجلد `crews/` |
|
||||
| **موقع المنطق الرئيسي** | `src/project_name/crew.py` | `src/project_name/main.py` (فئة Flow) |
|
||||
| **دالة نقطة الدخول** | `run()` في `main.py` | `kickoff()` في `main.py` |
|
||||
| **بنية المشروع** | جذر المشروع مع `crew.jsonc` و `agents/` | `src/project_name/` مع مجلد `crews/` |
|
||||
| **موقع المنطق الرئيسي** | `crew.jsonc` (كلاسيكي: `src/project_name/crew.py`) | `src/project_name/main.py` (فئة Flow) |
|
||||
| **دالة نقطة الدخول** | تُحمّل من `crew.jsonc` (كلاسيكي: `run()` في `main.py`) | `kickoff()` في `main.py` |
|
||||
| **نوع pyproject.toml** | `type = "crew"` | `type = "flow"` |
|
||||
| **أمر CLI للإنشاء** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **موقع التهيئة** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **موقع التهيئة** | `crew.jsonc` و `agents/` و `tools/` اختياريًا | `src/project_name/crews/crew_name/config/` أو مجلدات crew JSON مضمنة |
|
||||
| **يمكن أن يحتوي طواقم أخرى** | لا | نعم (في مجلد `crews/`) |
|
||||
|
||||
## مرجع بنية المشروع
|
||||
|
||||
### بنية مشروع الطاقم
|
||||
|
||||
عند تشغيل `crewai create crew my_crew`، تحصل على هذه البنية:
|
||||
عند تشغيل `crewai create crew my_crew`، تحصل على بنية JSON-first:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
@@ -54,24 +54,25 @@ my_crew/
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # REQUIRED for deployment
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # Entry point with run() function
|
||||
├── crew.py # Crew class with @CrewBase decorator
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # Agent definitions
|
||||
└── tasks.yaml # Task definitions
|
||||
├── crew.jsonc # إعدادات الـ crew والمهام والعملية والمدخلات
|
||||
├── agents/
|
||||
│ └── researcher.jsonc # تعريفات الـ Agents
|
||||
├── tools/ # أدوات custom:<name> اختيارية
|
||||
├── knowledge/
|
||||
└── skills/
|
||||
```
|
||||
|
||||
<Warning>
|
||||
بنية `src/project_name/` المتداخلة ضرورية للطواقم.
|
||||
وضع الملفات في المستوى الخاطئ سيسبب فشل النشر.
|
||||
في crews بنمط JSON-first، أبقِ `crew.jsonc` و `agents/` و `tools/` و `knowledge/` و `skills/`
|
||||
في جذر المشروع. وضعها داخل `src/` يمنع `crewai run` والتحقق قبل النشر من العثور على تعريف الـ crew.
|
||||
</Warning>
|
||||
|
||||
<Info>
|
||||
المشاريع الكلاسيكية التي تُنشأ عبر `crewai create crew my_crew --classic` تستخدم البنية القديمة
|
||||
`src/project_name/crew.py` و `src/project_name/config/agents.yaml` و
|
||||
`src/project_name/config/tasks.yaml`. تظل هذه البنية مدعومة للـ crews المكتوبة في Python مع decorators.
|
||||
</Info>
|
||||
|
||||
### بنية مشروع التدفق
|
||||
|
||||
عند تشغيل `crewai create flow my_flow`، تحصل على هذه البنية:
|
||||
@@ -100,9 +101,9 @@ my_flow/
|
||||
```
|
||||
|
||||
<Info>
|
||||
كلا الطواقم والتدفقات تستخدم بنية `src/project_name/`.
|
||||
الفرق الرئيسي أن التدفقات لها مجلد `crews/` للطواقم المضمنة،
|
||||
بينما الطواقم لها `crew.py` مباشرة في مجلد المشروع.
|
||||
الـ crews المستقلة بنمط JSON-first تستخدم ملفات JSON في جذر المشروع. أما Flows فتظل تستخدم
|
||||
`src/project_name/` ويمكن أن تحتوي crews مضمنة كلاسيكية أو مجلدات crew JSON يتم تحميلها عبر
|
||||
`crewai.project.load_crew`.
|
||||
</Info>
|
||||
|
||||
## قائمة فحص ما قبل النشر
|
||||
@@ -154,60 +155,89 @@ git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. التحقق من استخدام مُزخرف CrewBase
|
||||
### 3. التحقق من تعريف الـ Crew
|
||||
|
||||
**يجب أن تستخدم كل فئة طاقم مُزخرف `@CrewBase`.** ينطبق هذا على:
|
||||
<Tabs>
|
||||
<Tab title="JSON-first Crews">
|
||||
يجب أن تحتوي crews بنمط JSON-first على `crew.jsonc` أو `crew.json` في جذر المشروع.
|
||||
يجب أن يشير مصفوفة `agents` إلى ملفات داخل `agents/`، ويجب أن تشير كل task إلى اسم Agent صحيح.
|
||||
|
||||
- مشاريع الطاقم المستقلة
|
||||
- الطواقم المضمنة داخل مشاريع التدفق
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research {topic}.",
|
||||
"expected_output": "A concise report.",
|
||||
"agent": "researcher"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
تُشار الأدوات المخصصة بصيغة `"custom:<name>"` ويجب تنفيذها في
|
||||
`tools/<name>.py` كصنف يرث من `BaseTool`.
|
||||
</Tab>
|
||||
<Tab title="Crews كلاسيكية Python/YAML">
|
||||
يجب أن تستخدم الـ crews الكلاسيكية وPython crews المضمنة داخل Flows مزخرف `@CrewBase`.
|
||||
|
||||
@CrewBase # This decorator is REQUIRED
|
||||
class MyCrew():
|
||||
"""My crew description"""
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
@CrewBase
|
||||
class MyCrew():
|
||||
"""My crew description"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
<Warning>
|
||||
إذا نسيت مُزخرف `@CrewBase`، سيفشل النشر بأخطاء حول
|
||||
تهيئات الوكلاء أو المهام المفقودة.
|
||||
</Warning>
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 4. التحقق من نقاط دخول المشروع
|
||||
|
||||
كل من الطواقم والتدفقات لها نقطة دخول في `src/project_name/main.py`:
|
||||
لا تحتاج crews المستقلة بنمط JSON-first إلى ملف `src/project_name/main.py` مكتوب يدويًا؛
|
||||
يقوم `crewai run` وتغليف النشر بتحميل `crew.jsonc` مباشرة. تستخدم crews الكلاسيكية وFlows نقاط دخول Python:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="للطواقم">
|
||||
<Tab title="JSON-first Crews">
|
||||
شغّل محليًا من جذر المشروع:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Crews كلاسيكية">
|
||||
تستخدم نقطة الدخول دالة `run()`:
|
||||
|
||||
```python
|
||||
@@ -278,16 +308,17 @@ grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
# 2. Verify uv.lock exists
|
||||
ls -la uv.lock || echo "ERROR: uv.lock missing! Run 'uv lock'"
|
||||
|
||||
# 3. Verify src/ structure exists
|
||||
ls -la src/*/main.py 2>/dev/null || echo "No main.py found in src/"
|
||||
# 3. For JSON-first crews, verify crew.jsonc and agents/
|
||||
([ -f crew.jsonc ] || [ -f crew.json ]) || echo "No crew.jsonc or crew.json found"
|
||||
test -d agents || echo "No agents/ directory found"
|
||||
|
||||
# 4. For Crews - verify crew.py exists
|
||||
# 4. For classic Crews - verify crew.py exists
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "No crew.py (expected for Crews)"
|
||||
|
||||
# 5. For Flows - verify crews/ folder exists
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "No crews/ folder (expected for Flows)"
|
||||
|
||||
# 6. Check for CrewBase usage
|
||||
# 6. For classic Python crews - check for CrewBase usage
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
@@ -297,8 +328,9 @@ grep -r "@CrewBase" . --include="*.py"
|
||||
|-------|-------|---------|
|
||||
| `uv.lock` مفقود | فشل البناء أثناء حل الاعتماديات | شغّل `uv lock` وارفعه |
|
||||
| `type` خاطئ في pyproject.toml | نجاح البناء لكن فشل وقت التشغيل | غيّر إلى النوع الصحيح |
|
||||
| مُزخرف `@CrewBase` مفقود | أخطاء "Config not found" | أضف المُزخرف لجميع فئات الطاقم |
|
||||
| ملفات في الجذر بدل `src/` | نقطة الدخول غير موجودة | انقلها إلى `src/project_name/` |
|
||||
| `crew.jsonc` أو `agents/` مفقود في crew بنمط JSON-first | لا يمكن العثور على تعريف الـ crew | أبقِ `crew.jsonc` و `agents/` في جذر المشروع |
|
||||
| مُزخرف `@CrewBase` مفقود في crew كلاسيكية | أخطاء "Config not found" | أضف المُزخرف لجميع فئات الـ crew الكلاسيكية |
|
||||
| ملفات كلاسيكية في الجذر بدل `src/` | نقطة الدخول غير موجودة | انقل ملفات Python الكلاسيكية إلى `src/project_name/` |
|
||||
| `run()` أو `kickoff()` مفقودة | لا يمكن بدء الأتمتة | أضف دالة الدخول الصحيحة |
|
||||
|
||||
## الخطوات التالية
|
||||
|
||||
@@ -43,7 +43,7 @@ CrewAI مُصمَّم أصلاً للعمل مع الذكاء الاصطناعي
|
||||
|
||||
| المهارة | متى تُستخدم |
|
||||
|---------|-------------|
|
||||
| `getting-started` | مشاريع جديدة، الاختيار بين `LLM.call()` / `Agent` / `Crew` / `Flow`، ربط `crew.py` / `main.py` |
|
||||
| `getting-started` | مشاريع جديدة، الاختيار بين `LLM.call()` / `Agent` / `Crew` / `Flow`، ربط `crew.jsonc` / `main.py` |
|
||||
| `design-agent` | ضبط الوكلاء — الدور، الهدف، الخلفية، الأدوات، نماذج اللغة، الذاكرة، الحدود الآمنة |
|
||||
| `design-task` | وصف المهام، التبعيات، المخرجات المنظمة (`output_pydantic`، `output_json`)، المراجعة البشرية |
|
||||
| `ask-docs` | الاستعلام من [خادم CrewAI docs MCP](https://docs.crewai.com/mcp) للحصول على تفاصيل واجهة البرمجة الحالية |
|
||||
|
||||
@@ -1,321 +1,140 @@
|
||||
---
|
||||
title: ابنِ أول Crew لك
|
||||
description: دليل تفصيلي لإنشاء فريق AI تعاوني يعمل معًا لحل المشكلات المعقدة.
|
||||
title: ابنِ أول Crew
|
||||
description: دليل خطوة بخطوة لإنشاء فريق AI تعاوني باستخدام تهيئة JSON-first.
|
||||
icon: users-gear
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## إطلاق قوة الذكاء الاصطناعي التعاوني
|
||||
## بناء Crew للبحث
|
||||
|
||||
تخيل أن لديك فريقًا من Agents الذكاء الاصطناعي المتخصصة تعمل معًا بسلاسة لحل مشكلات معقدة، كل منها يساهم بمهاراته الفريدة لتحقيق هدف مشترك. هذه هي قوة CrewAI - إطار عمل يمكّنك من إنشاء أنظمة ذكاء اصطناعي تعاونية يمكنها إنجاز مهام تفوق بكثير ما يمكن لـ AI واحد تحقيقه بمفرده.
|
||||
في هذا الدليل ستنشئ crew من Agentين: واحد للبحث وآخر لكتابة تقرير markdown. مشاريع الـ crew الجديدة هي JSON-first: تُعرّف الـ Agents في `agents/*.jsonc`، وتُعرّف المهام وإعدادات الـ crew في `crew.jsonc`، ويحمّل `crewai run` هذا التعريف مباشرة.
|
||||
|
||||
في هذا الدليل، سنمشي عبر إنشاء Crew بحث يساعدنا في البحث والتحليل حول موضوع ما، ثم إنشاء تقرير شامل. يوضح هذا المثال العملي كيف يمكن لـ Agents الذكاء الاصطناعي التعاون لإنجاز مهام معقدة، لكنه مجرد البداية لما هو ممكن مع CrewAI.
|
||||
### المتطلبات
|
||||
|
||||
### ما ستبنيه وتتعلمه
|
||||
1. تثبيت CrewAI من [دليل التثبيت](/ar/installation)
|
||||
2. إعداد مفتاح LLM من [دليل LLMs](/ar/concepts/llms#setting-up-your-llm)
|
||||
3. مفتاح [Serper.dev](https://serper.dev/) إذا أردت استخدام البحث على الويب
|
||||
|
||||
بنهاية هذا الدليل، ستكون قد:
|
||||
|
||||
1. **أنشأت فريق بحث AI متخصص** بأدوار ومسؤوليات مميزة
|
||||
2. **نسّقت التعاون** بين عدة Agents ذكاء اصطناعي
|
||||
3. **أتممت سير عمل معقد** يتضمن جمع المعلومات والتحليل وإنشاء التقارير
|
||||
4. **بنيت مهارات أساسية** يمكنك تطبيقها على مشاريع أكثر طموحًا
|
||||
|
||||
### المتطلبات المسبقة
|
||||
|
||||
قبل البدء، تأكد من:
|
||||
|
||||
1. تثبيت CrewAI باتباع [دليل التثبيت](/ar/installation)
|
||||
2. إعداد مفتاح API لنموذج LLM في بيئتك، باتباع [دليل إعداد LLM](/ar/concepts/llms#setting-up-your-llm)
|
||||
3. فهم أساسي لـ Python
|
||||
|
||||
## الخطوة 1: إنشاء مشروع CrewAI جديد
|
||||
|
||||
أولاً، لننشئ مشروع CrewAI جديد باستخدام CLI. سينشئ هذا الأمر هيكل مشروع كامل بجميع الملفات الضرورية.
|
||||
## الخطوة 1: إنشاء Crew جديدة
|
||||
|
||||
```bash
|
||||
crewai create crew research_crew
|
||||
cd research_crew
|
||||
```
|
||||
|
||||
سينتج هيكل مشروع بالبنية الأساسية المطلوبة لـ Crew. ينشئ CLI تلقائيًا:
|
||||
البنية الناتجة:
|
||||
|
||||
- مجلد مشروع بالملفات اللازمة
|
||||
- ملفات تهيئة للـ Agents والمهام
|
||||
- تطبيق Crew أساسي
|
||||
- سكريبت رئيسي لتشغيل الـ Crew
|
||||
|
||||
<Frame caption="نظرة عامة على إطار عمل CrewAI">
|
||||
<img src="/images/crews.png" alt="نظرة عامة على إطار عمل CrewAI" />
|
||||
</Frame>
|
||||
|
||||
## الخطوة 2: استكشاف هيكل المشروع
|
||||
|
||||
لنخصص لحظة لفهم هيكل المشروع الذي أنشأه CLI.
|
||||
|
||||
```
|
||||
```text
|
||||
research_crew/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── research_crew/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
يتبع هذا الهيكل أفضل الممارسات لمشاريع Python ويسهّل تنظيم الكود. فصل ملفات التهيئة (YAML) عن كود التنفيذ (Python) يسهّل تعديل سلوك Crew دون تغيير الكود الأساسي.
|
||||
<Tip>
|
||||
إذا احتجت إلى البنية القديمة التي تحتوي على `crew.py` و `config/agents.yaml` و `config/tasks.yaml`، استخدم `crewai create crew research_crew --classic`.
|
||||
</Tip>
|
||||
|
||||
## الخطوة 3: تهيئة الـ Agents
|
||||
## الخطوة 2: تعريف الـ Agents
|
||||
|
||||
الآن يأتي الجزء الممتع - تعريف Agents الذكاء الاصطناعي! في CrewAI، الـ Agents هي كيانات متخصصة بأدوار وأهداف وخلفيات محددة تشكّل سلوكها.
|
||||
استبدل ملف `agents/researcher.jsonc` المُنشأ وأضف `agents/analyst.jsonc`. اسم الملف هو الاسم الذي تشير إليه في `crew.jsonc`.
|
||||
|
||||
لـ Crew البحث لدينا، سننشئ Agent اثنين:
|
||||
1. **باحث** يتفوق في إيجاد وتنظيم المعلومات
|
||||
2. **محلل** يمكنه تفسير نتائج البحث وإنشاء تقارير ثاقبة
|
||||
|
||||
لنعدّل ملف `agents.yaml`. تأكد من تعيين `llm` للمزود الذي تستخدمه.
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
Senior Research Specialist for {topic}
|
||||
goal: >
|
||||
Find comprehensive and accurate information about {topic}
|
||||
with a focus on recent developments and key insights
|
||||
backstory: >
|
||||
You are an experienced research specialist with a talent for
|
||||
finding relevant information from various sources. You excel at
|
||||
organizing information in a clear and structured manner, making
|
||||
complex topics accessible to others.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
analyst:
|
||||
role: >
|
||||
Data Analyst and Report Writer for {topic}
|
||||
goal: >
|
||||
Analyze research findings and create a comprehensive, well-structured
|
||||
report that presents insights in a clear and engaging way
|
||||
backstory: >
|
||||
You are a skilled analyst with a background in data interpretation
|
||||
and technical writing. You have a talent for identifying patterns
|
||||
and extracting meaningful insights from research data, then
|
||||
communicating those insights effectively through well-crafted reports.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "Senior Research Specialist for {topic}",
|
||||
"goal": "Find comprehensive and accurate information about {topic}, with a focus on recent developments and key insights.",
|
||||
"backstory": "You are an experienced research specialist who organizes complex information into clear, useful notes.",
|
||||
// استبدله بالنموذج الذي تستخدمه، مثل "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
لاحظ كيف أن لكل Agent دور وهدف وخلفية مميزة. هذه العناصر ليست وصفية فحسب - بل تشكّل بنشاط كيف يتعامل الـ Agent مع مهامه.
|
||||
|
||||
## الخطوة 4: تعريف المهام
|
||||
|
||||
مع تعريف الـ Agents، نحتاج الآن لمنحهم مهام محددة. لنعدّل ملف `tasks.yaml`:
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
Conduct thorough research on {topic}. Focus on:
|
||||
1. Key concepts and definitions
|
||||
2. Historical development and recent trends
|
||||
3. Major challenges and opportunities
|
||||
4. Notable applications or case studies
|
||||
5. Future outlook and potential developments
|
||||
|
||||
Make sure to organize your findings in a structured format with clear sections.
|
||||
expected_output: >
|
||||
A comprehensive research document with well-organized sections covering
|
||||
all the requested aspects of {topic}. Include specific facts, figures,
|
||||
and examples where relevant.
|
||||
agent: researcher
|
||||
|
||||
analysis_task:
|
||||
description: >
|
||||
Analyze the research findings and create a comprehensive report on {topic}.
|
||||
Your report should:
|
||||
1. Begin with an executive summary
|
||||
2. Include all key information from the research
|
||||
3. Provide insightful analysis of trends and patterns
|
||||
4. Offer recommendations or future considerations
|
||||
5. Be formatted in a professional, easy-to-read style with clear headings
|
||||
expected_output: >
|
||||
A polished, professional report on {topic} that presents the research
|
||||
findings with added analysis and insights. The report should be well-structured
|
||||
with an executive summary, main sections, and conclusion.
|
||||
agent: analyst
|
||||
context:
|
||||
- research_task
|
||||
output_file: output/report.md
|
||||
```jsonc agents/analyst.jsonc
|
||||
{
|
||||
"role": "Report Analyst for {topic}",
|
||||
"goal": "Turn research findings into a clear, well-structured report.",
|
||||
"backstory": "You are a careful analyst with strong technical writing skills and a talent for extracting useful insights.",
|
||||
// استبدله بالنموذج الذي تستخدمه، مثل "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
لاحظ حقل `context` في مهمة التحليل - هذه ميزة قوية تتيح للمحلل الوصول إلى مخرجات مهمة البحث.
|
||||
استبدل `provider/model-id` بالنموذج الذي تستخدمه، مثل `openai/gpt-4o` أو `anthropic/claude-sonnet-4-6` أو `gemini/gemini-2.0-flash-001`.
|
||||
|
||||
## الخطوة 5: تهيئة الـ Crew
|
||||
## الخطوة 3: تعريف المهام وإعدادات الـ Crew
|
||||
|
||||
الآن حان الوقت لجمع كل شيء معًا. لنعدّل ملف `crew.py`:
|
||||
استبدل `crew.jsonc` بما يلي:
|
||||
|
||||
```python
|
||||
# src/research_crew/crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew():
|
||||
"""Research crew for comprehensive topic analysis and reporting"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['researcher'], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()]
|
||||
)
|
||||
|
||||
@agent
|
||||
def analyst(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['analyst'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['research_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def analysis_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['analysis_task'], # type: ignore[index]
|
||||
output_file='output/report.md'
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the research crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
## الخطوة 6: إعداد السكريبت الرئيسي
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# src/research_crew/main.py
|
||||
import os
|
||||
from research_crew.crew import ResearchCrew
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs('output', exist_ok=True)
|
||||
|
||||
def run():
|
||||
"""
|
||||
Run the research crew.
|
||||
"""
|
||||
inputs = {
|
||||
'topic': 'Artificial Intelligence in Healthcare'
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research on {topic}. Focus on key concepts, recent developments, major challenges, notable applications, and future outlook.",
|
||||
"expected_output": "A comprehensive research document with organized sections, specific facts, and useful examples about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis_task",
|
||||
"description": "Analyze the research findings and create a polished report on {topic}. Include an executive summary, key insights, trend analysis, and recommendations.",
|
||||
"expected_output": "A professional markdown report with clear headings, a concise summary, main findings, and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research_task"],
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
|
||||
# Create and run the crew
|
||||
result = ResearchCrew().crew().kickoff(inputs=inputs)
|
||||
|
||||
# Print the result
|
||||
print("\n\n=== FINAL REPORT ===\n\n")
|
||||
print(result.raw)
|
||||
|
||||
print("\n\nReport has been saved to output/report.md")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "Artificial Intelligence in Healthcare"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## الخطوة 7: إعداد متغيرات البيئة
|
||||
يشير `context` إلى أسماء مهام سابقة، لذلك يحصل analyst على مخرجات مهمة البحث. يوفر `inputs` قيمة افتراضية لـ `{topic}`. إذا حذفت القيمة الافتراضية، سيطلبها `crewai run`.
|
||||
|
||||
أنشئ ملف `.env` في جذر مشروعك بمفاتيح API:
|
||||
## الخطوة 4: متغيرات البيئة
|
||||
|
||||
عدّل `.env`:
|
||||
|
||||
```sh
|
||||
SERPER_API_KEY=your_serper_api_key
|
||||
# Add your provider's API key here too.
|
||||
# أضف مفتاح مزود النموذج أيضًا.
|
||||
```
|
||||
|
||||
راجع [دليل إعداد LLM](/ar/concepts/llms#setting-up-your-llm) لتفاصيل تهيئة المزود المفضل لديك. يمكنك الحصول على مفتاح Serper API من [Serper.dev](https://serper.dev/).
|
||||
|
||||
## الخطوة 8: تثبيت التبعيات
|
||||
## الخطوة 5: التثبيت والتشغيل
|
||||
|
||||
```bash
|
||||
crewai install
|
||||
```
|
||||
|
||||
## الخطوة 9: تشغيل الـ Crew
|
||||
|
||||
الآن اللحظة المثيرة - حان وقت تشغيل Crew ومشاهدة التعاون بين الـ AI!
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
عند تشغيل هذا الأمر، سترى Crew يعمل. سيجمع الباحث معلومات حول الموضوع المحدد، ثم سينشئ المحلل تقريرًا شاملاً بناءً على ذلك البحث.
|
||||
بعد انتهاء التشغيل، افتح `output/report.md`.
|
||||
|
||||
## الخطوة 10: مراجعة المخرجات
|
||||
|
||||
بمجرد إتمام Crew عمله، ستجد التقرير النهائي في ملف `output/report.md`. سيتضمن التقرير:
|
||||
|
||||
1. ملخص تنفيذي
|
||||
2. معلومات مفصلة عن الموضوع
|
||||
3. تحليل ورؤى
|
||||
4. توصيات أو اعتبارات مستقبلية
|
||||
|
||||
## استكشاف أوامر CLI الأخرى
|
||||
|
||||
يوفر CrewAI عدة أوامر CLI مفيدة للعمل مع Crews:
|
||||
|
||||
```bash
|
||||
# View all available commands
|
||||
crewai --help
|
||||
|
||||
# Run the crew
|
||||
crewai run
|
||||
|
||||
# Test the crew
|
||||
crewai test
|
||||
|
||||
# Reset crew memories
|
||||
crewai reset-memories
|
||||
|
||||
# Replay from a specific task
|
||||
crewai replay -t <task_id>
|
||||
```
|
||||
|
||||
## ما بعد أول Crew: فن الممكن
|
||||
|
||||
ما بنيته في هذا الدليل مجرد البداية. يمكنك توسيع Crew البحث الأساسي بإضافة Agents متخصصة أخرى وأدوات وقدرات إضافية وسير عمل أكثر تعقيدًا. يمكن تطبيق نفس الأنماط لإنشاء Crews لإنشاء المحتوى وخدمة العملاء وتطوير المنتجات وتحليل البيانات.
|
||||
|
||||
## الخطوات التالية
|
||||
|
||||
1. جرّب تهيئات وشخصيات Agent مختلفة
|
||||
2. جرب هياكل مهام وسير عمل أكثر تعقيدًا
|
||||
3. طبّق أدوات مخصصة لمنح الـ Agents قدرات جديدة
|
||||
4. طبّق Crew على مواضيع أو مجالات مشكلات مختلفة
|
||||
5. استكشف [CrewAI Flows](/ar/guides/flows/first-flow) لسير عمل أكثر تقدمًا مع البرمجة الإجرائية
|
||||
<Warning>
|
||||
شغّل مشاريع JSON crew من مصادر تثق بها فقط. أدوات `custom:<name>` ومراجع `{"python": "module.attribute"}` تنفذ Python محليًا عند تحميل الـ crew.
|
||||
</Warning>
|
||||
|
||||
<Check>
|
||||
تهانينا! لقد بنيت بنجاح أول CrewAI Crew يمكنه البحث والتحليل في أي موضوع تقدمه. هذه التجربة الأساسية أهّلتك بالمهارات لإنشاء أنظمة AI متطورة بشكل متزايد يمكنها معالجة مشكلات معقدة متعددة المراحل من خلال الذكاء التعاوني.
|
||||
أصبحت لديك crew تعمل بأسلوب JSON-first تبحث في موضوع وتكتب تقريرًا.
|
||||
</Check>
|
||||
|
||||
@@ -42,6 +42,8 @@ cd guide_creator_flow
|
||||
|
||||
## الخطوة 2: فهم هيكل المشروع
|
||||
|
||||
يستخدم الـ crew المبدئي المضمّن في مشروع Flow بنية Python/YAML الكلاسيكية. لاستخدام crew بنمط JSON-first داخل Flow، أنشئ `crew.jsonc` و `agents/*.jsonc` داخل مجلد الـ crew وحمّله عبر `crewai.project.load_crew` كما في [Flows](/ar/concepts/flows#building-your-crews).
|
||||
|
||||
```
|
||||
guide_creator_flow/
|
||||
├── .gitignore
|
||||
@@ -69,139 +71,77 @@ crewai flow add-crew content-crew
|
||||
|
||||
## الخطوة 4: تهيئة Crew كتابة المحتوى
|
||||
|
||||
1. حدّث ملف تهيئة الـ Agents. تذكر تعيين `llm` للمزود الذي تستخدمه.
|
||||
سنهيئ crew كتابة المحتوى باستخدام JSONC. سنعرّف Agent للكتابة وAgent للمراجعة، ثم نحمّل `crew.jsonc` من خطوة Flow.
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/agents.yaml
|
||||
content_writer:
|
||||
role: >
|
||||
Educational Content Writer
|
||||
goal: >
|
||||
Create engaging, informative content that thoroughly explains the assigned topic
|
||||
and provides valuable insights to the reader
|
||||
backstory: >
|
||||
You are a talented educational writer with expertise in creating clear, engaging
|
||||
content. You have a gift for explaining complex concepts in accessible language
|
||||
and organizing information in a way that helps readers build their understanding.
|
||||
llm: provider/model-id
|
||||
1. أنشئ `src/guide_creator_flow/crews/content_crew/agents/content_writer.jsonc`:
|
||||
|
||||
content_reviewer:
|
||||
role: >
|
||||
Educational Content Reviewer and Editor
|
||||
goal: >
|
||||
Ensure content is accurate, comprehensive, well-structured, and maintains
|
||||
consistency with previously written sections
|
||||
backstory: >
|
||||
You are a meticulous editor with years of experience reviewing educational
|
||||
content. You have an eye for detail, clarity, and coherence.
|
||||
llm: provider/model-id
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Writer",
|
||||
"goal": "Create engaging, informative content that thoroughly explains the assigned topic and provides valuable insights to the reader.",
|
||||
"backstory": "You are a talented educational writer who explains complex concepts in accessible language and organizes information clearly.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. حدّث ملف تهيئة المهام:
|
||||
2. أنشئ `src/guide_creator_flow/crews/content_crew/agents/content_reviewer.jsonc`:
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/tasks.yaml
|
||||
write_section_task:
|
||||
description: >
|
||||
Write a comprehensive section on the topic: "{section_title}"
|
||||
|
||||
Section description: {section_description}
|
||||
Target audience: {audience_level} level learners
|
||||
|
||||
Your content should:
|
||||
1. Begin with a brief introduction to the section topic
|
||||
2. Explain all key concepts clearly with examples
|
||||
3. Include practical applications or exercises where appropriate
|
||||
4. End with a summary of key points
|
||||
5. Be approximately 500-800 words in length
|
||||
|
||||
Format your content in Markdown with appropriate headings, lists, and emphasis.
|
||||
|
||||
Previously written sections:
|
||||
{previous_sections}
|
||||
expected_output: >
|
||||
A well-structured, comprehensive section in Markdown format that thoroughly
|
||||
explains the topic and is appropriate for the target audience.
|
||||
agent: content_writer
|
||||
|
||||
review_section_task:
|
||||
description: >
|
||||
Review and improve the following section on "{section_title}":
|
||||
|
||||
{draft_content}
|
||||
|
||||
Target audience: {audience_level} level learners
|
||||
|
||||
Previously written sections:
|
||||
{previous_sections}
|
||||
|
||||
Your review should:
|
||||
1. Fix any grammatical or spelling errors
|
||||
2. Improve clarity and readability
|
||||
3. Ensure content is comprehensive and accurate
|
||||
4. Verify consistency with previously written sections
|
||||
5. Enhance the structure and flow
|
||||
6. Add any missing key information
|
||||
expected_output: >
|
||||
An improved, polished version of the section that maintains the original
|
||||
structure but enhances clarity, accuracy, and consistency.
|
||||
agent: content_reviewer
|
||||
context:
|
||||
- write_section_task
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Reviewer and Editor",
|
||||
"goal": "Ensure content is accurate, comprehensive, well-structured, and consistent with previously written sections.",
|
||||
"backstory": "You are a meticulous editor with an eye for detail, clarity, and coherence.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. حدّث ملف تنفيذ Crew:
|
||||
استبدل `provider/model-id` بالنموذج الذي تستخدمه، مثل `openai/gpt-4o` أو `gemini/gemini-2.0-flash-001` أو `anthropic/claude-sonnet-4-6`.
|
||||
|
||||
3. أنشئ `src/guide_creator_flow/crews/content_crew/crew.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "Content Crew",
|
||||
"agents": ["content_writer", "content_reviewer"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "write_section_task",
|
||||
"description": "Write a comprehensive section on the topic: \"{section_title}\".\n\nSection description: {section_description}\nTarget audience: {audience_level} level learners\n\nYour content should begin with a brief introduction, explain key concepts clearly with examples, include practical applications where appropriate, end with a summary, and be approximately 500-800 words.\n\nPreviously written sections:\n{previous_sections}",
|
||||
"expected_output": "A well-structured, comprehensive section in Markdown format that thoroughly explains the topic and is appropriate for the target audience.",
|
||||
"agent": "content_writer",
|
||||
"markdown": true
|
||||
},
|
||||
{
|
||||
"name": "review_section_task",
|
||||
"description": "Review and improve this section on \"{section_title}\":\n\n{draft_content}\n\nTarget audience: {audience_level} level learners\nPreviously written sections:\n{previous_sections}\n\nFix errors, improve clarity, verify consistency, enhance structure, and add missing key information.",
|
||||
"expected_output": "An improved, polished version of the section that maintains the original structure but enhances clarity, accuracy, and consistency.",
|
||||
"agent": "content_reviewer",
|
||||
"context": ["write_section_task"],
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
4. استبدل `src/guide_creator_flow/crews/content_crew/content_crew.py` بمحمل صغير:
|
||||
|
||||
```python
|
||||
# src/guide_creator_flow/crews/content_crew/content_crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
@CrewBase
|
||||
class ContentCrew():
|
||||
"""Content writing crew"""
|
||||
from crewai.project import load_crew
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def content_writer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_writer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@agent
|
||||
def content_reviewer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_reviewer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def write_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['write_section_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def review_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['review_section_task'], # type: ignore[index]
|
||||
context=[self.write_section_task()]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the content writing crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
## الخطوة 5: إنشاء Flow
|
||||
|
||||
@@ -113,7 +113,7 @@ python3 --version
|
||||
|
||||
# إنشاء مشروع CrewAI
|
||||
|
||||
نوصي باستخدام قالب `YAML` لنهج منظم في تعريف الـ Agents والمهام. إليك كيفية البدء:
|
||||
يقوم `crewai create crew` الآن بإنشاء مشروع crew بأسلوب JSON-first. توضع الـ Agents في `agents/*.jsonc`، وتوضع المهام وإعدادات الـ crew في `crew.jsonc`، ويحمّل `crewai run` هذا التعريف مباشرة.
|
||||
|
||||
<Steps>
|
||||
<Step title="إنشاء هيكل المشروع">
|
||||
@@ -126,21 +126,20 @@ python3 --version
|
||||
```
|
||||
my_project/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── my_project/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
- إذا احتجت إلى البنية القديمة Python/YAML التي تحتوي على `crew.py` و `config/agents.yaml` و `config/tasks.yaml`، شغّل:
|
||||
```shell
|
||||
crewai create crew <your_project_name> --classic
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -149,15 +148,15 @@ python3 --version
|
||||
- سيحتوي مشروعك على هذه الملفات الأساسية:
|
||||
| الملف | الغرض |
|
||||
| --- | --- |
|
||||
| `agents.yaml` | تعريف الـ Agents وأدوارهم |
|
||||
| `tasks.yaml` | إعداد مهام الـ Agents وسير العمل |
|
||||
| `crew.jsonc` | إعداد الـ crew وترتيب المهام والعملية وقيم الإدخال الافتراضية |
|
||||
| `agents/*.jsonc` | تعريف دور كل Agent وهدفه و backstory والـ LLM والأدوات والسلوك |
|
||||
| `.env` | تخزين مفاتيح API ومتغيرات البيئة |
|
||||
| `main.py` | نقطة دخول المشروع وتدفق التنفيذ |
|
||||
| `crew.py` | تنسيق وإدارة الـ Crew |
|
||||
| `tools/` | مجلد الأدوات المخصصة |
|
||||
| `knowledge/` | مجلد قاعدة المعرفة |
|
||||
| `tools/` | ملفات Python اختيارية لأدوات `custom:<name>` |
|
||||
| `knowledge/` | ملفات معرفة اختيارية للـ Agents |
|
||||
| `skills/` | ملفات skills اختيارية تطبق على الـ crew |
|
||||
|
||||
- ابدأ بتحرير `agents.yaml` و`tasks.yaml` لتعريف سلوك الـ Crew.
|
||||
- ابدأ بتحرير `crew.jsonc` والملفات داخل `agents/` لتعريف سلوك الـ crew.
|
||||
- استخدم قيم `{placeholder}` في نصوص الـ Agents والمهام، ثم ضع القيم الافتراضية في `inputs` داخل `crew.jsonc`. عند تشغيل `crewai run` ستطلب CLI أي قيم ناقصة.
|
||||
- احتفظ بالمعلومات الحساسة مثل مفاتيح API في `.env`.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -5,11 +5,15 @@ icon: "at"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
يشرح هذا الدليل كيفية استخدام التعليقات التوضيحية للإشارة بشكل صحيح إلى **الوكلاء** و**المهام** والمكونات الأخرى في ملف `crew.py`.
|
||||
يشرح هذا الدليل كيفية استخدام التعليقات التوضيحية للإشارة بشكل صحيح إلى **الوكلاء** و**المهام** والمكونات الأخرى في ملف `crew.py` كلاسيكي.
|
||||
|
||||
<Note>
|
||||
المشاريع الجديدة التي تُنشأ عبر `crewai create crew <name>` هي JSON-first وتستخدم `crew.jsonc` مع `agents/*.jsonc`. استخدم هذا الدليل عند العمل في مشروع كلاسيكي أُنشئ عبر `crewai create crew <name> --classic`، أو عند ترحيل مشروع Python/YAML موجود، أو عندما تحتاج تحكمًا عبر decorators في Python.
|
||||
</Note>
|
||||
|
||||
## مقدمة
|
||||
|
||||
تُستخدم التعليقات التوضيحية في إطار عمل CrewAI لتزيين الفئات والطرق، مما يوفر بيانات وصفية ووظائف للمكونات المختلفة في طاقمك. تساعد هذه التعليقات التوضيحية في تنظيم وهيكلة الكود الخاص بك، مما يجعله أكثر قابلية للقراءة والصيانة.
|
||||
تُستخدم التعليقات التوضيحية في إطار عمل CrewAI لتزيين الفئات والطرق، مما يوفر بيانات وصفية ووظائف للمكونات المختلفة في طاقمك. في مشاريع Python/YAML الكلاسيكية، تنظم الكود الذي يحمّل `config/agents.yaml` و `config/tasks.yaml` ويعيد كائن `Crew`.
|
||||
|
||||
## التعليقات التوضيحية المتاحة
|
||||
|
||||
@@ -113,9 +117,9 @@ def crew(self) -> Crew:
|
||||
|
||||
تُستخدم التعليقة التوضيحية `@crew` لتزيين الطريقة التي تنشئ وتُرجع كائن `Crew`. تجمع هذه الطريقة جميع المكونات (الوكلاء والمهام) في طاقم وظيفي.
|
||||
|
||||
## إعداد YAML
|
||||
## إعداد YAML الكلاسيكي
|
||||
|
||||
تُخزن إعدادات الوكلاء عادةً في ملف YAML. إليك مثالاً على كيفية ظهور ملف `agents.yaml` لوكيل الباحث:
|
||||
في المشاريع الكلاسيكية، تُخزن إعدادات الوكلاء عادةً في ملف YAML. إليك مثالاً على كيفية ظهور ملف `agents.yaml` لوكيل الباحث:
|
||||
|
||||
```yaml
|
||||
researcher:
|
||||
@@ -146,6 +150,6 @@ researcher:
|
||||
- **تسمية متسقة**: استخدم اصطلاحات تسمية واضحة ومتسقة لطرقك. على سبيل المثال، يمكن تسمية طرق الوكلاء بأسماء أدوارهم (مثل researcher، reporting_analyst).
|
||||
- **متغيرات البيئة**: استخدم متغيرات البيئة للمعلومات الحساسة مثل مفاتيح API.
|
||||
- **المرونة**: صمم طاقمك ليكون مرناً بالسماح بإضافة أو إزالة الوكلاء والمهام بسهولة.
|
||||
- **توافق YAML-الكود**: تأكد من أن الأسماء والهياكل في ملفات YAML تتوافق بشكل صحيح مع الطرق المزينة في كود Python الخاص بك.
|
||||
- **توافق YAML-الكود**: في المشاريع الكلاسيكية، تأكد من أن الأسماء والهياكل في ملفات YAML تتوافق بشكل صحيح مع الطرق المزينة في كود Python الخاص بك.
|
||||
|
||||
باتباع هذه الإرشادات واستخدام التعليقات التوضيحية بشكل صحيح، يمكنك إنشاء أطقم منظمة جيداً وسهلة الصيانة باستخدام إطار عمل CrewAI.
|
||||
باتباع هذه الإرشادات واستخدام التعليقات التوضيحية بشكل صحيح، يمكنك الحفاظ على أطقم كلاسيكية منظمة وسهلة الصيانة. للـ crews الجديدة، استخدم بنية JSON-first في [Crews](/ar/concepts/crews).
|
||||
|
||||
@@ -39,84 +39,60 @@ mode: "wide"
|
||||
يُنشئ ذلك تطبيق Flow ضمن `src/latest_ai_flow/`، بما في ذلك طاقمًا أوليًا في `crews/content_crew/` ستستبدله بطاقم بحث **بوكيل واحد** في الخطوات التالية.
|
||||
</Step>
|
||||
|
||||
<Step title="اضبط وكيلًا واحدًا في `agents.yaml`">
|
||||
استبدل محتوى `src/latest_ai_flow/crews/content_crew/config/agents.yaml` بباحث واحد. تُملأ المتغيرات مثل `{topic}` من `crew.kickoff(inputs=...)`.
|
||||
<Step title="اضبط وكيلًا واحدًا في JSONC">
|
||||
أنشئ `src/latest_ai_flow/crews/content_crew/agents/researcher.jsonc` (أنشئ مجلد `agents/` إذا لزم). تُملأ المتغيرات مثل `{topic}` من `crew.kickoff(inputs=...)`.
|
||||
|
||||
```yaml agents.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
باحث بيانات أول في {topic}
|
||||
goal: >
|
||||
اكتشاف أحدث التطورات في {topic}
|
||||
backstory: >
|
||||
أنت باحث مخضرم تكشف أحدث المستجدات في {topic}.
|
||||
تجد المعلومات الأكثر صلة وتعرضها بوضوح.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "باحث بيانات أول في {topic}",
|
||||
"goal": "اكتشاف أحدث التطورات في {topic}",
|
||||
"backstory": "أنت باحث يجد المعلومات الأكثر صلة ويعرضها بوضوح.",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="اضبط مهمة واحدة في `tasks.yaml`">
|
||||
```yaml tasks.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
أجرِ بحثًا معمقًا عن {topic}. استخدم البحث على الويب للعثور على معلومات
|
||||
حديثة وموثوقة. السنة الحالية 2026.
|
||||
expected_output: >
|
||||
تقرير بصيغة Markdown بأقسام واضحة: الاتجاهات الرئيسية، أدوات أو شركات بارزة،
|
||||
والآثار. بين 800 و1200 كلمة تقريبًا. دون إحاطة المستند بأكمله بكتل كود.
|
||||
agent: researcher
|
||||
output_file: output/report.md
|
||||
<Step title="اضبط الـ crew في `crew.jsonc`">
|
||||
أنشئ `src/latest_ai_flow/crews/content_crew/crew.jsonc`:
|
||||
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "أجرِ بحثًا معمقًا عن {topic}. استخدم البحث على الويب للعثور على معلومات حديثة وموثوقة. السنة الحالية 2026.",
|
||||
"expected_output": "تقرير بصيغة Markdown بأقسام واضحة: الاتجاهات الرئيسية، أدوات أو شركات بارزة، والآثار. بين 800 و1200 كلمة تقريبًا. دون إحاطة المستند بأكمله بكتل كود.",
|
||||
"agent": "researcher",
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="اربط صف الطاقم (`content_crew.py`)">
|
||||
اجعل الطاقم المُولَّد يشير إلى YAML وأرفق `SerperDevTool` بالباحث.
|
||||
<Step title="حمّل crew JSON (`content_crew.py`)">
|
||||
استبدل `content_crew.py` المُولّد بمحمل صغير يحول `crew.jsonc` إلى `Crew`.
|
||||
|
||||
```python content_crew.py
|
||||
# src/latest_ai_flow/crews/content_crew/content_crew.py
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.project import load_crew
|
||||
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew:
|
||||
"""طاقم بحث بوكيل واحد داخل Flow."""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
agents_config = "config/agents.yaml"
|
||||
tasks_config = "config/tasks.yaml"
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["researcher"], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()],
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config["research_task"], # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -130,7 +106,7 @@ mode: "wide"
|
||||
|
||||
from crewai.flow import Flow, listen, start
|
||||
|
||||
from latest_ai_flow.crews.content_crew.content_crew import ResearchCrew
|
||||
from latest_ai_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
|
||||
class ResearchFlowState(BaseModel):
|
||||
@@ -149,7 +125,7 @@ mode: "wide"
|
||||
|
||||
@listen(prepare_topic)
|
||||
def run_research(self):
|
||||
result = ResearchCrew().crew().kickoff(inputs={"topic": self.state.topic})
|
||||
result = kickoff_content_crew(inputs={"topic": self.state.topic})
|
||||
self.state.report = result.raw
|
||||
print("اكتمل طاقم البحث.")
|
||||
|
||||
@@ -171,7 +147,7 @@ mode: "wide"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
إذا كان اسم الحزمة ليس `latest_ai_flow`، عدّل استيراد `ResearchCrew` ليطابق مسار الوحدة في مشروعك.
|
||||
إذا كان اسم الحزمة ليس `latest_ai_flow`، عدّل استيراد `kickoff_content_crew` ليطابق مسار الوحدة في مشروعك.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
@@ -219,7 +195,7 @@ mode: "wide"
|
||||
## كيف يترابط هذا
|
||||
|
||||
1. **Flow** — يشغّل `LatestAiFlow` أولًا `prepare_topic` ثم `run_research` ثم `summarize`. الحالة (`topic`، `report`) على Flow.
|
||||
2. **الطاقم** — يشغّل `ResearchCrew` مهمة واحدة بوكيل واحد: الباحث يستخدم **Serper** للبحث على الويب ثم يكتب التقرير.
|
||||
2. **الطاقم** — يحمّل `kickoff_content_crew` ملف `crew.jsonc` ويشغّل مهمة واحدة بوكيل واحد: الباحث يستخدم **Serper** للبحث على الويب ثم يكتب التقرير.
|
||||
3. **المُخرَج** — يكتب `output_file` للمهمة التقرير في `output/report.md`.
|
||||
|
||||
للتعمق في أنماط Flow (التوجيه، الاستمرارية، الإنسان في الحلقة)، راجع [ابنِ أول Flow](/ar/guides/flows/first-flow) و[Flows](/ar/concepts/flows). للطواقم دون Flow، راجع [Crews](/ar/concepts/crews). لوكيل `Agent` واحد و`kickoff()` بلا مهام، راجع [Agents](/ar/concepts/agents#direct-agent-interaction-with-kickoff).
|
||||
@@ -230,7 +206,10 @@ mode: "wide"
|
||||
|
||||
### اتساق التسمية
|
||||
|
||||
يجب أن تطابق مفاتيح YAML (`researcher`، `research_task`) أسماء الدوال في صف `@CrewBase`. راجع [Crews](/ar/concepts/crews) لنمط الديكورات الكامل.
|
||||
يجب أن تطابق الأسماء في `crew.jsonc` الملفات والمراجع:
|
||||
|
||||
- `agents: ["researcher"]` يحمّل `agents/researcher.jsonc`
|
||||
- `context: ["research_task"]` يشير إلى مهمة سابقة اسمها `research_task`
|
||||
|
||||
## النشر
|
||||
|
||||
|
||||
@@ -71,81 +71,65 @@ The Visual Agent Builder enables:
|
||||
|
||||
## Creating Agents
|
||||
|
||||
There are two ways to create agents in CrewAI: using **YAML configuration (recommended)** or defining them **directly in code**.
|
||||
There are two common ways to create agents in CrewAI: using **JSONC project configuration (recommended for new crews)** or defining them **directly in code**.
|
||||
|
||||
### YAML Configuration (Recommended)
|
||||
### JSONC Configuration (Recommended)
|
||||
|
||||
Using YAML configuration provides a cleaner, more maintainable way to define agents. We strongly recommend using this approach in your CrewAI projects.
|
||||
New projects created with `crewai create crew <name>` use JSON-first configuration. Each agent is defined in `agents/<agent_name>.jsonc`, and `crew.jsonc` lists which agents are part of the crew.
|
||||
|
||||
After creating your CrewAI project as outlined in the [Installation](/en/installation) section, navigate to the `src/latest_ai_development/config/agents.yaml` file and modify the template to match your requirements.
|
||||
After creating your CrewAI project as outlined in the [Installation](/en/installation) section, edit the generated files in `agents/`.
|
||||
|
||||
<Note>
|
||||
Variables in your YAML files (like `{topic}`) will be replaced with values from your inputs when running the crew:
|
||||
```python Code
|
||||
crew.kickoff(inputs={'topic': 'AI Agents'})
|
||||
```
|
||||
Use `{placeholder}` values in `role`, `goal`, or `backstory`. Put defaults in `crew.jsonc` under `inputs`; `crewai run` prompts for any missing values.
|
||||
</Note>
|
||||
|
||||
Here's an example of how to configure agents using YAML:
|
||||
Here's an example `agents/researcher.jsonc` file:
|
||||
|
||||
```yaml agents.yaml
|
||||
# src/latest_ai_development/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
{topic} Senior Data Researcher
|
||||
goal: >
|
||||
Uncover cutting-edge developments in {topic}
|
||||
backstory: >
|
||||
You're a seasoned researcher with a knack for uncovering the latest
|
||||
developments in {topic}. Known for your ability to find the most relevant
|
||||
information and present it in a clear and concise manner.
|
||||
|
||||
reporting_analyst:
|
||||
role: >
|
||||
{topic} Reporting Analyst
|
||||
goal: >
|
||||
Create detailed reports based on {topic} data analysis and research findings
|
||||
backstory: >
|
||||
You're a meticulous analyst with a keen eye for detail. You're known for
|
||||
your ability to turn complex data into clear and concise reports, making
|
||||
it easy for others to understand and act on the information you provide.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} Senior Data Researcher",
|
||||
"goal": "Uncover cutting-edge developments in {topic}",
|
||||
"backstory": "You find the most relevant information and present it clearly.",
|
||||
"llm": "openai/gpt-4o",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false,
|
||||
"max_iter": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To use this YAML configuration in your code, create a crew class that inherits from `CrewBase`:
|
||||
Then include that agent from `crew.jsonc`:
|
||||
|
||||
```python Code
|
||||
# src/latest_ai_development/crew.py
|
||||
from crewai import Agent, Crew, Process
|
||||
from crewai.project import CrewBase, agent, crew
|
||||
from crewai_tools import SerperDevTool
|
||||
|
||||
@CrewBase
|
||||
class LatestAiDevelopmentCrew():
|
||||
"""LatestAiDevelopment crew"""
|
||||
|
||||
agents_config = "config/agents.yaml"
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['researcher'], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()]
|
||||
)
|
||||
|
||||
@agent
|
||||
def reporting_analyst(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['reporting_analyst'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research {topic}",
|
||||
"expected_output": "A concise briefing about {topic}",
|
||||
"agent": "researcher"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agent files support any public `Agent` field. Common fields include `role`, `goal`, `backstory`, `llm`, `tools`, `function_calling_llm`, `guardrail`, `step_callback`, and `settings`. Behavior options such as `verbose`, `allow_delegation`, `max_iter`, `max_rpm`, `memory`, `cache`, `planning`, and `use_system_prompt` can be placed at the top level or under `settings`; values in `settings` take precedence.
|
||||
|
||||
<Note>
|
||||
The names you use in your YAML files (`agents.yaml`) should match the method
|
||||
names in your Python code.
|
||||
JSONC supports comments and trailing commas. If both `agents/<name>.jsonc` and `agents/<name>.json` exist, CrewAI uses the JSONC file.
|
||||
</Note>
|
||||
|
||||
### Classic YAML Configuration
|
||||
|
||||
Classic projects created with `crewai create crew <name> --classic` use `config/agents.yaml` and a `@CrewBase` class in `crew.py`. This remains supported for teams that want Python decorators or existing YAML projects.
|
||||
|
||||
### Direct Code Definition
|
||||
|
||||
You can create agents directly in code by instantiating the `Agent` class. Here's a comprehensive example showing all available parameters:
|
||||
|
||||
@@ -52,6 +52,8 @@ crewai create crew my_new_crew
|
||||
crewai create flow my_new_flow
|
||||
```
|
||||
|
||||
By default, `crewai create crew` creates a JSON-first crew project with `crew.jsonc` and `agents/*.jsonc`. Use `crewai create crew my_new_crew --classic` only when you want the older Python/YAML scaffold with `crew.py`, `config/agents.yaml`, and `config/tasks.yaml`.
|
||||
|
||||
### 2. Version
|
||||
|
||||
Show the installed version of CrewAI.
|
||||
@@ -185,7 +187,20 @@ crewai chat
|
||||
Ensure you execute these commands from your CrewAI project's root directory.
|
||||
</Note>
|
||||
<Note>
|
||||
IMPORTANT: Set the `chat_llm` property in your `crew.py` file to enable this command.
|
||||
IMPORTANT: Set the `chat_llm` property in your crew definition to enable this command.
|
||||
|
||||
For JSON-first crews, add it to `crew.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "My Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [],
|
||||
"chat_llm": "openai/gpt-4o"
|
||||
}
|
||||
```
|
||||
|
||||
For classic Python/YAML crews, set it in `crew.py`:
|
||||
|
||||
```python
|
||||
@crew
|
||||
@@ -336,7 +351,7 @@ Notes:
|
||||
|
||||
### 12. API Keys
|
||||
|
||||
When running `crewai create crew` command, the CLI will show you a list of available LLM providers to choose from, followed by model selection for your chosen provider.
|
||||
When running the `crewai create crew` command, the CLI shows a list of available LLM providers, followed by model selection for your chosen provider. The selected model is saved in the generated `.env` file and each generated agent JSONC file can set its own `llm`.
|
||||
|
||||
Once you've selected an LLM provider and model, you will be prompted for API keys.
|
||||
|
||||
|
||||
@@ -48,108 +48,74 @@ A crew in crewAI represents a collaborative group of agents working together to
|
||||
|
||||
## Creating Crews
|
||||
|
||||
There are two ways to create crews in CrewAI: using **YAML configuration (recommended)** or defining them **directly in code**.
|
||||
There are two common ways to create crews in CrewAI: using **JSONC project configuration (recommended for new crews)** or defining them **directly in code**.
|
||||
|
||||
### YAML Configuration (Recommended)
|
||||
### JSONC Configuration (Recommended)
|
||||
|
||||
Using YAML configuration provides a cleaner, more maintainable way to define crews and is consistent with how agents and tasks are defined in CrewAI projects.
|
||||
New projects created with `crewai create crew <name>` use `crew.jsonc` for crew-level settings and tasks, plus one file per agent in `agents/`.
|
||||
|
||||
After creating your CrewAI project as outlined in the [Installation](/en/installation) section, you can define your crew in a class that inherits from `CrewBase` and uses decorators to define agents, tasks, and the crew itself.
|
||||
`crewai run` automatically detects `crew.jsonc` or `crew.json`, loads the referenced agents, prompts for missing placeholders, and kicks off the crew.
|
||||
|
||||
#### Example Crew Class with Decorators
|
||||
#### Example `crew.jsonc`
|
||||
|
||||
```python code
|
||||
from crewai import Agent, Crew, Task, Process
|
||||
from crewai.project import CrewBase, agent, task, crew, before_kickoff, after_kickoff
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase
|
||||
class YourCrewName:
|
||||
"""Description of your crew"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
# Paths to your YAML configuration files
|
||||
# To see an example agent and task defined in YAML, checkout the following:
|
||||
# - Task: https://docs.crewai.com/concepts/tasks#yaml-configuration-recommended
|
||||
# - Agents: https://docs.crewai.com/concepts/agents#yaml-configuration-recommended
|
||||
agents_config = 'config/agents.yaml'
|
||||
tasks_config = 'config/tasks.yaml'
|
||||
|
||||
@before_kickoff
|
||||
def prepare_inputs(self, inputs):
|
||||
# Modify inputs before the crew starts
|
||||
inputs['additional_data'] = "Some extra information"
|
||||
return inputs
|
||||
|
||||
@after_kickoff
|
||||
def process_output(self, output):
|
||||
# Modify output after the crew finishes
|
||||
output.raw += "\nProcessed after kickoff."
|
||||
return output
|
||||
|
||||
@agent
|
||||
def agent_one(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['agent_one'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@agent
|
||||
def agent_two(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['agent_two'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def task_one(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['task_one'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def task_two(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['task_two'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents, # Automatically collected by the @agent decorator
|
||||
tasks=self.tasks, # Automatically collected by the @task decorator.
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Market Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research",
|
||||
"description": "Research {topic} and collect the most relevant facts.",
|
||||
"expected_output": "Structured research notes about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis",
|
||||
"description": "Analyze the research and write a concise report.",
|
||||
"expected_output": "A markdown report with findings and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research"],
|
||||
"output_file": "output/report.md"
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
How to run the above code:
|
||||
Each string in `agents` resolves to `agents/<name>.jsonc` first, then `agents/<name>.json`.
|
||||
|
||||
```python code
|
||||
YourCrewName().crew().kickoff(inputs={"any": "input here"})
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} Senior Researcher",
|
||||
"goal": "Find accurate and current information about {topic}.",
|
||||
"backstory": "You are a careful researcher who cites clear evidence.",
|
||||
"llm": "openai/gpt-4o",
|
||||
"tools": ["SerperDevTool"]
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Tasks will be executed in the order they are defined.
|
||||
Tasks run in the order they appear in `tasks` when `process` is `"sequential"`.
|
||||
</Note>
|
||||
|
||||
The `CrewBase` class, along with these decorators, automates the collection of agents and tasks, reducing the need for manual management.
|
||||
For hierarchical crews, set `"process": "hierarchical"` and provide either `manager_llm` or `manager_agent`. A `manager_agent` can reference an `agents/<name>.jsonc` file that is not included in the top-level `agents` list.
|
||||
|
||||
#### Decorators overview from `annotations.py`
|
||||
JSON crew definitions support crew-level fields such as `process`, `verbose`, `memory`, `cache`, `max_rpm`, `planning`, `planning_llm`, `manager_llm`, `manager_agent`, `function_calling_llm`, `output_log_file`, `stream`, `tracing`, `before_kickoff_callbacks`, and `after_kickoff_callbacks`.
|
||||
|
||||
CrewAI provides several decorators in the `annotations.py` file that are used to mark methods within your crew class for special handling:
|
||||
Python callbacks and custom classes use `{"python": "module.attribute"}`. Custom tools use `"custom:<name>"` and load `tools/<name>.py` at runtime.
|
||||
|
||||
- `@CrewBase`: Marks the class as a crew base class.
|
||||
- `@agent`: Denotes a method that returns an `Agent` object.
|
||||
- `@task`: Denotes a method that returns a `Task` object.
|
||||
- `@crew`: Denotes the method that returns the `Crew` object.
|
||||
- `@before_kickoff`: (Optional) Marks a method to be executed before the crew starts.
|
||||
- `@after_kickoff`: (Optional) Marks a method to be executed after the crew finishes.
|
||||
<Warning>
|
||||
Only run JSON crew projects from sources you trust. `custom:<name>` tools and `{"python": "module.attribute"}` references execute local Python code when the crew loads.
|
||||
</Warning>
|
||||
|
||||
These decorators help in organizing your crew's structure and automatically collecting agents and tasks without manually listing them.
|
||||
### Classic Python/YAML Configuration
|
||||
|
||||
Classic projects created with `crewai create crew <name> --classic` use `crew.py`, `config/agents.yaml`, `config/tasks.yaml`, and the `@CrewBase`, `@agent`, `@task`, and `@crew` decorators. That pattern remains supported and is documented in [Using Annotations](/en/learn/using-annotations).
|
||||
|
||||
### Direct Code Definition (Alternative)
|
||||
|
||||
|
||||
@@ -226,6 +226,49 @@ After the Flow has run, you can access the final state to see the updates made b
|
||||
By ensuring that the final method's output is returned and providing access to the state, CrewAI Flows make it easy to integrate the results of your AI workflows into larger applications or systems,
|
||||
while also maintaining and accessing the state throughout the Flow's execution.
|
||||
|
||||
## Flow Usage Metrics
|
||||
|
||||
After a Flow execution completes, you can access the `usage_metrics` property to view aggregated token usage across **every LLM call** made during the run — including calls from every Crew the Flow orchestrated, calls inside Agent tools, and bare `LLM.call(...)` invocations from Flow methods. This is the SDK-side equivalent of the totals shown in the CrewAI Enterprise UI.
|
||||
|
||||
```python Code
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
|
||||
class UsageMetricsFlow(Flow):
|
||||
@start()
|
||||
def run_first_crew(self):
|
||||
self.state.first_result = FirstCrew().crew().kickoff()
|
||||
|
||||
@listen(run_first_crew)
|
||||
def call_llm_directly(self):
|
||||
# Bare LLM call — still counted by flow.usage_metrics
|
||||
llm = LLM(model="openai/gpt-4o-mini")
|
||||
self.state.summary = llm.call("Summarize the key takeaways.")
|
||||
|
||||
@listen(call_llm_directly)
|
||||
def run_second_crew(self):
|
||||
self.state.second_result = SecondCrew().crew().kickoff()
|
||||
|
||||
flow = UsageMetricsFlow()
|
||||
flow.kickoff()
|
||||
|
||||
print(flow.usage_metrics)
|
||||
# UsageMetrics(total_tokens=8579, prompt_tokens=6210, completion_tokens=2369,
|
||||
# cached_prompt_tokens=0, reasoning_tokens=0,
|
||||
# cache_creation_tokens=0, successful_requests=5)
|
||||
```
|
||||
|
||||
<Note>
|
||||
`flow.usage_metrics` is **not** the same as `flow.kickoff().token_usage`. The
|
||||
latter returns the `CrewOutput.token_usage` of the **last** `@listen` method
|
||||
that returned a `CrewOutput`, which means it only reflects the final Crew and
|
||||
ignores prior Crews and bare `LLM.call(...)` invocations entirely. Use
|
||||
`flow.usage_metrics` whenever you need the **full** token rollup for the Flow
|
||||
execution.
|
||||
</Note>
|
||||
|
||||
Each entry in the returned [`UsageMetrics`](https://github.com/crewAIInc/crewAI/blob/main/lib/crewai/src/crewai/types/usage_metrics.py) is the sum across all LLM calls made within a single `flow.kickoff()` invocation. Counters reset on the next `kickoff()` call (or on each iteration of `kickoff_for_each`), so successive runs don't double-count. The property is safe to read at any point after `kickoff()` completes; reading it during execution returns the partial total accumulated so far.
|
||||
|
||||
## Flow State Management
|
||||
|
||||
Managing state effectively is crucial for building reliable and maintainable AI workflows. CrewAI Flows provides robust mechanisms for both unstructured and structured state management,
|
||||
@@ -788,7 +831,7 @@ You can generate a new CrewAI project that includes all the scaffolding needed t
|
||||
crewai create flow name_of_flow
|
||||
```
|
||||
|
||||
This command will generate a new CrewAI project with the necessary folder structure. The generated project includes a prebuilt crew called `poem_crew` that is already working. You can use this crew as a template by copying, pasting, and editing it to create other crews.
|
||||
This command will generate a new CrewAI project with the necessary folder structure. The generated project includes a prebuilt crew called `poem_crew` that is already working. The starter embedded crew uses the classic Python/YAML layout; new standalone crews created with `crewai create crew` use the JSON-first layout.
|
||||
|
||||
### Folder Structure
|
||||
|
||||
@@ -812,13 +855,35 @@ After running the `crewai create flow name_of_flow` command, you will see a fold
|
||||
|
||||
### Building Your Crews
|
||||
|
||||
In the `crews` folder, you can define multiple crews. Each crew will have its own folder containing configuration files and the crew definition file. For example, the `poem_crew` folder contains:
|
||||
In the `crews` folder, you can define multiple crews. The generated `poem_crew` uses the classic embedded-crew structure:
|
||||
|
||||
- `config/agents.yaml`: Defines the agents for the crew.
|
||||
- `config/tasks.yaml`: Defines the tasks for the crew.
|
||||
- `poem_crew.py`: Contains the crew definition, including agents, tasks, and the crew itself.
|
||||
|
||||
You can copy, paste, and edit the `poem_crew` to create other crews.
|
||||
You can copy, paste, and edit the `poem_crew` to create other classic embedded crews.
|
||||
|
||||
For JSON-first embedded crews, use a folder with `crew.jsonc` and `agents/*.jsonc` instead:
|
||||
|
||||
```text
|
||||
crews/
|
||||
└── research_crew/
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
└── crew.jsonc
|
||||
```
|
||||
|
||||
Then load it from a Flow step:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.project import load_crew
|
||||
|
||||
crew, default_inputs = load_crew(
|
||||
Path(__file__).parent / "crews" / "research_crew" / "crew.jsonc"
|
||||
)
|
||||
result = crew.kickoff(inputs={**default_inputs, "topic": "AI Agents"})
|
||||
```
|
||||
|
||||
### Connecting Crews in `main.py`
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ crew = Crew(
|
||||
)
|
||||
```
|
||||
|
||||
When `memory=True`, the crew creates a default `Memory()` and passes the crew's `embedder` configuration through automatically. All agents in the crew share the crew's memory unless an agent has its own.
|
||||
When `memory=True`, the crew creates a default `Memory()` and passes the crew's `embedder` configuration through automatically. All agents in the crew share the crew's memory unless an agent has its own. Without a custom `embedder`, memory uses OpenAI `text-embedding-3-large` embeddings.
|
||||
|
||||
After each task, the crew automatically extracts discrete facts from the task output and stores them. Before each task, the agent recalls relevant context from memory and injects it into the task prompt.
|
||||
|
||||
@@ -515,7 +515,11 @@ memory = Memory(
|
||||
|
||||
## Embedder Configuration
|
||||
|
||||
Memory needs an embedding model to convert text into vectors for semantic search. You can configure this in three ways.
|
||||
Memory needs an embedding model to convert text into vectors for semantic search. By default, `Memory()` uses OpenAI `text-embedding-3-large` embeddings, which produce 3072-dimensional vectors. Set `OPENAI_API_KEY` for the default path, or configure a custom embedder in one of three ways.
|
||||
|
||||
<Warning>
|
||||
Existing local memory stores created with 1536-dimensional embeddings, such as `text-embedding-3-small` or `text-embedding-ada-002`, may not be compatible with the `text-embedding-3-large` default. This applies to both the OpenAI and Azure OpenAI providers — Azure's default embedding model also changed from `text-embedding-ada-002` to `text-embedding-3-large`. If local testing fails with an embedding dimension mismatch, reset memory with `crewai reset-memories -m`, delete the local memory storage directory, or explicitly configure the older embedder model until you migrate.
|
||||
</Warning>
|
||||
|
||||
### Passing to Memory Directly
|
||||
|
||||
@@ -523,7 +527,7 @@ Memory needs an embedding model to convert text into vectors for semantic search
|
||||
from crewai import Memory
|
||||
|
||||
# As a config dict
|
||||
memory = Memory(embedder={"provider": "openai", "config": {"model_name": "text-embedding-3-small"}})
|
||||
memory = Memory(embedder={"provider": "openai", "config": {"model_name": "text-embedding-3-large"}})
|
||||
|
||||
# As a pre-built callable
|
||||
from crewai.rag.embeddings.factory import build_embedder
|
||||
@@ -542,7 +546,7 @@ crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True,
|
||||
embedder={"provider": "openai", "config": {"model_name": "text-embedding-3-small"}},
|
||||
embedder={"provider": "openai", "config": {"model_name": "text-embedding-3-large"}},
|
||||
)
|
||||
```
|
||||
|
||||
@@ -554,7 +558,7 @@ crew = Crew(
|
||||
memory = Memory(embedder={
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model_name": "text-embedding-3-small",
|
||||
"model_name": "text-embedding-3-large",
|
||||
# "api_key": "sk-...", # or set OPENAI_API_KEY env var
|
||||
},
|
||||
})
|
||||
@@ -701,9 +705,9 @@ memory = Memory(embedder=my_embedder)
|
||||
|
||||
| Provider | Key | Typical Model | Notes |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| OpenAI | `openai` | `text-embedding-3-small` | Default. Set `OPENAI_API_KEY`. |
|
||||
| OpenAI | `openai` | `text-embedding-3-large` | Default. Set `OPENAI_API_KEY`. |
|
||||
| Ollama | `ollama` | `mxbai-embed-large` | Local, no API key needed. |
|
||||
| Azure OpenAI | `azure` | `text-embedding-ada-002` | Requires `deployment_id`. |
|
||||
| Azure OpenAI | `azure` | `text-embedding-3-large` | Default model. Requires `deployment_id`. |
|
||||
| Google AI | `google-generativeai` | `gemini-embedding-001` | Set `GOOGLE_API_KEY`. |
|
||||
| Google Vertex | `google-vertex` | `gemini-embedding-001` | Requires `project_id`. |
|
||||
| Cohere | `cohere` | `embed-english-v3.0` | Strong multilingual support. |
|
||||
@@ -836,6 +840,9 @@ class MemoryMonitor(BaseEventListener):
|
||||
**Background save errors in logs?**
|
||||
- Memory saves run in a background thread. Errors are emitted as `MemorySaveFailedEvent` but don't crash the agent. Check logs for the root cause (usually LLM or embedder connection issues).
|
||||
|
||||
**Embedding dimension mismatch?**
|
||||
- Existing local memory stores may have been created with a different embedding model. The default OpenAI memory embedder is now `text-embedding-3-large` (3072 dimensions), while older stores commonly used 1536-dimensional embeddings. For local testing, run `crewai reset-memories -m`, delete the local memory storage directory, or configure the previous embedder model explicitly.
|
||||
|
||||
**Concurrent write conflicts?**
|
||||
- LanceDB operations are serialized with a shared lock and retried automatically on conflict. This handles multiple `Memory` instances pointing at the same database (e.g. agent memory + crew memory). No action needed.
|
||||
|
||||
@@ -862,7 +869,7 @@ All configuration is passed as keyword arguments to `Memory(...)`. Every paramet
|
||||
| :--- | :--- | :--- |
|
||||
| `llm` | `"gpt-4o-mini"` | LLM for analysis (model name or `BaseLLM` instance). |
|
||||
| `storage` | `"lancedb"` | Storage backend (`"lancedb"`, a path string, or a `StorageBackend` instance). |
|
||||
| `embedder` | `None` (OpenAI default) | Embedder (config dict, callable, or `None` for default OpenAI). |
|
||||
| `embedder` | `None` (OpenAI `text-embedding-3-large`) | Embedder (config dict, callable, or `None` for default OpenAI). |
|
||||
| `recency_weight` | `0.3` | Weight for recency in composite score. |
|
||||
| `semantic_weight` | `0.5` | Weight for semantic similarity in composite score. |
|
||||
| `importance_weight` | `0.2` | Weight for importance in composite score. |
|
||||
|
||||
@@ -74,104 +74,54 @@ crew = Crew(
|
||||
|
||||
## Creating Tasks
|
||||
|
||||
There are two ways to create tasks in CrewAI: using **YAML configuration (recommended)** or defining them **directly in code**.
|
||||
There are two common ways to create tasks in CrewAI: using **JSONC project configuration (recommended for new crews)** or defining them **directly in code**.
|
||||
|
||||
### YAML Configuration (Recommended)
|
||||
### JSONC Configuration (Recommended)
|
||||
|
||||
Using YAML configuration provides a cleaner, more maintainable way to define tasks. We strongly recommend using this approach to define tasks in your CrewAI projects.
|
||||
New projects created with `crewai create crew <name>` define tasks in `crew.jsonc`. The `agents` array points to files in `agents/`, and the `tasks` array defines the ordered work the crew should run.
|
||||
|
||||
After creating your CrewAI project as outlined in the [Installation](/en/installation) section, navigate to the `src/latest_ai_development/config/tasks.yaml` file and modify the template to match your specific task requirements.
|
||||
After creating your CrewAI project as outlined in the [Installation](/en/installation) section, edit the generated `crew.jsonc`.
|
||||
|
||||
<Note>
|
||||
Variables in your YAML files (like `{topic}`) will be replaced with values from your inputs when running the crew:
|
||||
```python Code
|
||||
crew.kickoff(inputs={'topic': 'AI Agents'})
|
||||
```
|
||||
Use `{placeholder}` values in task `description`, `expected_output`, and `output_file`. Put defaults in the top-level `inputs` object; `crewai run` prompts for any missing values.
|
||||
</Note>
|
||||
|
||||
Here's an example of how to configure tasks using YAML:
|
||||
Here's an example `crew.jsonc` with two ordered tasks:
|
||||
|
||||
````yaml tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
Conduct a thorough research about {topic}
|
||||
Make sure you find any interesting and relevant information given
|
||||
the current year is 2025.
|
||||
expected_output: >
|
||||
A list with 10 bullet points of the most relevant information about {topic}
|
||||
agent: researcher
|
||||
|
||||
reporting_task:
|
||||
description: >
|
||||
Review the context you got and expand each topic into a full section for a report.
|
||||
Make sure the report is detailed and contains any and all relevant information.
|
||||
expected_output: >
|
||||
A fully fledge reports with the mains topics, each with a full section of information.
|
||||
Formatted as markdown without '```'
|
||||
agent: reporting_analyst
|
||||
markdown: true
|
||||
output_file: report.md
|
||||
````jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "reporting_analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research about {topic}. Include current and relevant information.",
|
||||
"expected_output": "A list of the most relevant information about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "reporting_task",
|
||||
"description": "Review the research and expand it into a detailed report.",
|
||||
"expected_output": "A polished markdown report without fenced code blocks.",
|
||||
"agent": "reporting_analyst",
|
||||
"context": ["research_task"],
|
||||
"markdown": true,
|
||||
"output_file": "report.md"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
To use this YAML configuration in your code, create a crew class that inherits from `CrewBase`:
|
||||
Each task must include `description` and `expected_output`. The `agent` value should match an agent name listed in `agents`. `context` is a list of prior task names; forward references are rejected so sequential context stays explicit.
|
||||
|
||||
```python crew.py
|
||||
# src/latest_ai_development/crew.py
|
||||
Task entries support any public `Task` field. Common fields include `name`, `agent`, `context`, `output_file`, `tools`, `human_input`, `async_execution`, `guardrail`, `guardrails`, `guardrail_max_retries`, `markdown`, `input_files`, `output_json`, `output_pydantic`, `response_model`, and `converter_cls`. Use `"type": "ConditionalTask"` with a `condition` field for conditional tasks.
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
### Classic YAML Configuration
|
||||
|
||||
@CrewBase
|
||||
class LatestAiDevelopmentCrew():
|
||||
"""LatestAiDevelopment crew"""
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['researcher'], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()]
|
||||
)
|
||||
|
||||
@agent
|
||||
def reporting_analyst(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['reporting_analyst'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['research_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def reporting_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['reporting_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=[
|
||||
self.researcher(),
|
||||
self.reporting_analyst()
|
||||
],
|
||||
tasks=[
|
||||
self.research_task(),
|
||||
self.reporting_task()
|
||||
],
|
||||
process=Process.sequential
|
||||
)
|
||||
```
|
||||
|
||||
<Note>
|
||||
The names you use in your YAML files (`agents.yaml` and `tasks.yaml`) should
|
||||
match the method names in your Python code.
|
||||
</Note>
|
||||
Classic projects created with `crewai create crew <name> --classic` use `config/tasks.yaml` and a `@CrewBase` class in `crew.py`. This remains supported for existing YAML projects or teams that prefer decorator-based Python wiring.
|
||||
|
||||
### Direct Code Definition (Alternative)
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ Before running this verification:
|
||||
|
||||
## Step 1 — Scaffold a Verification Crew
|
||||
|
||||
Create a new crew project. The CrewAI CLI scaffolds the structure:
|
||||
Create a classic crew project because this example wires a Python tool through `crew.py`:
|
||||
|
||||
```bash
|
||||
crewai create crew rotation_verifier --skip_provider
|
||||
crewai create crew rotation_verifier --classic --skip_provider
|
||||
cd rotation_verifier
|
||||
```
|
||||
|
||||
|
||||
@@ -374,17 +374,17 @@ git push
|
||||
|
||||
**Solution**: Verify your project matches the expected structure:
|
||||
|
||||
- **Both Crews and Flows**: Must have entry point at `src/project_name/main.py`
|
||||
- **Crews**: Use a `run()` function as entry point
|
||||
- **Flows**: Use a `kickoff()` function as entry point
|
||||
- **JSON-first Crews**: Keep `crew.jsonc` or `crew.json` and `agents/` at the project root
|
||||
- **Classic Crews**: Use `src/project_name/main.py` with a `run()` entry point
|
||||
- **Flows**: Use `src/project_name/main.py` with a `kickoff()` entry point
|
||||
|
||||
See [Prepare for Deployment](/en/enterprise/guides/prepare-for-deployment) for detailed structure diagrams.
|
||||
|
||||
#### Missing CrewBase Decorator
|
||||
#### Missing CrewBase Decorator in a Classic Crew
|
||||
|
||||
**Symptom**: "Crew not found", "Config not found", or agent/task configuration errors
|
||||
|
||||
**Solution**: Ensure **all** crew classes use the `@CrewBase` decorator:
|
||||
**Solution**: For classic Python/YAML crews, ensure all crew classes use the `@CrewBase` decorator. JSON-first crews do not need this decorator.
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
@@ -404,8 +404,8 @@ class YourCrew():
|
||||
```
|
||||
|
||||
<Info>
|
||||
This applies to standalone Crews AND crews embedded inside Flow projects.
|
||||
Every crew class needs the decorator.
|
||||
This applies to classic Python crew classes, including classic crews embedded inside Flow projects.
|
||||
JSON-first crews are validated from `crew.jsonc` and `agents/` instead.
|
||||
</Info>
|
||||
|
||||
#### Incorrect pyproject.toml Type
|
||||
@@ -442,8 +442,8 @@ type = "flow"
|
||||
**Solution**:
|
||||
1. Check the execution logs in the AMP dashboard (Traces tab)
|
||||
2. Verify all tools have required API keys configured
|
||||
3. Ensure agent configurations in `agents.yaml` are valid
|
||||
4. Check task configurations in `tasks.yaml` for syntax errors
|
||||
3. For JSON-first crews, validate `crew.jsonc` and the referenced files in `agents/`
|
||||
4. For classic crews, ensure `agents.yaml` and `tasks.yaml` are valid
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for assistance with deployment issues or questions
|
||||
|
||||
@@ -24,10 +24,9 @@ company-ai/
|
||||
`-- crews/
|
||||
|-- support_agent/
|
||||
| |-- pyproject.toml
|
||||
| `-- src/
|
||||
| `-- support_agent/
|
||||
| |-- main.py
|
||||
| `-- crew.py
|
||||
| |-- crew.jsonc
|
||||
| `-- agents/
|
||||
| `-- support_agent.jsonc
|
||||
`-- research_flow/
|
||||
|-- pyproject.toml
|
||||
`-- src/
|
||||
@@ -48,7 +47,7 @@ folder as the automation project root.
|
||||
|
||||
When a working directory is set, AMP uses that folder for:
|
||||
|
||||
- Project validation, including `pyproject.toml`, `src/`, and the Crew or Flow entry point
|
||||
- Project validation, including `pyproject.toml`, JSON crew files, and any classic Crew or Flow entry point
|
||||
- Dependency installation with `uv`
|
||||
- The running process working directory
|
||||
- The `CREW_ROOT_DIR` environment variable
|
||||
|
||||
@@ -24,7 +24,7 @@ Understanding which type you're deploying is essential because they have differe
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Projects" icon="users">
|
||||
Standalone AI agent teams with `crew.py` defining agents and tasks. Best for focused, collaborative tasks.
|
||||
Standalone AI agent teams. New crews are JSON-first with `crew.jsonc` and `agents/`; classic crews can still use `crew.py`.
|
||||
</Card>
|
||||
<Card title="Flow Projects" icon="diagram-project">
|
||||
Orchestrated workflows with embedded crews in a `crews/` folder. Best for complex, multi-stage processes.
|
||||
@@ -33,19 +33,19 @@ Understanding which type you're deploying is essential because they have differe
|
||||
|
||||
| Aspect | Crew | Flow |
|
||||
|--------|------|------|
|
||||
| **Project structure** | `src/project_name/` with `crew.py` | `src/project_name/` with `crews/` folder |
|
||||
| **Main logic location** | `src/project_name/crew.py` | `src/project_name/main.py` (Flow class) |
|
||||
| **Entry point function** | `run()` in `main.py` | `kickoff()` in `main.py` |
|
||||
| **Project structure** | Project root with `crew.jsonc` and `agents/` | `src/project_name/` with `crews/` folder |
|
||||
| **Main logic location** | `crew.jsonc` (classic: `src/project_name/crew.py`) | `src/project_name/main.py` (Flow class) |
|
||||
| **Entry point function** | Loaded from `crew.jsonc` (classic: `run()` in `main.py`) | `kickoff()` in `main.py` |
|
||||
| **pyproject.toml type** | `type = "crew"` | `type = "flow"` |
|
||||
| **CLI create command** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **Config location** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **Config location** | `crew.jsonc`, `agents/`, optional `tools/` | `src/project_name/crews/crew_name/config/` or embedded JSON crew folders |
|
||||
| **Can contain other crews** | No | Yes (in `crews/` folder) |
|
||||
|
||||
## Project Structure Reference
|
||||
|
||||
### Crew Project Structure
|
||||
|
||||
When you run `crewai create crew my_crew`, you get this structure:
|
||||
When you run `crewai create crew my_crew`, you get the JSON-first structure:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
@@ -54,24 +54,27 @@ my_crew/
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # REQUIRED for deployment
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # Entry point with run() function
|
||||
├── crew.py # Crew class with @CrewBase decorator
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # Agent definitions
|
||||
└── tasks.yaml # Task definitions
|
||||
├── crew.jsonc # Crew settings, tasks, process, inputs
|
||||
├── agents/
|
||||
│ └── researcher.jsonc # Agent definitions
|
||||
├── tools/ # Optional custom:<name> tools
|
||||
├── knowledge/
|
||||
└── skills/
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The nested `src/project_name/` structure is critical for Crews.
|
||||
Placing files at the wrong level will cause deployment failures.
|
||||
For JSON-first crews, keep `crew.jsonc`, `agents/`, `tools/`, `knowledge/`, and `skills/`
|
||||
at the project root. Placing them under `src/` will prevent `crewai run` and deployment
|
||||
validation from finding the crew definition.
|
||||
</Warning>
|
||||
|
||||
<Info>
|
||||
Classic projects created with `crewai create crew my_crew --classic` use the older
|
||||
`src/project_name/crew.py`, `src/project_name/config/agents.yaml`, and
|
||||
`src/project_name/config/tasks.yaml` layout. That layout remains supported for
|
||||
decorator-based Python crews.
|
||||
</Info>
|
||||
|
||||
### Flow Project Structure
|
||||
|
||||
When you run `crewai create flow my_flow`, you get this structure:
|
||||
@@ -100,9 +103,9 @@ my_flow/
|
||||
```
|
||||
|
||||
<Info>
|
||||
Both Crews and Flows use the `src/project_name/` structure.
|
||||
The key difference is that Flows have a `crews/` folder for embedded crews,
|
||||
while Crews have `crew.py` directly in the project folder.
|
||||
JSON-first standalone crews use project-root JSON files. Flows still use
|
||||
`src/project_name/` and can contain either classic embedded crews or embedded
|
||||
JSON crew folders loaded with `crewai.project.load_crew`.
|
||||
</Info>
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
@@ -154,60 +157,91 @@ git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. Validate CrewBase Decorator Usage
|
||||
### 3. Validate the Crew Definition
|
||||
|
||||
**Every crew class must use the `@CrewBase` decorator.** This applies to:
|
||||
<Tabs>
|
||||
<Tab title="JSON-first Crews">
|
||||
JSON-first crews must have a `crew.jsonc` or `crew.json` file at the project root.
|
||||
The `agents` array must reference files in `agents/`, and each task should reference
|
||||
a valid agent name.
|
||||
|
||||
- Standalone crew projects
|
||||
- Crews embedded inside Flow projects
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research {topic}.",
|
||||
"expected_output": "A concise report.",
|
||||
"agent": "researcher"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
Custom tools are referenced as `"custom:<name>"` and must be implemented in
|
||||
`tools/<name>.py` with a `BaseTool` subclass.
|
||||
</Tab>
|
||||
<Tab title="Classic Python/YAML Crews">
|
||||
Classic crews and Python crews embedded in Flows must use the `@CrewBase` decorator.
|
||||
|
||||
@CrewBase # This decorator is REQUIRED
|
||||
class MyCrew():
|
||||
"""My crew description"""
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
@CrewBase
|
||||
class MyCrew():
|
||||
"""My crew description"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
<Warning>
|
||||
If you forget the `@CrewBase` decorator, your deployment will fail with
|
||||
errors about missing agents or tasks configurations.
|
||||
</Warning>
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 4. Check Project Entry Points
|
||||
|
||||
Both Crews and Flows have their entry point in `src/project_name/main.py`:
|
||||
JSON-first standalone crews do not need a hand-written `src/project_name/main.py`; `crewai run`
|
||||
and deployment packaging load `crew.jsonc` directly. Classic crews and Flows use Python entry
|
||||
points:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="For Crews">
|
||||
<Tab title="JSON-first Crews">
|
||||
Run locally from the project root:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Classic Crews">
|
||||
The entry point uses a `run()` function:
|
||||
|
||||
```python
|
||||
@@ -278,16 +312,17 @@ grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
# 2. Verify uv.lock exists
|
||||
ls -la uv.lock || echo "ERROR: uv.lock missing! Run 'uv lock'"
|
||||
|
||||
# 3. Verify src/ structure exists
|
||||
ls -la src/*/main.py 2>/dev/null || echo "No main.py found in src/"
|
||||
# 3. For JSON-first crews, verify crew.jsonc and agents/ exist
|
||||
([ -f crew.jsonc ] || [ -f crew.json ]) || echo "No crew.jsonc or crew.json found"
|
||||
test -d agents || echo "No agents/ directory found"
|
||||
|
||||
# 4. For Crews - verify crew.py exists
|
||||
# 4. For classic Crews - verify crew.py exists
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "No crew.py (expected for Crews)"
|
||||
|
||||
# 5. For Flows - verify crews/ folder exists
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "No crews/ folder (expected for Flows)"
|
||||
|
||||
# 6. Check for CrewBase usage
|
||||
# 6. For classic Python crews - check for CrewBase usage
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
@@ -297,8 +332,9 @@ grep -r "@CrewBase" . --include="*.py"
|
||||
|---------|---------|-----|
|
||||
| Missing `uv.lock` | Build fails during dependency resolution | Run `uv lock` and commit |
|
||||
| Wrong `type` in pyproject.toml | Build succeeds but runtime fails | Change to correct type |
|
||||
| Missing `@CrewBase` decorator | "Config not found" errors | Add decorator to all crew classes |
|
||||
| Files at root instead of `src/` | Entry point not found | Move to `src/project_name/` |
|
||||
| Missing `crew.jsonc` or `agents/` in a JSON-first crew | Crew definition not found | Keep `crew.jsonc` and `agents/` at the project root |
|
||||
| Missing `@CrewBase` decorator in a classic crew | "Config not found" errors | Add decorator to all classic crew classes |
|
||||
| Classic files at root instead of `src/` | Entry point not found | Move classic Python files to `src/project_name/` |
|
||||
| Missing `run()` or `kickoff()` | Cannot start automation | Add correct entry function |
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -43,7 +43,7 @@ CrewAI is AI-native. This page brings together everything an AI coding agent nee
|
||||
|
||||
| Skill | When it runs |
|
||||
|-------|--------------|
|
||||
| `getting-started` | Scaffolding new projects, choosing between `LLM.call()` / `Agent` / `Crew` / `Flow`, wiring `crew.py` / `main.py` |
|
||||
| `getting-started` | Scaffolding new projects, choosing between `LLM.call()` / `Agent` / `Crew` / `Flow`, wiring `crew.jsonc` / `main.py` |
|
||||
| `design-agent` | Configuring agents — role, goal, backstory, tools, LLMs, memory, guardrails |
|
||||
| `design-task` | Writing task descriptions, dependencies, structured output (`output_pydantic`, `output_json`), human review |
|
||||
| `ask-docs` | Querying the live [CrewAI docs MCP server](https://docs.crewai.com/mcp) for up-to-date API details |
|
||||
|
||||
@@ -1,396 +1,162 @@
|
||||
---
|
||||
title: Build Your First Crew
|
||||
description: Step-by-step tutorial to create a collaborative AI team that works together to solve complex problems.
|
||||
description: Step-by-step tutorial to create a collaborative AI team with JSON-first crew configuration.
|
||||
icon: users-gear
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Unleashing the Power of Collaborative AI
|
||||
## Build a Research Crew
|
||||
|
||||
Imagine having a team of specialized AI agents working together seamlessly to solve complex problems, each contributing their unique skills to achieve a common goal. This is the power of CrewAI - a framework that enables you to create collaborative AI systems that can accomplish tasks far beyond what a single AI could achieve alone.
|
||||
|
||||
In this guide, we'll walk through creating a research crew that will help us research and analyze a topic, then create a comprehensive report. This practical example demonstrates how AI agents can collaborate to accomplish complex tasks, but it's just the beginning of what's possible with CrewAI.
|
||||
|
||||
### What You'll Build and Learn
|
||||
|
||||
By the end of this guide, you'll have:
|
||||
|
||||
1. **Created a specialized AI research team** with distinct roles and responsibilities
|
||||
2. **Orchestrated collaboration** between multiple AI agents
|
||||
3. **Automated a complex workflow** that involves gathering information, analysis, and report generation
|
||||
4. **Built foundational skills** that you can apply to more ambitious projects
|
||||
|
||||
While we're building a simple research crew in this guide, the same patterns and techniques can be applied to create much more sophisticated teams for tasks like:
|
||||
|
||||
- Multi-stage content creation with specialized writers, editors, and fact-checkers
|
||||
- Complex customer service systems with tiered support agents
|
||||
- Autonomous business analysts that gather data, create visualizations, and generate insights
|
||||
- Product development teams that ideate, design, and plan implementation
|
||||
|
||||
Let's get started building your first crew!
|
||||
In this guide, you will create a two-agent research crew that gathers information about a topic and writes a markdown report. New crew projects are JSON-first: agents are defined in `agents/*.jsonc`, tasks and crew settings are defined in `crew.jsonc`, and `crewai run` loads the JSON definition directly.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before starting, make sure you have:
|
||||
|
||||
1. Installed CrewAI following the [installation guide](/en/installation)
|
||||
2. Set up your LLM API key in your environment, following the [LLM setup
|
||||
guide](/en/concepts/llms#setting-up-your-llm)
|
||||
3. Basic understanding of Python
|
||||
2. Set up your LLM API key following the [LLM setup guide](/en/concepts/llms#setting-up-your-llm)
|
||||
3. A [Serper.dev](https://serper.dev/) API key if you want the researcher to use web search
|
||||
|
||||
## Step 1: Create a New CrewAI Project
|
||||
|
||||
First, let's create a new CrewAI project using the CLI. This command will set up a complete project structure with all the necessary files, allowing you to focus on defining your agents and their tasks rather than setting up boilerplate code.
|
||||
## Step 1: Create a New Crew
|
||||
|
||||
```bash
|
||||
crewai create crew research_crew
|
||||
cd research_crew
|
||||
```
|
||||
|
||||
This will generate a project with the basic structure needed for your crew. The CLI automatically creates:
|
||||
The CLI creates a JSON-first project:
|
||||
|
||||
- A project directory with the necessary files
|
||||
- Configuration files for agents and tasks
|
||||
- A basic crew implementation
|
||||
- A main script to run the crew
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/crews.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
|
||||
## Step 2: Explore the Project Structure
|
||||
|
||||
Let's take a moment to understand the project structure created by the CLI. CrewAI follows best practices for Python projects, making it easy to maintain and extend your code as your crews become more complex.
|
||||
|
||||
```
|
||||
```text
|
||||
research_crew/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── research_crew/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
This structure follows best practices for Python projects and makes it easy to organize your code. The separation of configuration files (in YAML) from implementation code (in Python) makes it easy to modify your crew's behavior without changing the underlying code.
|
||||
<Tip>
|
||||
Need the older `crew.py`, `config/agents.yaml`, and `config/tasks.yaml` layout? Create it with `crewai create crew research_crew --classic`.
|
||||
</Tip>
|
||||
|
||||
## Step 3: Configure Your Agents
|
||||
## Step 2: Define Your Agents
|
||||
|
||||
Now comes the fun part - defining your AI agents! In CrewAI, agents are specialized entities with specific roles, goals, and backstories that shape their behavior. Think of them as characters in a play, each with their own personality and purpose.
|
||||
Replace the generated `agents/researcher.jsonc` file and add `agents/analyst.jsonc`. The file names are the names you reference from `crew.jsonc`.
|
||||
|
||||
For our research crew, we'll create two agents:
|
||||
1. A **researcher** who excels at finding and organizing information
|
||||
2. An **analyst** who can interpret research findings and create insightful reports
|
||||
|
||||
Let's modify the `agents.yaml` file to define these specialized agents. Be sure
|
||||
to set `llm` to the provider you are using.
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
Senior Research Specialist for {topic}
|
||||
goal: >
|
||||
Find comprehensive and accurate information about {topic}
|
||||
with a focus on recent developments and key insights
|
||||
backstory: >
|
||||
You are an experienced research specialist with a talent for
|
||||
finding relevant information from various sources. You excel at
|
||||
organizing information in a clear and structured manner, making
|
||||
complex topics accessible to others.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
analyst:
|
||||
role: >
|
||||
Data Analyst and Report Writer for {topic}
|
||||
goal: >
|
||||
Analyze research findings and create a comprehensive, well-structured
|
||||
report that presents insights in a clear and engaging way
|
||||
backstory: >
|
||||
You are a skilled analyst with a background in data interpretation
|
||||
and technical writing. You have a talent for identifying patterns
|
||||
and extracting meaningful insights from research data, then
|
||||
communicating those insights effectively through well-crafted reports.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "Senior Research Specialist for {topic}",
|
||||
"goal": "Find comprehensive and accurate information about {topic}, with a focus on recent developments and key insights.",
|
||||
"backstory": "You are an experienced research specialist who organizes complex information into clear, useful notes.",
|
||||
// Replace with your model, for example "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice how each agent has a distinct role, goal, and backstory. These elements aren't just descriptive - they actively shape how the agent approaches its tasks. By crafting these carefully, you can create agents with specialized skills and perspectives that complement each other.
|
||||
|
||||
## Step 4: Define Your Tasks
|
||||
|
||||
With our agents defined, we now need to give them specific tasks to perform. Tasks in CrewAI represent the concrete work that agents will perform, with detailed instructions and expected outputs.
|
||||
|
||||
For our research crew, we'll define two main tasks:
|
||||
1. A **research task** for gathering comprehensive information
|
||||
2. An **analysis task** for creating an insightful report
|
||||
|
||||
Let's modify the `tasks.yaml` file:
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
Conduct thorough research on {topic}. Focus on:
|
||||
1. Key concepts and definitions
|
||||
2. Historical development and recent trends
|
||||
3. Major challenges and opportunities
|
||||
4. Notable applications or case studies
|
||||
5. Future outlook and potential developments
|
||||
|
||||
Make sure to organize your findings in a structured format with clear sections.
|
||||
expected_output: >
|
||||
A comprehensive research document with well-organized sections covering
|
||||
all the requested aspects of {topic}. Include specific facts, figures,
|
||||
and examples where relevant.
|
||||
agent: researcher
|
||||
|
||||
analysis_task:
|
||||
description: >
|
||||
Analyze the research findings and create a comprehensive report on {topic}.
|
||||
Your report should:
|
||||
1. Begin with an executive summary
|
||||
2. Include all key information from the research
|
||||
3. Provide insightful analysis of trends and patterns
|
||||
4. Offer recommendations or future considerations
|
||||
5. Be formatted in a professional, easy-to-read style with clear headings
|
||||
expected_output: >
|
||||
A polished, professional report on {topic} that presents the research
|
||||
findings with added analysis and insights. The report should be well-structured
|
||||
with an executive summary, main sections, and conclusion.
|
||||
agent: analyst
|
||||
context:
|
||||
- research_task
|
||||
output_file: output/report.md
|
||||
```jsonc agents/analyst.jsonc
|
||||
{
|
||||
"role": "Report Analyst for {topic}",
|
||||
"goal": "Turn research findings into a clear, well-structured report.",
|
||||
"backstory": "You are a careful analyst with strong technical writing skills and a talent for extracting useful insights.",
|
||||
// Replace with your model, for example "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note the `context` field in the analysis task - this is a powerful feature that allows the analyst to access the output of the research task. This creates a workflow where information flows naturally between agents, just as it would in a human team.
|
||||
Replace `provider/model-id` with the model you use, for example `openai/gpt-4o`, `anthropic/claude-sonnet-4-6`, or `gemini/gemini-2.0-flash-001`.
|
||||
|
||||
## Step 5: Configure Your Crew
|
||||
## Step 3: Define Tasks and Crew Settings
|
||||
|
||||
Now it's time to bring everything together by configuring our crew. The crew is the container that orchestrates how agents work together to complete tasks.
|
||||
Replace `crew.jsonc` with:
|
||||
|
||||
Let's modify the `crew.py` file:
|
||||
|
||||
```python
|
||||
# src/research_crew/crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew():
|
||||
"""Research crew for comprehensive topic analysis and reporting"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['researcher'], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()]
|
||||
)
|
||||
|
||||
@agent
|
||||
def analyst(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['analyst'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['research_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def analysis_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['analysis_task'], # type: ignore[index]
|
||||
output_file='output/report.md'
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the research crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
In this code, we're:
|
||||
1. Creating the researcher agent and equipping it with the SerperDevTool to search the web
|
||||
2. Creating the analyst agent
|
||||
3. Setting up the research and analysis tasks
|
||||
4. Configuring the crew to run tasks sequentially (the analyst will wait for the researcher to finish)
|
||||
|
||||
This is where the magic happens - with just a few lines of code, we've defined a collaborative AI system where specialized agents work together in a coordinated process.
|
||||
|
||||
## Step 6: Set Up Your Main Script
|
||||
|
||||
Now, let's set up the main script that will run our crew. This is where we provide the specific topic we want our crew to research.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# src/research_crew/main.py
|
||||
import os
|
||||
from research_crew.crew import ResearchCrew
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs('output', exist_ok=True)
|
||||
|
||||
def run():
|
||||
"""
|
||||
Run the research crew.
|
||||
"""
|
||||
inputs = {
|
||||
'topic': 'Artificial Intelligence in Healthcare'
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research on {topic}. Focus on key concepts, recent developments, major challenges, notable applications, and future outlook.",
|
||||
"expected_output": "A comprehensive research document with organized sections, specific facts, and useful examples about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis_task",
|
||||
"description": "Analyze the research findings and create a polished report on {topic}. Include an executive summary, key insights, trend analysis, and recommendations.",
|
||||
"expected_output": "A professional markdown report with clear headings, a concise summary, main findings, and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research_task"],
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
|
||||
# Create and run the crew
|
||||
result = ResearchCrew().crew().kickoff(inputs=inputs)
|
||||
|
||||
# Print the result
|
||||
print("\n\n=== FINAL REPORT ===\n\n")
|
||||
print(result.raw)
|
||||
|
||||
print("\n\nReport has been saved to output/report.md")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "Artificial Intelligence in Healthcare"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This script prepares the environment, specifies our research topic, and kicks off the crew's work. The power of CrewAI is evident in how simple this code is - all the complexity of managing multiple AI agents is handled by the framework.
|
||||
`context` points to prior task names, so the analyst receives the research task output. The `inputs` object provides default values for `{topic}`. If you remove a default, `crewai run` prompts for it.
|
||||
|
||||
## Step 7: Set Up Your Environment Variables
|
||||
## Step 4: Set Environment Variables
|
||||
|
||||
Create a `.env` file in your project root with your API keys:
|
||||
Open `.env` and add the keys your model and tools need:
|
||||
|
||||
```sh
|
||||
SERPER_API_KEY=your_serper_api_key
|
||||
# Add your provider's API key here too.
|
||||
# Add your model provider API key here too.
|
||||
```
|
||||
|
||||
See the [LLM Setup guide](/en/concepts/llms#setting-up-your-llm) for details on configuring your provider of choice. You can get a Serper API key from [Serper.dev](https://serper.dev/).
|
||||
See the [LLM setup guide](/en/concepts/llms#setting-up-your-llm) for provider-specific keys.
|
||||
|
||||
## Step 8: Install Dependencies
|
||||
|
||||
Install the required dependencies using the CrewAI CLI:
|
||||
## Step 5: Install and Run
|
||||
|
||||
```bash
|
||||
crewai install
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. Read the dependencies from your project configuration
|
||||
2. Create a virtual environment if needed
|
||||
3. Install all required packages
|
||||
|
||||
## Step 9: Run Your Crew
|
||||
|
||||
Now for the exciting moment - it's time to run your crew and see AI collaboration in action!
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
When you run this command, you'll see your crew spring to life. The researcher will gather information about the specified topic, and the analyst will then create a comprehensive report based on that research. You'll see the agents' thought processes, actions, and outputs in real-time as they work together to complete their tasks.
|
||||
`crewai run` detects `crew.jsonc`, loads the agents from `agents/`, prompts for missing placeholders, and runs the crew. When the run finishes, open `output/report.md`.
|
||||
|
||||
## Step 10: Review the Output
|
||||
## How It Works
|
||||
|
||||
Once the crew completes its work, you'll find the final report in the `output/report.md` file. The report will include:
|
||||
1. `crew.jsonc` defines the crew, task order, process, memory, and runtime inputs.
|
||||
2. `agents/researcher.jsonc` and `agents/analyst.jsonc` define the agents.
|
||||
3. The researcher runs first.
|
||||
4. The analyst runs second with `context: ["research_task"]`.
|
||||
5. The final task writes `output/report.md`.
|
||||
|
||||
1. An executive summary
|
||||
2. Detailed information about the topic
|
||||
3. Analysis and insights
|
||||
4. Recommendations or future considerations
|
||||
## Extending Your Crew
|
||||
|
||||
Take a moment to appreciate what you've accomplished - you've created a system where multiple AI agents collaborated on a complex task, each contributing their specialized skills to produce a result that's greater than what any single agent could achieve alone.
|
||||
You can add:
|
||||
|
||||
## Exploring Other CLI Commands
|
||||
- More agents by creating new `agents/<name>.jsonc` files and listing them in `crew.jsonc`
|
||||
- More tasks by appending objects to the `tasks` array
|
||||
- Built-in tools by adding tool class names such as `"FileReadTool"` or `"SerperDevTool"`
|
||||
- Custom tools with `"custom:<name>"`, which loads `tools/<name>.py`
|
||||
- Hierarchical execution with `"process": "hierarchical"` and a `manager_llm` or `manager_agent`
|
||||
|
||||
CrewAI offers several other useful CLI commands for working with crews:
|
||||
|
||||
```bash
|
||||
# View all available commands
|
||||
crewai --help
|
||||
|
||||
# Run the crew
|
||||
crewai run
|
||||
|
||||
# Test the crew
|
||||
crewai test
|
||||
|
||||
# Reset crew memories
|
||||
crewai reset-memories
|
||||
|
||||
# Replay from a specific task
|
||||
crewai replay -t <task_id>
|
||||
```
|
||||
|
||||
## The Art of the Possible: Beyond Your First Crew
|
||||
|
||||
What you've built in this guide is just the beginning. The skills and patterns you've learned can be applied to create increasingly sophisticated AI systems. Here are some ways you could extend this basic research crew:
|
||||
|
||||
### Expanding Your Crew
|
||||
|
||||
You could add more specialized agents to your crew:
|
||||
- A **fact-checker** to verify research findings
|
||||
- A **data visualizer** to create charts and graphs
|
||||
- A **domain expert** with specialized knowledge in a particular area
|
||||
- A **critic** to identify weaknesses in the analysis
|
||||
|
||||
### Adding Tools and Capabilities
|
||||
|
||||
You could enhance your agents with additional tools:
|
||||
- Web browsing tools for real-time research
|
||||
- CSV/database tools for data analysis
|
||||
- Code execution tools for data processing
|
||||
- API connections to external services
|
||||
|
||||
### Creating More Complex Workflows
|
||||
|
||||
You could implement more sophisticated processes:
|
||||
- Hierarchical processes where manager agents delegate to worker agents
|
||||
- Iterative processes with feedback loops for refinement
|
||||
- Parallel processes where multiple agents work simultaneously
|
||||
- Dynamic processes that adapt based on intermediate results
|
||||
|
||||
### Applying to Different Domains
|
||||
|
||||
The same patterns can be applied to create crews for:
|
||||
- **Content creation**: Writers, editors, fact-checkers, and designers working together
|
||||
- **Customer service**: Triage agents, specialists, and quality control working together
|
||||
- **Product development**: Researchers, designers, and planners collaborating
|
||||
- **Data analysis**: Data collectors, analysts, and visualization specialists
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you've built your first crew, you can:
|
||||
|
||||
1. Experiment with different agent configurations and personalities
|
||||
2. Try more complex task structures and workflows
|
||||
3. Implement custom tools to give your agents new capabilities
|
||||
4. Apply your crew to different topics or problem domains
|
||||
5. Explore [CrewAI Flows](/en/guides/flows/first-flow) for more advanced workflows with procedural programming
|
||||
<Warning>
|
||||
Only run JSON crew projects from sources you trust. `custom:<name>` tools and `{"python": "module.attribute"}` references execute local Python code when the crew loads.
|
||||
</Warning>
|
||||
|
||||
<Check>
|
||||
Congratulations! You've successfully built your first CrewAI crew that can research and analyze any topic you provide. This foundational experience has equipped you with the skills to create increasingly sophisticated AI systems that can tackle complex, multi-stage problems through collaborative intelligence.
|
||||
You now have a working JSON-first crew that researches a topic and writes a report.
|
||||
</Check>
|
||||
|
||||
@@ -65,7 +65,7 @@ This will generate a project with the basic structure needed for your flow.
|
||||
|
||||
## Step 2: Understanding the Project Structure
|
||||
|
||||
The generated project has the following structure. Take a moment to familiarize yourself with it, as understanding this structure will help you create more complex flows in the future.
|
||||
The generated project has the following structure. The starter embedded crew uses the classic Python/YAML layout, and in Step 4 we will replace the content crew with a JSONC crew.
|
||||
|
||||
```
|
||||
guide_creator_flow/
|
||||
@@ -103,157 +103,82 @@ This command automatically creates the necessary directories and template files
|
||||
|
||||
## Step 4: Configure the Content Writer Crew
|
||||
|
||||
Now, let's modify the generated files for the content writer crew. We'll set up two specialized agents - a writer and a reviewer - that will collaborate to create high-quality content for our guide.
|
||||
Now, let's configure the content writer crew with JSONC. We'll set up two specialized agents - a writer and a reviewer - that collaborate to create high-quality content for our guide.
|
||||
|
||||
1. First, update the agents configuration file to define our content creation team:
|
||||
1. Create `src/guide_creator_flow/crews/content_crew/agents/content_writer.jsonc`:
|
||||
|
||||
Remember to set `llm` to the provider you are using.
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/agents.yaml
|
||||
content_writer:
|
||||
role: >
|
||||
Educational Content Writer
|
||||
goal: >
|
||||
Create engaging, informative content that thoroughly explains the assigned topic
|
||||
and provides valuable insights to the reader
|
||||
backstory: >
|
||||
You are a talented educational writer with expertise in creating clear, engaging
|
||||
content. You have a gift for explaining complex concepts in accessible language
|
||||
and organizing information in a way that helps readers build their understanding.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
content_reviewer:
|
||||
role: >
|
||||
Educational Content Reviewer and Editor
|
||||
goal: >
|
||||
Ensure content is accurate, comprehensive, well-structured, and maintains
|
||||
consistency with previously written sections
|
||||
backstory: >
|
||||
You are a meticulous editor with years of experience reviewing educational
|
||||
content. You have an eye for detail, clarity, and coherence. You excel at
|
||||
improving content while maintaining the original author's voice and ensuring
|
||||
consistent quality across multiple sections.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Writer",
|
||||
"goal": "Create engaging, informative content that thoroughly explains the assigned topic and provides valuable insights to the reader.",
|
||||
"backstory": "You are a talented educational writer who explains complex concepts in accessible language and organizes information clearly.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These agent definitions establish the specialized roles and perspectives that will shape how our AI agents approach content creation. Notice how each agent has a distinct purpose and expertise.
|
||||
2. Create `src/guide_creator_flow/crews/content_crew/agents/content_reviewer.jsonc`:
|
||||
|
||||
2. Next, update the tasks configuration file to define the specific writing and reviewing tasks:
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/tasks.yaml
|
||||
write_section_task:
|
||||
description: >
|
||||
Write a comprehensive section on the topic: "{section_title}"
|
||||
|
||||
Section description: {section_description}
|
||||
Target audience: {audience_level} level learners
|
||||
|
||||
Your content should:
|
||||
1. Begin with a brief introduction to the section topic
|
||||
2. Explain all key concepts clearly with examples
|
||||
3. Include practical applications or exercises where appropriate
|
||||
4. End with a summary of key points
|
||||
5. Be approximately 500-800 words in length
|
||||
|
||||
Format your content in Markdown with appropriate headings, lists, and emphasis.
|
||||
|
||||
Previously written sections:
|
||||
{previous_sections}
|
||||
|
||||
Make sure your content maintains consistency with previously written sections
|
||||
and builds upon concepts that have already been explained.
|
||||
expected_output: >
|
||||
A well-structured, comprehensive section in Markdown format that thoroughly
|
||||
explains the topic and is appropriate for the target audience.
|
||||
agent: content_writer
|
||||
|
||||
review_section_task:
|
||||
description: >
|
||||
Review and improve the following section on "{section_title}":
|
||||
|
||||
{draft_content}
|
||||
|
||||
Target audience: {audience_level} level learners
|
||||
|
||||
Previously written sections:
|
||||
{previous_sections}
|
||||
|
||||
Your review should:
|
||||
1. Fix any grammatical or spelling errors
|
||||
2. Improve clarity and readability
|
||||
3. Ensure content is comprehensive and accurate
|
||||
4. Verify consistency with previously written sections
|
||||
5. Enhance the structure and flow
|
||||
6. Add any missing key information
|
||||
|
||||
Provide the improved version of the section in Markdown format.
|
||||
expected_output: >
|
||||
An improved, polished version of the section that maintains the original
|
||||
structure but enhances clarity, accuracy, and consistency.
|
||||
agent: content_reviewer
|
||||
context:
|
||||
- write_section_task
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Reviewer and Editor",
|
||||
"goal": "Ensure content is accurate, comprehensive, well-structured, and consistent with previously written sections.",
|
||||
"backstory": "You are a meticulous editor with an eye for detail, clarity, and coherence.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These task definitions provide detailed instructions to our agents, ensuring they produce content that meets our quality standards. Note how the `context` parameter in the review task creates a workflow where the reviewer has access to the writer's output.
|
||||
Replace `provider/model-id` with the model you use, for example `openai/gpt-4o`, `gemini/gemini-2.0-flash-001`, or `anthropic/claude-sonnet-4-6`.
|
||||
|
||||
3. Now, update the crew implementation file to define how our agents and tasks work together:
|
||||
3. Create `src/guide_creator_flow/crews/content_crew/crew.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "Content Crew",
|
||||
"agents": ["content_writer", "content_reviewer"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "write_section_task",
|
||||
"description": "Write a comprehensive section on the topic: \"{section_title}\".\n\nSection description: {section_description}\nTarget audience: {audience_level} level learners\n\nYour content should begin with a brief introduction, explain key concepts clearly with examples, include practical applications where appropriate, end with a summary, and be approximately 500-800 words.\n\nPreviously written sections:\n{previous_sections}",
|
||||
"expected_output": "A well-structured, comprehensive section in Markdown format that thoroughly explains the topic and is appropriate for the target audience.",
|
||||
"agent": "content_writer",
|
||||
"markdown": true
|
||||
},
|
||||
{
|
||||
"name": "review_section_task",
|
||||
"description": "Review and improve this section on \"{section_title}\":\n\n{draft_content}\n\nTarget audience: {audience_level} level learners\nPreviously written sections:\n{previous_sections}\n\nFix errors, improve clarity, verify consistency, enhance structure, and add missing key information.",
|
||||
"expected_output": "An improved, polished version of the section that maintains the original structure but enhances clarity, accuracy, and consistency.",
|
||||
"agent": "content_reviewer",
|
||||
"context": ["write_section_task"],
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
The `context` field lets the reviewer use the writer's output.
|
||||
|
||||
4. Replace `src/guide_creator_flow/crews/content_crew/content_crew.py` with a small loader:
|
||||
|
||||
```python
|
||||
# src/guide_creator_flow/crews/content_crew/content_crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
@CrewBase
|
||||
class ContentCrew():
|
||||
"""Content writing crew"""
|
||||
from crewai.project import load_crew
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def content_writer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_writer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@agent
|
||||
def content_reviewer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_reviewer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def write_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['write_section_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def review_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['review_section_task'], # type: ignore[index]
|
||||
context=[self.write_section_task()]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the content writing crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
This crew definition establishes the relationship between our agents and tasks, setting up a sequential process where the content writer creates a draft and then the reviewer improves it. While this crew can function independently, in our flow it will be orchestrated as part of a larger system.
|
||||
This loader turns `crew.jsonc` into a `Crew` at runtime. While this crew can function independently, in our flow it will be orchestrated as part of a larger system.
|
||||
|
||||
## Step 5: Create the Flow
|
||||
|
||||
@@ -275,7 +200,7 @@ from typing import List, Dict
|
||||
from pydantic import BaseModel, Field
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from guide_creator_flow.crews.content_crew.content_crew import ContentCrew
|
||||
from guide_creator_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
# Define our models for structured data
|
||||
class Section(BaseModel):
|
||||
@@ -380,7 +305,7 @@ class GuideCreatorFlow(Flow[GuideCreatorState]):
|
||||
previous_sections_text = "No previous sections written yet."
|
||||
|
||||
# Run the content crew for this section
|
||||
result = ContentCrew().crew().kickoff(inputs={
|
||||
result = kickoff_content_crew(inputs={
|
||||
"section_title": section.title,
|
||||
"section_description": section.description,
|
||||
"audience_level": self.state.audience_level,
|
||||
@@ -600,7 +525,7 @@ This provides a type-safe way to track and transform data throughout your flow.
|
||||
Flows can seamlessly integrate with crews for complex collaborative tasks:
|
||||
|
||||
```python
|
||||
result = ContentCrew().crew().kickoff(inputs={
|
||||
result = kickoff_content_crew(inputs={
|
||||
"section_title": section.title,
|
||||
# ...
|
||||
})
|
||||
|
||||
@@ -141,7 +141,7 @@ crew = Crew(
|
||||
process=Process.sequential, # or Process.hierarchical
|
||||
memory=True,
|
||||
cache=True,
|
||||
embedder={"provider": "openai", "config": {"model": "text-embedding-3-small"}},
|
||||
embedder={"provider": "openai", "config": {"model": "text-embedding-3-large"}},
|
||||
)
|
||||
```
|
||||
|
||||
@@ -173,7 +173,7 @@ write = Task(
|
||||
|
||||
### Memory & embedder config {#memory-embedder-config}
|
||||
|
||||
If `memory=True` and you're not using the default OpenAI embeddings, you must pass an `embedder`:
|
||||
If `memory=True` and you're not using the default OpenAI `text-embedding-3-large` embeddings, you must pass an `embedder`:
|
||||
|
||||
```python
|
||||
crew = Crew(
|
||||
@@ -187,4 +187,4 @@ crew = Crew(
|
||||
)
|
||||
```
|
||||
|
||||
Set the relevant provider credentials (`OPENAI_API_KEY`, `OLLAMA_HOST`, etc.) in your `.env` file. Memory storage paths are project-local by default — delete the project's memory directory if you change embedders, since dimensions don't mix.
|
||||
Set the relevant provider credentials (`OPENAI_API_KEY`, `OLLAMA_HOST`, etc.) in your `.env` file. Memory storage paths are project-local by default. Existing local memory stores created with 1536-dimensional embeddings may not be compatible with the default OpenAI `text-embedding-3-large` embedder, which uses 3072 dimensions. If you hit a dimension mismatch, delete the project's memory directory, run `crewai reset-memories -m`, or explicitly configure the older embedder model until you migrate.
|
||||
|
||||
@@ -116,7 +116,7 @@ If you haven't installed `uv` yet, follow **step 1** to quickly get it set up on
|
||||
|
||||
# Creating a CrewAI Project
|
||||
|
||||
We recommend using the `YAML` template scaffolding for a structured approach to defining agents and tasks. Here's how to get started:
|
||||
`crewai create crew` now creates a JSON-first crew project. Agents live in `agents/*.jsonc`, tasks and crew-level settings live in `crew.jsonc`, and `crewai run` loads that JSON definition directly.
|
||||
|
||||
<Steps>
|
||||
<Step title="Generate Project Scaffolding">
|
||||
@@ -129,21 +129,20 @@ We recommend using the `YAML` template scaffolding for a structured approach to
|
||||
```
|
||||
my_project/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── my_project/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
- If you need the older Python/YAML scaffold with `crew.py`, `config/agents.yaml`, and `config/tasks.yaml`, run:
|
||||
```shell
|
||||
crewai create crew <your_project_name> --classic
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -152,15 +151,15 @@ We recommend using the `YAML` template scaffolding for a structured approach to
|
||||
- Your project will contain these essential files:
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `agents.yaml` | Define your AI agents and their roles |
|
||||
| `tasks.yaml` | Set up agent tasks and workflows |
|
||||
| `crew.jsonc` | Configure the crew, task order, process, and input defaults |
|
||||
| `agents/*.jsonc` | Define each agent's role, goal, backstory, LLM, tools, and behavior |
|
||||
| `.env` | Store API keys and environment variables |
|
||||
| `main.py` | Project entry point and execution flow |
|
||||
| `crew.py` | Crew orchestration and coordination |
|
||||
| `tools/` | Directory for custom agent tools |
|
||||
| `knowledge/` | Directory for knowledge base |
|
||||
| `tools/` | Optional Python files for `custom:<name>` tools |
|
||||
| `knowledge/` | Optional knowledge files for agents |
|
||||
| `skills/` | Optional skill files applied to the crew |
|
||||
|
||||
- Start by editing `agents.yaml` and `tasks.yaml` to define your crew's behavior.
|
||||
- Start by editing `crew.jsonc` and the files in `agents/` to define your crew's behavior.
|
||||
- Use `{placeholder}` values in agent and task text, then set defaults in `crew.jsonc` under `inputs`. When you run `crewai run`, the CLI prompts for any missing values.
|
||||
- Keep sensitive information like API keys in `.env`.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
---
|
||||
title: "Using Annotations in crew.py"
|
||||
description: "Learn how to use annotations to properly structure agents, tasks, and components in CrewAI"
|
||||
description: "Learn how to use classic Python annotations to structure agents, tasks, and components in CrewAI"
|
||||
icon: "at"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
This guide explains how to use annotations to properly reference **agents**, **tasks**, and other components in the `crew.py` file.
|
||||
This guide explains how to use annotations to properly reference **agents**, **tasks**, and other components in a classic `crew.py` file.
|
||||
|
||||
<Note>
|
||||
New crew projects created with `crewai create crew <name>` are JSON-first and use `crew.jsonc` plus `agents/*.jsonc`. Use this annotations guide when you are working in a classic project created with `crewai create crew <name> --classic`, migrating an existing Python/YAML project, or need decorator-based Python control.
|
||||
</Note>
|
||||
|
||||
## Introduction
|
||||
|
||||
Annotations in the CrewAI framework are used to decorate classes and methods, providing metadata and functionality to various components of your crew. These annotations help in organizing and structuring your code, making it more readable and maintainable.
|
||||
Annotations in the CrewAI framework are used to decorate classes and methods, providing metadata and functionality to various components of your crew. In classic Python/YAML projects, these annotations help organize the code that loads `config/agents.yaml`, `config/tasks.yaml`, and returns the `Crew` object.
|
||||
|
||||
## Available Annotations
|
||||
|
||||
@@ -113,9 +117,9 @@ def crew(self) -> Crew:
|
||||
|
||||
The `@crew` annotation is used to decorate the method that creates and returns the `Crew` object. This method assembles all the components (agents and tasks) into a functional crew.
|
||||
|
||||
## YAML Configuration
|
||||
## Classic YAML Configuration
|
||||
|
||||
The agent configurations are typically stored in a YAML file. Here's an example of how the `agents.yaml` file might look for the researcher agent:
|
||||
In classic projects, agent configurations are typically stored in a YAML file. Here's an example of how the `agents.yaml` file might look for the researcher agent:
|
||||
|
||||
```yaml
|
||||
researcher:
|
||||
@@ -146,6 +150,6 @@ Note how the `llm` and `tools` in the YAML file correspond to the methods decora
|
||||
- **Consistent Naming**: Use clear and consistent naming conventions for your methods. For example, agent methods could be named after their roles (e.g., researcher, reporting_analyst).
|
||||
- **Environment Variables**: Use environment variables for sensitive information like API keys.
|
||||
- **Flexibility**: Design your crew to be flexible by allowing easy addition or removal of agents and tasks.
|
||||
- **YAML-Code Correspondence**: Ensure that the names and structures in your YAML files correspond correctly to the decorated methods in your Python code.
|
||||
- **YAML-Code Correspondence**: In classic projects, ensure that the names and structures in your YAML files correspond correctly to the decorated methods in your Python code.
|
||||
|
||||
By following these guidelines and properly using annotations, you can create well-structured and maintainable crews using the CrewAI framework.
|
||||
By following these guidelines and properly using annotations, you can maintain classic Python/YAML crews cleanly. For new crews, prefer the JSON-first structure covered in [Crews](/en/concepts/crews).
|
||||
|
||||
@@ -39,85 +39,60 @@ If you have not installed CrewAI yet, follow the [installation guide](/en/instal
|
||||
This creates a Flow app under `src/latest_ai_flow/`, including a starter crew under `crews/content_crew/` that you will replace with a minimal **single-agent** research crew in the next steps.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure one agent in `agents.yaml`">
|
||||
Replace the contents of `src/latest_ai_flow/crews/content_crew/config/agents.yaml` with a single researcher. Variables like `{topic}` are filled from `crew.kickoff(inputs=...)`.
|
||||
<Step title="Configure one agent in JSONC">
|
||||
Create `src/latest_ai_flow/crews/content_crew/agents/researcher.jsonc` (create the `agents/` directory if needed). Variables like `{topic}` are filled from `crew.kickoff(inputs=...)`.
|
||||
|
||||
```yaml agents.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
{topic} Senior Data Researcher
|
||||
goal: >
|
||||
Uncover cutting-edge developments in {topic}
|
||||
backstory: >
|
||||
You're a seasoned researcher with a knack for uncovering the latest
|
||||
developments in {topic}. You find the most relevant information and
|
||||
present it clearly.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} Senior Data Researcher",
|
||||
"goal": "Uncover cutting-edge developments in {topic}",
|
||||
"backstory": "You're a seasoned researcher who finds relevant information and presents it clearly.",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure one task in `tasks.yaml`">
|
||||
```yaml tasks.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
Conduct thorough research about {topic}. Use web search to find current,
|
||||
credible information. The current year is 2026.
|
||||
expected_output: >
|
||||
A markdown report with clear sections: key trends, notable tools or companies,
|
||||
and implications. Aim for 800–1200 words. No fenced code blocks around the whole document.
|
||||
agent: researcher
|
||||
output_file: output/report.md
|
||||
<Step title="Configure the crew in `crew.jsonc`">
|
||||
Create `src/latest_ai_flow/crews/content_crew/crew.jsonc`:
|
||||
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research about {topic}. Use web search to find current, credible information. The current year is 2026.",
|
||||
"expected_output": "A markdown report with clear sections: key trends, notable tools or companies, and implications. Aim for 800-1200 words. No fenced code blocks around the whole document.",
|
||||
"agent": "researcher",
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Wire the crew class (`content_crew.py`)">
|
||||
Point the generated crew at your YAML and attach `SerperDevTool` to the researcher.
|
||||
<Step title="Load the JSON crew (`content_crew.py`)">
|
||||
Replace the generated `content_crew.py` with a small loader that turns `crew.jsonc` into a `Crew`.
|
||||
|
||||
```python content_crew.py
|
||||
# src/latest_ai_flow/crews/content_crew/content_crew.py
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.project import load_crew
|
||||
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew:
|
||||
"""Single-agent research crew used inside the Flow."""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
agents_config = "config/agents.yaml"
|
||||
tasks_config = "config/tasks.yaml"
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["researcher"], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()],
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config["research_task"], # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -131,7 +106,7 @@ If you have not installed CrewAI yet, follow the [installation guide](/en/instal
|
||||
|
||||
from crewai.flow import Flow, listen, start
|
||||
|
||||
from latest_ai_flow.crews.content_crew.content_crew import ResearchCrew
|
||||
from latest_ai_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
|
||||
class ResearchFlowState(BaseModel):
|
||||
@@ -150,7 +125,7 @@ If you have not installed CrewAI yet, follow the [installation guide](/en/instal
|
||||
|
||||
@listen(prepare_topic)
|
||||
def run_research(self):
|
||||
result = ResearchCrew().crew().kickoff(inputs={"topic": self.state.topic})
|
||||
result = kickoff_content_crew(inputs={"topic": self.state.topic})
|
||||
self.state.report = result.raw
|
||||
print("Research crew finished.")
|
||||
|
||||
@@ -172,7 +147,7 @@ If you have not installed CrewAI yet, follow the [installation guide](/en/instal
|
||||
```
|
||||
|
||||
<Tip>
|
||||
If your package name differs from `latest_ai_flow`, change the import of `ResearchCrew` to match your project’s module path.
|
||||
If your package name differs from `latest_ai_flow`, change the `kickoff_content_crew` import to match your project’s module path.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
@@ -223,7 +198,7 @@ If you have not installed CrewAI yet, follow the [installation guide](/en/instal
|
||||
## How this run fits together
|
||||
|
||||
1. **Flow** — `LatestAiFlow` runs `prepare_topic` first, then `run_research`, then `summarize`. State (`topic`, `report`) lives on the Flow.
|
||||
2. **Crew** — `ResearchCrew` runs one task with one agent: the researcher uses **Serper** to search the web, then writes the structured report.
|
||||
2. **Crew** — `kickoff_content_crew` loads `crew.jsonc` and runs one task with one agent: the researcher uses **Serper** to search the web, then writes the structured report.
|
||||
3. **Artifact** — The task’s `output_file` writes the report under `output/report.md`.
|
||||
|
||||
To go deeper on Flow patterns (routing, persistence, human-in-the-loop), see [Build your first Flow](/en/guides/flows/first-flow) and [Flows](/en/concepts/flows). For crews without a Flow, see [Crews](/en/concepts/crews). For a single `Agent` and `kickoff()` without tasks, see [Agents](/en/concepts/agents#direct-agent-interaction-with-kickoff).
|
||||
@@ -234,7 +209,10 @@ You now have an end-to-end Flow with an agent crew and a saved report — a soli
|
||||
|
||||
### Naming consistency
|
||||
|
||||
YAML keys (`researcher`, `research_task`) must match the method names on your `@CrewBase` class. See [Crews](/en/concepts/crews) for the full decorator pattern.
|
||||
The names in `crew.jsonc` must match the files and task references you use:
|
||||
|
||||
- `agents: ["researcher"]` loads `agents/researcher.jsonc`
|
||||
- `context: ["research_task"]` references a prior task named `research_task`
|
||||
|
||||
## Deploying
|
||||
|
||||
|
||||
@@ -66,13 +66,39 @@ CrewAI AOP에는 코드를 작성하지 않고도 에이전트 생성 및 구성
|
||||
|
||||
## 에이전트 생성
|
||||
|
||||
CrewAI에서 에이전트를 생성하는 방법에는 **YAML 구성(권장)**을 사용하는 방법과 **코드에서 직접 정의**하는 두 가지가 있습니다.
|
||||
CrewAI에서 에이전트를 생성하는 일반적인 방법은 **JSONC 프로젝트 구성(새 crew 권장)** 또는 **코드에서 직접 정의**입니다.
|
||||
|
||||
### YAML 구성 (권장)
|
||||
### JSONC 구성 (권장)
|
||||
|
||||
YAML 구성을 사용하면 에이전트를 보다 깔끔하고 유지 관리하기 쉽도록 정의할 수 있습니다. CrewAI 프로젝트에서 이 방식을 사용하는 것을 강력히 권장합니다.
|
||||
`crewai create crew <name>`으로 만든 새 프로젝트는 JSON-first 구성을 사용합니다. 각 에이전트는 `agents/<agent_name>.jsonc`에 정의하고, `crew.jsonc`에서 crew에 포함할 에이전트를 나열합니다.
|
||||
|
||||
[설치](/ko/installation) 섹션에 설명된 대로 CrewAI 프로젝트를 생성한 후, `src/latest_ai_development/config/agents.yaml` 파일로 이동하여 템플릿을 여러분의 요구 사항에 맞게 수정하세요.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} Senior Data Researcher",
|
||||
"goal": "Uncover cutting-edge developments in {topic}",
|
||||
"backstory": "You find the most relevant information and present it clearly.",
|
||||
"llm": "openai/gpt-4o",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`role`, `goal`, `backstory`에 `{placeholder}`를 사용할 수 있습니다. 기본값은 `crew.jsonc`의 `inputs`에 넣고, 빠진 값은 `crewai run`이 실행 시 물어봅니다. `verbose`, `allow_delegation`, `max_iter`, `memory`, `cache`, `planning` 같은 동작 필드는 최상위 또는 `settings` 안에 둘 수 있습니다.
|
||||
|
||||
<Note>
|
||||
JSONC는 주석과 trailing comma를 지원합니다. `agents/<name>.jsonc`와 `agents/<name>.json`이 모두 있으면 CrewAI는 JSONC 파일을 사용합니다.
|
||||
</Note>
|
||||
|
||||
### 클래식 YAML 구성
|
||||
|
||||
`crewai create crew <name> --classic`으로 만든 클래식 프로젝트는 `config/agents.yaml`과 `crew.py`의 `@CrewBase` 클래스를 사용합니다.
|
||||
|
||||
YAML 구성은 기존 Python/YAML 프로젝트와 `@CrewBase` 클래스에서 에이전트를 정의하려는 팀을 위해 계속 지원됩니다.
|
||||
|
||||
클래식 프로젝트를 만든 후, `src/latest_ai_development/config/agents.yaml` 파일로 이동하여 템플릿을 여러분의 요구 사항에 맞게 수정하세요.
|
||||
|
||||
<Note>
|
||||
YAML 파일의 변수(예: `{topic}`)는 crew를 실행할 때 입력값에서 가져온 값으로 대체됩니다:
|
||||
|
||||
@@ -52,6 +52,8 @@ crewai create crew my_new_crew
|
||||
crewai create flow my_new_flow
|
||||
```
|
||||
|
||||
기본적으로 `crewai create crew`는 `crew.jsonc`와 `agents/*.jsonc`가 있는 JSON-first 프로젝트를 만듭니다. `crew.py`, `config/agents.yaml`, `config/tasks.yaml`을 사용하는 기존 Python/YAML 스캐폴드가 필요할 때만 `crewai create crew my_new_crew --classic`을 사용하세요.
|
||||
|
||||
### 2. 버전
|
||||
|
||||
설치된 CrewAI의 버전을 표시합니다.
|
||||
@@ -183,7 +185,20 @@ crewai chat
|
||||
이 명령어들은 CrewAI 프로젝트의 루트 디렉터리에서 실행해야 합니다.
|
||||
</Note>
|
||||
<Note>
|
||||
중요: 이 명령어를 사용하려면 `crew.py` 파일에서 `chat_llm` 속성을 설정해야 합니다.
|
||||
중요: 이 명령어를 사용하려면 crew 정의에 `chat_llm` 속성을 설정해야 합니다.
|
||||
|
||||
JSON-first crew에서는 `crew.jsonc`에 추가합니다:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "My Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [],
|
||||
"chat_llm": "openai/gpt-4o"
|
||||
}
|
||||
```
|
||||
|
||||
클래식 Python/YAML crew에서는 `crew.py`에 설정합니다:
|
||||
|
||||
```python
|
||||
@crew
|
||||
@@ -313,7 +328,7 @@ CLI를 사용하여 [CrewAI AMP](http://app.crewai.com)에 crew를 배포하는
|
||||
|
||||
### 11. API 키
|
||||
|
||||
`crewai create crew` 명령어를 실행하면, CLI에서 선택할 수 있는 LLM 제공업체 목록이 표시되고, 그 다음으로 선택한 제공업체에 대한 모델 선택이 이어집니다.
|
||||
`crewai create crew` 명령어를 실행하면, CLI에서 선택할 수 있는 LLM 제공업체 목록이 표시되고, 그 다음으로 선택한 제공업체에 대한 모델 선택이 이어집니다. 선택한 모델은 생성된 `.env`에 저장되며 각 에이전트 JSONC 파일은 자체 `llm`을 설정할 수 있습니다.
|
||||
|
||||
LLM 제공업체와 모델을 선택하면, API 키를 입력하라는 메시지가 표시됩니다.
|
||||
|
||||
|
||||
@@ -41,13 +41,54 @@ crewAI에서 crew는 일련의 작업을 달성하기 위해 함께 협력하는
|
||||
|
||||
## 크루 생성하기
|
||||
|
||||
CrewAI에서 크루를 생성하는 방법은 두 가지가 있습니다: **YAML 구성(권장)**을 사용하는 방법과 **코드에서 직접 정의**하는 방법입니다.
|
||||
CrewAI에서 크루를 생성하는 주요 방법은 **JSONC 구성(새 crew 권장)**을 사용하는 방법과 클래식 프로젝트나 고급 사용 사례에서 **코드로 직접 정의**하는 방법입니다.
|
||||
|
||||
### YAML 구성 (권장)
|
||||
### JSONC 구성 (권장)
|
||||
|
||||
YAML 구성을 사용하면 crew를 정의할 때 더 깔끔하고 유지 관리하기 쉬운 방법을 제공하며, CrewAI 프로젝트에서 agent 및 task를 정의하는 방식과 일관성을 유지할 수 있습니다.
|
||||
`crewai create crew <name>`으로 만든 새 프로젝트는 crew 수준 설정과 태스크를 `crew.jsonc`에 두고, 각 에이전트를 `agents/`의 별도 파일에 둡니다. `crewai run`은 `crew.jsonc` 또는 `crew.json`을 감지해 에이전트를 로드하고, 빠진 placeholder 값을 물은 뒤 crew를 시작합니다.
|
||||
|
||||
[설치](/ko/installation) 섹션에 설명된 대로 CrewAI 프로젝트를 생성한 후, `CrewBase`를 상속받는 클래스에서 데코레이터를 이용해 agent, task, 그리고 crew 자체를 정의할 수 있습니다.
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Market Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research",
|
||||
"description": "Research {topic} and collect the most relevant facts.",
|
||||
"expected_output": "Structured research notes about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis",
|
||||
"description": "Analyze the research and write a concise report.",
|
||||
"expected_output": "A markdown report with findings and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research"],
|
||||
"output_file": "output/report.md"
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`agents`의 각 문자열은 먼저 `agents/<name>.jsonc`, 그 다음 `agents/<name>.json`으로 해석됩니다. 계층형 crew는 `"process": "hierarchical"`와 `manager_llm` 또는 `manager_agent`를 사용하세요.
|
||||
|
||||
<Warning>
|
||||
신뢰하는 출처의 JSON crew 프로젝트만 실행하세요. `custom:<name>` 도구와 `{"python": "module.attribute"}` 참조는 crew 로드 시 로컬 Python 코드를 실행합니다.
|
||||
</Warning>
|
||||
|
||||
### 클래식 YAML 구성
|
||||
|
||||
`crewai create crew <name> --classic`으로 만든 클래식 프로젝트는 `crew.py`, `config/agents.yaml`, `config/tasks.yaml`, `@CrewBase`, `@agent`, `@task`, `@crew` 데코레이터를 사용합니다.
|
||||
|
||||
이 방식은 기존 Python/YAML 프로젝트와 Python 데코레이터 제어가 필요한 팀을 위해 계속 지원됩니다.
|
||||
|
||||
클래식 프로젝트를 만든 후, `CrewBase`를 상속받는 클래스에서 데코레이터를 이용해 agent, task, 그리고 crew 자체를 정의할 수 있습니다.
|
||||
|
||||
#### 데코레이터가 적용된 예시 Crew 클래스
|
||||
|
||||
@@ -416,4 +457,4 @@ crewai log-tasks-outputs
|
||||
crewai replay -t <task_id>
|
||||
```
|
||||
|
||||
이 명령어들을 사용하면 이전에 실행된 작업의 컨텍스트를 유지하면서 최신 kickoff 작업부터 다시 실행할 수 있습니다.
|
||||
이 명령어들을 사용하면 이전에 실행된 작업의 컨텍스트를 유지하면서 최신 kickoff 작업부터 다시 실행할 수 있습니다.
|
||||
|
||||
@@ -221,6 +221,48 @@ Flow가 실행된 후, 이러한 메소드들에 의해 수행된 업데이트
|
||||
최종 메소드의 출력이 반환되고 상태에 접근할 수 있도록 함으로써, CrewAI Flow는 AI 워크플로우의 결과를 더 큰 애플리케이션이나 시스템에 쉽게 통합할 수 있게 하며,
|
||||
Flow 실행 과정 전반에 걸쳐 상태를 유지하고 접근하면서도 이를 용이하게 만듭니다.
|
||||
|
||||
## 플로우 사용 메트릭
|
||||
|
||||
Flow 실행이 완료된 후, `usage_metrics` 속성에 접근하여 실행 동안 발생한 **모든 LLM 호출**의 토큰 사용량 집계를 확인할 수 있습니다. 여기에는 Flow가 오케스트레이션한 모든 Crew의 호출, Agent의 도구 내부에서 발생한 호출, 그리고 Flow 메서드에서 직접 호출한 `LLM.call(...)`이 모두 포함됩니다. 이는 CrewAI Enterprise UI에 표시되는 총량과 동등한 SDK 측 값입니다.
|
||||
|
||||
```python Code
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
|
||||
class UsageMetricsFlow(Flow):
|
||||
@start()
|
||||
def run_first_crew(self):
|
||||
self.state.first_result = FirstCrew().crew().kickoff()
|
||||
|
||||
@listen(run_first_crew)
|
||||
def call_llm_directly(self):
|
||||
# 직접 LLM 호출 — flow.usage_metrics에서도 집계됩니다
|
||||
llm = LLM(model="openai/gpt-4o-mini")
|
||||
self.state.summary = llm.call("핵심 내용을 요약해 주세요.")
|
||||
|
||||
@listen(call_llm_directly)
|
||||
def run_second_crew(self):
|
||||
self.state.second_result = SecondCrew().crew().kickoff()
|
||||
|
||||
flow = UsageMetricsFlow()
|
||||
flow.kickoff()
|
||||
|
||||
print(flow.usage_metrics)
|
||||
# UsageMetrics(total_tokens=8579, prompt_tokens=6210, completion_tokens=2369,
|
||||
# cached_prompt_tokens=0, reasoning_tokens=0,
|
||||
# cache_creation_tokens=0, successful_requests=5)
|
||||
```
|
||||
|
||||
<Note>
|
||||
`flow.usage_metrics`는 `flow.kickoff().token_usage`와 **동일하지 않습니다**.
|
||||
후자는 `CrewOutput`을 반환한 **마지막** `@listen` 메서드의
|
||||
`CrewOutput.token_usage`만 반환하므로, 이전에 실행된 Crew들과 Flow 메서드에서
|
||||
직접 호출한 `LLM.call(...)`은 전혀 포함되지 않습니다. Flow 실행에 대한
|
||||
**전체** 토큰 집계가 필요할 때는 항상 `flow.usage_metrics`를 사용하십시오.
|
||||
</Note>
|
||||
|
||||
반환되는 [`UsageMetrics`](https://github.com/crewAIInc/crewAI/blob/main/lib/crewai/src/crewai/types/usage_metrics.py)의 각 항목은 단일 `flow.kickoff()` 실행 동안 발생한 모든 LLM 호출의 합계입니다. 다음 `kickoff()` 호출(및 `kickoff_for_each`의 각 반복)에서 카운터가 초기화되므로 연속 실행이 이중으로 집계되지 않습니다. 이 속성은 `kickoff()` 완료 후 언제든지 안전하게 읽을 수 있으며, 실행 중에 읽으면 그 시점까지 누적된 부분 합계를 반환합니다.
|
||||
|
||||
## 플로우 상태 관리
|
||||
|
||||
상태를 효과적으로 관리하는 것은 신뢰할 수 있고 유지 보수가 용이한 AI 워크플로를 구축하는 데 매우 중요합니다. CrewAI 플로우는 비정형 및 정형 상태 관리를 위한 강력한 메커니즘을 제공하여, 개발자가 자신의 애플리케이션에 가장 적합한 접근 방식을 선택할 수 있도록 합니다.
|
||||
@@ -781,7 +823,7 @@ CrewAI에서 여러 crews로 flow를 생성하는 것은 간단합니다.
|
||||
crewai create flow name_of_flow
|
||||
```
|
||||
|
||||
이 명령어는 필요한 폴더 구조를 갖춘 새 CrewAI 프로젝트를 생성합니다. 생성된 프로젝트에는 이미 동작 중인 미리 구축된 crew인 `poem_crew`가 포함되어 있습니다. 이 crew를 템플릿으로 사용하여 복사, 붙여넣기, 수정함으로써 다른 crew를 만들 수 있습니다.
|
||||
이 명령어는 필요한 폴더 구조를 갖춘 새 CrewAI 프로젝트를 생성합니다. 생성된 프로젝트에는 이미 동작 중인 미리 구축된 crew인 `poem_crew`가 포함되어 있습니다. 시작용 embedded crew는 클래식 Python/YAML 레이아웃을 사용하며, `crewai create crew`로 만든 새 독립 실행형 crew는 JSON-first 레이아웃을 사용합니다.
|
||||
|
||||
### 폴더 구조
|
||||
|
||||
@@ -811,7 +853,29 @@ crewai create flow name_of_flow
|
||||
- `config/tasks.yaml`: 크루의 task를 정의합니다.
|
||||
- `poem_crew.py`: agent, task, 그리고 크루 자체를 포함한 crew 정의가 들어 있습니다.
|
||||
|
||||
`poem_crew`를 복사, 붙여넣기, 그리고 편집하여 다른 크루를 생성할 수 있습니다.
|
||||
`poem_crew`를 복사, 붙여넣기, 그리고 편집하여 다른 클래식 embedded crew를 생성할 수 있습니다.
|
||||
|
||||
JSON-first embedded crew는 `crew.jsonc`와 `agents/*.jsonc`가 있는 폴더를 사용하세요:
|
||||
|
||||
```text
|
||||
crews/
|
||||
└── research_crew/
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
└── crew.jsonc
|
||||
```
|
||||
|
||||
그런 다음 Flow 단계에서 로드합니다:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.project import load_crew
|
||||
|
||||
crew, default_inputs = load_crew(
|
||||
Path(__file__).parent / "crews" / "research_crew" / "crew.jsonc"
|
||||
)
|
||||
result = crew.kickoff(inputs={**default_inputs, "topic": "AI Agents"})
|
||||
```
|
||||
|
||||
### `main.py`에서 Crew 연결하기
|
||||
|
||||
|
||||
@@ -64,13 +64,48 @@ crew = Crew(
|
||||
|
||||
## 작업 생성하기
|
||||
|
||||
CrewAI에서 작업을 생성하는 방법에는 **YAML 구성(권장)** 을 사용하는 방법과 **코드에서 직접 정의하는 방법** 두 가지가 있습니다.
|
||||
CrewAI에서 작업을 생성하는 일반적인 방법은 **JSONC 프로젝트 구성(새 crew 권장)** 또는 **코드에서 직접 정의**입니다.
|
||||
|
||||
### YAML 구성 (권장)
|
||||
### JSONC 구성 (권장)
|
||||
|
||||
YAML 구성을 사용하면 작업을 정의할 때 더 깔끔하고 유지 관리가 용이한 방법을 제공합니다. CrewAI 프로젝트에서 작업을 정의할 때 이 방식을 사용하는 것을 강력히 권장합니다.
|
||||
`crewai create crew <name>`으로 만든 새 프로젝트는 `crew.jsonc`에 태스크를 정의합니다.
|
||||
|
||||
[설치](/ko/installation) 섹션에 따라 CrewAI 프로젝트를 생성한 후, `src/latest_ai_development/config/tasks.yaml` 파일로 이동하여 템플릿을 귀하의 특정 작업 요구 사항에 맞게 수정하세요.
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "reporting_analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research about {topic}.",
|
||||
"expected_output": "A list of the most relevant information about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "reporting_task",
|
||||
"description": "Review the research and expand it into a detailed report.",
|
||||
"expected_output": "A polished markdown report.",
|
||||
"agent": "reporting_analyst",
|
||||
"context": ["research_task"],
|
||||
"markdown": true,
|
||||
"output_file": "report.md"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
각 태스크에는 `description`과 `expected_output`이 필요합니다. `agent` 값은 `agents`에 나열된 에이전트 이름과 일치해야 합니다. `context`는 이전 태스크 이름만 참조할 수 있으며, 이후 태스크 참조는 거부됩니다.
|
||||
|
||||
### 클래식 YAML 구성
|
||||
|
||||
`crewai create crew <name> --classic`으로 만든 클래식 프로젝트는 `config/tasks.yaml`과 `crew.py`의 `@CrewBase` 클래스를 사용합니다.
|
||||
|
||||
YAML 구성은 기존 Python/YAML 프로젝트와 `@CrewBase` 클래스에서 태스크를 정의하려는 팀을 위해 계속 지원됩니다.
|
||||
|
||||
클래식 프로젝트를 만든 후, `src/latest_ai_development/config/tasks.yaml` 파일로 이동하여 템플릿을 귀하의 특정 작업 요구 사항에 맞게 수정하세요.
|
||||
|
||||
<Note>
|
||||
YAML 파일 내 변수(예: `{topic}`)는 크루를 실행할 때 입력값에서 가져온 값으로 대체됩니다:
|
||||
|
||||
@@ -26,10 +26,10 @@ icon: "arrows-rotate"
|
||||
|
||||
## 1단계 — 검증 Crew 스캐폴딩
|
||||
|
||||
새 Crew 프로젝트를 만듭니다. CrewAI CLI가 구조를 스캐폴딩합니다:
|
||||
이 예제는 `crew.py`를 통해 Python 도구를 연결하므로 클래식 crew 프로젝트를 만듭니다:
|
||||
|
||||
```bash
|
||||
crewai create crew rotation_verifier --skip_provider
|
||||
crewai create crew rotation_verifier --classic --skip_provider
|
||||
cd rotation_verifier
|
||||
```
|
||||
|
||||
|
||||
@@ -373,17 +373,17 @@ git push
|
||||
|
||||
**해결책**: 프로젝트가 예상 구조와 일치하는지 확인합니다:
|
||||
|
||||
- **Crews와 Flows 모두**: 진입점이 `src/project_name/main.py`에 있어야 합니다
|
||||
- **Crews**: 진입점으로 `run()` 함수 사용
|
||||
- **Flows**: 진입점으로 `kickoff()` 함수 사용
|
||||
- **JSON-first Crews**: `crew.jsonc` 또는 `crew.json`과 `agents/`를 프로젝트 루트에 둡니다
|
||||
- **클래식 Crews**: `src/project_name/main.py`에 `run()` 진입점을 둡니다
|
||||
- **Flows**: `src/project_name/main.py`에 `kickoff()` 진입점을 둡니다
|
||||
|
||||
자세한 구조 다이어그램은 [배포 준비하기](/ko/enterprise/guides/prepare-for-deployment)를 참조하세요.
|
||||
|
||||
#### CrewBase 데코레이터 누락
|
||||
#### 클래식 Crew의 CrewBase 데코레이터 누락
|
||||
|
||||
**증상**: "Crew not found", "Config not found" 또는 agent/task 구성 오류
|
||||
|
||||
**해결책**: **모든** crew 클래스가 `@CrewBase` 데코레이터를 사용하는지 확인합니다:
|
||||
**해결책**: 클래식 Python/YAML crew에서는 모든 crew 클래스가 `@CrewBase` 데코레이터를 사용하는지 확인합니다. JSON-first crew에는 이 데코레이터가 필요하지 않습니다.
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
@@ -403,8 +403,8 @@ class YourCrew():
|
||||
```
|
||||
|
||||
<Info>
|
||||
이것은 독립 실행형 Crews와 Flow 프로젝트 내에 포함된 crews 모두에 적용됩니다.
|
||||
모든 crew 클래스에 데코레이터가 필요합니다.
|
||||
이것은 Flow 프로젝트에 포함된 클래식 crew를 포함하여 클래식 Python crew 클래스에 적용됩니다.
|
||||
JSON-first crew는 `crew.jsonc`와 `agents/`를 기준으로 검증됩니다.
|
||||
</Info>
|
||||
|
||||
#### 잘못된 pyproject.toml 타입
|
||||
@@ -441,8 +441,8 @@ type = "flow"
|
||||
**해결책**:
|
||||
1. AMP 대시보드에서 실행 로그를 확인합니다 (Traces 탭)
|
||||
2. 모든 도구에 필요한 API 키가 구성되어 있는지 확인합니다
|
||||
3. `agents.yaml`의 agent 구성이 유효한지 확인합니다
|
||||
4. `tasks.yaml`의 task 구성에 구문 오류가 없는지 확인합니다
|
||||
3. JSON-first crew의 경우 `crew.jsonc`와 `agents/`에서 참조한 파일을 검증합니다
|
||||
4. 클래식 crew의 경우 `agents.yaml`과 `tasks.yaml`이 유효한지 확인합니다
|
||||
|
||||
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
|
||||
배포 문제 또는 AMP 플랫폼에 대한 문의 사항이 있으시면 지원팀에 연락해 주세요.
|
||||
|
||||
@@ -24,10 +24,9 @@ company-ai/
|
||||
`-- crews/
|
||||
|-- support_agent/
|
||||
| |-- pyproject.toml
|
||||
| `-- src/
|
||||
| `-- support_agent/
|
||||
| |-- main.py
|
||||
| `-- crew.py
|
||||
| |-- crew.jsonc
|
||||
| `-- agents/
|
||||
| `-- support_agent.jsonc
|
||||
`-- research_flow/
|
||||
|-- pyproject.toml
|
||||
`-- src/
|
||||
@@ -48,7 +47,7 @@ AMP는 여전히 전체 저장소를 가져오거나 업로드하지만, 선택
|
||||
|
||||
작업 디렉터리가 설정되면 AMP는 해당 폴더를 다음 용도로 사용합니다:
|
||||
|
||||
- `pyproject.toml`, `src/`, Crew 또는 Flow 진입점을 포함한 프로젝트 검증
|
||||
- `pyproject.toml`, JSON crew 파일, 클래식 Crew 또는 Flow 진입점을 포함한 프로젝트 검증
|
||||
- `uv`를 사용한 종속성 설치
|
||||
- 실행 중인 프로세스의 작업 디렉터리
|
||||
- `CREW_ROOT_DIR` 환경 변수
|
||||
|
||||
@@ -24,7 +24,7 @@ CrewAI AMP에서 **자동화(automations)**는 배포 가능한 Agentic AI 프
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew 프로젝트" icon="users">
|
||||
에이전트와 작업을 정의하는 `crew.py`가 있는 독립 실행형 AI 에이전트 팀. 집중적이고 협업적인 작업에 적합합니다.
|
||||
독립 실행형 AI 에이전트 팀입니다. 새 crew는 `crew.jsonc`와 `agents/`를 사용하는 JSON-first 구조이며, 클래식 crew는 계속 `crew.py`를 사용할 수 있습니다.
|
||||
</Card>
|
||||
<Card title="Flow 프로젝트" icon="diagram-project">
|
||||
`crews/` 폴더에 포함된 crew가 있는 오케스트레이션된 워크플로우. 복잡한 다단계 프로세스에 적합합니다.
|
||||
@@ -33,19 +33,19 @@ CrewAI AMP에서 **자동화(automations)**는 배포 가능한 Agentic AI 프
|
||||
|
||||
| 측면 | Crew | Flow |
|
||||
|------|------|------|
|
||||
| **프로젝트 구조** | `crew.py`가 있는 `src/project_name/` | `crews/` 폴더가 있는 `src/project_name/` |
|
||||
| **메인 로직 위치** | `src/project_name/crew.py` | `src/project_name/main.py` (Flow 클래스) |
|
||||
| **진입점 함수** | `main.py`의 `run()` | `main.py`의 `kickoff()` |
|
||||
| **프로젝트 구조** | 프로젝트 루트의 `crew.jsonc`와 `agents/` | `crews/` 폴더가 있는 `src/project_name/` |
|
||||
| **메인 로직 위치** | `crew.jsonc` (클래식: `src/project_name/crew.py`) | `src/project_name/main.py` (Flow 클래스) |
|
||||
| **진입점 함수** | `crew.jsonc`에서 로드됨 (클래식: `main.py`의 `run()`) | `main.py`의 `kickoff()` |
|
||||
| **pyproject.toml 타입** | `type = "crew"` | `type = "flow"` |
|
||||
| **CLI 생성 명령어** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **설정 위치** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **설정 위치** | `crew.jsonc`, `agents/`, 선택적 `tools/` | `src/project_name/crews/crew_name/config/` 또는 포함된 JSON crew 폴더 |
|
||||
| **다른 crew 포함 가능** | 아니오 | 예 (`crews/` 폴더 내) |
|
||||
|
||||
## 프로젝트 구조 참조
|
||||
|
||||
### Crew 프로젝트 구조
|
||||
|
||||
`crewai create crew my_crew`를 실행하면 다음 구조를 얻습니다:
|
||||
`crewai create crew my_crew`를 실행하면 JSON-first 구조를 얻습니다:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
@@ -54,24 +54,25 @@ my_crew/
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # 배포에 필수
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # run() 함수가 있는 진입점
|
||||
├── crew.py # @CrewBase 데코레이터가 있는 Crew 클래스
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # 에이전트 정의
|
||||
└── tasks.yaml # 작업 정의
|
||||
├── crew.jsonc # Crew 설정, 태스크, 프로세스, 입력
|
||||
├── agents/
|
||||
│ └── researcher.jsonc # 에이전트 정의
|
||||
├── tools/ # 선택적 custom:<name> 도구
|
||||
├── knowledge/
|
||||
└── skills/
|
||||
```
|
||||
|
||||
<Warning>
|
||||
중첩된 `src/project_name/` 구조는 Crews에 매우 중요합니다.
|
||||
잘못된 레벨에 파일을 배치하면 배포 실패의 원인이 됩니다.
|
||||
JSON-first crew에서는 `crew.jsonc`, `agents/`, `tools/`, `knowledge/`, `skills/`를
|
||||
프로젝트 루트에 두세요. 이를 `src/` 아래에 두면 `crewai run`과 배포 검증이 crew 정의를 찾지 못합니다.
|
||||
</Warning>
|
||||
|
||||
<Info>
|
||||
`crewai create crew my_crew --classic`으로 만든 클래식 프로젝트는 기존
|
||||
`src/project_name/crew.py`, `src/project_name/config/agents.yaml`,
|
||||
`src/project_name/config/tasks.yaml` 구조를 사용합니다. 이 구조는 decorator 기반 Python crew를 위해 계속 지원됩니다.
|
||||
</Info>
|
||||
|
||||
### Flow 프로젝트 구조
|
||||
|
||||
`crewai create flow my_flow`를 실행하면 다음 구조를 얻습니다:
|
||||
@@ -100,9 +101,9 @@ my_flow/
|
||||
```
|
||||
|
||||
<Info>
|
||||
Crews와 Flows 모두 `src/project_name/` 구조를 사용합니다.
|
||||
핵심 차이점은 Flows는 포함된 crews를 위한 `crews/` 폴더가 있고,
|
||||
Crews는 프로젝트 폴더에 직접 `crew.py`가 있다는 것입니다.
|
||||
JSON-first 독립 실행형 crew는 프로젝트 루트의 JSON 파일을 사용합니다.
|
||||
Flow는 여전히 `src/project_name/`을 사용하며, 클래식 포함 crew나
|
||||
`crewai.project.load_crew`로 로드하는 포함 JSON crew 폴더를 둘 수 있습니다.
|
||||
</Info>
|
||||
|
||||
## 배포 전 체크리스트
|
||||
@@ -154,60 +155,88 @@ git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. CrewBase 데코레이터 사용 확인
|
||||
### 3. Crew 정의 검증
|
||||
|
||||
**모든 crew 클래스는 `@CrewBase` 데코레이터를 사용해야 합니다.** 이것은 다음에 적용됩니다:
|
||||
<Tabs>
|
||||
<Tab title="JSON-first Crews">
|
||||
JSON-first crew는 프로젝트 루트에 `crew.jsonc` 또는 `crew.json` 파일이 있어야 합니다.
|
||||
`agents` 배열은 `agents/` 안의 파일을 참조해야 하며, 각 task는 유효한 agent 이름을 참조해야 합니다.
|
||||
|
||||
- 독립 실행형 crew 프로젝트
|
||||
- Flow 프로젝트 내에 포함된 crews
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research {topic}.",
|
||||
"expected_output": "A concise report.",
|
||||
"agent": "researcher"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
커스텀 도구는 `"custom:<name>"`으로 참조하며, `tools/<name>.py`에 `BaseTool` 서브클래스로 구현해야 합니다.
|
||||
</Tab>
|
||||
<Tab title="클래식 Python/YAML Crews">
|
||||
클래식 crew와 Flow 안에 포함된 Python crew는 `@CrewBase` 데코레이터를 사용해야 합니다.
|
||||
|
||||
@CrewBase # 이 데코레이터는 필수입니다
|
||||
class MyCrew():
|
||||
"""내 crew 설명"""
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
@CrewBase
|
||||
class MyCrew():
|
||||
"""내 crew 설명"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
<Warning>
|
||||
`@CrewBase` 데코레이터를 잊으면 에이전트나 작업 구성이 누락되었다는
|
||||
오류와 함께 배포가 실패합니다.
|
||||
</Warning>
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 4. 프로젝트 진입점 확인
|
||||
|
||||
Crews와 Flows 모두 `src/project_name/main.py`에 진입점이 있습니다:
|
||||
JSON-first 독립 실행형 crew는 직접 작성한 `src/project_name/main.py`가 필요하지 않습니다.
|
||||
`crewai run`과 배포 패키징이 `crew.jsonc`를 직접 로드합니다. 클래식 crew와 Flow는 Python 진입점을 사용합니다:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Crews의 경우">
|
||||
<Tab title="JSON-first Crews">
|
||||
프로젝트 루트에서 로컬 실행합니다:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="클래식 Crews">
|
||||
진입점은 `run()` 함수를 사용합니다:
|
||||
|
||||
```python
|
||||
@@ -278,16 +307,17 @@ grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
# 2. uv.lock 존재 확인
|
||||
ls -la uv.lock || echo "오류: uv.lock이 없습니다! 'uv lock'을 실행하세요"
|
||||
|
||||
# 3. src/ 구조 존재 확인
|
||||
ls -la src/*/main.py 2>/dev/null || echo "src/에서 main.py를 찾을 수 없습니다"
|
||||
# 3. JSON-first crew의 경우 crew.jsonc와 agents/ 확인
|
||||
([ -f crew.jsonc ] || [ -f crew.json ]) || echo "crew.jsonc 또는 crew.json을 찾을 수 없습니다"
|
||||
test -d agents || echo "agents/ 디렉터리를 찾을 수 없습니다"
|
||||
|
||||
# 4. Crews의 경우 - crew.py 존재 확인
|
||||
# 4. 클래식 Crews의 경우 - crew.py 존재 확인
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "crew.py가 없습니다 (Crews에서 예상됨)"
|
||||
|
||||
# 5. Flows의 경우 - crews/ 폴더 존재 확인
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "crews/ 폴더가 없습니다 (Flows에서 예상됨)"
|
||||
|
||||
# 6. CrewBase 사용 확인
|
||||
# 6. 클래식 Python crews의 경우 - CrewBase 사용 확인
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
@@ -297,8 +327,9 @@ grep -r "@CrewBase" . --include="*.py"
|
||||
|------|------|----------|
|
||||
| `uv.lock` 누락 | 의존성 해결 중 빌드 실패 | `uv lock` 실행 후 커밋 |
|
||||
| pyproject.toml의 잘못된 `type` | 빌드 성공하지만 런타임 실패 | 올바른 타입으로 변경 |
|
||||
| `@CrewBase` 데코레이터 누락 | "Config not found" 오류 | 모든 crew 클래스에 데코레이터 추가 |
|
||||
| `src/` 대신 루트에 파일 배치 | 진입점을 찾을 수 없음 | `src/project_name/`으로 이동 |
|
||||
| JSON-first crew에서 `crew.jsonc` 또는 `agents/` 누락 | Crew 정의를 찾을 수 없음 | `crew.jsonc`와 `agents/`를 프로젝트 루트에 둠 |
|
||||
| 클래식 crew에서 `@CrewBase` 데코레이터 누락 | "Config not found" 오류 | 모든 클래식 crew 클래스에 데코레이터 추가 |
|
||||
| 클래식 파일을 `src/` 대신 루트에 배치 | 진입점을 찾을 수 없음 | 클래식 Python 파일을 `src/project_name/`으로 이동 |
|
||||
| `run()` 또는 `kickoff()` 누락 | 자동화를 시작할 수 없음 | 올바른 진입 함수 추가 |
|
||||
|
||||
## 다음 단계
|
||||
|
||||
@@ -43,7 +43,7 @@ CrewAI는 AI 네이티브입니다. 이 페이지는 Claude Code, Codex, Cursor,
|
||||
|
||||
| 스킬 | 실행 시점 |
|
||||
|------|-------------|
|
||||
| `getting-started` | 새 프로젝트 스캐폴딩, `LLM.call()` / `Agent` / `Crew` / `Flow` 선택, `crew.py` / `main.py` 연결 |
|
||||
| `getting-started` | 새 프로젝트 스캐폴딩, `LLM.call()` / `Agent` / `Crew` / `Flow` 선택, `crew.jsonc` / `main.py` 연결 |
|
||||
| `design-agent` | 에이전트 구성 — 역할, 목표, 배경 이야기, 도구, LLM, 메모리, 가드레일 |
|
||||
| `design-task` | 태스크 설명, 의존성, 구조화된 출력(`output_pydantic`, `output_json`), 사람 검토 |
|
||||
| `ask-docs` | 최신 API 정보를 위해 [CrewAI 문서 MCP 서버](https://docs.crewai.com/mcp) 조회 |
|
||||
|
||||
@@ -1,392 +1,140 @@
|
||||
---
|
||||
title: 첫 번째 크루 만들기
|
||||
description: 복잡한 문제를 함께 해결할 수 있는 협업 AI 팀을 만드는 단계별 튜토리얼입니다.
|
||||
title: 첫 번째 Crew 만들기
|
||||
description: JSON-first crew 설정으로 협업 AI 팀을 만드는 단계별 튜토리얼입니다.
|
||||
icon: users-gear
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 협업 AI의 힘을 발휘하기
|
||||
## 리서치 Crew 만들기
|
||||
|
||||
여러 AI 에이전트가 각자의 전문성을 바탕으로 원활하게 협력하며 복잡한 문제를 해결한다고 상상해 보세요. 각자 고유한 기술을 발휘해 공동의 목표를 달성합니다. 이것이 바로 CrewAI의 힘입니다. CrewAI 프레임워크를 통해 단일 AI로는 달성할 수 없는 과업을 협업 AI 시스템으로 실현할 수 있습니다.
|
||||
이 가이드에서는 두 에이전트가 주제를 조사하고 markdown 보고서를 작성하는 crew를 만듭니다. 새 crew 프로젝트는 JSON-first입니다. 에이전트는 `agents/*.jsonc`, 태스크와 crew 설정은 `crew.jsonc`에 두며, `crewai run`이 이 정의를 직접 로드합니다.
|
||||
|
||||
이 가이드에서는 연구 크루를 만들어 주제를 조사 및 분석하고, 종합적인 보고서를 작성하는 과정을 안내합니다. 이 실용적인 예시는 AI 에이전트들이 어떻게 협력하여 복잡한 작업을 수행할 수 있는지 보여 주지만, CrewAI로 실현할 수 있는 가능성의 시작에 불과합니다.
|
||||
### 준비 사항
|
||||
|
||||
### 무엇을 만들고 배우게 될까요
|
||||
1. [설치 가이드](/ko/installation)에 따라 CrewAI 설치
|
||||
2. [LLM 설정](/ko/concepts/llms#setting-up-your-llm)에 따라 모델 API 키 설정
|
||||
3. 웹 검색을 사용할 경우 [Serper.dev](https://serper.dev/) API 키 준비
|
||||
|
||||
이 가이드를 마치면 다음을 할 수 있게 됩니다:
|
||||
|
||||
1. **특화된 AI 연구팀 조직**: 각기 다른 역할과 책임을 가진 연구팀을 만듭니다
|
||||
2. **여러 AI 에이전트 간의 협업 조율**
|
||||
3. **정보 수집, 분석, 보고서 생성을 포함한 복잡한 workflow 자동화**
|
||||
4. **더 야심찬 프로젝트에도 적용할 수 있는 기초 역량 구축**
|
||||
|
||||
이 가이드에서는 간단한 research crew를 만들지만, 동일한 패턴과 기법을 활용하여 다음과 같은 훨씬 더 정교한 팀도 만들 수 있습니다:
|
||||
|
||||
- 전문 writer, editor, fact-checker가 참여하는 다단계 콘텐츠 생성
|
||||
- 단계별 지원 에이전트가 있는 복잡한 고객 서비스 시스템
|
||||
- 데이터 수집, 시각화, 인사이트 생성까지 하는 자율 business analyst
|
||||
- 아이디어 구상, 디자인, 구현 계획까지 진행하는 product development 팀
|
||||
|
||||
이제 여러분의 첫 crew를 만들어 봅시다!
|
||||
|
||||
### 필수 조건
|
||||
|
||||
시작하기 전에 다음을 확인하세요:
|
||||
|
||||
1. [설치 가이드](/ko/installation)를 참고하여 CrewAI를 설치했는지 확인하세요.
|
||||
2. [LLM 설정 가이드](/ko/concepts/llms#setting-up-your-llm)를 참고하여 환경에 LLM API 키를 설정했는지 확인하세요.
|
||||
3. Python에 대한 기본적인 이해
|
||||
|
||||
## 1단계: 새로운 CrewAI 프로젝트 생성
|
||||
|
||||
먼저, CLI를 사용하여 새로운 CrewAI 프로젝트를 생성해봅시다. 이 명령어는 필요한 모든 파일을 포함한 전체 프로젝트 구조를 설정해 주어, 보일러플레이트 코드를 설정하는 대신 에이전트와 그들의 작업 정의에 집중할 수 있습니다.
|
||||
## 1단계: 새 Crew 만들기
|
||||
|
||||
```bash
|
||||
crewai create crew research_crew
|
||||
cd research_crew
|
||||
```
|
||||
|
||||
이렇게 하면 crew에 필요한 기본 구조를 갖춘 프로젝트가 생성됩니다. CLI는 다음을 자동으로 생성합니다:
|
||||
생성되는 구조:
|
||||
|
||||
- 필요한 파일이 포함된 프로젝트 디렉터리
|
||||
- 에이전트와 작업에 대한 구성 파일
|
||||
- 기본 crew 구현
|
||||
- crew를 실행하는 메인 스크립트
|
||||
|
||||
<Frame caption="CrewAI 프레임워크 개요">
|
||||
<img src="/images/crews.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
## 2단계: 프로젝트 구조 살펴보기
|
||||
|
||||
CLI가 생성한 프로젝트 구조를 이해하는 시간을 가져봅시다. CrewAI는 Python 프로젝트의 모범 사례를 따르므로, crew가 더 복잡해질수록 코드를 쉽게 유지 관리하고 확장할 수 있습니다.
|
||||
|
||||
```
|
||||
```text
|
||||
research_crew/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── research_crew/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
이 구조는 Python 프로젝트의 모범 사례를 따르며, 코드를 체계적으로 구성할 수 있도록 해줍니다. 설정 파일(YAML)과 구현 코드(Python)의 분리로 인해, 기본 코드를 변경하지 않고도 crew의 동작을 쉽게 수정할 수 있습니다.
|
||||
<Tip>
|
||||
`crew.py`, `config/agents.yaml`, `config/tasks.yaml`을 쓰는 기존 레이아웃이 필요하면 `crewai create crew research_crew --classic`을 사용하세요.
|
||||
</Tip>
|
||||
|
||||
## 3단계: 에이전트 구성하기
|
||||
## 2단계: 에이전트 정의
|
||||
|
||||
이제 재미있는 단계가 시작됩니다 - 여러분의 AI 에이전트를 정의하는 것입니다! CrewAI에서 에이전트는 특정 역할, 목표 및 배경을 가진 전문화된 엔터티로, 이들이 어떻게 행동할지를 결정합니다. 각각 고유한 성격과 목적을 지닌 연극의 등장인물로 생각하면 됩니다.
|
||||
생성된 `agents/researcher.jsonc` 파일을 교체하고 `agents/analyst.jsonc`를 추가합니다. 파일 이름이 `crew.jsonc`에서 참조하는 에이전트 이름입니다.
|
||||
|
||||
우리의 리서치 crew를 위해 두 명의 에이전트를 만들겠습니다:
|
||||
1. 정보를 찾아 정리하는 데 뛰어난 **리서처**
|
||||
2. 연구 결과를 해석하고 통찰력 있는 보고서를 작성할 수 있는 **애널리스트**
|
||||
|
||||
이러한 전문화된 에이전트를 정의하기 위해 `agents.yaml` 파일을 수정해봅시다. `llm` 항목은 사용 중인 제공업체에 맞게 설정하세요.
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
Senior Research Specialist for {topic}
|
||||
goal: >
|
||||
Find comprehensive and accurate information about {topic}
|
||||
with a focus on recent developments and key insights
|
||||
backstory: >
|
||||
You are an experienced research specialist with a talent for
|
||||
finding relevant information from various sources. You excel at
|
||||
organizing information in a clear and structured manner, making
|
||||
complex topics accessible to others.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
analyst:
|
||||
role: >
|
||||
Data Analyst and Report Writer for {topic}
|
||||
goal: >
|
||||
Analyze research findings and create a comprehensive, well-structured
|
||||
report that presents insights in a clear and engaging way
|
||||
backstory: >
|
||||
You are a skilled analyst with a background in data interpretation
|
||||
and technical writing. You have a talent for identifying patterns
|
||||
and extracting meaningful insights from research data, then
|
||||
communicating those insights effectively through well-crafted reports.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "Senior Research Specialist for {topic}",
|
||||
"goal": "Find comprehensive and accurate information about {topic}, with a focus on recent developments and key insights.",
|
||||
"backstory": "You are an experienced research specialist who organizes complex information into clear, useful notes.",
|
||||
// 사용하는 모델로 바꾸세요. 예: "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
각 에이전트가 고유한 역할, 목표, 그리고 배경을 가지고 있다는 점에 주목하세요. 이 요소들은 단순한 설명 그 이상으로, 실제로 에이전트가 자신의 과업을 어떻게 접근하는지에 적극적으로 영향을 미칩니다. 이러한 부분을 신중하게 설계함으로써 서로 보완하는 전문적인 역량과 관점을 가진 에이전트를 만들 수 있습니다.
|
||||
|
||||
## 4단계: 작업 정의하기
|
||||
|
||||
이제 agent들을 정의했으니, 이들에게 수행할 구체적인 작업을 지정해야 합니다. CrewAI의 작업(task)은 agent가 수행할 구체적인 업무를 나타내며, 자세한 지침과 예상 결과물이 포함됩니다.
|
||||
|
||||
연구 crew를 위해 두 가지 주요 작업을 정의하겠습니다:
|
||||
1. 포괄적인 정보 수집을 위한 **연구 작업**
|
||||
2. 인사이트 있는 보고서 생성을 위한 **분석 작업**
|
||||
|
||||
`tasks.yaml` 파일을 다음과 같이 수정해 보겠습니다:
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
{topic}에 대해 철저한 연구를 수행하세요. 다음에 중점을 두세요:
|
||||
1. 주요 개념 및 정의
|
||||
2. 역사적 발전과 최근 동향
|
||||
3. 주요 과제와 기회
|
||||
4. 주목할 만한 적용 사례 또는 케이스 스터디
|
||||
5. 향후 전망과 잠재적 발전
|
||||
|
||||
반드시 명확한 섹션으로 구성된 구조화된 형식으로 결과를 정리하세요.
|
||||
expected_output: >
|
||||
{topic}의 모든 요구 사항을 다루는, 잘 구성된 섹션이 포함된 포괄적인 연구 문서.
|
||||
필요에 따라 구체적인 사실, 수치, 예시를 포함하세요.
|
||||
agent: researcher
|
||||
|
||||
analysis_task:
|
||||
description: >
|
||||
연구 결과를 분석하고 {topic}에 대한 포괄적인 보고서를 작성하세요.
|
||||
보고서에는 다음 내용이 포함되어야 합니다:
|
||||
1. 간결한 요약(executive summary)으로 시작
|
||||
2. 연구의 모든 주요 정보 포함
|
||||
3. 동향과 패턴에 대한 통찰력 있는 분석 제공
|
||||
4. 추천사항 또는 미래 고려 사항 제시
|
||||
5. 명확한 제목과 함께 전문적이고 읽기 쉬운 형식으로 작성
|
||||
|
||||
expected_output: >
|
||||
연구 결과와 추가 분석, 인사이트를 포함한 {topic}에 대한 정제되고 전문적인 보고서.
|
||||
보고서는 간결한 요약, 본문, 결론 등으로 잘 구조화되어 있어야 합니다.
|
||||
agent: analyst
|
||||
context:
|
||||
- research_task
|
||||
output_file: output/report.md
|
||||
```jsonc agents/analyst.jsonc
|
||||
{
|
||||
"role": "Report Analyst for {topic}",
|
||||
"goal": "Turn research findings into a clear, well-structured report.",
|
||||
"backstory": "You are a careful analyst with strong technical writing skills and a talent for extracting useful insights.",
|
||||
// 사용하는 모델로 바꾸세요. 예: "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
분석 작업 내의 `context` 필드에 주목하세요. 이 강력한 기능을 통해 analyst가 연구 작업의 결과물을 참조할 수 있습니다. 이를 통해 정보가 human team에서처럼 agent 간에 자연스럽게 흐르는 워크플로우가 만들어집니다.
|
||||
`provider/model-id`를 `openai/gpt-4o`, `anthropic/claude-sonnet-4-6`, `gemini/gemini-2.0-flash-001` 같은 모델로 바꾸세요.
|
||||
|
||||
## 5단계: 크루 구성 설정하기
|
||||
## 3단계: 태스크와 Crew 설정
|
||||
|
||||
이제 크루를 구성하여 모든 것을 하나로 모을 시간입니다. 크루는 에이전트들이 함께 작업을 완료하는 방식을 조율하는 컨테이너 역할을 합니다.
|
||||
`crew.jsonc`를 다음으로 교체합니다:
|
||||
|
||||
`crew.py` 파일을 다음과 같이 수정해보겠습니다:
|
||||
|
||||
```python
|
||||
# src/research_crew/crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew():
|
||||
"""Research crew for comprehensive topic analysis and reporting"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['researcher'], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()]
|
||||
)
|
||||
|
||||
@agent
|
||||
def analyst(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['analyst'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['research_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def analysis_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['analysis_task'], # type: ignore[index]
|
||||
output_file='output/report.md'
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the research crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
이 코드에서는 다음을 수행합니다:
|
||||
1. researcher 에이전트를 생성하고, SerperDevTool을 장착하여 웹 검색 기능을 추가합니다.
|
||||
2. analyst 에이전트를 생성합니다.
|
||||
3. research와 analysis 작업(task)을 설정합니다.
|
||||
4. 크루가 작업을 순차적으로 수행하도록 설정합니다(analyst가 researcher가 끝날 때까지 대기).
|
||||
|
||||
여기서 마법이 일어납니다. 몇 줄의 코드만으로도, 특화된 에이전트들이 조율된 프로세스 내에서 협업하는 협동 AI 시스템을 정의할 수 있습니다.
|
||||
|
||||
## 6단계: 메인 스크립트 설정
|
||||
|
||||
이제 우리 crew를 실행할 메인 스크립트를 설정해 보겠습니다. 이곳에서 crew가 리서치할 구체적인 주제를 지정합니다.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# src/research_crew/main.py
|
||||
import os
|
||||
from research_crew.crew import ResearchCrew
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs('output', exist_ok=True)
|
||||
|
||||
def run():
|
||||
"""
|
||||
Run the research crew.
|
||||
"""
|
||||
inputs = {
|
||||
'topic': 'Artificial Intelligence in Healthcare'
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research on {topic}. Focus on key concepts, recent developments, major challenges, notable applications, and future outlook.",
|
||||
"expected_output": "A comprehensive research document with organized sections, specific facts, and useful examples about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis_task",
|
||||
"description": "Analyze the research findings and create a polished report on {topic}. Include an executive summary, key insights, trend analysis, and recommendations.",
|
||||
"expected_output": "A professional markdown report with clear headings, a concise summary, main findings, and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research_task"],
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
|
||||
# Create and run the crew
|
||||
result = ResearchCrew().crew().kickoff(inputs=inputs)
|
||||
|
||||
# Print the result
|
||||
print("\n\n=== FINAL REPORT ===\n\n")
|
||||
print(result.raw)
|
||||
|
||||
print("\n\nReport has been saved to output/report.md")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "Artificial Intelligence in Healthcare"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 스크립트는 환경을 준비하고, 리서치 주제를 지정하며, crew의 작업을 시작합니다. CrewAI의 강력함은 이 코드가 얼마나 간단한지에서 드러납니다. 여러 AI 에이전트를 관리하는 모든 복잡함이 프레임워크에 의해 처리됩니다.
|
||||
`context`는 이전 태스크 이름을 가리키므로 analyst가 research 태스크 출력을 받습니다. `inputs`는 `{topic}`의 기본값을 제공합니다. 기본값이 없으면 `crewai run`이 실행 중에 물어봅니다.
|
||||
|
||||
## 7단계: 환경 변수 설정하기
|
||||
## 4단계: 환경 변수 설정
|
||||
|
||||
프로젝트 루트에 `.env` 파일을 생성하고 API 키를 입력하세요:
|
||||
`.env`를 편집합니다:
|
||||
|
||||
```sh
|
||||
SERPER_API_KEY=your_serper_api_key
|
||||
# Add your provider's API key here too.
|
||||
# 모델 제공자 API 키도 추가하세요.
|
||||
```
|
||||
|
||||
선택한 provider를 구성하는 방법에 대한 자세한 내용은 [LLM 설정 가이드](/ko/concepts/llms#setting-up-your-llm)를 참고하세요. Serper API 키는 [Serper.dev](https://serper.dev/)에서 받을 수 있습니다.
|
||||
|
||||
## 8단계: 필수 종속성 설치
|
||||
|
||||
CrewAI CLI를 사용하여 필요한 종속성을 설치하세요:
|
||||
## 5단계: 설치 및 실행
|
||||
|
||||
```bash
|
||||
crewai install
|
||||
```
|
||||
|
||||
이 명령어는 다음을 수행합니다:
|
||||
1. 프로젝트 구성에서 종속성을 읽어옵니다
|
||||
2. 필요하다면 가상 환경을 생성합니다
|
||||
3. 모든 필수 패키지를 설치합니다
|
||||
|
||||
## 9단계: Crew 실행하기
|
||||
|
||||
이제 흥미로운 순간입니다 - crew를 실행하여 AI 협업이 어떻게 이루어지는지 직접 확인해보세요!
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
이 명령어를 실행하면 crew가 즉시 작동하는 모습을 볼 수 있습니다. researcher는 지정된 주제에 대한 정보를 수집하고, analyst가 그 연구를 바탕으로 종합 보고서를 작성합니다. 에이전트들의 사고 과정, 행동, 결과물이 실시간으로 표시되며 서로 협력하여 작업을 완수하는 모습을 확인할 수 있습니다.
|
||||
실행이 끝나면 `output/report.md`를 확인하세요.
|
||||
|
||||
## 10단계: 결과물 검토
|
||||
|
||||
crew가 작업을 완료하면, 최종 보고서는 `output/report.md` 파일에서 확인할 수 있습니다. 보고서에는 다음과 같은 내용이 포함됩니다:
|
||||
|
||||
1. 요약 보고서
|
||||
2. 주제에 대한 상세 정보
|
||||
3. 분석 및 인사이트
|
||||
4. 권장사항 또는 향후 고려사항
|
||||
|
||||
지금까지 달성한 것을 잠시 돌아보세요. 여러분은 여러 AI 에이전트가 협업하여 각자의 전문적인 기술을 발휘함으로써, 단일 에이전트가 혼자서 이루어낼 수 있는 것보다 더 뛰어난 결과를 만들어내는 시스템을 구축한 것입니다.
|
||||
|
||||
## 기타 CLI 명령어 탐색
|
||||
|
||||
CrewAI는 crew 작업을 위한 몇 가지 유용한 CLI 명령어를 추가로 제공합니다:
|
||||
|
||||
```bash
|
||||
# 모든 사용 가능한 명령어 보기
|
||||
crewai --help
|
||||
|
||||
# crew 실행
|
||||
crewai run
|
||||
|
||||
# crew 테스트
|
||||
crewai test
|
||||
|
||||
# crew 메모리 초기화
|
||||
crewai reset-memories
|
||||
|
||||
# 특정 task에서 재실행
|
||||
crewai replay -t <task_id>
|
||||
```
|
||||
|
||||
## 가능한 것의 예술: 당신의 첫 crew를 넘어서
|
||||
|
||||
이 가이드에서 구축한 것은 시작에 불과합니다. 여러분이 배운 기술과 패턴은 점점 더 정교한 AI 시스템을 만드는 데 적용할 수 있습니다. 다음은 이 기본 research crew를 확장할 수 있는 몇 가지 방법입니다:
|
||||
|
||||
### 팀원 확장하기
|
||||
|
||||
더 전문화된 에이전트를 팀원으로 추가할 수 있습니다:
|
||||
- 연구 결과를 검증하는 **팩트체커**
|
||||
- 차트와 그래프를 만드는 **데이터 시각화 담당자**
|
||||
- 특정 분야에 전문 지식을 가진 **도메인 전문가**
|
||||
- 분석의 약점을 파악하는 **비평가**
|
||||
|
||||
### 도구 및 기능 추가
|
||||
|
||||
에이전트에 추가 도구를 통해 기능을 확장할 수 있습니다:
|
||||
- 실시간 연구를 위한 웹 브라우징 도구
|
||||
- 데이터 분석을 위한 CSV/데이터베이스 도구
|
||||
- 데이터 처리를 위한 코드 실행 도구
|
||||
- 외부 서비스와의 API 연결
|
||||
|
||||
### 더 복잡한 워크플로우 생성
|
||||
|
||||
더 정교한 프로세스를 구현할 수 있습니다:
|
||||
- 매니저 에이전트가 워커 에이전트에게 위임하는 계층적 프로세스
|
||||
- 반복적 피드백 루프로 정제하는 반복 프로세스
|
||||
- 여러 에이전트가 동시에 작업하는 병렬 프로세스
|
||||
- 중간 결과에 따라 적응하는 동적 프로세스
|
||||
|
||||
### 다양한 도메인에 적용하기
|
||||
|
||||
동일한 패턴은 다음과 같은 분야에서 crew를 구성하는 데 적용할 수 있습니다:
|
||||
- **콘텐츠 제작**: 작가, 에디터, 팩트체커, 디자이너가 함께 협업
|
||||
- **고객 서비스**: 분류 담당자, 전문가, 품질 관리자가 함께 협업
|
||||
- **제품 개발**: 연구원, 디자이너, 기획자가 협업
|
||||
- **데이터 분석**: 데이터 수집가, 분석가, 시각화 전문가
|
||||
|
||||
## 다음 단계
|
||||
|
||||
이제 첫 crew를 구축했으니, 다음과 같은 작업을 시도해 볼 수 있습니다:
|
||||
|
||||
1. 다양한 에이전트 구성 및 성격을 실험해 보세요
|
||||
2. 더 복잡한 작업 구조와 워크플로우를 시도해 보세요
|
||||
3. 맞춤 도구를 구현하여 에이전트에게 새로운 기능을 제공하세요
|
||||
4. crew를 다양한 주제나 문제 도메인에 적용해 보세요
|
||||
5. [CrewAI Flows](/ko/guides/flows/first-flow)를 탐색하여 절차적 프로그래밍을 활용한 더 고급 워크플로우를 경험해 보세요
|
||||
<Warning>
|
||||
신뢰하는 출처의 JSON crew 프로젝트만 실행하세요. `custom:<name>` 도구와 `{"python": "module.attribute"}` 참조는 crew 로드 시 로컬 Python 코드를 실행합니다.
|
||||
</Warning>
|
||||
|
||||
<Check>
|
||||
축하합니다! 이제 주어진 모든 주제를 조사하고 분석할 수 있는 첫 번째 CrewAI crew를 성공적으로 구축하셨습니다. 이 기본적인 경험은 협업 인텔리전스를 통해 복잡하고 다단계의 문제를 해결할 수 있는 점점 더 정교한 AI 시스템을 제작하는 데 필요한 역량을 갖추는 데 도움이 됩니다.
|
||||
</Check>
|
||||
주제를 조사하고 보고서를 작성하는 JSON-first crew를 만들었습니다.
|
||||
</Check>
|
||||
|
||||
@@ -64,7 +64,7 @@ cd guide_creator_flow
|
||||
|
||||
## 2단계: 프로젝트 구조 이해하기
|
||||
|
||||
생성된 프로젝트는 다음과 같은 구조를 가지고 있습니다. 잠시 시간을 내어 이 구조에 익숙해지세요. 구조를 이해하면 앞으로 더 복잡한 flow를 만드는 데 도움이 됩니다.
|
||||
생성된 프로젝트는 다음과 같은 구조를 가지고 있습니다. 시작용 embedded crew는 클래식 Python/YAML 레이아웃을 사용합니다. Flow 안에서 JSON-first crew를 사용하려면 crew 폴더에 `crew.jsonc`와 `agents/*.jsonc`를 만들고 `crewai.project.load_crew`로 로드하세요. 예시는 [Flows](/ko/concepts/flows#building-your-crews)를 참고하세요.
|
||||
|
||||
```
|
||||
guide_creator_flow/
|
||||
@@ -102,149 +102,82 @@ crewai flow add-crew content-crew
|
||||
|
||||
## 4단계: 콘텐츠 작가 Crew 구성
|
||||
|
||||
이제 콘텐츠 작가 crew를 위해 생성된 파일을 수정해보겠습니다. 우리는 가이드의 고품질 콘텐츠를 만들기 위해 협업하는 두 명의 전문 에이전트 - 작가와 리뷰어 - 를 설정할 것입니다.
|
||||
이제 콘텐츠 작가 crew를 JSONC로 구성합니다. 가이드의 고품질 콘텐츠를 만들기 위해 협업하는 두 명의 전문 에이전트 - 작가와 리뷰어 - 를 설정합니다.
|
||||
|
||||
1. 먼저, 에이전트 구성 파일을 업데이트하여 콘텐츠 제작 팀을 정의합니다:
|
||||
1. `src/guide_creator_flow/crews/content_crew/agents/content_writer.jsonc`를 만듭니다:
|
||||
|
||||
`llm`을 사용 중인 공급자로 설정해야 함을 기억하세요.
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/agents.yaml
|
||||
content_writer:
|
||||
role: >
|
||||
교육 콘텐츠 작가
|
||||
goal: >
|
||||
할당된 주제를 철저히 설명하고 독자에게 소중한 통찰력을 제공하는 흥미롭고 유익한 콘텐츠를 제작합니다
|
||||
backstory: >
|
||||
당신은 명확하고 흥미로운 콘텐츠를 만드는 데 능숙한 교육 전문 작가입니다. 복잡한 개념도 쉽게 설명하며,
|
||||
정보를 독자가 이해하기 쉽게 조직할 수 있습니다.
|
||||
llm: provider/model-id # 예: openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
content_reviewer:
|
||||
role: >
|
||||
교육 콘텐츠 검토자 및 에디터
|
||||
goal: >
|
||||
콘텐츠가 정확하고, 포괄적이며, 잘 구조화되어 있고, 이전에 작성된 섹션과의 일관성을 유지하도록 합니다
|
||||
backstory: >
|
||||
당신은 수년간 교육 콘텐츠를 검토해 온 꼼꼼한 에디터입니다. 세부 사항, 명확성, 일관성에 뛰어나며,
|
||||
원 저자의 목소리를 유지하면서도 콘텐츠의 품질을 향상시키는 데 능숙합니다.
|
||||
llm: provider/model-id # 예: openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Writer",
|
||||
"goal": "Create engaging, informative content that thoroughly explains the assigned topic and provides valuable insights to the reader.",
|
||||
"backstory": "You are a talented educational writer who explains complex concepts in accessible language and organizes information clearly.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 에이전트 정의는 AI 에이전트가 콘텐츠 제작을 접근하는 전문화된 역할과 관점을 구성합니다. 각 에이전트가 뚜렷한 목적과 전문성을 지니고 있음을 확인하세요.
|
||||
2. `src/guide_creator_flow/crews/content_crew/agents/content_reviewer.jsonc`를 만듭니다:
|
||||
|
||||
2. 다음으로, 작업 구성 파일을 업데이트하여 구체적인 작성 및 검토 작업을 정의합니다:
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/tasks.yaml
|
||||
write_section_task:
|
||||
description: >
|
||||
주제에 대한 포괄적인 섹션을 작성하세요: "{section_title}"
|
||||
|
||||
섹션 설명: {section_description}
|
||||
대상 독자: {audience_level} 수준 학습자
|
||||
|
||||
작성 시 아래 사항을 반드시 지켜주세요:
|
||||
1. 섹션 주제에 대한 간략한 소개로 시작
|
||||
2. 모든 주요 개념을 예시와 함께 명확하게 설명
|
||||
3. 적절하다면 실용적인 활용 사례나 연습문제 포함
|
||||
4. 주요 포인트 요약으로 마무리
|
||||
5. 대략 500-800단어 분량
|
||||
|
||||
콘텐츠는 적절한 제목, 목록, 강조를 포함해 Markdown 형식으로 작성하세요.
|
||||
|
||||
이전에 작성된 섹션:
|
||||
{previous_sections}
|
||||
|
||||
반드시 콘텐츠가 이전에 쓴 섹션과 일관성을 유지하고 앞에서 설명된 개념을 바탕으로 작성되도록 하세요.
|
||||
expected_output: >
|
||||
주제를 철저히 설명하고 대상 독자에게 적합한, 구조가 잘 잡힌 Markdown 형식의 포괄적 섹션
|
||||
agent: content_writer
|
||||
|
||||
review_section_task:
|
||||
description: >
|
||||
아래 "{section_title}" 섹션의 내용을 검토하고 개선하세요:
|
||||
|
||||
{draft_content}
|
||||
|
||||
대상 독자: {audience_level} 수준 학습자
|
||||
|
||||
이전에 작성된 섹션:
|
||||
{previous_sections}
|
||||
|
||||
검토 시 아래 사항을 반드시 지켜주세요:
|
||||
1. 문법/철자 오류 수정
|
||||
2. 명확성 및 가독성 향상
|
||||
3. 내용이 포괄적이고 정확한지 확인
|
||||
4. 이전에 쓴 섹션과 일관성 유지
|
||||
5. 구조와 흐름 강화
|
||||
6. 누락된 핵심 정보 추가
|
||||
|
||||
개선된 버전의 섹션을 Markdown 형식으로 제공하세요.
|
||||
expected_output: >
|
||||
원래의 구조를 유지하면서도 명확성, 정확성, 일관성을 향상시킨 세련된 개선본
|
||||
agent: content_reviewer
|
||||
context:
|
||||
- write_section_task
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Reviewer and Editor",
|
||||
"goal": "Ensure content is accurate, comprehensive, well-structured, and consistent with previously written sections.",
|
||||
"backstory": "You are a meticulous editor with an eye for detail, clarity, and coherence.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 작업 정의는 에이전트에게 세부적인 지침을 제공하여 우리의 품질 기준에 부합하는 콘텐츠를 생산하게 합니다. review 작업의 `context` 파라미터를 통해 리뷰어가 작가의 결과물에 접근할 수 있는 워크플로우가 생성됨에 주의하세요.
|
||||
`provider/model-id`를 사용하는 모델로 바꾸세요. 예: `openai/gpt-4o`, `gemini/gemini-2.0-flash-001`, `anthropic/claude-sonnet-4-6`.
|
||||
|
||||
3. 이제 crew 구현 파일을 업데이트하여 에이전트와 작업이 어떻게 연동되는지 정의합니다:
|
||||
3. `src/guide_creator_flow/crews/content_crew/crew.jsonc`를 만듭니다:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "Content Crew",
|
||||
"agents": ["content_writer", "content_reviewer"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "write_section_task",
|
||||
"description": "Write a comprehensive section on the topic: \"{section_title}\".\n\nSection description: {section_description}\nTarget audience: {audience_level} level learners\n\nYour content should begin with a brief introduction, explain key concepts clearly with examples, include practical applications where appropriate, end with a summary, and be approximately 500-800 words.\n\nPreviously written sections:\n{previous_sections}",
|
||||
"expected_output": "A well-structured, comprehensive section in Markdown format that thoroughly explains the topic and is appropriate for the target audience.",
|
||||
"agent": "content_writer",
|
||||
"markdown": true
|
||||
},
|
||||
{
|
||||
"name": "review_section_task",
|
||||
"description": "Review and improve this section on \"{section_title}\":\n\n{draft_content}\n\nTarget audience: {audience_level} level learners\nPreviously written sections:\n{previous_sections}\n\nFix errors, improve clarity, verify consistency, enhance structure, and add missing key information.",
|
||||
"expected_output": "An improved, polished version of the section that maintains the original structure but enhances clarity, accuracy, and consistency.",
|
||||
"agent": "content_reviewer",
|
||||
"context": ["write_section_task"],
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
`context` 필드를 통해 리뷰어가 작가의 출력을 사용할 수 있습니다.
|
||||
|
||||
4. `src/guide_creator_flow/crews/content_crew/content_crew.py`를 작은 loader로 교체합니다:
|
||||
|
||||
```python
|
||||
# src/guide_creator_flow/crews/content_crew/content_crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
@CrewBase
|
||||
class ContentCrew():
|
||||
"""Content writing crew"""
|
||||
from crewai.project import load_crew
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def content_writer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_writer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@agent
|
||||
def content_reviewer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_reviewer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def write_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['write_section_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def review_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['review_section_task'], # type: ignore[index]
|
||||
context=[self.write_section_task()]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the content writing crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
이 crew 정의는 에이전트와 작업 간의 관계를 설정하여, 콘텐츠 작가가 초안을 작성하고 리뷰어가 이를 개선하는 순차적 과정을 만듭니다. 이 crew는 독립적으로도 작동할 수 있지만, 우리의 플로우에서 더 큰 시스템의 일부로 오케스트레이션될 예정입니다.
|
||||
이 loader는 런타임에 `crew.jsonc`를 `Crew`로 바꿉니다. 이 crew는 독립적으로도 작동할 수 있지만, 우리의 플로우에서는 더 큰 시스템의 일부로 오케스트레이션됩니다.
|
||||
|
||||
## 5단계: 플로우(Flow) 생성
|
||||
|
||||
@@ -266,7 +199,7 @@ from typing import List, Dict
|
||||
from pydantic import BaseModel, Field
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from guide_creator_flow.crews.content_crew.content_crew import ContentCrew
|
||||
from guide_creator_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
# Define our models for structured data
|
||||
class Section(BaseModel):
|
||||
@@ -371,7 +304,7 @@ class GuideCreatorFlow(Flow[GuideCreatorState]):
|
||||
previous_sections_text = "No previous sections written yet."
|
||||
|
||||
# Run the content crew for this section
|
||||
result = ContentCrew().crew().kickoff(inputs={
|
||||
result = kickoff_content_crew(inputs={
|
||||
"section_title": section.title,
|
||||
"section_description": section.description,
|
||||
"audience_level": self.state.audience_level,
|
||||
@@ -590,7 +523,7 @@ class GuideCreatorState(BaseModel):
|
||||
Flow는 복잡한 협업 작업을 위해 crew와 원활하게 통합될 수 있습니다:
|
||||
|
||||
```python
|
||||
result = ContentCrew().crew().kickoff(inputs={
|
||||
result = kickoff_content_crew(inputs={
|
||||
"section_title": section.title,
|
||||
# ...
|
||||
})
|
||||
@@ -611,4 +544,4 @@ result = ContentCrew().crew().kickoff(inputs={
|
||||
|
||||
<Check>
|
||||
축하합니다! 정규 코드, 직접적인 LLM 호출, crew 기반 처리를 결합하여 포괄적인 가이드를 생성하는 첫 번째 CrewAI Flow를 성공적으로 구축하셨습니다. 이러한 기초적인 역량을 바탕으로 절차적 제어와 협업적 인텔리전스를 결합하여 복잡하고 다단계의 문제를 해결할 수 있는 점점 더 정교한 AI 애플리케이션을 만들 수 있습니다.
|
||||
</Check>
|
||||
</Check>
|
||||
|
||||
@@ -106,7 +106,7 @@ CrewAI는 의존성 관리와 패키지 처리를 위해 `uv`를 사용합니다
|
||||
|
||||
# CrewAI 프로젝트 생성하기
|
||||
|
||||
에이전트 및 태스크를 정의할 때 구조적인 접근 방식을 위해 `YAML` 템플릿 스캐폴딩을 사용하는 것을 권장합니다. 다음은 시작 방법입니다:
|
||||
`crewai create crew`는 이제 JSON-first crew 프로젝트를 생성합니다. 에이전트는 `agents/*.jsonc`에, 태스크와 crew 수준 설정은 `crew.jsonc`에 두며, `crewai run`은 이 JSON 정의를 직접 로드합니다.
|
||||
|
||||
<Steps>
|
||||
<Step title="프로젝트 스캐폴딩 생성">
|
||||
@@ -119,21 +119,20 @@ CrewAI는 의존성 관리와 패키지 처리를 위해 `uv`를 사용합니다
|
||||
```
|
||||
my_project/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── my_project/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
- `crew.py`, `config/agents.yaml`, `config/tasks.yaml`을 사용하는 기존 Python/YAML 스캐폴드가 필요하다면 다음을 실행하세요:
|
||||
```shell
|
||||
crewai create crew <your_project_name> --classic
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -142,15 +141,15 @@ CrewAI는 의존성 관리와 패키지 처리를 위해 `uv`를 사용합니다
|
||||
- 프로젝트에는 다음과 같은 주요 파일들이 포함되어 있습니다:
|
||||
| 파일 | 용도 |
|
||||
| --- | --- |
|
||||
| `agents.yaml` | AI 에이전트 및 역할 정의 |
|
||||
| `tasks.yaml` | 에이전트 태스크 및 워크플로우 설정 |
|
||||
| `crew.jsonc` | crew, 태스크 순서, 프로세스, 기본 입력값 설정 |
|
||||
| `agents/*.jsonc` | 각 에이전트의 역할, 목표, backstory, LLM, 도구, 동작 정의 |
|
||||
| `.env` | API 키 및 환경 변수 저장 |
|
||||
| `main.py` | 프로젝트 진입점 및 실행 흐름 |
|
||||
| `crew.py` | Crew 오케스트레이션 및 코디네이션 |
|
||||
| `tools/` | 커스텀 에이전트 도구 디렉터리 |
|
||||
| `knowledge/` | 지식 베이스 디렉터리 |
|
||||
| `tools/` | `custom:<name>` 도구를 위한 선택적 Python 파일 |
|
||||
| `knowledge/` | 에이전트용 선택적 지식 파일 |
|
||||
| `skills/` | crew에 적용할 선택적 skill 파일 |
|
||||
|
||||
- `agents.yaml` 및 `tasks.yaml`을 편집하여 crew의 동작을 정의하는 것부터 시작하세요.
|
||||
- `crew.jsonc`와 `agents/` 안의 파일을 편집하여 crew 동작을 정의하세요.
|
||||
- 에이전트와 태스크 텍스트에 `{placeholder}`를 사용하고, `crew.jsonc`의 `inputs`에 기본값을 넣으세요. `crewai run` 실행 시 빠진 값은 CLI가 묻습니다.
|
||||
- API 키와 같은 민감한 정보는 `.env` 파일에 보관하세요.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -5,11 +5,15 @@ icon: "at"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
이 가이드는 `crew.py` 파일에서 **agent**, **task**, 및 기타 구성 요소를 올바르게 참조하기 위해 주석을 사용하는 방법을 설명합니다.
|
||||
이 가이드는 클래식 `crew.py` 파일에서 **agent**, **task**, 및 기타 구성 요소를 올바르게 참조하기 위해 어노테이션을 사용하는 방법을 설명합니다.
|
||||
|
||||
<Note>
|
||||
`crewai create crew <name>`으로 만든 새 프로젝트는 JSON-first이며 `crew.jsonc`와 `agents/*.jsonc`를 사용합니다. 이 가이드는 `crewai create crew <name> --classic`으로 만든 클래식 프로젝트, 기존 Python/YAML 프로젝트 마이그레이션, 또는 Python 데코레이터 제어가 필요한 경우에 사용하세요.
|
||||
</Note>
|
||||
|
||||
## 소개
|
||||
|
||||
CrewAI 프레임워크에서 어노테이션은 클래스와 메소드를 데코레이트하는 데 사용되며, crew의 다양한 컴포넌트에 메타데이터와 기능을 제공합니다. 이러한 어노테이션은 코드의 구성과 구조화를 돕고, 코드의 가독성과 유지 관리를 용이하게 만듭니다.
|
||||
CrewAI 프레임워크에서 어노테이션은 클래스와 메소드를 데코레이트하는 데 사용되며, crew의 다양한 컴포넌트에 메타데이터와 기능을 제공합니다. 클래식 Python/YAML 프로젝트에서는 `config/agents.yaml`, `config/tasks.yaml`을 로드하고 `Crew` 객체를 반환하는 코드를 구조화합니다.
|
||||
|
||||
## 사용 가능한 어노테이션
|
||||
|
||||
@@ -113,9 +117,9 @@ def crew(self) -> Crew:
|
||||
|
||||
`@crew` 어노테이션은 `Crew` 객체를 생성하고 반환하는 메서드를 데코레이션하는 데 사용됩니다. 이 메서드는 모든 구성 요소(agents와 tasks)를 기능적인 crew로 조합합니다.
|
||||
|
||||
## YAML 구성
|
||||
## 클래식 YAML 구성
|
||||
|
||||
에이전트 구성은 일반적으로 YAML 파일에 저장됩니다. 아래는 연구원 에이전트에 대한 `agents.yaml` 파일 예시입니다.
|
||||
클래식 프로젝트에서 에이전트 구성은 일반적으로 YAML 파일에 저장됩니다. 아래는 연구원 에이전트에 대한 `agents.yaml` 파일 예시입니다.
|
||||
|
||||
```yaml
|
||||
researcher:
|
||||
@@ -146,6 +150,6 @@ YAML 파일의 `llm`과 `tools`가 Python 클래스에서 `@llm` 및 `@tool`로
|
||||
- **일관성 있는 명명**: 메서드에 대해 명확하고 일관성 있는 명명 규칙을 사용하세요. 예를 들어, agent 메서드는 역할에 따라 이름을 지정할 수 있습니다(예: researcher, reporting_analyst).
|
||||
- **환경 변수**: API 키와 같은 민감한 정보를 위해 환경 변수를 사용하세요.
|
||||
- **유연성**: agent와 task를 쉽게 추가 및 제거할 수 있도록 crew를 유연하게 설계하세요.
|
||||
- **YAML-코드 일치**: YAML 파일의 이름과 구조가 Python 코드의 데코레이터가 적용된 메서드와 정확히 일치하는지 확인하세요.
|
||||
- **YAML-코드 일치**: 클래식 프로젝트에서는 YAML 파일의 이름과 구조가 Python 코드의 데코레이터가 적용된 메서드와 정확히 일치하는지 확인하세요.
|
||||
|
||||
이 지침을 따르고 주석을 올바르게 사용하면 CrewAI 프레임워크를 이용해 구조적이고 유지보수가 쉬운 crew를 만들 수 있습니다.
|
||||
이 지침을 따르고 어노테이션을 올바르게 사용하면 클래식 crew를 구조적이고 유지보수하기 쉽게 유지할 수 있습니다. 새 crew에는 [Crews](/ko/concepts/crews)의 JSON-first 구조를 권장합니다.
|
||||
|
||||
@@ -39,84 +39,60 @@ CrewAI를 아직 설치하지 않았다면 먼저 [설치 가이드](/ko/install
|
||||
이렇게 하면 `src/latest_ai_flow/` 아래에 Flow 앱이 만들어지고, 다음 단계에서 **단일 에이전트** 연구 crew로 바꿀 시작용 crew가 `crews/content_crew/`에 포함됩니다.
|
||||
</Step>
|
||||
|
||||
<Step title="`agents.yaml`에 에이전트 하나 설정">
|
||||
`src/latest_ai_flow/crews/content_crew/config/agents.yaml` 내용을 한 명의 연구원만 남기도록 바꿉니다. `{topic}` 같은 변수는 `crew.kickoff(inputs=...)`로 채워집니다.
|
||||
<Step title="JSONC로 에이전트 하나 설정">
|
||||
`src/latest_ai_flow/crews/content_crew/agents/researcher.jsonc`를 만듭니다(`agents/` 디렉터리가 없으면 생성). `{topic}` 같은 변수는 `crew.kickoff(inputs=...)`로 채워집니다.
|
||||
|
||||
```yaml agents.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
{topic} 시니어 데이터 리서처
|
||||
goal: >
|
||||
{topic} 분야의 최신 동향을 파악한다
|
||||
backstory: >
|
||||
당신은 {topic}의 최신 흐름을 찾아내는 데 능숙한 연구원입니다.
|
||||
가장 관련성 높은 정보를 찾아 명확하게 전달합니다.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} 시니어 데이터 리서처",
|
||||
"goal": "{topic} 분야의 최신 동향을 파악한다",
|
||||
"backstory": "당신은 가장 관련성 높은 정보를 찾아 명확하게 전달하는 연구원입니다.",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="`tasks.yaml`에 작업 하나 설정">
|
||||
```yaml tasks.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
{topic}에 대해 철저히 조사하세요. 웹 검색으로 최신이고 신뢰할 수 있는 정보를 찾으세요.
|
||||
현재 연도는 2026년입니다.
|
||||
expected_output: >
|
||||
마크다운 보고서로, 주요 트렌드·주목할 도구나 기업·시사점 등으로 섹션을 나누세요.
|
||||
분량은 약 800~1200단어. 문서 전체를 코드 펜스로 감싸지 마세요.
|
||||
agent: researcher
|
||||
output_file: output/report.md
|
||||
<Step title="`crew.jsonc`에 crew 설정">
|
||||
`src/latest_ai_flow/crews/content_crew/crew.jsonc`를 만듭니다:
|
||||
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "{topic}에 대해 철저히 조사하세요. 웹 검색으로 최신이고 신뢰할 수 있는 정보를 찾으세요. 현재 연도는 2026년입니다.",
|
||||
"expected_output": "마크다운 보고서로, 주요 트렌드·주목할 도구나 기업·시사점 등으로 섹션을 나누세요. 분량은 약 800~1200단어. 문서 전체를 코드 펜스로 감싸지 마세요.",
|
||||
"agent": "researcher",
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="crew 클래스 연결 (`content_crew.py`)">
|
||||
생성된 crew가 YAML을 읽고 연구원에게 `SerperDevTool`을 붙이도록 합니다.
|
||||
<Step title="JSON crew 로드 (`content_crew.py`)">
|
||||
생성된 `content_crew.py`를 `crew.jsonc`를 `Crew`로 바꾸는 작은 loader로 교체합니다.
|
||||
|
||||
```python content_crew.py
|
||||
# src/latest_ai_flow/crews/content_crew/content_crew.py
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.project import load_crew
|
||||
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew:
|
||||
"""Flow 안에서 사용하는 단일 에이전트 연구 crew."""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
agents_config = "config/agents.yaml"
|
||||
tasks_config = "config/tasks.yaml"
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["researcher"], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()],
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config["research_task"], # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -130,7 +106,7 @@ CrewAI를 아직 설치하지 않았다면 먼저 [설치 가이드](/ko/install
|
||||
|
||||
from crewai.flow import Flow, listen, start
|
||||
|
||||
from latest_ai_flow.crews.content_crew.content_crew import ResearchCrew
|
||||
from latest_ai_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
|
||||
class ResearchFlowState(BaseModel):
|
||||
@@ -149,7 +125,7 @@ CrewAI를 아직 설치하지 않았다면 먼저 [설치 가이드](/ko/install
|
||||
|
||||
@listen(prepare_topic)
|
||||
def run_research(self):
|
||||
result = ResearchCrew().crew().kickoff(inputs={"topic": self.state.topic})
|
||||
result = kickoff_content_crew(inputs={"topic": self.state.topic})
|
||||
self.state.report = result.raw
|
||||
print("연구 crew 실행 완료.")
|
||||
|
||||
@@ -171,7 +147,7 @@ CrewAI를 아직 설치하지 않았다면 먼저 [설치 가이드](/ko/install
|
||||
```
|
||||
|
||||
<Tip>
|
||||
패키지 이름이 `latest_ai_flow`가 아니면 `ResearchCrew` import 경로를 프로젝트 모듈 경로에 맞게 바꾸세요.
|
||||
패키지 이름이 `latest_ai_flow`가 아니면 `kickoff_content_crew` import 경로를 프로젝트 모듈 경로에 맞게 바꾸세요.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
@@ -219,7 +195,7 @@ CrewAI를 아직 설치하지 않았다면 먼저 [설치 가이드](/ko/install
|
||||
## 한 번에 이해하기
|
||||
|
||||
1. **Flow** — `LatestAiFlow`는 `prepare_topic` → `run_research` → `summarize` 순으로 실행됩니다. 상태(`topic`, `report`)는 Flow에 있습니다.
|
||||
2. **Crew** — `ResearchCrew`는 에이전트 한 명·작업 하나로 실행됩니다. 연구원이 **Serper**로 웹을 검색하고 구조화된 보고서를 씁니다.
|
||||
2. **Crew** — `kickoff_content_crew`가 `crew.jsonc`를 로드하고 에이전트 한 명·작업 하나로 실행합니다. 연구원이 **Serper**로 웹을 검색하고 구조화된 보고서를 씁니다.
|
||||
3. **결과물** — 작업의 `output_file`이 `output/report.md`에 보고서를 씁니다.
|
||||
|
||||
Flow 패턴(라우팅, 지속성, human-in-the-loop)을 더 보려면 [첫 Flow 만들기](/ko/guides/flows/first-flow)와 [Flows](/ko/concepts/flows)를 참고하세요. Flow 없이 crew만 쓰려면 [Crews](/ko/concepts/crews)를, 작업 없이 단일 `Agent`의 `kickoff()`만 쓰려면 [Agents](/ko/concepts/agents#direct-agent-interaction-with-kickoff)를 참고하세요.
|
||||
@@ -230,7 +206,10 @@ Flow 패턴(라우팅, 지속성, human-in-the-loop)을 더 보려면 [첫 Flow
|
||||
|
||||
### 이름 일치
|
||||
|
||||
YAML 키(`researcher`, `research_task`)는 `@CrewBase` 클래스의 메서드 이름과 같아야 합니다. 전체 데코레이터 패턴은 [Crews](/ko/concepts/crews)를 참고하세요.
|
||||
`crew.jsonc`의 이름은 파일과 참조에 맞아야 합니다:
|
||||
|
||||
- `agents: ["researcher"]`는 `agents/researcher.jsonc`를 로드합니다.
|
||||
- `context: ["research_task"]`는 이전 태스크 `research_task`를 참조합니다.
|
||||
|
||||
## 배포
|
||||
|
||||
|
||||
@@ -71,13 +71,39 @@ O Construtor Visual de Agentes permite:
|
||||
|
||||
## Criando Agentes
|
||||
|
||||
Existem duas maneiras de criar agentes no CrewAI: usando **configuração YAML (recomendado)** ou definindo-os **diretamente em código**.
|
||||
Existem duas formas comuns de criar agentes no CrewAI: usando **configuração JSONC (recomendado para novas crews)** ou definindo-os **diretamente em código**.
|
||||
|
||||
### Configuração em YAML (Recomendado)
|
||||
### Configuração JSONC (Recomendado)
|
||||
|
||||
Usar configuração em YAML proporciona uma maneira mais limpa e fácil de manter para definir agentes. Recomendamos fortemente esse método em seus projetos CrewAI.
|
||||
Novos projetos criados com `crewai create crew <name>` usam configuração JSON-first. Cada agente fica em `agents/<agent_name>.jsonc`, e `crew.jsonc` lista quais agentes fazem parte da crew.
|
||||
|
||||
Depois de criar seu projeto CrewAI conforme descrito na seção de [Instalação](/pt-BR/installation), navegue até o arquivo `src/latest_ai_development/config/agents.yaml` e edite o template para atender aos seus requisitos.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "{topic} Senior Data Researcher",
|
||||
"goal": "Uncover cutting-edge developments in {topic}",
|
||||
"backstory": "You find the most relevant information and present it clearly.",
|
||||
"llm": "openai/gpt-4o",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `{placeholder}` em `role`, `goal` ou `backstory`. Defina padrões em `crew.jsonc` dentro de `inputs`; `crewai run` pergunta por valores que estiverem faltando. Campos de comportamento como `verbose`, `allow_delegation`, `max_iter`, `memory`, `cache` e `planning` podem ficar no topo ou em `settings`.
|
||||
|
||||
<Note>
|
||||
JSONC aceita comentários e vírgulas finais. Se `agents/<name>.jsonc` e `agents/<name>.json` existirem, CrewAI usa o arquivo JSONC.
|
||||
</Note>
|
||||
|
||||
### Configuração YAML Clássica
|
||||
|
||||
Projetos clássicos criados com `crewai create crew <name> --classic` usam `config/agents.yaml` e uma classe `@CrewBase` em `crew.py`.
|
||||
|
||||
A configuração YAML continua suportada para projetos existentes em Python/YAML e para equipes que preferem definir agentes a partir de uma classe `@CrewBase`.
|
||||
|
||||
Depois de criar um projeto clássico, navegue até o arquivo `src/latest_ai_development/config/agents.yaml` e edite o template para atender aos seus requisitos.
|
||||
|
||||
<Note>
|
||||
Variáveis em seus arquivos YAML (como `{topic}`) serão substituídas pelos valores fornecidos em seus inputs ao executar o crew:
|
||||
|
||||
@@ -53,6 +53,8 @@ crewai create crew my_new_crew
|
||||
crewai create flow my_new_flow
|
||||
```
|
||||
|
||||
Por padrão, `crewai create crew` cria um projeto JSON-first com `crew.jsonc` e `agents/*.jsonc`. Use `crewai create crew my_new_crew --classic` somente quando quiser o scaffold antigo em Python/YAML com `crew.py`, `config/agents.yaml` e `config/tasks.yaml`.
|
||||
|
||||
### 2. Version
|
||||
|
||||
Mostre a versão instalada do CrewAI.
|
||||
@@ -203,7 +205,20 @@ crewai chat
|
||||
Garanta que você execute estes comandos a partir do diretório raiz do seu projeto CrewAI.
|
||||
</Note>
|
||||
<Note>
|
||||
IMPORTANTE: Defina a propriedade `chat_llm` no seu arquivo `crew.py` para habilitar este comando.
|
||||
IMPORTANTE: Defina a propriedade `chat_llm` na definição da sua crew para habilitar este comando.
|
||||
|
||||
Para crews JSON-first, adicione em `crew.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "My Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [],
|
||||
"chat_llm": "openai/gpt-4o"
|
||||
}
|
||||
```
|
||||
|
||||
Para crews clássicas Python/YAML, defina em `crew.py`:
|
||||
|
||||
```python
|
||||
@crew
|
||||
@@ -334,7 +349,7 @@ Assista ao vídeo tutorial para uma demonstração passo-a-passo de implantaçã
|
||||
|
||||
### 11. Chaves de API
|
||||
|
||||
Ao executar o comando `crewai create crew`, o CLI primeiro mostrará os 5 provedores de LLM mais comuns e pedirá para você selecionar um.
|
||||
Ao executar o comando `crewai create crew`, o CLI mostrará provedores de LLM disponíveis e depois a seleção de modelo para o provedor escolhido. O modelo selecionado é salvo no `.env` gerado, e cada agente JSONC pode definir seu próprio `llm`.
|
||||
|
||||
Após selecionar um provedor de LLM, será solicitado que você informe as chaves de API.
|
||||
|
||||
|
||||
@@ -41,13 +41,54 @@ Uma crew no crewAI representa um grupo colaborativo de agentes trabalhando em co
|
||||
|
||||
## Criando Crews
|
||||
|
||||
Existem duas maneiras de criar crews no CrewAI: utilizando **configuração YAML (recomendado)** ou definindo diretamente **em código**.
|
||||
Existem duas maneiras principais de criar crews no CrewAI: utilizando **configuração JSONC (recomendada para novas crews)** ou definindo a crew **em código** para projetos clássicos e casos avançados.
|
||||
|
||||
### Configuração YAML (Recomendado)
|
||||
### Configuração JSONC (Recomendado)
|
||||
|
||||
O uso da configuração YAML proporciona uma forma mais limpa e fácil de manter para definir crews, sendo consistente com a definição de agentes e tasks em projetos CrewAI.
|
||||
Novos projetos criados com `crewai create crew <name>` usam `crew.jsonc` para configurações da crew e tarefas, além de um arquivo por agente em `agents/`. `crewai run` detecta `crew.jsonc` ou `crew.json`, carrega os agentes referenciados, pergunta por placeholders ausentes e inicia a crew.
|
||||
|
||||
Após criar seu projeto CrewAI conforme descrito na seção [Instalação](/pt-BR/installation), você pode definir sua crew em uma classe que herda de `CrewBase` e utiliza decorators para definir agentes, tarefas e a própria crew.
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Market Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research",
|
||||
"description": "Research {topic} and collect the most relevant facts.",
|
||||
"expected_output": "Structured research notes about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis",
|
||||
"description": "Analyze the research and write a concise report.",
|
||||
"expected_output": "A markdown report with findings and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research"],
|
||||
"output_file": "output/report.md"
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cada string em `agents` resolve primeiro para `agents/<name>.jsonc` e depois para `agents/<name>.json`. Para crews hierárquicas, use `"process": "hierarchical"` com `manager_llm` ou `manager_agent`.
|
||||
|
||||
<Warning>
|
||||
Execute projetos JSON apenas de fontes confiáveis. Ferramentas `custom:<name>` e referências `{"python": "module.attribute"}` executam código Python local quando a crew é carregada.
|
||||
</Warning>
|
||||
|
||||
### Configuração YAML Clássica
|
||||
|
||||
Projetos clássicos criados com `crewai create crew <name> --classic` usam `crew.py`, `config/agents.yaml`, `config/tasks.yaml` e os decorators `@CrewBase`, `@agent`, `@task` e `@crew`.
|
||||
|
||||
Essa abordagem continua suportada para projetos existentes em Python/YAML e para equipes que precisam de controle explícito via decorators.
|
||||
|
||||
Após criar um projeto clássico, você pode definir sua crew em uma classe que herda de `CrewBase` e utiliza decorators para definir agentes, tarefas e a própria crew.
|
||||
|
||||
#### Exemplo de Classe Crew com Decorators
|
||||
|
||||
|
||||
@@ -219,6 +219,49 @@ Após o término da execução, é possível acessar o estado final e observar a
|
||||
Ao garantir que a saída do método final seja retornada e oferecer acesso ao estado, o CrewAI Flows facilita a integração dos resultados dos seus workflows de IA em aplicações maiores,
|
||||
além de permitir o gerenciamento e o acesso ao estado durante toda a execução do Flow.
|
||||
|
||||
## Métricas de Uso do Flow
|
||||
|
||||
Após a execução de um Flow, você pode acessar a propriedade `usage_metrics` para visualizar o consumo agregado de tokens em **todas as chamadas de LLM** realizadas durante a execução — incluindo chamadas das Crews orquestradas pelo Flow, chamadas dentro de tools de Agents, e invocações diretas de `LLM.call(...)` feitas a partir de métodos do Flow. Esse é o equivalente, do lado do SDK, ao total exibido na interface do CrewAI Enterprise.
|
||||
|
||||
```python Code
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
|
||||
class UsageMetricsFlow(Flow):
|
||||
@start()
|
||||
def run_first_crew(self):
|
||||
self.state.first_result = FirstCrew().crew().kickoff()
|
||||
|
||||
@listen(run_first_crew)
|
||||
def call_llm_directly(self):
|
||||
# Chamada direta de LLM — também contabilizada por flow.usage_metrics
|
||||
llm = LLM(model="openai/gpt-4o-mini")
|
||||
self.state.summary = llm.call("Resuma os principais pontos.")
|
||||
|
||||
@listen(call_llm_directly)
|
||||
def run_second_crew(self):
|
||||
self.state.second_result = SecondCrew().crew().kickoff()
|
||||
|
||||
flow = UsageMetricsFlow()
|
||||
flow.kickoff()
|
||||
|
||||
print(flow.usage_metrics)
|
||||
# UsageMetrics(total_tokens=8579, prompt_tokens=6210, completion_tokens=2369,
|
||||
# cached_prompt_tokens=0, reasoning_tokens=0,
|
||||
# cache_creation_tokens=0, successful_requests=5)
|
||||
```
|
||||
|
||||
<Note>
|
||||
`flow.usage_metrics` **não** é o mesmo que `flow.kickoff().token_usage`. Este
|
||||
último retorna apenas o `CrewOutput.token_usage` do **último** método
|
||||
`@listen` que retornou um `CrewOutput`, ou seja, reflete somente a Crew
|
||||
final e ignora completamente as Crews anteriores e quaisquer chamadas
|
||||
diretas de `LLM.call(...)`. Use `flow.usage_metrics` sempre que precisar do
|
||||
rollup **completo** de tokens da execução do Flow.
|
||||
</Note>
|
||||
|
||||
Cada campo do [`UsageMetrics`](https://github.com/crewAIInc/crewAI/blob/main/lib/crewai/src/crewai/types/usage_metrics.py) retornado representa a soma de todas as chamadas de LLM feitas em uma única invocação de `flow.kickoff()`. Os contadores são resetados a cada novo `kickoff()` (e em cada iteração de `kickoff_for_each`), de modo que execuções sucessivas não duplicam o total. A propriedade é segura para ser lida em qualquer momento após o `kickoff()`; lê-la durante a execução retorna o total parcial acumulado até aquele instante.
|
||||
|
||||
## Gerenciamento de Estado em Flows
|
||||
|
||||
Gerenciar o estado de forma eficaz é fundamental para construir fluxos de trabalho de IA confiáveis e de fácil manutenção. O CrewAI Flows oferece mecanismos robustos para o gerenciamento de estado tanto não estruturado quanto estruturado,
|
||||
@@ -777,7 +820,7 @@ Você pode gerar um novo projeto CrewAI que já inclui toda a estrutura para cri
|
||||
crewai create flow name_of_flow
|
||||
```
|
||||
|
||||
Esse comando irá gerar um novo projeto CrewAI com a estrutura de pastas necessária. O projeto gerado inclui uma crew pré-criada chamada `poem_crew`, já funcional. Você pode usar essa crew como modelo, copiando, colando e editando para criar outras crews.
|
||||
Esse comando irá gerar um novo projeto CrewAI com a estrutura de pastas necessária. O projeto gerado inclui uma crew pré-criada chamada `poem_crew`, já funcional. A crew embutida inicial usa a estrutura clássica Python/YAML; novas crews independentes criadas com `crewai create crew` usam a estrutura JSON-first.
|
||||
|
||||
### Estrutura de Pastas
|
||||
|
||||
@@ -807,7 +850,29 @@ Na pasta `crews`, você pode definir múltiplas crews. Cada crew tem sua própri
|
||||
- `config/tasks.yaml`: Define as tarefas da crew.
|
||||
- `poem_crew.py`: Contém a definição da crew, incluindo agentes, tarefas, etc.
|
||||
|
||||
Você pode copiar, colar e editar a `poem_crew` para criar outras crews.
|
||||
Você pode copiar, colar e editar a `poem_crew` para criar outras crews clássicas embutidas.
|
||||
|
||||
Para crews embutidas JSON-first, use uma pasta com `crew.jsonc` e `agents/*.jsonc`:
|
||||
|
||||
```text
|
||||
crews/
|
||||
└── research_crew/
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
└── crew.jsonc
|
||||
```
|
||||
|
||||
Depois carregue a crew em uma etapa do Flow:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.project import load_crew
|
||||
|
||||
crew, default_inputs = load_crew(
|
||||
Path(__file__).parent / "crews" / "research_crew" / "crew.jsonc"
|
||||
)
|
||||
result = crew.kickoff(inputs={**default_inputs, "topic": "AI Agents"})
|
||||
```
|
||||
|
||||
### Conectando Crews no `main.py`
|
||||
|
||||
|
||||
@@ -67,13 +67,48 @@ crew = Crew(
|
||||
|
||||
## Criando Tarefas
|
||||
|
||||
Existem duas maneiras de criar tarefas no CrewAI: utilizando **configuração YAML (recomendado)** ou definindo-as **diretamente no código**.
|
||||
Existem duas formas comuns de criar tarefas no CrewAI: usando **configuração JSONC (recomendado para novas crews)** ou definindo-as **diretamente no código**.
|
||||
|
||||
### Configuração YAML (Recomendado)
|
||||
### Configuração JSONC (Recomendado)
|
||||
|
||||
Utilizar configuração YAML oferece uma forma mais limpa e de fácil manutenção para definir tarefas. Recomendamos fortemente esse método em seus projetos CrewAI.
|
||||
Novos projetos criados com `crewai create crew <name>` definem tarefas no `crew.jsonc`.
|
||||
|
||||
Após criar seu projeto CrewAI conforme indicado na seção [Instalação](/pt-BR/installation), navegue até o arquivo `src/latest_ai_development/config/tasks.yaml` e modifique o template para refletir os requisitos específicos das tarefas.
|
||||
````jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "reporting_analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research about {topic}.",
|
||||
"expected_output": "A list of the most relevant information about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "reporting_task",
|
||||
"description": "Review the research and expand it into a detailed report.",
|
||||
"expected_output": "A polished markdown report.",
|
||||
"agent": "reporting_analyst",
|
||||
"context": ["research_task"],
|
||||
"markdown": true,
|
||||
"output_file": "report.md"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
Cada tarefa precisa de `description` e `expected_output`. O valor de `agent` deve corresponder a um agente listado em `agents`. `context` referencia nomes de tarefas anteriores; referências futuras são rejeitadas. Campos comuns incluem `name`, `agent`, `context`, `output_file`, `tools`, `human_input`, `async_execution`, `guardrail`, `guardrails`, `markdown`, `output_json`, `output_pydantic` e `response_model`.
|
||||
|
||||
### Configuração YAML Clássica
|
||||
|
||||
Projetos clássicos criados com `crewai create crew <name> --classic` usam `config/tasks.yaml` e uma classe `@CrewBase` em `crew.py`.
|
||||
|
||||
A configuração YAML continua suportada para projetos existentes em Python/YAML e para equipes que preferem definir tarefas a partir de uma classe `@CrewBase`.
|
||||
|
||||
Após criar um projeto clássico, navegue até o arquivo `src/latest_ai_development/config/tasks.yaml` e modifique o template para refletir os requisitos específicos das tarefas.
|
||||
|
||||
<Note>
|
||||
Variáveis em seus arquivos YAML (como `{topic}`) serão substituídas por valores vindos dos seus inputs ao executar o crew:
|
||||
|
||||
@@ -26,10 +26,10 @@ Antes de executar esta verificação:
|
||||
|
||||
## Passo 1 — Estruturar um Crew de Verificação
|
||||
|
||||
Crie um novo projeto de crew. O CrewAI CLI estrutura tudo:
|
||||
Crie um projeto de crew clássico porque este exemplo conecta uma ferramenta Python via `crew.py`:
|
||||
|
||||
```bash
|
||||
crewai create crew rotation_verifier --skip_provider
|
||||
crewai create crew rotation_verifier --classic --skip_provider
|
||||
cd rotation_verifier
|
||||
```
|
||||
|
||||
|
||||
@@ -373,17 +373,17 @@ git push
|
||||
|
||||
**Solução**: Verifique se seu projeto corresponde à estrutura esperada:
|
||||
|
||||
- **Tanto Crews quanto Flows**: Devem ter ponto de entrada em `src/project_name/main.py`
|
||||
- **Crews**: Usam uma função `run()` como ponto de entrada
|
||||
- **Flows**: Usam uma função `kickoff()` como ponto de entrada
|
||||
- **Crews JSON-first**: Mantenha `crew.jsonc` ou `crew.json` e `agents/` na raiz do projeto
|
||||
- **Crews clássicas**: Use `src/project_name/main.py` com uma função de entrada `run()`
|
||||
- **Flows**: Use `src/project_name/main.py` com uma função de entrada `kickoff()`
|
||||
|
||||
Veja [Preparar para Implantação](/pt-BR/enterprise/guides/prepare-for-deployment) para diagramas de estrutura detalhados.
|
||||
|
||||
#### Decorador CrewBase Ausente
|
||||
#### Decorador CrewBase Ausente em uma Crew Clássica
|
||||
|
||||
**Sintoma**: Erros "Crew not found", "Config not found" ou erros de configuração de agent/task
|
||||
|
||||
**Solução**: Certifique-se de que **todas** as classes crew usam o decorador `@CrewBase`:
|
||||
**Solução**: Para crews clássicas Python/YAML, certifique-se de que todas as classes crew usam o decorador `@CrewBase`. Crews JSON-first não precisam desse decorador.
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
@@ -403,8 +403,8 @@ class YourCrew():
|
||||
```
|
||||
|
||||
<Info>
|
||||
Isso se aplica a Crews independentes E crews embutidos dentro de projetos Flow.
|
||||
Toda classe crew precisa do decorador.
|
||||
Isso se aplica a classes crew Python clássicas, incluindo crews clássicas embutidas em projetos Flow.
|
||||
Crews JSON-first são validadas a partir de `crew.jsonc` e `agents/`.
|
||||
</Info>
|
||||
|
||||
#### Tipo Incorreto no pyproject.toml
|
||||
@@ -441,8 +441,8 @@ type = "flow"
|
||||
**Solução**:
|
||||
1. Verifique os logs de execução no dashboard AMP (aba Traces)
|
||||
2. Verifique se todas as ferramentas têm as chaves API necessárias configuradas
|
||||
3. Certifique-se de que as configurações de agents em `agents.yaml` são válidas
|
||||
4. Verifique se há erros de sintaxe nas configurações de tasks em `tasks.yaml`
|
||||
3. Para crews JSON-first, valide `crew.jsonc` e os arquivos referenciados em `agents/`
|
||||
4. Para crews clássicas, verifique se `agents.yaml` e `tasks.yaml` são válidos
|
||||
|
||||
<Card title="Precisa de Ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nossa equipe de suporte para ajuda com questões de
|
||||
|
||||
@@ -24,10 +24,9 @@ company-ai/
|
||||
`-- crews/
|
||||
|-- support_agent/
|
||||
| |-- pyproject.toml
|
||||
| `-- src/
|
||||
| `-- support_agent/
|
||||
| |-- main.py
|
||||
| `-- crew.py
|
||||
| |-- crew.jsonc
|
||||
| `-- agents/
|
||||
| `-- support_agent.jsonc
|
||||
`-- research_flow/
|
||||
|-- pyproject.toml
|
||||
`-- src/
|
||||
@@ -48,7 +47,7 @@ selecionada como a raiz do projeto da automação.
|
||||
|
||||
Quando um diretório de trabalho é definido, o AMP usa essa pasta para:
|
||||
|
||||
- Validação do projeto, incluindo `pyproject.toml`, `src/` e o ponto de entrada do Crew ou Flow
|
||||
- Validação do projeto, incluindo `pyproject.toml`, arquivos de crew JSON e qualquer ponto de entrada clássico de Crew ou Flow
|
||||
- Instalação de dependências com `uv`
|
||||
- O diretório de trabalho do processo em execução
|
||||
- A variável de ambiente `CREW_ROOT_DIR`
|
||||
|
||||
@@ -24,7 +24,7 @@ Entender qual tipo você está implantando é essencial porque eles têm estrutu
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Projetos Crew" icon="users">
|
||||
Equipes de agentes de IA independentes com `crew.py` definindo agentes e tarefas. Ideal para tarefas focadas e colaborativas.
|
||||
Equipes independentes de agentes de IA. Novas crews são JSON-first com `crew.jsonc` e `agents/`; crews clássicas ainda podem usar `crew.py`.
|
||||
</Card>
|
||||
<Card title="Projetos Flow" icon="diagram-project">
|
||||
Workflows orquestrados com crews embutidos em uma pasta `crews/`. Ideal para processos complexos de múltiplas etapas.
|
||||
@@ -33,19 +33,19 @@ Entender qual tipo você está implantando é essencial porque eles têm estrutu
|
||||
|
||||
| Aspecto | Crew | Flow |
|
||||
|---------|------|------|
|
||||
| **Estrutura do projeto** | `src/project_name/` com `crew.py` | `src/project_name/` com pasta `crews/` |
|
||||
| **Localização da lógica principal** | `src/project_name/crew.py` | `src/project_name/main.py` (classe Flow) |
|
||||
| **Função de ponto de entrada** | `run()` em `main.py` | `kickoff()` em `main.py` |
|
||||
| **Estrutura do projeto** | Raiz do projeto com `crew.jsonc` e `agents/` | `src/project_name/` com pasta `crews/` |
|
||||
| **Localização da lógica principal** | `crew.jsonc` (clássico: `src/project_name/crew.py`) | `src/project_name/main.py` (classe Flow) |
|
||||
| **Função de ponto de entrada** | Carregada a partir de `crew.jsonc` (clássico: `run()` em `main.py`) | `kickoff()` em `main.py` |
|
||||
| **Tipo no pyproject.toml** | `type = "crew"` | `type = "flow"` |
|
||||
| **Comando CLI de criação** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **Localização da configuração** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **Localização da configuração** | `crew.jsonc`, `agents/`, `tools/` opcional | `src/project_name/crews/crew_name/config/` ou pastas de crew JSON embutidas |
|
||||
| **Pode conter outros crews** | Não | Sim (na pasta `crews/`) |
|
||||
|
||||
## Referência de Estrutura de Projeto
|
||||
|
||||
### Estrutura de Projeto Crew
|
||||
|
||||
Quando você executa `crewai create crew my_crew`, você obtém esta estrutura:
|
||||
Quando você executa `crewai create crew my_crew`, recebe a estrutura JSON-first:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
@@ -54,24 +54,26 @@ my_crew/
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # OBRIGATÓRIO para implantação
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # Ponto de entrada com função run()
|
||||
├── crew.py # Classe Crew com decorador @CrewBase
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # Definições de agentes
|
||||
└── tasks.yaml # Definições de tarefas
|
||||
├── crew.jsonc # Configurações, tarefas, processo e inputs
|
||||
├── agents/
|
||||
│ └── researcher.jsonc # Definições de agentes
|
||||
├── tools/ # Ferramentas custom:<name> opcionais
|
||||
├── knowledge/
|
||||
└── skills/
|
||||
```
|
||||
|
||||
<Warning>
|
||||
A estrutura aninhada `src/project_name/` é crítica para Crews.
|
||||
Colocar arquivos no nível errado causará falhas na implantação.
|
||||
Para crews JSON-first, mantenha `crew.jsonc`, `agents/`, `tools/`, `knowledge/` e `skills/`
|
||||
na raiz do projeto. Colocá-los dentro de `src/` impede que `crewai run` e a validação de
|
||||
implantação encontrem a definição da crew.
|
||||
</Warning>
|
||||
|
||||
<Info>
|
||||
Projetos clássicos criados com `crewai create crew my_crew --classic` usam a estrutura antiga
|
||||
`src/project_name/crew.py`, `src/project_name/config/agents.yaml` e
|
||||
`src/project_name/config/tasks.yaml`. Essa estrutura continua suportada para crews Python com decorators.
|
||||
</Info>
|
||||
|
||||
### Estrutura de Projeto Flow
|
||||
|
||||
Quando você executa `crewai create flow my_flow`, você obtém esta estrutura:
|
||||
@@ -100,9 +102,9 @@ my_flow/
|
||||
```
|
||||
|
||||
<Info>
|
||||
Tanto Crews quanto Flows usam a estrutura `src/project_name/`.
|
||||
A diferença chave é que Flows têm uma pasta `crews/` para crews embutidos,
|
||||
enquanto Crews têm `crew.py` diretamente na pasta do projeto.
|
||||
Crews independentes JSON-first usam arquivos JSON na raiz do projeto. Flows ainda usam
|
||||
`src/project_name/` e podem conter crews embutidas clássicas ou pastas de crew JSON carregadas com
|
||||
`crewai.project.load_crew`.
|
||||
</Info>
|
||||
|
||||
## Checklist Pré-Implantação
|
||||
@@ -154,60 +156,90 @@ git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. Validar Uso do Decorador CrewBase
|
||||
### 3. Validar a Definição da Crew
|
||||
|
||||
**Toda classe crew deve usar o decorador `@CrewBase`.** Isso se aplica a:
|
||||
<Tabs>
|
||||
<Tab title="Crews JSON-first">
|
||||
Crews JSON-first precisam ter `crew.jsonc` ou `crew.json` na raiz do projeto.
|
||||
O array `agents` deve apontar para arquivos em `agents/`, e cada task deve referenciar
|
||||
um nome de agent válido.
|
||||
|
||||
- Projetos crew independentes
|
||||
- Crews embutidos dentro de projetos Flow
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research {topic}.",
|
||||
"expected_output": "A concise report.",
|
||||
"agent": "researcher"
|
||||
}
|
||||
],
|
||||
"inputs": {
|
||||
"topic": "AI Agents"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
Ferramentas customizadas são referenciadas como `"custom:<name>"` e devem existir em
|
||||
`tools/<name>.py` com uma subclasse de `BaseTool`.
|
||||
</Tab>
|
||||
<Tab title="Crews Python/YAML Clássicas">
|
||||
Crews clássicas e crews Python embutidas em Flows devem usar o decorador `@CrewBase`.
|
||||
|
||||
@CrewBase # Este decorador é OBRIGATÓRIO
|
||||
class MyCrew():
|
||||
"""Descrição do meu crew"""
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
@CrewBase
|
||||
class MyCrew():
|
||||
"""Descrição do meu crew"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
<Warning>
|
||||
Se você esquecer o decorador `@CrewBase`, sua implantação falhará com
|
||||
erros sobre configurações de agents ou tasks ausentes.
|
||||
</Warning>
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 4. Verificar Pontos de Entrada do Projeto
|
||||
|
||||
Tanto Crews quanto Flows têm seu ponto de entrada em `src/project_name/main.py`:
|
||||
Crews JSON-first independentes não precisam de um `src/project_name/main.py` escrito manualmente;
|
||||
`crewai run` e o empacotamento de implantação carregam `crew.jsonc` diretamente. Crews clássicas e Flows usam pontos de entrada Python:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Para Crews">
|
||||
<Tab title="Crews JSON-first">
|
||||
Execute localmente a partir da raiz do projeto:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Crews Clássicas">
|
||||
O ponto de entrada usa uma função `run()`:
|
||||
|
||||
```python
|
||||
@@ -278,16 +310,17 @@ grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
# 2. Verificar se uv.lock existe
|
||||
ls -la uv.lock || echo "ERRO: uv.lock ausente! Execute 'uv lock'"
|
||||
|
||||
# 3. Verificar se estrutura src/ existe
|
||||
ls -la src/*/main.py 2>/dev/null || echo "Nenhum main.py encontrado em src/"
|
||||
# 3. Para crews JSON-first, verificar crew.jsonc e agents/
|
||||
([ -f crew.jsonc ] || [ -f crew.json ]) || echo "Nenhum crew.jsonc ou crew.json encontrado"
|
||||
test -d agents || echo "Nenhum diretório agents/ encontrado"
|
||||
|
||||
# 4. Para Crews - verificar se crew.py existe
|
||||
# 4. Para Crews clássicas - verificar se crew.py existe
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "Nenhum crew.py (esperado para Crews)"
|
||||
|
||||
# 5. Para Flows - verificar se pasta crews/ existe
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "Nenhuma pasta crews/ (esperado para Flows)"
|
||||
|
||||
# 6. Verificar uso do CrewBase
|
||||
# 6. Para crews Python clássicas - verificar uso do CrewBase
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
@@ -297,8 +330,9 @@ grep -r "@CrewBase" . --include="*.py"
|
||||
|------|---------|----------|
|
||||
| `uv.lock` ausente | Build falha durante resolução de dependências | Execute `uv lock` e faça commit |
|
||||
| `type` errado no pyproject.toml | Build bem-sucedido mas falha em runtime | Altere para o tipo correto |
|
||||
| Decorador `@CrewBase` ausente | Erros "Config not found" | Adicione decorador a todas as classes crew |
|
||||
| Arquivos na raiz ao invés de `src/` | Ponto de entrada não encontrado | Mova para `src/project_name/` |
|
||||
| `crew.jsonc` ou `agents/` ausente em uma crew JSON-first | Definição da crew não encontrada | Mantenha `crew.jsonc` e `agents/` na raiz do projeto |
|
||||
| Decorador `@CrewBase` ausente em uma crew clássica | Erros "Config not found" | Adicione o decorador a todas as classes crew clássicas |
|
||||
| Arquivos clássicos na raiz ao invés de `src/` | Ponto de entrada não encontrado | Mova arquivos Python clássicos para `src/project_name/` |
|
||||
| `run()` ou `kickoff()` ausente | Não é possível iniciar automação | Adicione a função de entrada correta |
|
||||
|
||||
## Próximos Passos
|
||||
|
||||
@@ -43,7 +43,7 @@ O CrewAI é nativo de IA. Esta página reúne o que um agente de codificação c
|
||||
|
||||
| Skill | Quando é usada |
|
||||
|-------|----------------|
|
||||
| `getting-started` | Novos projetos, escolha entre `LLM.call()` / `Agent` / `Crew` / `Flow`, arquivos `crew.py` / `main.py` |
|
||||
| `getting-started` | Novos projetos, escolha entre `LLM.call()` / `Agent` / `Crew` / `Flow`, arquivos `crew.jsonc` / `main.py` |
|
||||
| `design-agent` | Configurar agentes — papel, objetivo, história, ferramentas, LLMs, memória, guardrails |
|
||||
| `design-task` | Descrever tarefas, dependências, saída estruturada (`output_pydantic`, `output_json`), revisão humana |
|
||||
| `ask-docs` | Consultar o [servidor MCP da documentação CrewAI](https://docs.crewai.com/mcp) em tempo real para detalhes de API |
|
||||
|
||||
@@ -1,393 +1,142 @@
|
||||
---
|
||||
title: Monte sua Primeira Crew
|
||||
description: Tutorial passo a passo para criar uma equipe colaborativa de IA que trabalha junta para resolver problemas complexos.
|
||||
title: Crie sua primeira Crew
|
||||
description: Tutorial passo a passo para criar uma equipe colaborativa de IA com configuração JSON-first.
|
||||
icon: users-gear
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Liberando o Poder da IA Colaborativa
|
||||
## Crie uma Crew de Pesquisa
|
||||
|
||||
Imagine ter uma equipe de agentes de IA especializados trabalhando juntos de forma harmoniosa para resolver problemas complexos, cada um contribuindo com suas habilidades únicas para alcançar um objetivo comum. Esse é o poder da CrewAI – um framework que permite criar sistemas colaborativos de IA que podem realizar tarefas muito além do que uma única IA conseguiria sozinha.
|
||||
|
||||
Neste guia, vamos criar uma crew de pesquisa que irá nos ajudar a pesquisar e analisar um tema, e então criar um relatório abrangente. Este exemplo prático demonstra como agentes de IA podem colaborar para realizar tarefas complexas, mas é apenas o começo do que é possível com a CrewAI.
|
||||
|
||||
### O que Você Vai Construir e Aprender
|
||||
|
||||
Ao final deste guia, você terá:
|
||||
|
||||
1. **Criado uma equipe de pesquisa em IA especializada** com papéis e responsabilidades distintas
|
||||
2. **Orquestrado a colaboração** entre múltiplos agentes de IA
|
||||
3. **Automatizado um fluxo de trabalho complexo** que envolve coleta de informações, análise e geração de relatórios
|
||||
4. **Desenvolvido habilidades fundamentais** que podem ser aplicadas em projetos mais ambiciosos
|
||||
|
||||
Embora estejamos criando uma crew de pesquisa simples neste guia, os mesmos padrões e técnicas podem ser aplicados para criar equipes muito mais sofisticadas para tarefas como:
|
||||
|
||||
- Criação de conteúdo em múltiplas etapas com redatores, editores e checadores de fatos especializados
|
||||
- Sistemas de atendimento ao cliente complexos com agentes de suporte em diferentes níveis
|
||||
- Analistas de negócios autônomos que coletam dados, criam visualizações e geram insights
|
||||
- Equipes de desenvolvimento de produto que idealizam, projetam e planejam a implementação
|
||||
|
||||
Vamos começar a construir sua primeira crew!
|
||||
Neste guia, você criará uma crew com dois agentes que pesquisa um tópico e escreve um relatório em markdown. Novos projetos de crew são JSON-first: agentes ficam em `agents/*.jsonc`, tarefas e configurações ficam em `crew.jsonc`, e `crewai run` carrega essa definição diretamente.
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
Antes de começar, certifique-se de que você:
|
||||
Antes de começar:
|
||||
|
||||
1. Instalou a CrewAI seguindo o [guia de instalação](/pt-BR/installation)
|
||||
2. Configurou sua chave de API de LLM no ambiente, conforme o [guia de configuração do LLM](/pt-BR/concepts/llms#setting-up-your-llm)
|
||||
3. Tem conhecimento básico de Python
|
||||
1. Instale o CrewAI seguindo o [guia de instalação](/pt-BR/installation)
|
||||
2. Configure sua chave de LLM seguindo o [guia de LLMs](/pt-BR/concepts/llms#setting-up-your-llm)
|
||||
3. Tenha uma chave [Serper.dev](https://serper.dev/) se quiser usar busca web
|
||||
|
||||
## Passo 1: Crie um Novo Projeto CrewAI
|
||||
|
||||
Primeiro, vamos criar um novo projeto CrewAI usando a CLI. Este comando irá configurar toda a estrutura do projeto com os arquivos necessários, permitindo que você foque em definir seus agentes e suas tarefas, em vez de se preocupar com código boilerplate.
|
||||
## Etapa 1: Criar uma nova Crew
|
||||
|
||||
```bash
|
||||
crewai create crew research_crew
|
||||
cd research_crew
|
||||
```
|
||||
|
||||
Isso irá gerar um projeto com a estrutura básica necessária para sua crew. A CLI cria automaticamente:
|
||||
Estrutura criada:
|
||||
|
||||
- Um diretório de projeto com os arquivos necessários
|
||||
- Arquivos de configuração para agentes e tarefas
|
||||
- Uma implementação básica da crew
|
||||
- Um script principal para rodar a crew
|
||||
|
||||
<Frame caption="CrewAI Framework Overview">
|
||||
<img src="/images/crews.png" alt="CrewAI Framework Overview" />
|
||||
</Frame>
|
||||
|
||||
|
||||
## Passo 2: Explore a Estrutura do Projeto
|
||||
|
||||
Vamos dedicar um momento para entender a estrutura do projeto criada pela CLI. A CrewAI segue boas práticas para projetos Python, tornando fácil manter e estender seu código à medida que suas crews se tornam mais complexas.
|
||||
|
||||
```
|
||||
```text
|
||||
research_crew/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── research_crew/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
|
||||
Esta estrutura segue as melhores práticas para projetos Python e facilita a organização do seu código. A separação dos arquivos de configuração (em YAML) do código de implementação (em Python) permite modificar o comportamento da sua crew sem alterar o código subjacente.
|
||||
<Tip>
|
||||
Precisa do layout antigo com `crew.py`, `config/agents.yaml` e `config/tasks.yaml`? Use `crewai create crew research_crew --classic`.
|
||||
</Tip>
|
||||
|
||||
## Passo 3: Configure seus Agentes
|
||||
## Etapa 2: Definir os agentes
|
||||
|
||||
Agora vem a parte divertida – definir seus agentes de IA! Na CrewAI, agentes são entidades especializadas com papéis, objetivos e históricos específicos que moldam seu comportamento. Pense neles como personagens em uma peça, cada um com sua personalidade e propósito próprios.
|
||||
Substitua o arquivo gerado `agents/researcher.jsonc` e adicione `agents/analyst.jsonc`. Os nomes dos arquivos são os nomes referenciados em `crew.jsonc`.
|
||||
|
||||
Para nossa crew de pesquisa, vamos criar dois agentes:
|
||||
1. Um **pesquisador** que é especialista em encontrar e organizar informações
|
||||
2. Um **analista** que pode interpretar os resultados da pesquisa e criar relatórios perspicazes
|
||||
|
||||
Vamos modificar o arquivo `agents.yaml` para definir esses agentes especializados. Certifique-se de
|
||||
definir `llm` para o provedor que você está utilizando.
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
Especialista Sênior em Pesquisa para {topic}
|
||||
goal: >
|
||||
Encontrar informações abrangentes e precisas sobre {topic}
|
||||
com foco em desenvolvimentos recentes e insights chave
|
||||
backstory: >
|
||||
Você é um especialista em pesquisa experiente com talento para
|
||||
encontrar informações relevantes de diversas fontes. Você se destaca em
|
||||
organizar informações de forma clara e estruturada, tornando temas complexos acessíveis para outros.
|
||||
llm: provider/model-id # ex: openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
analyst:
|
||||
role: >
|
||||
Analista de Dados e Redator de Relatórios para {topic}
|
||||
goal: >
|
||||
Analisar os resultados da pesquisa e criar um relatório abrangente e bem estruturado
|
||||
que apresente os insights de forma clara e envolvente
|
||||
backstory: >
|
||||
Você é um analista habilidoso com experiência em interpretação de dados
|
||||
e redação técnica. Tem talento para identificar padrões
|
||||
e extrair insights relevantes dos dados de pesquisa, comunicando esses insights de forma eficaz por meio de relatórios bem elaborados.
|
||||
llm: provider/model-id # ex: openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "Senior Research Specialist for {topic}",
|
||||
"goal": "Find comprehensive and accurate information about {topic}, with a focus on recent developments and key insights.",
|
||||
"backstory": "You are an experienced research specialist who organizes complex information into clear, useful notes.",
|
||||
// Substitua pelo seu modelo, por exemplo "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Perceba como cada agente tem um papel, objetivo e histórico distintos. Esses elementos não são apenas descritivos – eles efetivamente moldam como o agente aborda suas tarefas. Ao criar cuidadosamente esses detalhes, você pode ter agentes com habilidades e perspectivas que se complementam.
|
||||
|
||||
## Passo 4: Defina suas Tarefas
|
||||
|
||||
Com nossos agentes definidos, agora precisamos atribuir tarefas específicas para eles realizarem. Tarefas na CrewAI representam o trabalho concreto que os agentes irão executar, com instruções detalhadas e saídas esperadas.
|
||||
|
||||
Para nossa crew de pesquisa, vamos definir duas tarefas principais:
|
||||
1. Uma **tarefa de pesquisa** para coletar informações abrangentes
|
||||
2. Uma **tarefa de análise** para criar um relatório com insights
|
||||
|
||||
Vamos modificar o arquivo `tasks.yaml`:
|
||||
|
||||
```yaml
|
||||
# src/research_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
Realize uma pesquisa aprofundada sobre {topic}. Foque em:
|
||||
1. Conceitos e definições chave
|
||||
2. Desenvolvimento histórico e tendências recentes
|
||||
3. Principais desafios e oportunidades
|
||||
4. Aplicações relevantes ou estudos de caso
|
||||
5. Perspectivas futuras e novos desenvolvimentos
|
||||
|
||||
Certifique-se de organizar seus achados em um formato estruturado, com seções claras.
|
||||
expected_output: >
|
||||
Um documento de pesquisa abrangente com seções bem organizadas cobrindo
|
||||
todos os aspectos solicitados de {topic}. Inclua fatos, números
|
||||
e exemplos específicos sempre que possível.
|
||||
agent: researcher
|
||||
|
||||
analysis_task:
|
||||
description: >
|
||||
Analise os resultados da pesquisa e crie um relatório abrangente sobre {topic}.
|
||||
Seu relatório deve:
|
||||
1. Iniciar com um resumo executivo
|
||||
2. Incluir todas as informações relevantes da pesquisa
|
||||
3. Oferecer uma análise perspicaz de tendências e padrões
|
||||
4. Apresentar recomendações ou considerações futuras
|
||||
5. Estar formatado de forma profissional, clara e com títulos bem definidos
|
||||
expected_output: >
|
||||
Um relatório profissional, polido e estruturado sobre {topic} com apresentação dos resultados da pesquisa,
|
||||
acrescentando análise e insights. O relatório deve ser bem estruturado,
|
||||
incluindo resumo executivo, sessões principais e conclusão.
|
||||
agent: analyst
|
||||
context:
|
||||
- research_task
|
||||
output_file: output/report.md
|
||||
```jsonc agents/analyst.jsonc
|
||||
{
|
||||
"role": "Report Analyst for {topic}",
|
||||
"goal": "Turn research findings into a clear, well-structured report.",
|
||||
"backstory": "You are a careful analyst with strong technical writing skills and a talent for extracting useful insights.",
|
||||
// Substitua pelo seu modelo, por exemplo "openai/gpt-4o".
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true,
|
||||
"allow_delegation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note o campo `context` na tarefa de análise – esse é um recurso poderoso que permite ao analista acessar a saída da tarefa de pesquisa. Isso cria um fluxo de trabalho em que a informação circula naturalmente entre os agentes, como aconteceria em uma equipe humana.
|
||||
Substitua `provider/model-id` pelo modelo usado, como `openai/gpt-4o`, `anthropic/claude-sonnet-4-6` ou `gemini/gemini-2.0-flash-001`.
|
||||
|
||||
## Passo 5: Configure sua Crew
|
||||
## Etapa 3: Definir tarefas e configurações
|
||||
|
||||
Agora é hora de juntar tudo configurando nossa crew. A crew é o container que orquestra como os agentes trabalham juntos para completar as tarefas.
|
||||
Substitua `crew.jsonc` por:
|
||||
|
||||
Vamos modificar o arquivo `crew.py`:
|
||||
|
||||
```python
|
||||
# src/research_crew/crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew():
|
||||
"""Research crew for comprehensive topic analysis and reporting"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['researcher'], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()]
|
||||
)
|
||||
|
||||
@agent
|
||||
def analyst(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['analyst'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['research_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def analysis_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['analysis_task'], # type: ignore[index]
|
||||
output_file='output/report.md'
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Creates the research crew"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
Neste código, estamos:
|
||||
1. Criando o agente pesquisador e equipando-o com o SerperDevTool para buscas web
|
||||
2. Criando o agente analista
|
||||
3. Definindo as tarefas de pesquisa e análise
|
||||
4. Configurando a crew para executar as tarefas sequencialmente (o analista espera o pesquisador terminar)
|
||||
|
||||
É aqui que a mágica acontece – com poucas linhas de código, definimos um sistema colaborativo de IA onde agentes especializados trabalham juntos em um processo coordenado.
|
||||
|
||||
## Passo 6: Prepare seu Script Principal
|
||||
|
||||
Agora, vamos preparar o script principal que irá rodar nossa crew. É aqui que informamos o tema específico que queremos pesquisar.
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# src/research_crew/main.py
|
||||
import os
|
||||
from research_crew.crew import ResearchCrew
|
||||
|
||||
# Crie o diretório de saída se não existir
|
||||
os.makedirs('output', exist_ok=True)
|
||||
|
||||
def run():
|
||||
"""
|
||||
Rodar a crew de pesquisa.
|
||||
"""
|
||||
inputs = {
|
||||
'topic': 'Inteligência Artificial na Saúde'
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher", "analyst"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Conduct thorough research on {topic}. Focus on key concepts, recent developments, major challenges, notable applications, and future outlook.",
|
||||
"expected_output": "A comprehensive research document with organized sections, specific facts, and useful examples about {topic}.",
|
||||
"agent": "researcher"
|
||||
},
|
||||
{
|
||||
"name": "analysis_task",
|
||||
"description": "Analyze the research findings and create a polished report on {topic}. Include an executive summary, key insights, trend analysis, and recommendations.",
|
||||
"expected_output": "A professional markdown report with clear headings, a concise summary, main findings, and recommendations.",
|
||||
"agent": "analyst",
|
||||
"context": ["research_task"],
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
|
||||
# Criar e rodar a crew
|
||||
result = ResearchCrew().crew().kickoff(inputs=inputs)
|
||||
|
||||
# Imprimir o resultado
|
||||
print("\n\n=== RELATÓRIO FINAL ===\n\n")
|
||||
print(result.raw)
|
||||
|
||||
print("\n\nRelatório salvo em output/report.md")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true,
|
||||
"memory": true,
|
||||
"inputs": {
|
||||
"topic": "Artificial Intelligence in Healthcare"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Este script prepara o ambiente, define o tema de pesquisa e inicia o trabalho da crew. O poder da CrewAI fica evidente em como esse código é simples – toda a complexidade do gerenciamento de múltiplos agentes de IA é tratada pelo framework.
|
||||
`context` aponta para tarefas anteriores, então o analista recebe a saída da pesquisa. `inputs` define valores padrão para `{topic}`; se um valor faltar, `crewai run` perguntará no terminal.
|
||||
|
||||
## Passo 7: Defina suas Variáveis de Ambiente
|
||||
## Etapa 4: Variáveis de ambiente
|
||||
|
||||
Crie um arquivo `.env` na raiz do seu projeto com suas chaves de API:
|
||||
Edite `.env`:
|
||||
|
||||
```sh
|
||||
SERPER_API_KEY=sua_serper_api_key
|
||||
# Adicione a chave de API do seu provedor também.
|
||||
SERPER_API_KEY=your_serper_api_key
|
||||
# Adicione também a chave do seu provedor de modelo.
|
||||
```
|
||||
|
||||
Confira o [guia de configuração do LLM](/pt-BR/concepts/llms#setting-up-your-llm) para detalhes sobre como configurar o provedor de sua escolha. Você pode obter a chave da Serper em [Serper.dev](https://serper.dev/).
|
||||
|
||||
## Passo 8: Instale as Dependências
|
||||
|
||||
Instale as dependências necessárias usando a CLI da CrewAI:
|
||||
## Etapa 5: Instalar e executar
|
||||
|
||||
```bash
|
||||
crewai install
|
||||
```
|
||||
|
||||
Este comando irá:
|
||||
1. Ler as dependências da configuração do seu projeto
|
||||
2. Criar um ambiente virtual se necessário
|
||||
3. Instalar todos os pacotes necessários
|
||||
|
||||
## Passo 9: Execute sua Crew
|
||||
|
||||
Agora chega o momento empolgante – é hora de rodar sua crew e assistir à colaboração de IA em ação!
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
Ao rodar esse comando, você verá sua crew ganhando vida. O pesquisador irá coletar informações sobre o tema especificado, e o analista irá criar um relatório abrangente baseado nessa pesquisa. Você poderá acompanhar em tempo real o raciocínio, as ações e os resultados dos agentes à medida que colaboram para concluir as tarefas.
|
||||
Quando a execução terminar, abra `output/report.md`.
|
||||
|
||||
## Passo 10: Revise o Resultado
|
||||
|
||||
Após a conclusão do trabalho da crew, você encontrará o relatório final em `output/report.md`. O relatório incluirá:
|
||||
|
||||
1. Um resumo executivo
|
||||
2. Informações detalhadas sobre o tema
|
||||
3. Análises e insights
|
||||
4. Recomendações ou considerações futuras
|
||||
|
||||
Tire um momento para valorizar o que você realizou – você criou um sistema no qual múltiplos agentes de IA colaboraram em uma tarefa complexa, cada um contribuindo com suas habilidades especializadas para gerar um resultado maior do que qualquer agente conseguiria sozinho.
|
||||
|
||||
## Explorando Outros Comandos da CLI
|
||||
|
||||
A CrewAI oferece vários outros comandos úteis de CLI para trabalhar com crews:
|
||||
|
||||
```bash
|
||||
# Ver todos os comandos disponíveis
|
||||
crewai --help
|
||||
|
||||
# Rodar a crew
|
||||
crewai run
|
||||
|
||||
# Testar a crew
|
||||
crewai test
|
||||
|
||||
# Resetar as memórias da crew
|
||||
crewai reset-memories
|
||||
|
||||
# Repetir a partir de uma tarefa específica
|
||||
crewai replay -t <task_id>
|
||||
```
|
||||
|
||||
## O que Mais é Possível: Além da sua Primeira Crew
|
||||
|
||||
O que você construiu neste guia é só o começo. As habilidades e padrões aprendidos aqui podem ser aplicados para criar sistemas de IA cada vez mais sofisticados. Veja algumas maneiras de expandir sua crew de pesquisa básica:
|
||||
|
||||
### Expandindo sua Crew
|
||||
|
||||
Você pode adicionar mais agentes especializados à sua crew:
|
||||
- Um **checador de fatos** para verificar as informações encontradas
|
||||
- Um **visualizador de dados** para criar gráficos e tabelas
|
||||
- Um **especialista de domínio** com conhecimento aprofundado em uma área específica
|
||||
- Um **crítico** para identificar pontos fracos na análise
|
||||
|
||||
### Adicionando Ferramentas e Capacidades
|
||||
|
||||
Você pode potencializar seus agentes com ferramentas adicionais:
|
||||
- Ferramentas de navegação web para pesquisa em tempo real
|
||||
- Ferramentas para CSV ou bancos de dados para análise de dados
|
||||
- Ferramentas de execução de código para processamento de dados
|
||||
- Conexões de API com serviços externos
|
||||
|
||||
### Criando Fluxos de Trabalho Mais Complexos
|
||||
|
||||
Você pode implementar processos mais sofisticados:
|
||||
- Processos hierárquicos em que agentes gestores delegam para agentes
|
||||
- Processos iterativos com loops de feedback para refinamento
|
||||
- Processos paralelos onde múltiplos agentes trabalham simultaneamente
|
||||
- Processos dinâmicos que se adaptam a resultados intermediários
|
||||
|
||||
### Aplicando em Diferentes Domínios
|
||||
|
||||
Os mesmos padrões podem ser aplicados para criar crews para:
|
||||
- **Criação de conteúdo:** Redatores, editores, checadores de fatos e designers trabalhando juntos
|
||||
- **Atendimento ao cliente:** Agentes de triagem, especialistas e controle de qualidade atuando colaborativamente
|
||||
- **Desenvolvimento de produto:** Pesquisadores, designers e planejadores trabalhando em conjunto
|
||||
- **Análise de dados:** Coletores de dados, analistas e especialistas em visualização
|
||||
|
||||
## Próximos Passos
|
||||
|
||||
Agora que você montou sua primeira crew, você pode:
|
||||
|
||||
1. Experimentar diferentes configurações e personalidades de agentes
|
||||
2. Testar estruturas de tarefas e fluxos de trabalho mais complexos
|
||||
3. Implementar ferramentas customizadas para dar novas capacidades aos agentes
|
||||
4. Aplicar sua crew em outros temas ou domínios de problemas
|
||||
5. Explorar [CrewAI Flows](/pt-BR/guides/flows/first-flow) para fluxos de trabalho avançados usando programação procedural
|
||||
<Warning>
|
||||
Execute projetos JSON crew apenas de fontes confiáveis. Ferramentas `custom:<name>` e referências `{"python": "module.attribute"}` executam Python local ao carregar a crew.
|
||||
</Warning>
|
||||
|
||||
<Check>
|
||||
Parabéns! Você construiu com sucesso sua primeira crew com o CrewAI, capaz de pesquisar e analisar qualquer tema que desejar. Essa experiência fundamental lhe deu as habilidades para criar sistemas de IA cada vez mais sofisticados, aptos a encarar problemas complexos e de múltiplas etapas por meio da inteligência colaborativa.
|
||||
Você criou uma crew JSON-first funcional que pesquisa um tópico e escreve um relatório.
|
||||
</Check>
|
||||
|
||||
@@ -64,7 +64,7 @@ Isso gerará um projeto com a estrutura básica necessária para seu flow.
|
||||
|
||||
## Passo 2: Entendendo a Estrutura do Projeto
|
||||
|
||||
O projeto gerado possui a seguinte estrutura. Reserve um momento para conhecê-la, pois isso ajudará você a criar flows mais complexos no futuro.
|
||||
O projeto gerado possui a seguinte estrutura. A crew inicial embutida usa o layout clássico Python/YAML. Para usar uma crew JSON-first dentro de um Flow, crie `crew.jsonc` e `agents/*.jsonc` na pasta da crew e carregue com `crewai.project.load_crew`, como mostrado em [Flows](/pt-BR/concepts/flows#building-your-crews).
|
||||
|
||||
```
|
||||
guide_creator_flow/
|
||||
@@ -102,156 +102,82 @@ Este comando cria automaticamente os diretórios e arquivos de template necessá
|
||||
|
||||
## Passo 4: Configure o Crew de Redator de Conteúdo
|
||||
|
||||
Agora, vamos modificar os arquivos gerados para o crew de redatores. Vamos configurar dois agentes especializados – um escritor e um revisor – que irão colaborar para criar um conteúdo de alta qualidade para o nosso guia.
|
||||
Agora, vamos configurar o crew de redatores com JSONC. Vamos definir dois agentes especializados - um escritor e um revisor - que colaboram para criar conteúdo de alta qualidade para o guia.
|
||||
|
||||
1. Primeiro, atualize o arquivo de configuração de agents para definir a equipe de criação de conteúdo:
|
||||
1. Crie `src/guide_creator_flow/crews/content_crew/agents/content_writer.jsonc`:
|
||||
|
||||
Lembre-se de configurar o `llm` com o provedor que está utilizando.
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/agents.yaml
|
||||
content_writer:
|
||||
role: >
|
||||
Redator de Conteúdo Educacional
|
||||
goal: >
|
||||
Criar conteúdo envolvente e informativo que explique completamente o tema proposto
|
||||
e forneça insights valiosos ao leitor
|
||||
backstory: >
|
||||
Você é um talentoso escritor educacional com experiência em criar conteúdo claro
|
||||
e atraente. Você tem facilidade para explicar conceitos complexos em linguagem acessível
|
||||
e organizar as informações de forma a ajudar o leitor a construir seu entendimento.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
|
||||
content_reviewer:
|
||||
role: >
|
||||
Revisor(a) e Editor(a) de Conteúdo Educacional
|
||||
goal: >
|
||||
Garantir que o conteúdo seja preciso, abrangente, bem estruturado e mantenha
|
||||
consistência com as seções previamente escritas
|
||||
backstory: >
|
||||
Você é um editor(a) meticuloso(a) com anos de experiência revisando conteúdo educacional.
|
||||
Tem atenção aos detalhes, clareza e coesão. Você se destaca em aprimorar conteúdo
|
||||
mantendo o estilo do autor original e garantindo qualidade consistente em várias seções.
|
||||
llm: provider/model-id # e.g. openai/gpt-4o, google/gemini-2.0-flash, anthropic/claude...
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Writer",
|
||||
"goal": "Create engaging, informative content that thoroughly explains the assigned topic and provides valuable insights to the reader.",
|
||||
"backstory": "You are a talented educational writer who explains complex concepts in accessible language and organizes information clearly.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Essas definições de agents estabelecem papéis e perspectivas especializadas que irão moldar como nossos agentes de IA abordam a criação de conteúdo. Note como cada agent possui um propósito e expertise distintos.
|
||||
2. Crie `src/guide_creator_flow/crews/content_crew/agents/content_reviewer.jsonc`:
|
||||
|
||||
2. Em seguida, atualize o arquivo de configuração de tarefas para definir as tarefas específicas de escrita e revisão:
|
||||
|
||||
```yaml
|
||||
# src/guide_creator_flow/crews/content_crew/config/tasks.yaml
|
||||
write_section_task:
|
||||
description: >
|
||||
Escreva uma seção abrangente sobre o tema: "{section_title}"
|
||||
|
||||
Descrição da seção: {section_description}
|
||||
Público-alvo: {audience_level} aprendizes
|
||||
|
||||
Seu conteúdo deve:
|
||||
1. Começar com uma breve introdução ao tema da seção
|
||||
2. Explicar claramente todos os conceitos principais com exemplos
|
||||
3. Incluir aplicações práticas ou exercícios onde apropriado
|
||||
4. Terminar com um resumo dos pontos principais
|
||||
5. Ter aproximadamente 500-800 palavras
|
||||
|
||||
Formate seu conteúdo em Markdown com títulos, listas e ênfase apropriados.
|
||||
|
||||
Seções previamente escritas:
|
||||
{previous_sections}
|
||||
|
||||
Certifique-se de que seu conteúdo mantenha consistência com as seções já escritas
|
||||
e amplie os conceitos que já foram explicados.
|
||||
expected_output: >
|
||||
Uma seção bem estruturada e abrangente em formato Markdown que explique
|
||||
totalmente o tema e é apropriada para o público-alvo.
|
||||
agent: content_writer
|
||||
|
||||
review_section_task:
|
||||
description: >
|
||||
Revise e melhore a seguinte seção sobre "{section_title}":
|
||||
|
||||
{draft_content}
|
||||
|
||||
Público-alvo: {audience_level} aprendizes
|
||||
|
||||
Seções previamente escritas:
|
||||
{previous_sections}
|
||||
|
||||
Sua revisão deve:
|
||||
1. Corrigir qualquer erro gramatical ou de ortografia
|
||||
2. Melhorar clareza e legibilidade
|
||||
3. Garantir que o conteúdo seja abrangente e preciso
|
||||
4. Verificar a consistência com as seções já escritas
|
||||
5. Aprimorar a estrutura e o fluxo
|
||||
6. Adicionar qualquer informação-chave ausente
|
||||
|
||||
Forneça a versão aprimorada da seção em formato Markdown.
|
||||
expected_output: >
|
||||
Uma versão melhorada e refinada da seção, mantendo a estrutura original,
|
||||
mas aprimorando clareza, precisão e consistência.
|
||||
agent: content_reviewer
|
||||
context:
|
||||
- write_section_task
|
||||
```jsonc
|
||||
{
|
||||
"role": "Educational Content Reviewer and Editor",
|
||||
"goal": "Ensure content is accurate, comprehensive, well-structured, and consistent with previously written sections.",
|
||||
"backstory": "You are a meticulous editor with an eye for detail, clarity, and coherence.",
|
||||
"llm": "provider/model-id",
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Essas definições de tarefas fornecem instruções detalhadas para nossos agents, garantindo que eles produzam conteúdo que atenda aos padrões de qualidade. Observe como o parâmetro `context` na tarefa de revisão cria um fluxo onde o revisor tem acesso à produção do redator.
|
||||
Substitua `provider/model-id` pelo modelo que você usa, como `openai/gpt-4o`, `gemini/gemini-2.0-flash-001` ou `anthropic/claude-sonnet-4-6`.
|
||||
|
||||
3. Agora, atualize o arquivo de implementação do crew para definir como nossos agents e tasks trabalham juntos:
|
||||
3. Crie `src/guide_creator_flow/crews/content_crew/crew.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "Content Crew",
|
||||
"agents": ["content_writer", "content_reviewer"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "write_section_task",
|
||||
"description": "Write a comprehensive section on the topic: \"{section_title}\".\n\nSection description: {section_description}\nTarget audience: {audience_level} level learners\n\nYour content should begin with a brief introduction, explain key concepts clearly with examples, include practical applications where appropriate, end with a summary, and be approximately 500-800 words.\n\nPreviously written sections:\n{previous_sections}",
|
||||
"expected_output": "A well-structured, comprehensive section in Markdown format that thoroughly explains the topic and is appropriate for the target audience.",
|
||||
"agent": "content_writer",
|
||||
"markdown": true
|
||||
},
|
||||
{
|
||||
"name": "review_section_task",
|
||||
"description": "Review and improve this section on \"{section_title}\":\n\n{draft_content}\n\nTarget audience: {audience_level} level learners\nPreviously written sections:\n{previous_sections}\n\nFix errors, improve clarity, verify consistency, enhance structure, and add missing key information.",
|
||||
"expected_output": "An improved, polished version of the section that maintains the original structure but enhances clarity, accuracy, and consistency.",
|
||||
"agent": "content_reviewer",
|
||||
"context": ["write_section_task"],
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
O campo `context` permite que o revisor use a saída do escritor.
|
||||
|
||||
4. Substitua `src/guide_creator_flow/crews/content_crew/content_crew.py` por um pequeno loader:
|
||||
|
||||
```python
|
||||
# src/guide_creator_flow/crews/content_crew/content_crew.py
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
@CrewBase
|
||||
class ContentCrew():
|
||||
"""Crew de redação de conteúdo"""
|
||||
from crewai.project import load_crew
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def content_writer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_writer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@agent
|
||||
def content_reviewer(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['content_reviewer'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def write_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['write_section_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@task
|
||||
def review_section_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['review_section_task'], # type: ignore[index]
|
||||
context=[self.write_section_task()]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
"""Cria o crew de redação de conteúdo"""
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
Essa definição de crew estabelece o relacionamento entre nossos agents e tasks, definindo um processo sequencial onde o redator cria o rascunho e o revisor o aprimora. Embora este crew possa funcionar de forma independente, em nosso flow ele será orquestrado como parte de um sistema maior.
|
||||
Esse loader transforma `crew.jsonc` em uma `Crew` em runtime. Embora essa crew possa funcionar de forma independente, no nosso flow ela será orquestrada como parte de um sistema maior.
|
||||
|
||||
## Passo 5: Crie o Flow
|
||||
|
||||
@@ -273,7 +199,7 @@ from typing import List, Dict
|
||||
from pydantic import BaseModel, Field
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from guide_creator_flow.crews.content_crew.content_crew import ContentCrew
|
||||
from guide_creator_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
# Definir nossos modelos para dados estruturados
|
||||
class Section(BaseModel):
|
||||
@@ -378,7 +304,7 @@ class GuideCreatorFlow(Flow[GuideCreatorState]):
|
||||
previous_sections_text = "No previous sections written yet."
|
||||
|
||||
# Executar a crew de conteúdo para esta seção
|
||||
result = ContentCrew().crew().kickoff(inputs={
|
||||
result = kickoff_content_crew(inputs={
|
||||
"section_title": section.title,
|
||||
"section_description": section.description,
|
||||
"audience_level": self.state.audience_level,
|
||||
@@ -597,7 +523,7 @@ Isso fornece uma maneira segura e tipada de rastrear e transformar dados ao long
|
||||
Flows podem integrar crews para tarefas colaborativas complexas:
|
||||
|
||||
```python
|
||||
result = ContentCrew().crew().kickoff(inputs={
|
||||
result = kickoff_content_crew(inputs={
|
||||
"section_title": section.title,
|
||||
# ...
|
||||
})
|
||||
|
||||
@@ -106,7 +106,7 @@ Se você ainda não instalou o `uv`, siga o **passo 1** para instalá-lo rapidam
|
||||
|
||||
# Criando um Projeto CrewAI
|
||||
|
||||
Recomendamos utilizar o template de scaffolding `YAML` para uma abordagem estruturada na definição dos agentes e tarefas. Veja como começar:
|
||||
`crewai create crew` agora cria um projeto de crew JSON-first. Os agentes ficam em `agents/*.jsonc`, as tarefas e configurações da crew ficam em `crew.jsonc`, e `crewai run` carrega essa definição JSON diretamente.
|
||||
|
||||
<Steps>
|
||||
<Step title="Gerar Scaffolding do Projeto">
|
||||
@@ -120,39 +120,38 @@ Recomendamos utilizar o template de scaffolding `YAML` para uma abordagem estrut
|
||||
```
|
||||
my_project/
|
||||
├── .gitignore
|
||||
├── .env
|
||||
├── agents/
|
||||
│ └── researcher.jsonc
|
||||
├── crew.jsonc
|
||||
├── knowledge/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── .env
|
||||
└── src/
|
||||
└── my_project/
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
├── crew.py
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml
|
||||
└── tasks.yaml
|
||||
├── skills/
|
||||
└── tools/
|
||||
```
|
||||
</Frame>
|
||||
|
||||
- Se você precisar do scaffold antigo em Python/YAML com `crew.py`, `config/agents.yaml` e `config/tasks.yaml`, execute:
|
||||
```shell
|
||||
crewai create crew <your_project_name> --classic
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Personalize Seu Projeto">
|
||||
- Seu projeto conterá estes arquivos essenciais:
|
||||
| Arquivo | Finalidade |
|
||||
| --- | --- |
|
||||
| `agents.yaml` | Defina seus agentes de IA e seus papéis |
|
||||
| `tasks.yaml` | Configure as tarefas e fluxos de trabalho dos agentes |
|
||||
| `crew.jsonc` | Configure a crew, a ordem das tarefas, o processo e os inputs padrão |
|
||||
| `agents/*.jsonc` | Defina o papel, objetivo, backstory, LLM, ferramentas e comportamento de cada agente |
|
||||
| `.env` | Armazene chaves de API e variáveis de ambiente |
|
||||
| `main.py` | Ponto de entrada e fluxo de execução do projeto |
|
||||
| `crew.py` | Orquestração e coordenação do crew |
|
||||
| `tools/` | Diretório para ferramentas customizadas dos agentes |
|
||||
| `knowledge/` | Diretório para base de conhecimento |
|
||||
| `tools/` | Arquivos Python opcionais para ferramentas `custom:<name>` |
|
||||
| `knowledge/` | Arquivos opcionais de conhecimento para agentes |
|
||||
| `skills/` | Arquivos opcionais de skills aplicadas à crew |
|
||||
|
||||
- Comece editando `agents.yaml` e `tasks.yaml` para definir o comportamento do seu crew.
|
||||
- Comece editando `crew.jsonc` e os arquivos em `agents/` para definir o comportamento da crew.
|
||||
- Use valores `{placeholder}` nos textos de agentes e tarefas e defina padrões em `crew.jsonc` dentro de `inputs`. Ao executar `crewai run`, a CLI pergunta por valores que estiverem faltando.
|
||||
- Mantenha informações sensíveis como chaves de API no arquivo `.env`.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -5,11 +5,15 @@ icon: "at"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
Este guia explica como utilizar anotações para referenciar corretamente **agentes**, **tarefas** e outros componentes no arquivo `crew.py`.
|
||||
Este guia explica como utilizar anotações para referenciar corretamente **agentes**, **tarefas** e outros componentes em um arquivo `crew.py` clássico.
|
||||
|
||||
<Note>
|
||||
Novos projetos criados com `crewai create crew <name>` são JSON-first e usam `crew.jsonc` com `agents/*.jsonc`. Use este guia ao trabalhar em um projeto clássico criado com `crewai create crew <name> --classic`, ao migrar um projeto Python/YAML existente ou quando precisar de controle via decorators em Python.
|
||||
</Note>
|
||||
|
||||
## Introdução
|
||||
|
||||
As anotações no framework CrewAI são utilizadas para decorar classes e métodos, fornecendo metadados e funcionalidades para diversos componentes do seu crew. Essas anotações auxiliam na organização e estruturação do seu código, tornando-o mais legível e fácil de manter.
|
||||
As anotações no framework CrewAI são utilizadas para decorar classes e métodos, fornecendo metadados e funcionalidades para diversos componentes do seu crew. Em projetos clássicos Python/YAML, elas organizam o código que carrega `config/agents.yaml`, `config/tasks.yaml` e retorna o objeto `Crew`.
|
||||
|
||||
## Anotações Disponíveis
|
||||
|
||||
@@ -113,9 +117,9 @@ def crew(self) -> Crew:
|
||||
|
||||
A anotação `@crew` é usada para decorar o método que cria e retorna o objeto `Crew`. Este método reúne todos os componentes (agentes e tarefas) em um crew funcional.
|
||||
|
||||
## Configuração YAML
|
||||
## Configuração YAML Clássica
|
||||
|
||||
As configurações dos agentes geralmente são armazenadas em um arquivo YAML. Veja um exemplo de como o arquivo `agents.yaml` pode ser estruturado para o agente researcher:
|
||||
Em projetos clássicos, as configurações dos agentes geralmente são armazenadas em um arquivo YAML. Veja um exemplo de como o arquivo `agents.yaml` pode ser estruturado para o agente researcher:
|
||||
|
||||
```yaml
|
||||
researcher:
|
||||
@@ -146,6 +150,6 @@ Repare como os campos `llm` e `tools` no arquivo YAML correspondem aos métodos
|
||||
- **Nomenclatura Consistente**: Utilize nomenclatura clara e consistente para seus métodos. Por exemplo, métodos de agentes podem ser nomeados de acordo com suas funções (ex: researcher, reporting_analyst).
|
||||
- **Variáveis de Ambiente**: Utilize variáveis de ambiente para informações sensíveis como chaves de API.
|
||||
- **Flexibilidade**: Estruture seu crew de forma flexível, permitindo fácil adição ou remoção de agentes e tarefas.
|
||||
- **Correspondência YAML-Código**: Assegure que os nomes e estruturas nos arquivos YAML correspondam corretamente aos métodos decorados em seu código Python.
|
||||
- **Correspondência YAML-Código**: Em projetos clássicos, assegure que os nomes e estruturas nos arquivos YAML correspondam corretamente aos métodos decorados em seu código Python.
|
||||
|
||||
Seguindo essas orientações e utilizando corretamente as anotações, você conseguirá criar crews bem estruturados e de fácil manutenção utilizando o framework CrewAI.
|
||||
Seguindo essas orientações e utilizando corretamente as anotações, você conseguirá manter crews clássicos bem estruturados. Para novas crews, prefira a estrutura JSON-first em [Crews](/pt-BR/concepts/crews).
|
||||
|
||||
@@ -39,84 +39,60 @@ Se ainda não instalou o CrewAI, siga primeiro o [guia de instalação](/pt-BR/i
|
||||
Isso cria um app Flow em `src/latest_ai_flow/`, incluindo um crew inicial em `crews/content_crew/` que você substituirá por um crew de pesquisa **com um único agente** nos próximos passos.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure um agente em `agents.yaml`">
|
||||
Substitua o conteúdo de `src/latest_ai_flow/crews/content_crew/config/agents.yaml` por um único pesquisador. Variáveis como `{topic}` são preenchidas a partir de `crew.kickoff(inputs=...)`.
|
||||
<Step title="Configure um agente em JSONC">
|
||||
Crie `src/latest_ai_flow/crews/content_crew/agents/researcher.jsonc` (crie o diretório `agents/` se necessário). Variáveis como `{topic}` são preenchidas a partir de `crew.kickoff(inputs=...)`.
|
||||
|
||||
```yaml agents.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/agents.yaml
|
||||
researcher:
|
||||
role: >
|
||||
Pesquisador(a) Sênior de Dados em {topic}
|
||||
goal: >
|
||||
Descobrir os desenvolvimentos mais recentes em {topic}
|
||||
backstory: >
|
||||
Você é um pesquisador experiente que descobre os últimos avanços em {topic}.
|
||||
Encontra as informações mais relevantes e apresenta tudo com clareza.
|
||||
```jsonc agents/researcher.jsonc
|
||||
{
|
||||
"role": "Pesquisador(a) Sênior de Dados em {topic}",
|
||||
"goal": "Descobrir os desenvolvimentos mais recentes em {topic}",
|
||||
"backstory": "Você é um pesquisador experiente que encontra as informações mais relevantes e apresenta tudo com clareza.",
|
||||
"tools": ["SerperDevTool"],
|
||||
"settings": {
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure uma tarefa em `tasks.yaml`">
|
||||
```yaml tasks.yaml
|
||||
# src/latest_ai_flow/crews/content_crew/config/tasks.yaml
|
||||
research_task:
|
||||
description: >
|
||||
Faça uma pesquisa aprofundada sobre {topic}. Use busca na web para obter
|
||||
informações atuais e confiáveis. O ano atual é 2026.
|
||||
expected_output: >
|
||||
Um relatório em markdown com seções claras: tendências principais, ferramentas
|
||||
ou empresas relevantes e implicações. Entre 800 e 1200 palavras. Sem cercas de código em volta do documento inteiro.
|
||||
agent: researcher
|
||||
output_file: output/report.md
|
||||
<Step title="Configure a crew em `crew.jsonc`">
|
||||
Crie `src/latest_ai_flow/crews/content_crew/crew.jsonc`:
|
||||
|
||||
```jsonc crew.jsonc
|
||||
{
|
||||
"name": "Research Crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Faça uma pesquisa aprofundada sobre {topic}. Use busca na web para obter informações atuais e confiáveis. O ano atual é 2026.",
|
||||
"expected_output": "Um relatório em markdown com seções claras: tendências principais, ferramentas ou empresas relevantes e implicações. Entre 800 e 1200 palavras. Sem cercas de código em volta do documento inteiro.",
|
||||
"agent": "researcher",
|
||||
"output_file": "output/report.md",
|
||||
"markdown": true
|
||||
}
|
||||
],
|
||||
"process": "sequential",
|
||||
"verbose": true
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Conecte a classe do crew (`content_crew.py`)">
|
||||
Aponte o crew gerado para o YAML e anexe `SerperDevTool` ao pesquisador.
|
||||
<Step title="Carregue a crew JSON (`content_crew.py`)">
|
||||
Substitua o `content_crew.py` gerado por um pequeno loader que transforma `crew.jsonc` em uma `Crew`.
|
||||
|
||||
```python content_crew.py
|
||||
# src/latest_ai_flow/crews/content_crew/content_crew.py
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai_tools import SerperDevTool
|
||||
from crewai.project import load_crew
|
||||
|
||||
|
||||
@CrewBase
|
||||
class ResearchCrew:
|
||||
"""Crew de pesquisa com um agente, usado dentro do Flow."""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
agents_config = "config/agents.yaml"
|
||||
tasks_config = "config/tasks.yaml"
|
||||
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["researcher"], # type: ignore[index]
|
||||
verbose=True,
|
||||
tools=[SerperDevTool()],
|
||||
)
|
||||
|
||||
@task
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config["research_task"], # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
def kickoff_content_crew(inputs: dict):
|
||||
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
|
||||
return crew.kickoff(inputs={**default_inputs, **inputs})
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -130,7 +106,7 @@ Se ainda não instalou o CrewAI, siga primeiro o [guia de instalação](/pt-BR/i
|
||||
|
||||
from crewai.flow import Flow, listen, start
|
||||
|
||||
from latest_ai_flow.crews.content_crew.content_crew import ResearchCrew
|
||||
from latest_ai_flow.crews.content_crew.content_crew import kickoff_content_crew
|
||||
|
||||
|
||||
class ResearchFlowState(BaseModel):
|
||||
@@ -149,7 +125,7 @@ Se ainda não instalou o CrewAI, siga primeiro o [guia de instalação](/pt-BR/i
|
||||
|
||||
@listen(prepare_topic)
|
||||
def run_research(self):
|
||||
result = ResearchCrew().crew().kickoff(inputs={"topic": self.state.topic})
|
||||
result = kickoff_content_crew(inputs={"topic": self.state.topic})
|
||||
self.state.report = result.raw
|
||||
print("Crew de pesquisa concluído.")
|
||||
|
||||
@@ -171,7 +147,7 @@ Se ainda não instalou o CrewAI, siga primeiro o [guia de instalação](/pt-BR/i
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Se o nome do pacote não for `latest_ai_flow`, ajuste o import de `ResearchCrew` para o caminho de módulo do seu projeto.
|
||||
Se o nome do pacote não for `latest_ai_flow`, ajuste o import de `kickoff_content_crew` para o caminho de módulo do seu projeto.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
@@ -219,7 +195,7 @@ Se ainda não instalou o CrewAI, siga primeiro o [guia de instalação](/pt-BR/i
|
||||
## Como isso se encaixa
|
||||
|
||||
1. **Flow** — `LatestAiFlow` executa `prepare_topic`, depois `run_research`, depois `summarize`. O estado (`topic`, `report`) fica no Flow.
|
||||
2. **Crew** — `ResearchCrew` executa uma tarefa com um agente: o pesquisador usa **Serper** na web e escreve o relatório.
|
||||
2. **Crew** — `kickoff_content_crew` carrega `crew.jsonc` e executa uma tarefa com um agente: o pesquisador usa **Serper** na web e escreve o relatório.
|
||||
3. **Artefato** — O `output_file` da tarefa grava o relatório em `output/report.md`.
|
||||
|
||||
Para ir além em Flows (roteamento, persistência, human-in-the-loop), veja [Construa seu primeiro Flow](/pt-BR/guides/flows/first-flow) e [Flows](/pt-BR/concepts/flows). Para crews sem Flow, veja [Crews](/pt-BR/concepts/crews). Para um único `Agent` com `kickoff()` sem tarefas, veja [Agents](/pt-BR/concepts/agents#direct-agent-interaction-with-kickoff).
|
||||
@@ -230,7 +206,10 @@ Você tem um Flow ponta a ponta com um crew de agente e um relatório salvo —
|
||||
|
||||
### Consistência de nomes
|
||||
|
||||
As chaves do YAML (`researcher`, `research_task`) devem coincidir com os nomes dos métodos na classe `@CrewBase`. Veja [Crews](/pt-BR/concepts/crews) para o padrão completo com decoradores.
|
||||
Os nomes em `crew.jsonc` devem coincidir com os arquivos e referências:
|
||||
|
||||
- `agents: ["researcher"]` carrega `agents/researcher.jsonc`
|
||||
- `context: ["research_task"]` referencia uma tarefa anterior chamada `research_task`
|
||||
|
||||
## Implantação
|
||||
|
||||
|
||||
@@ -3,41 +3,94 @@ from __future__ import annotations
|
||||
from importlib.metadata import version as get_version
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
from crewai_cli.add_crew_to_flow import add_crew_to_flow
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
from crewai_cli.config import Settings
|
||||
from crewai_cli.create_crew import create_crew
|
||||
from crewai_cli.create_flow import create_flow
|
||||
from crewai_cli.crew_chat import run_chat
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
from crewai_cli.enterprise.main import EnterpriseConfigureCommand
|
||||
from crewai_cli.evaluate_crew import evaluate_crew
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
from crewai_cli.install_crew import install_crew
|
||||
from crewai_cli.kickoff_flow import kickoff_flow
|
||||
from crewai_cli.organization.main import OrganizationCommand
|
||||
from crewai_cli.plot_flow import plot_flow
|
||||
from crewai_cli.remote_template.main import TemplateCommand
|
||||
from crewai_cli.replay_from_task import replay_task_command
|
||||
from crewai_cli.reset_memories_command import reset_memories_command
|
||||
from crewai_cli.run_crew import run_crew
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
from crewai_cli.task_outputs import load_task_outputs
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
from crewai_cli.train_crew import train_crew
|
||||
from crewai_cli.triggers.main import TriggersCommand
|
||||
from crewai_cli.update_crew import update_crew
|
||||
from crewai_cli.user_data import (
|
||||
_load_user_data,
|
||||
is_tracing_enabled,
|
||||
update_user_data,
|
||||
)
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
read_toml,
|
||||
)
|
||||
|
||||
|
||||
def train_crew(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.train_crew import train_crew as _train_crew
|
||||
|
||||
return _train_crew(*args, **kwargs)
|
||||
|
||||
|
||||
def evaluate_crew(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.evaluate_crew import evaluate_crew as _evaluate_crew
|
||||
|
||||
return _evaluate_crew(*args, **kwargs)
|
||||
|
||||
|
||||
def replay_task_command(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.replay_from_task import replay_task_command as _replay_task_command
|
||||
|
||||
return _replay_task_command(*args, **kwargs)
|
||||
|
||||
|
||||
def run_flow_definition(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.run_flow_definition import (
|
||||
run_flow_definition as _run_flow_definition,
|
||||
)
|
||||
|
||||
return _run_flow_definition(*args, **kwargs)
|
||||
|
||||
|
||||
def run_crew(*args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.run_crew import run_crew as _run_crew
|
||||
|
||||
return _run_crew(*args, **kwargs)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# mypy sees the real classes; at runtime the shims below defer the
|
||||
# heavy imports until a command actually instantiates them.
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
from crewai_cli.organization.main import OrganizationCommand
|
||||
from crewai_cli.remote_template.main import TemplateCommand
|
||||
else:
|
||||
|
||||
class AuthenticationCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.authentication.main import (
|
||||
AuthenticationCommand as _AuthenticationCommand,
|
||||
)
|
||||
|
||||
return _AuthenticationCommand(*args, **kwargs)
|
||||
|
||||
class DeployCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.deploy.main import DeployCommand as _DeployCommand
|
||||
|
||||
return _DeployCommand(*args, **kwargs)
|
||||
|
||||
class TemplateCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.remote_template.main import (
|
||||
TemplateCommand as _TemplateCommand,
|
||||
)
|
||||
|
||||
return _TemplateCommand(*args, **kwargs)
|
||||
|
||||
class OrganizationCommand:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
from crewai_cli.organization.main import (
|
||||
OrganizationCommand as _OrganizationCommand,
|
||||
)
|
||||
|
||||
return _OrganizationCommand(*args, **kwargs)
|
||||
|
||||
|
||||
def _get_cli_version() -> str:
|
||||
@@ -90,17 +143,57 @@ def uv(uv_args: tuple[str, ...]) -> None:
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@click.argument("type", type=click.Choice(["crew", "flow"]))
|
||||
@click.argument("name")
|
||||
@click.argument(
|
||||
"type", required=False, default=None, type=click.Choice(["crew", "flow"])
|
||||
)
|
||||
@click.argument("name", required=False, default=None)
|
||||
@click.option("--provider", type=str, help="The provider to use for the crew")
|
||||
@click.option("--skip_provider", is_flag=True, help="Skip provider validation")
|
||||
@click.option(
|
||||
"--classic",
|
||||
is_flag=True,
|
||||
help="Use classic Python/YAML project structure instead of JSON",
|
||||
)
|
||||
def create(
|
||||
type: str, name: str, provider: str | None, skip_provider: bool = False
|
||||
type: str | None,
|
||||
name: str | None,
|
||||
provider: str | None,
|
||||
skip_provider: bool = False,
|
||||
classic: bool = False,
|
||||
) -> None:
|
||||
"""Create a new crew, or flow."""
|
||||
if not type:
|
||||
from crewai_cli.tui_picker import pick
|
||||
|
||||
options = [
|
||||
("crew", "A team of AI agents working together"),
|
||||
(
|
||||
"flow",
|
||||
"A deterministic workflow with full control over agents and crews",
|
||||
),
|
||||
]
|
||||
type = pick("What would you like to create?", options)
|
||||
if type is None:
|
||||
raise SystemExit(0)
|
||||
click.echo()
|
||||
if not name:
|
||||
enable_prompt_line_editing()
|
||||
name = click.prompt(
|
||||
click.style(f" Name of your {type}", fg="cyan", bold=True),
|
||||
prompt_suffix=click.style(" › ", fg="bright_white"), # noqa: RUF001
|
||||
)
|
||||
if type == "crew":
|
||||
create_crew(name, provider, skip_provider)
|
||||
if classic:
|
||||
from crewai_cli.create_crew import create_crew
|
||||
|
||||
create_crew(name, provider, skip_provider)
|
||||
else:
|
||||
from crewai_cli.create_json_crew import create_json_crew
|
||||
|
||||
create_json_crew(name, provider, skip_provider)
|
||||
elif type == "flow":
|
||||
from crewai_cli.create_flow import create_flow
|
||||
|
||||
create_flow(name)
|
||||
else:
|
||||
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
|
||||
@@ -185,6 +278,8 @@ def replay(task_id: str, trained_agents_file: str | None) -> None:
|
||||
def log_tasks_outputs() -> None:
|
||||
"""Retrieve your latest crew.kickoff() task outputs."""
|
||||
try:
|
||||
from crewai_cli.task_outputs import load_task_outputs
|
||||
|
||||
tasks = load_task_outputs()
|
||||
|
||||
if not tasks:
|
||||
@@ -273,6 +368,8 @@ def reset_memories(
|
||||
"Please specify at least one memory type to reset using the appropriate flags."
|
||||
)
|
||||
return
|
||||
from crewai_cli.reset_memories_command import reset_memories_command
|
||||
|
||||
reset_memories_command(memory, knowledge, agent_knowledge, kickoff_outputs, all)
|
||||
except Exception as e:
|
||||
click.echo(f"An error occurred while resetting memories: {e}", err=True)
|
||||
@@ -295,7 +392,7 @@ def reset_memories(
|
||||
"--embedder-model",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Embedder model name (e.g. text-embedding-3-small, gemini-embedding-001).",
|
||||
help="Embedder model name (e.g. text-embedding-3-large, gemini-embedding-001).",
|
||||
)
|
||||
@click.option(
|
||||
"--embedder-config",
|
||||
@@ -350,7 +447,7 @@ def memory(
|
||||
"-m",
|
||||
"--model",
|
||||
type=str,
|
||||
default="gpt-4o-mini",
|
||||
default="gpt-5.4-mini",
|
||||
help="LLM Model to run the tests on the Crew. For now only accepting only OpenAI models.",
|
||||
)
|
||||
@click.option(
|
||||
@@ -381,6 +478,8 @@ def test(n_iterations: int, model: str, trained_agents_file: str | None) -> None
|
||||
@click.pass_context
|
||||
def install(context: click.Context) -> None:
|
||||
"""Install the Crew."""
|
||||
from crewai_cli.install_crew import install_crew
|
||||
|
||||
install_crew(context.args)
|
||||
|
||||
|
||||
@@ -398,14 +497,46 @@ def install(context: click.Context) -> None:
|
||||
"CREWAI_TRAINED_AGENTS_FILE."
|
||||
),
|
||||
)
|
||||
def run(trained_agents_file: str | None) -> None:
|
||||
"""Run the Crew."""
|
||||
@click.option(
|
||||
"--definition",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"Experimental: path to a Flow Definition YAML/JSON file, "
|
||||
"or an inline YAML/JSON string."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--inputs",
|
||||
type=str,
|
||||
default=None,
|
||||
help='Experimental: JSON object passed to flow.kickoff(), e.g. \'{"topic":"AI"}\'.',
|
||||
)
|
||||
def run(
|
||||
trained_agents_file: str | None,
|
||||
definition: str | None,
|
||||
inputs: str | None,
|
||||
) -> None:
|
||||
"""Run the Crew or Flow."""
|
||||
if inputs is not None and definition is None:
|
||||
raise click.UsageError("--inputs requires --definition")
|
||||
|
||||
if definition is not None:
|
||||
click.secho(
|
||||
"Warning: `crewai run --definition` is experimental and may change without notice.",
|
||||
fg="yellow",
|
||||
)
|
||||
run_flow_definition(definition=definition, inputs=inputs)
|
||||
return
|
||||
|
||||
run_crew(trained_agents_file=trained_agents_file)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
def update() -> None:
|
||||
"""Update the pyproject.toml of the Crew project to use uv."""
|
||||
from crewai_cli.update_crew import update_crew
|
||||
|
||||
update_crew()
|
||||
|
||||
|
||||
@@ -515,6 +646,8 @@ def tool() -> None:
|
||||
@tool.command(name="create")
|
||||
@click.argument("handle")
|
||||
def tool_create(handle: str) -> None:
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.create(handle)
|
||||
|
||||
@@ -522,6 +655,8 @@ def tool_create(handle: str) -> None:
|
||||
@tool.command(name="install")
|
||||
@click.argument("handle")
|
||||
def tool_install(handle: str) -> None:
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.login()
|
||||
tool_cmd.install(handle)
|
||||
@@ -538,6 +673,8 @@ def tool_install(handle: str) -> None:
|
||||
@click.option("--public", "is_public", flag_value=True, default=False)
|
||||
@click.option("--private", "is_public", flag_value=False)
|
||||
def tool_publish(is_public: bool, force: bool) -> None:
|
||||
from crewai_cli.tools.main import ToolCommand
|
||||
|
||||
tool_cmd = ToolCommand()
|
||||
tool_cmd.login()
|
||||
tool_cmd.publish(is_public, force)
|
||||
@@ -570,6 +707,8 @@ def skill() -> None:
|
||||
help="Create skill in current dir instead of ./skills/",
|
||||
)
|
||||
def skill_create(name: str, in_project: bool) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.create(name, in_project=in_project)
|
||||
|
||||
@@ -577,6 +716,8 @@ def skill_create(name: str, in_project: bool) -> None:
|
||||
@skill.command(name="install")
|
||||
@click.argument("ref")
|
||||
def skill_install(ref: str) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.install(ref)
|
||||
|
||||
@@ -593,6 +734,8 @@ def skill_install(ref: str) -> None:
|
||||
@click.option("--private", "is_public", flag_value=False)
|
||||
@click.option("--org", default=None, help="Organisation slug (overrides settings).")
|
||||
def skill_publish(is_public: bool, org: str | None, force: bool) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.publish(is_public, org=org, force=force)
|
||||
|
||||
@@ -600,6 +743,8 @@ def skill_publish(is_public: bool, org: str | None, force: bool) -> None:
|
||||
@skill.command(name="list")
|
||||
def skill_list() -> None:
|
||||
"""List locally installed skills."""
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.list_cached()
|
||||
|
||||
@@ -639,6 +784,8 @@ def flow() -> None:
|
||||
@flow.command(name="kickoff")
|
||||
def flow_run() -> None:
|
||||
"""Kickoff the Flow."""
|
||||
from crewai_cli.kickoff_flow import kickoff_flow
|
||||
|
||||
click.echo("Running the Flow")
|
||||
kickoff_flow()
|
||||
|
||||
@@ -646,6 +793,8 @@ def flow_run() -> None:
|
||||
@flow.command(name="plot")
|
||||
def flow_plot() -> None:
|
||||
"""Plot the Flow."""
|
||||
from crewai_cli.plot_flow import plot_flow
|
||||
|
||||
click.echo("Plotting the Flow")
|
||||
plot_flow()
|
||||
|
||||
@@ -654,6 +803,8 @@ def flow_plot() -> None:
|
||||
@click.argument("crew_name")
|
||||
def flow_add_crew(crew_name: str) -> None:
|
||||
"""Add a crew to an existing flow."""
|
||||
from crewai_cli.add_crew_to_flow import add_crew_to_flow
|
||||
|
||||
click.echo(f"Adding crew {crew_name} to the flow")
|
||||
add_crew_to_flow(crew_name)
|
||||
|
||||
@@ -666,6 +817,8 @@ def triggers() -> None:
|
||||
@triggers.command(name="list")
|
||||
def triggers_list() -> None:
|
||||
"""List all available triggers from integrations."""
|
||||
from crewai_cli.triggers.main import TriggersCommand
|
||||
|
||||
triggers_cmd = TriggersCommand()
|
||||
triggers_cmd.list_triggers()
|
||||
|
||||
@@ -674,6 +827,8 @@ def triggers_list() -> None:
|
||||
@click.argument("trigger_path")
|
||||
def triggers_run(trigger_path: str) -> None:
|
||||
"""Execute crew with trigger payload. Format: app_slug/trigger_slug"""
|
||||
from crewai_cli.triggers.main import TriggersCommand
|
||||
|
||||
triggers_cmd = TriggersCommand()
|
||||
triggers_cmd.execute_with_trigger(trigger_path)
|
||||
|
||||
@@ -686,6 +841,8 @@ def chat() -> None:
|
||||
click.secho(
|
||||
"\nStarting a conversation with the Crew\nType 'exit' or Ctrl+C to quit.\n",
|
||||
)
|
||||
from crewai_cli.crew_chat import run_chat
|
||||
|
||||
run_chat()
|
||||
|
||||
|
||||
@@ -725,6 +882,8 @@ def enterprise() -> None:
|
||||
@click.argument("enterprise_url")
|
||||
def enterprise_configure(enterprise_url: str) -> None:
|
||||
"""Configure CrewAI AMP OAuth2 settings from the provided Enterprise URL."""
|
||||
from crewai_cli.enterprise.main import EnterpriseConfigureCommand
|
||||
|
||||
enterprise_command = EnterpriseConfigureCommand()
|
||||
enterprise_command.configure(enterprise_url)
|
||||
|
||||
@@ -737,6 +896,8 @@ def config() -> None:
|
||||
@config.command("list")
|
||||
def config_list() -> None:
|
||||
"""List all CLI configuration parameters."""
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
|
||||
config_command = SettingsCommand()
|
||||
config_command.list()
|
||||
|
||||
@@ -746,6 +907,8 @@ def config_list() -> None:
|
||||
@click.argument("value")
|
||||
def config_set(key: str, value: str) -> None:
|
||||
"""Set a CLI configuration parameter."""
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
|
||||
config_command = SettingsCommand()
|
||||
config_command.set(key, value)
|
||||
|
||||
@@ -753,6 +916,8 @@ def config_set(key: str, value: str) -> None:
|
||||
@config.command("reset")
|
||||
def config_reset() -> None:
|
||||
"""Reset all CLI configuration parameters to default values."""
|
||||
from crewai_cli.settings.main import SettingsCommand
|
||||
|
||||
config_command = SettingsCommand()
|
||||
config_command.reset_all_settings()
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ from crewai_cli.plus_api import PlusAPI
|
||||
console = Console()
|
||||
|
||||
|
||||
class AuthenticationRequiredError(SystemExit):
|
||||
"""Raised when a Plus API command needs the user to log in first."""
|
||||
|
||||
|
||||
class BaseCommand:
|
||||
def __init__(self) -> None:
|
||||
self._telemetry = Telemetry()
|
||||
@@ -31,7 +35,7 @@ class PlusAPIMixin:
|
||||
style="bold red",
|
||||
)
|
||||
console.print("Run 'crewai login' to sign up/login.", style="bold green")
|
||||
raise SystemExit from None
|
||||
raise AuthenticationRequiredError from None
|
||||
|
||||
def _validate_response(self, response: httpx.Response) -> None:
|
||||
"""Handle and display error messages from API responses.
|
||||
|
||||
1126
lib/cli/src/crewai_cli/create_json_crew.py
Normal file
1126
lib/cli/src/crewai_cli/create_json_crew.py
Normal file
File diff suppressed because it is too large
Load Diff
2098
lib/cli/src/crewai_cli/crew_run_tui.py
Normal file
2098
lib/cli/src/crewai_cli/crew_run_tui.py
Normal file
File diff suppressed because it is too large
Load Diff
409
lib/cli/src/crewai_cli/deploy/archive.py
Normal file
409
lib/cli/src/crewai_cli/deploy/archive.py
Normal file
@@ -0,0 +1,409 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import Any
|
||||
import zipfile
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.deploy.validate import normalize_package_name
|
||||
from crewai_cli.utils import parse_toml
|
||||
|
||||
|
||||
_EXCLUDED_DIRS = {
|
||||
".crewai",
|
||||
".git",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
".ruff_cache",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pycache__",
|
||||
"build",
|
||||
"dist",
|
||||
"env",
|
||||
"venv",
|
||||
}
|
||||
_EXCLUDED_FILES = {
|
||||
".DS_Store",
|
||||
".env",
|
||||
}
|
||||
_ALLOWED_ENV_EXAMPLES = {
|
||||
".env.example",
|
||||
".env.sample",
|
||||
}
|
||||
_EXCLUDED_SUFFIXES = {
|
||||
".pyc",
|
||||
".pyo",
|
||||
}
|
||||
_SCRIPT_KEY_PATTERN = re.compile(r"^\s*(?P<key>[A-Za-z0-9_.-]+|\"[^\"]+\"|'[^']+')\s*=")
|
||||
_SECTION_PATTERN = re.compile(r"^\s*\[[^\]]+\]\s*(?:#.*)?$")
|
||||
|
||||
|
||||
def create_project_zip(
|
||||
project_name: str,
|
||||
*,
|
||||
project_dir: Path | None = None,
|
||||
repository: git.Repository | None = None,
|
||||
) -> Path:
|
||||
"""Create a deployable ZIP archive for a CrewAI project."""
|
||||
root = (project_dir or Path.cwd()).resolve()
|
||||
files = _project_files(root, repository)
|
||||
if not files:
|
||||
raise ValueError("No deployable project files were found.")
|
||||
|
||||
staged_root = _stage_project(root, files)
|
||||
archive_handle = tempfile.NamedTemporaryFile(
|
||||
prefix=f"{project_name}-",
|
||||
suffix=".zip",
|
||||
delete=False,
|
||||
)
|
||||
archive_path = Path(archive_handle.name)
|
||||
archive_handle.close()
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for relative_path in _walk_files(staged_root):
|
||||
absolute_path = staged_root / relative_path
|
||||
zip_file.write(absolute_path, relative_path.as_posix())
|
||||
finally:
|
||||
shutil.rmtree(staged_root, ignore_errors=True)
|
||||
|
||||
return archive_path
|
||||
|
||||
|
||||
def _project_files(root: Path, repository: git.Repository | None = None) -> list[Path]:
|
||||
"""Return project-relative files to include in the archive."""
|
||||
if repository is not None:
|
||||
return _repository_project_files(root, repository)
|
||||
|
||||
try:
|
||||
repository = git.Repository(path=str(root), fetch=False)
|
||||
except ValueError:
|
||||
repository = None
|
||||
|
||||
if repository is not None:
|
||||
return _repository_project_files(root, repository)
|
||||
|
||||
return [
|
||||
path
|
||||
for path in _walk_files(root)
|
||||
if not _is_excluded(path) and _is_regular_file(root / path)
|
||||
]
|
||||
|
||||
|
||||
def _repository_project_files(root: Path, repository: git.Repository) -> list[Path]:
|
||||
"""Return deployable files from Git while applying local safety excludes."""
|
||||
files = [Path(path) for path in repository.deployable_files()]
|
||||
return [
|
||||
path
|
||||
for path in files
|
||||
if not _is_excluded(path) and _is_regular_file(root / path)
|
||||
]
|
||||
|
||||
|
||||
def _walk_files(root: Path) -> list[Path]:
|
||||
"""List regular files below root as project-relative paths."""
|
||||
return [
|
||||
path.relative_to(root) for path in root.rglob("*") if _is_regular_file(path)
|
||||
]
|
||||
|
||||
|
||||
def _is_regular_file(path: Path) -> bool:
|
||||
"""Return True for regular files, excluding symlinks to files."""
|
||||
return path.is_file() and not path.is_symlink()
|
||||
|
||||
|
||||
def _is_excluded(path: Path) -> bool:
|
||||
"""Return True when a file should be omitted from deployment ZIPs."""
|
||||
parts = set(path.parts)
|
||||
if parts.intersection(_EXCLUDED_DIRS):
|
||||
return True
|
||||
|
||||
name = path.name
|
||||
if name in _EXCLUDED_FILES:
|
||||
return True
|
||||
if name.startswith(".env.") and name not in _ALLOWED_ENV_EXAMPLES:
|
||||
return True
|
||||
return path.suffix in _EXCLUDED_SUFFIXES
|
||||
|
||||
|
||||
def _stage_project(root: Path, files: list[Path]) -> Path:
|
||||
"""Copy archive files into a temporary staging directory."""
|
||||
staging_root = Path(tempfile.mkdtemp(prefix="crewai-deploy-"))
|
||||
|
||||
try:
|
||||
for relative_path in files:
|
||||
source = root / relative_path
|
||||
if not _is_regular_file(source):
|
||||
continue
|
||||
|
||||
destination = staging_root / relative_path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
if _is_json_crew_project(staging_root):
|
||||
_add_json_crew_deploy_wrapper(staging_root)
|
||||
except Exception:
|
||||
shutil.rmtree(staging_root, ignore_errors=True)
|
||||
raise
|
||||
return staging_root
|
||||
|
||||
|
||||
def _is_json_crew_project(root: Path) -> bool:
|
||||
"""Return True for JSON crew projects that need a Python deploy wrapper."""
|
||||
if not ((root / "crew.jsonc").is_file() or (root / "crew.json").is_file()):
|
||||
return False
|
||||
|
||||
project = _read_pyproject(root)
|
||||
tool_config = project.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
|
||||
)
|
||||
if declared_type == "flow":
|
||||
return False
|
||||
|
||||
package_name = _package_name(root)
|
||||
if package_name is None:
|
||||
raise ValueError(
|
||||
"Could not derive a valid Python package name from [project].name."
|
||||
)
|
||||
|
||||
return not (root / "src" / package_name / "crew.py").is_file()
|
||||
|
||||
|
||||
def _read_pyproject(root: Path) -> dict[str, Any]:
|
||||
"""Read pyproject.toml, returning an empty mapping on missing or invalid data."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
if not pyproject_path.is_file():
|
||||
return {}
|
||||
try:
|
||||
pyproject = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
return pyproject if isinstance(pyproject, dict) else {}
|
||||
|
||||
|
||||
def _package_name(root: Path) -> str | None:
|
||||
"""Return the normalized Python package name for the project."""
|
||||
project = _read_pyproject(root).get("project")
|
||||
if not isinstance(project, dict):
|
||||
return None
|
||||
|
||||
name = project.get("name")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
return None
|
||||
|
||||
package_name = normalize_package_name(name)
|
||||
return package_name or None
|
||||
|
||||
|
||||
def _class_name(package_name: str) -> str:
|
||||
"""Return the generated wrapper class name for a package."""
|
||||
parts = [part for part in re.split(r"[^a-zA-Z0-9]+", package_name) if part]
|
||||
class_name = "".join(part[:1].upper() + part[1:] for part in parts)
|
||||
if not class_name:
|
||||
return "JsonCrew"
|
||||
if class_name[0].isdigit():
|
||||
return f"Crew{class_name}"
|
||||
return class_name
|
||||
|
||||
|
||||
def _add_json_crew_deploy_wrapper(root: Path) -> None:
|
||||
"""Add Python wrapper files required to deploy a JSON crew project."""
|
||||
package_name = _package_name(root)
|
||||
if package_name is None:
|
||||
raise ValueError(
|
||||
"Could not derive a valid Python package name from [project].name."
|
||||
)
|
||||
|
||||
package_dir = root / "src" / package_name
|
||||
config_dir = package_dir / "config"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class_name = _class_name(package_name)
|
||||
crew_filename = "crew.jsonc" if (root / "crew.jsonc").is_file() else "crew.json"
|
||||
|
||||
(package_dir / "__init__.py").write_text("", encoding="utf-8")
|
||||
(config_dir / "agents.yaml").write_text("{}\n", encoding="utf-8")
|
||||
(config_dir / "tasks.yaml").write_text("{}\n", encoding="utf-8")
|
||||
(package_dir / "crew.py").write_text(
|
||||
_json_crew_py(class_name, crew_filename),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(package_dir / "main.py").write_text(
|
||||
_json_main_py(package_name, class_name),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_ensure_project_scripts(root, package_name)
|
||||
|
||||
|
||||
def _json_crew_py(class_name: str, crew_filename: str) -> str:
|
||||
"""Render the generated crew.py module for a JSON crew."""
|
||||
return f'''from pathlib import Path
|
||||
|
||||
from crewai import Crew
|
||||
from crewai.project import CrewBase, crew
|
||||
from crewai.project.crew_loader import load_crew
|
||||
|
||||
|
||||
def _crew_path() -> Path:
|
||||
return Path(__file__).resolve().parents[2] / "{crew_filename}"
|
||||
|
||||
|
||||
@CrewBase
|
||||
class {class_name}:
|
||||
"""Compatibility wrapper for a JSON-defined CrewAI project."""
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
crew_instance, default_inputs = load_crew(_crew_path())
|
||||
self.default_inputs = default_inputs
|
||||
return crew_instance
|
||||
'''
|
||||
|
||||
|
||||
def _json_main_py(package_name: str, class_name: str) -> str:
|
||||
"""Render the generated main.py entrypoints for a JSON crew."""
|
||||
return f"""#!/usr/bin/env python
|
||||
import json
|
||||
import sys
|
||||
|
||||
from {package_name}.crew import {class_name}
|
||||
|
||||
|
||||
def _load():
|
||||
wrapper = {class_name}()
|
||||
crew = wrapper.crew()
|
||||
return crew, getattr(wrapper, "default_inputs", {{}})
|
||||
|
||||
|
||||
def run():
|
||||
crew, inputs = _load()
|
||||
return crew.kickoff(inputs=inputs)
|
||||
|
||||
|
||||
def train():
|
||||
crew, inputs = _load()
|
||||
return crew.train(
|
||||
n_iterations=int(sys.argv[1]),
|
||||
filename=sys.argv[2],
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
|
||||
def replay():
|
||||
crew, _ = _load()
|
||||
return crew.replay(task_id=sys.argv[1])
|
||||
|
||||
|
||||
def test():
|
||||
crew, inputs = _load()
|
||||
return crew.test(
|
||||
n_iterations=int(sys.argv[1]),
|
||||
eval_llm=sys.argv[2],
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
|
||||
def run_with_trigger():
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError("No trigger payload provided.")
|
||||
|
||||
crew, inputs = _load()
|
||||
trigger_payload = json.loads(sys.argv[1])
|
||||
return crew.kickoff(
|
||||
inputs={{**inputs, "crewai_trigger_payload": trigger_payload}}
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _ensure_project_scripts(root: Path, package_name: str) -> None:
|
||||
"""Ensure generated wrappers have project script entrypoints."""
|
||||
pyproject_path = root / "pyproject.toml"
|
||||
if not pyproject_path.is_file():
|
||||
return
|
||||
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
entries = _project_script_entries(package_name)
|
||||
pyproject_path.write_text(
|
||||
_update_project_scripts(content, entries),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _project_script_entries(package_name: str) -> dict[str, str]:
|
||||
"""Return script entrypoints required by the generated JSON wrapper."""
|
||||
return {
|
||||
package_name: f"{package_name}.main:run",
|
||||
"run_crew": f"{package_name}.main:run",
|
||||
"train": f"{package_name}.main:train",
|
||||
"replay": f"{package_name}.main:replay",
|
||||
"test": f"{package_name}.main:test",
|
||||
"run_with_trigger": f"{package_name}.main:run_with_trigger",
|
||||
}
|
||||
|
||||
|
||||
def _update_project_scripts(content: str, entries: dict[str, str]) -> str:
|
||||
"""Add or replace generated script entries in pyproject.toml content."""
|
||||
lines = content.rstrip().splitlines()
|
||||
header_index = _project_scripts_header_index(lines)
|
||||
if header_index is None:
|
||||
return content.rstrip() + _project_scripts_block(entries)
|
||||
|
||||
end_index = _section_end_index(lines, header_index + 1)
|
||||
seen: set[str] = set()
|
||||
for index in range(header_index + 1, end_index):
|
||||
key = _script_key(lines[index])
|
||||
if key in entries:
|
||||
lines[index] = _script_line(key, entries[key])
|
||||
seen.add(key)
|
||||
|
||||
missing_lines = [
|
||||
_script_line(key, value) for key, value in entries.items() if key not in seen
|
||||
]
|
||||
lines[end_index:end_index] = missing_lines
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def _project_scripts_header_index(lines: list[str]) -> int | None:
|
||||
"""Return the line index of the project scripts table, if present."""
|
||||
for index, line in enumerate(lines):
|
||||
if line.strip() == "[project.scripts]":
|
||||
return index
|
||||
return None
|
||||
|
||||
|
||||
def _section_end_index(lines: list[str], start_index: int) -> int:
|
||||
"""Return the exclusive end index for a TOML table section."""
|
||||
for index in range(start_index, len(lines)):
|
||||
if _SECTION_PATTERN.match(lines[index]):
|
||||
return index
|
||||
return len(lines)
|
||||
|
||||
|
||||
def _script_key(line: str) -> str | None:
|
||||
"""Return the script key for a pyproject script line."""
|
||||
match = _SCRIPT_KEY_PATTERN.match(line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
key = match.group("key")
|
||||
if key.startswith(("'", '"')) and key.endswith(("'", '"')):
|
||||
return key[1:-1]
|
||||
return key
|
||||
|
||||
|
||||
def _script_line(key: str, value: str) -> str:
|
||||
"""Render a project script TOML entry."""
|
||||
return f'{key} = "{value}"'
|
||||
|
||||
|
||||
def _project_scripts_block(entries: dict[str, str]) -> str:
|
||||
"""Render a project scripts TOML table."""
|
||||
lines = ["", "", "[project.scripts]"]
|
||||
lines.extend(_script_line(key, value) for key, value in entries.items())
|
||||
return "\n".join(lines) + "\n"
|
||||
@@ -1,3 +1,5 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from crewai_core.plus_api import CreateCrewPayload
|
||||
@@ -5,14 +7,19 @@ from rich.console import Console
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.command import BaseCommand, PlusAPIMixin
|
||||
from crewai_cli.deploy.validate import validate_project
|
||||
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
|
||||
|
||||
|
||||
console = Console()
|
||||
_MISSING_LOCKFILE_ERROR_CODES = {"missing_lockfile"}
|
||||
|
||||
|
||||
def _run_predeploy_validation(skip_validate: bool) -> bool:
|
||||
def _run_predeploy_validation(
|
||||
skip_validate: bool,
|
||||
ignored_error_codes: set[str] | None = None,
|
||||
) -> bool:
|
||||
"""Run pre-deploy validation unless skipped.
|
||||
|
||||
Returns True if deployment should proceed, False if it should abort.
|
||||
@@ -24,8 +31,22 @@ def _run_predeploy_validation(skip_validate: bool) -> bool:
|
||||
return True
|
||||
|
||||
console.print("Running pre-deploy validation...", style="bold blue")
|
||||
validator = validate_project()
|
||||
if not validator.ok:
|
||||
validator = DeployValidator()
|
||||
validator.run()
|
||||
|
||||
ignored_error_codes = ignored_error_codes or set()
|
||||
visible_results = [
|
||||
result
|
||||
for result in validator.results
|
||||
if result.severity is not Severity.ERROR
|
||||
or result.code not in ignored_error_codes
|
||||
]
|
||||
render_report(visible_results)
|
||||
|
||||
blocking_errors = [
|
||||
result for result in validator.errors if result.code not in ignored_error_codes
|
||||
]
|
||||
if blocking_errors:
|
||||
console.print(
|
||||
"\n[bold red]Pre-deploy validation failed. "
|
||||
"Fix the issues above or re-run with --skip-validate.[/bold red]"
|
||||
@@ -34,6 +55,79 @@ def _run_predeploy_validation(skip_validate: bool) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _display_git_repository_help() -> None:
|
||||
"""Explain how to prepare a new project for deployment."""
|
||||
console.print(
|
||||
"Initialized a local Git repository and created an initial commit.",
|
||||
style="green",
|
||||
)
|
||||
|
||||
|
||||
def _display_git_remote_help() -> None:
|
||||
"""Explain that ZIP deployment will be used without an origin remote."""
|
||||
console.print(
|
||||
"No origin remote found. Deploying from a ZIP upload instead.",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
|
||||
def _env_summary(env_vars: dict[str, str]) -> str:
|
||||
"""Return a compact description of environment variables for prompts."""
|
||||
if not env_vars:
|
||||
return "0 env vars"
|
||||
keys = ", ".join(sorted(env_vars))
|
||||
return f"{len(env_vars)} env vars: {keys}"
|
||||
|
||||
|
||||
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()
|
||||
if not (root / "pyproject.toml").is_file():
|
||||
return False
|
||||
return not (root / "uv.lock").is_file() and not (root / "poetry.lock").is_file()
|
||||
|
||||
|
||||
def _ensure_lockfile_for_deploy() -> None:
|
||||
"""Create a uv lockfile before deploy when a project has not been run yet."""
|
||||
if not _needs_lockfile_for_deploy():
|
||||
return
|
||||
|
||||
from crewai_cli.install_crew import install_crew
|
||||
|
||||
console.print(
|
||||
"No lockfile found. Installing dependencies before deployment...",
|
||||
style="bold blue",
|
||||
)
|
||||
try:
|
||||
install_crew([], raise_on_error=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SystemExit(e.returncode) from e
|
||||
except Exception as e:
|
||||
raise SystemExit(1) from e
|
||||
|
||||
|
||||
def _prepare_project_for_deploy(skip_validate: bool) -> bool:
|
||||
"""Validate deploy inputs before creating a missing lockfile."""
|
||||
if skip_validate:
|
||||
_run_predeploy_validation(skip_validate)
|
||||
_ensure_lockfile_for_deploy()
|
||||
return True
|
||||
|
||||
needs_lockfile = _needs_lockfile_for_deploy()
|
||||
ignored_error_codes = _MISSING_LOCKFILE_ERROR_CODES if needs_lockfile else None
|
||||
if not _run_predeploy_validation(
|
||||
skip_validate,
|
||||
ignored_error_codes=ignored_error_codes,
|
||||
):
|
||||
return False
|
||||
|
||||
if not needs_lockfile:
|
||||
return True
|
||||
|
||||
_ensure_lockfile_for_deploy()
|
||||
return _run_predeploy_validation(skip_validate)
|
||||
|
||||
|
||||
class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
"""
|
||||
A class to handle deployment-related operations for CrewAI projects.
|
||||
@@ -92,14 +186,30 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
uuid (Optional[str]): The UUID of the crew to deploy.
|
||||
skip_validate (bool): Skip pre-deploy validation checks.
|
||||
"""
|
||||
if not _run_predeploy_validation(skip_validate):
|
||||
if not _prepare_project_for_deploy(skip_validate):
|
||||
return
|
||||
self._telemetry.start_deployment_span(uuid)
|
||||
console.print("Starting deployment...", style="bold blue")
|
||||
if uuid:
|
||||
repository = self._prepare_git_repository()
|
||||
remote_repo_url = repository.origin_url() if repository else None
|
||||
|
||||
if remote_repo_url and uuid:
|
||||
response = self.plus_api_client.deploy_by_uuid(uuid)
|
||||
elif self.project_name:
|
||||
elif remote_repo_url and self.project_name:
|
||||
response = self.plus_api_client.deploy_by_name(self.project_name)
|
||||
elif uuid:
|
||||
_display_git_remote_help()
|
||||
env_vars = fetch_and_json_env_file()
|
||||
response = self._update_crew_from_zip(uuid, repository, env_vars)
|
||||
elif self.project_name:
|
||||
_display_git_remote_help()
|
||||
deployment_uuid = self._deployment_uuid_by_name()
|
||||
env_vars = fetch_and_json_env_file()
|
||||
response = self._update_crew_from_zip(
|
||||
deployment_uuid,
|
||||
repository,
|
||||
env_vars,
|
||||
)
|
||||
else:
|
||||
self._standard_no_param_error_message()
|
||||
return
|
||||
@@ -107,6 +217,19 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
self._validate_response(response)
|
||||
self._display_deployment_info(response.json())
|
||||
|
||||
def _deployment_uuid_by_name(self) -> str:
|
||||
"""Resolve the current project's deployment UUID by project name."""
|
||||
if not self.project_name:
|
||||
raise ValueError("project_name is required to find a deployment")
|
||||
|
||||
response = self.plus_api_client.crew_status_by_name(self.project_name)
|
||||
self._validate_response(response)
|
||||
json_response = response.json()
|
||||
uuid = json_response.get("uuid")
|
||||
if not uuid:
|
||||
raise ValueError("Deployment status response did not include a uuid")
|
||||
return str(uuid)
|
||||
|
||||
def create_crew(self, confirm: bool = False, skip_validate: bool = False) -> None:
|
||||
"""
|
||||
Create a new crew deployment.
|
||||
@@ -115,32 +238,143 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
confirm (bool): Whether to skip the interactive confirmation prompt.
|
||||
skip_validate (bool): Skip pre-deploy validation checks.
|
||||
"""
|
||||
if not _run_predeploy_validation(skip_validate):
|
||||
if not _prepare_project_for_deploy(skip_validate):
|
||||
return
|
||||
self._telemetry.create_crew_deployment_span()
|
||||
console.print("Creating deployment...", style="bold blue")
|
||||
env_vars = fetch_and_json_env_file()
|
||||
repository = self._prepare_git_repository()
|
||||
remote_repo_url = repository.origin_url() if repository else None
|
||||
|
||||
try:
|
||||
remote_repo_url = git.Repository().origin_url()
|
||||
except ValueError:
|
||||
remote_repo_url = None
|
||||
|
||||
if remote_repo_url is None:
|
||||
console.print("No remote repository URL found.", style="bold red")
|
||||
console.print(
|
||||
"Please ensure your project has a valid remote repository.",
|
||||
style="yellow",
|
||||
)
|
||||
return
|
||||
|
||||
self._confirm_input(env_vars, remote_repo_url, confirm)
|
||||
payload = self._create_payload(env_vars, remote_repo_url)
|
||||
response = self.plus_api_client.create_crew(payload)
|
||||
if remote_repo_url:
|
||||
self._confirm_input(env_vars, remote_repo_url, confirm)
|
||||
payload = self._create_payload(env_vars, remote_repo_url)
|
||||
response = self.plus_api_client.create_crew(payload)
|
||||
else:
|
||||
_display_git_remote_help()
|
||||
response = self._create_crew_from_zip(env_vars, repository, confirm)
|
||||
|
||||
self._validate_response(response)
|
||||
self._display_creation_success(response.json())
|
||||
|
||||
def _prepare_git_repository(self) -> git.Repository | None:
|
||||
"""Prepare Git for deploy while preserving remote deploy when possible."""
|
||||
try:
|
||||
repository = git.Repository(fetch=False)
|
||||
except ValueError as exc:
|
||||
if "not a Git repository" not in str(exc):
|
||||
console.print(
|
||||
f"{exc} Continuing with ZIP deployment.",
|
||||
style="yellow",
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
repository = git.Repository.initialize()
|
||||
except Exception as init_error:
|
||||
console.print(
|
||||
"Git auto-setup did not complete. Continuing with ZIP deployment.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print(str(init_error), style="dim")
|
||||
try:
|
||||
return git.Repository(fetch=False)
|
||||
except Exception as repository_error:
|
||||
console.print(str(repository_error), style="dim")
|
||||
return None
|
||||
|
||||
_display_git_repository_help()
|
||||
return repository
|
||||
|
||||
remote_repo_url = repository.origin_url()
|
||||
if remote_repo_url:
|
||||
try:
|
||||
repository.fetch()
|
||||
except ValueError as fetch_error:
|
||||
console.print(
|
||||
"Could not fetch from origin. Continuing with remote deployment.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print(str(fetch_error), style="dim")
|
||||
|
||||
try:
|
||||
if repository.create_initial_commit_if_needed():
|
||||
console.print(
|
||||
"Created an initial Git commit for this project.",
|
||||
style="green",
|
||||
)
|
||||
except Exception as commit_error:
|
||||
console.print(
|
||||
"Could not create an initial Git commit. "
|
||||
"Continuing with remote deployment.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print(str(commit_error), style="dim")
|
||||
|
||||
return repository
|
||||
|
||||
try:
|
||||
if repository.create_initial_commit_if_needed():
|
||||
console.print(
|
||||
"Created an initial Git commit for this project.",
|
||||
style="green",
|
||||
)
|
||||
except Exception as commit_error:
|
||||
console.print(
|
||||
"Could not create an initial Git commit. "
|
||||
"Continuing with ZIP deployment using Git file listing.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print(str(commit_error), style="dim")
|
||||
return repository
|
||||
|
||||
return repository
|
||||
|
||||
def _create_crew_from_zip(
|
||||
self,
|
||||
env_vars: dict[str, str],
|
||||
repository: git.Repository | None,
|
||||
confirm: bool,
|
||||
) -> Any:
|
||||
"""Create a deployment by uploading a project ZIP archive."""
|
||||
if not self.project_name:
|
||||
raise ValueError("project_name is required to create a ZIP deployment")
|
||||
|
||||
console.print("Preparing project ZIP...", style="bold blue")
|
||||
zip_file_path = create_project_zip(self.project_name, repository=repository)
|
||||
try:
|
||||
self._confirm_zip_input(env_vars, confirm)
|
||||
console.print("Uploading project ZIP...", style="bold blue")
|
||||
return self.plus_api_client.create_crew_from_zip(
|
||||
zip_file_path,
|
||||
name=self.project_name,
|
||||
env=env_vars,
|
||||
)
|
||||
finally:
|
||||
zip_file_path.unlink(missing_ok=True)
|
||||
|
||||
def _update_crew_from_zip(
|
||||
self,
|
||||
uuid: str,
|
||||
repository: git.Repository | None,
|
||||
env_vars: dict[str, str],
|
||||
) -> Any:
|
||||
"""Update an existing deployment by uploading a project ZIP archive."""
|
||||
if not self.project_name:
|
||||
raise ValueError("project_name is required to update a ZIP deployment")
|
||||
|
||||
console.print("Preparing project ZIP...", style="bold blue")
|
||||
zip_file_path = create_project_zip(self.project_name, repository=repository)
|
||||
try:
|
||||
console.print("Uploading project ZIP...", style="bold blue")
|
||||
return self.plus_api_client.update_crew_from_zip(
|
||||
uuid,
|
||||
zip_file_path,
|
||||
env=env_vars,
|
||||
)
|
||||
finally:
|
||||
zip_file_path.unlink(missing_ok=True)
|
||||
|
||||
def _confirm_input(
|
||||
self, env_vars: dict[str, str], remote_repo_url: str, confirm: bool
|
||||
) -> None:
|
||||
@@ -153,11 +387,16 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
confirm (bool): Whether to confirm input.
|
||||
"""
|
||||
if not confirm:
|
||||
input(f"Press Enter to continue with the following Env vars: {env_vars}")
|
||||
input(f"Press Enter to continue with {_env_summary(env_vars)}")
|
||||
input(
|
||||
f"Press Enter to continue with the following remote repository: {remote_repo_url}\n"
|
||||
)
|
||||
|
||||
def _confirm_zip_input(self, env_vars: dict[str, str], confirm: bool) -> None:
|
||||
"""Prompt before ZIP upload unless confirmation was already supplied."""
|
||||
if not confirm:
|
||||
input(f"Press Enter to continue with {_env_summary(env_vars)}")
|
||||
|
||||
def _create_payload(
|
||||
self,
|
||||
env_vars: dict[str, str],
|
||||
|
||||
@@ -38,6 +38,12 @@ import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from crewai.project.json_loader import (
|
||||
JSONProjectValidationError,
|
||||
find_crew_json_file,
|
||||
find_json_project_file,
|
||||
validate_crew_project,
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli.utils import parse_toml
|
||||
@@ -151,9 +157,33 @@ class DeployValidator:
|
||||
def ok(self) -> bool:
|
||||
return not self.errors
|
||||
|
||||
@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
|
||||
pyproject_path = self.project_root / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return True
|
||||
try:
|
||||
data = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return True
|
||||
declared_type: str | None = (
|
||||
(data.get("tool") or {}).get("crewai", {}).get("type")
|
||||
)
|
||||
return declared_type != "flow"
|
||||
|
||||
def run(self) -> list[ValidationResult]:
|
||||
"""Run all checks. Later checks are skipped when earlier ones make
|
||||
them impossible (e.g. no pyproject.toml → no lockfile check)."""
|
||||
if self._is_json_crew:
|
||||
return self._run_json_checks()
|
||||
|
||||
if not self._check_pyproject():
|
||||
return self.results
|
||||
|
||||
@@ -176,6 +206,110 @@ class DeployValidator:
|
||||
|
||||
return self.results
|
||||
|
||||
def _run_json_checks(self) -> list[ValidationResult]:
|
||||
"""Validation suite for JSON-defined crew projects."""
|
||||
crew_path = find_crew_json_file(self.project_root)
|
||||
if crew_path is None:
|
||||
return self.results
|
||||
|
||||
try:
|
||||
project = validate_crew_project(crew_path, self.project_root / "agents")
|
||||
except JSONProjectValidationError as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"invalid_crew_json",
|
||||
f"{crew_path.name} has invalid JSON crew configuration",
|
||||
detail="\n".join(e.errors),
|
||||
hint="Fix the JSON crew, agent, and task references before deploying.",
|
||||
)
|
||||
return self.results
|
||||
except Exception as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"invalid_crew_json",
|
||||
f"Cannot parse {crew_path.name}",
|
||||
detail=str(e),
|
||||
)
|
||||
return self.results
|
||||
|
||||
agents_dir = self.project_root / "agents"
|
||||
|
||||
self._check_pyproject()
|
||||
self._check_lockfile()
|
||||
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
|
||||
self._check_version_vs_lockfile()
|
||||
|
||||
return self.results
|
||||
|
||||
def _check_env_vars_json(
|
||||
self, crew_path: Path, agents_dir: Path, agent_names: list[str]
|
||||
) -> None:
|
||||
"""Check for env var references in JSON crew files."""
|
||||
referenced: set[str] = set()
|
||||
pattern = re.compile(r"\$\{?([A-Z][A-Z0-9_]+)\}?")
|
||||
|
||||
try:
|
||||
referenced.update(pattern.findall(crew_path.read_text(errors="ignore")))
|
||||
except OSError as exc:
|
||||
logger.debug("Skipping unreadable crew file %s: %s", crew_path, exc)
|
||||
|
||||
for name in agent_names:
|
||||
agent_path = find_json_project_file(agents_dir, name)
|
||||
if agent_path is None:
|
||||
continue
|
||||
try:
|
||||
referenced.update(
|
||||
pattern.findall(agent_path.read_text(errors="ignore"))
|
||||
)
|
||||
except OSError as exc:
|
||||
logger.debug("Skipping unreadable agent file %s: %s", agent_path, exc)
|
||||
|
||||
for py_path in self.project_root.rglob("*.py"):
|
||||
if ".venv" in py_path.parts:
|
||||
continue
|
||||
try:
|
||||
text = py_path.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
env_pattern = re.compile(
|
||||
r"""(?x)
|
||||
(?:os\.environ\s*(?:\[\s*|\.get\s*\(\s*)
|
||||
|os\.getenv\s*\(\s*
|
||||
|getenv\s*\(\s*)
|
||||
['"]([A-Z][A-Z0-9_]*)['"]
|
||||
"""
|
||||
)
|
||||
referenced.update(env_pattern.findall(text))
|
||||
|
||||
env_file = self.project_root / ".env"
|
||||
env_keys: set[str] = set()
|
||||
if env_file.exists():
|
||||
for line in env_file.read_text(errors="ignore").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
env_keys.add(line.split("=", 1)[0].strip())
|
||||
|
||||
missing_known = sorted(
|
||||
var
|
||||
for var in referenced
|
||||
if var in _KNOWN_API_KEY_HINTS
|
||||
and var not in env_keys
|
||||
and var not in os.environ
|
||||
)
|
||||
if missing_known:
|
||||
self._add(
|
||||
Severity.WARNING,
|
||||
"env_vars_not_in_dotenv",
|
||||
f"{len(missing_known)} referenced API key(s) not in .env",
|
||||
detail=(
|
||||
"These env vars are referenced in your project but not set "
|
||||
f"locally: {', '.join(missing_known)}. Deploys will fail "
|
||||
"unless they are added to the deployment's Environment "
|
||||
"Variables in the CrewAI dashboard."
|
||||
),
|
||||
)
|
||||
|
||||
def _check_pyproject(self) -> bool:
|
||||
pyproject_path = self.project_root / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
_INITIAL_COMMIT_EXCLUDE_PATTERNS = [
|
||||
".crewai/",
|
||||
".env",
|
||||
".env.*",
|
||||
"!.env.example",
|
||||
"!.env.sample",
|
||||
".mypy_cache/",
|
||||
".pytest_cache/",
|
||||
".ruff_cache/",
|
||||
".tox/",
|
||||
".venv/",
|
||||
"__pycache__/",
|
||||
"build/",
|
||||
"dist/",
|
||||
"env/",
|
||||
"venv/",
|
||||
]
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self, path: str = ".") -> None:
|
||||
def __init__(self, path: str = ".", fetch: bool = True) -> None:
|
||||
self.path = path
|
||||
|
||||
if not self.is_git_installed():
|
||||
@@ -12,7 +34,8 @@ class Repository:
|
||||
if not self.is_git_repo:
|
||||
raise ValueError(f"{self.path} is not a Git repository.")
|
||||
|
||||
self.fetch()
|
||||
if fetch:
|
||||
self.fetch()
|
||||
|
||||
@staticmethod
|
||||
def is_git_installed() -> bool:
|
||||
@@ -30,7 +53,33 @@ class Repository:
|
||||
|
||||
def fetch(self) -> None:
|
||||
"""Fetch latest updates from the remote."""
|
||||
subprocess.run(["git", "fetch"], cwd=self.path, check=True) # noqa: S607
|
||||
command = ["git", "fetch"]
|
||||
result = subprocess.run( # noqa: S603
|
||||
command,
|
||||
cwd=self.path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
if "No remote repository specified" in result.stderr:
|
||||
return
|
||||
details = result.stderr.strip() or result.stdout.strip() or "no output"
|
||||
raise ValueError(
|
||||
f"Git fetch failed with exit code {result.returncode} "
|
||||
f"for command {command!r}: {details}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def initialize(cls, path: str = ".") -> Repository:
|
||||
"""Initialize a Git repository and create an initial commit if needed."""
|
||||
if not cls.is_git_installed():
|
||||
raise ValueError("Git is not installed or not found in your PATH.")
|
||||
|
||||
subprocess.run(["git", "init"], cwd=path, check=True) # noqa: S607
|
||||
repository = cls(path=path, fetch=False)
|
||||
repository.create_initial_commit_if_needed()
|
||||
return repository
|
||||
|
||||
def status(self) -> str:
|
||||
"""Get the git status in porcelain format."""
|
||||
@@ -48,6 +97,7 @@ class Repository:
|
||||
["git", "rev-parse", "--is-inside-work-tree"], # noqa: S607
|
||||
cwd=self.path,
|
||||
encoding="utf-8",
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
@@ -70,6 +120,74 @@ class Repository:
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_commits(self) -> bool:
|
||||
"""Return True if the repository has at least one commit."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "rev-parse", "--verify", "HEAD"], # noqa: S607
|
||||
cwd=self.path,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def create_initial_commit_if_needed(self) -> bool:
|
||||
"""Create a local initial commit when the repository has no commits."""
|
||||
if self.has_commits():
|
||||
return False
|
||||
|
||||
self._ensure_initial_commit_excludes()
|
||||
subprocess.run(["git", "add", "."], cwd=self.path, check=True) # noqa: S607
|
||||
command = [
|
||||
"git",
|
||||
"-c",
|
||||
"user.name=CrewAI",
|
||||
"-c",
|
||||
"user.email=deploy@crewai.com",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"Initial crew",
|
||||
]
|
||||
subprocess.run( # noqa: S603
|
||||
command,
|
||||
cwd=self.path,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
|
||||
def _ensure_initial_commit_excludes(self) -> None:
|
||||
"""Add local-only ignore patterns before auto-staging an initial commit."""
|
||||
exclude_file = Path(self.path) / ".git" / "info" / "exclude"
|
||||
exclude_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
existing = exclude_file.read_text() if exclude_file.exists() else ""
|
||||
existing_lines = set(existing.splitlines())
|
||||
missing_patterns = [
|
||||
pattern
|
||||
for pattern in _INITIAL_COMMIT_EXCLUDE_PATTERNS
|
||||
if pattern not in existing_lines
|
||||
]
|
||||
if not missing_patterns:
|
||||
return
|
||||
|
||||
prefix = "" if existing.endswith("\n") or not existing else "\n"
|
||||
patterns = "\n".join(missing_patterns)
|
||||
exclude_file.write_text(
|
||||
f"{existing}{prefix}# CrewAI deploy auto-commit excludes\n{patterns}\n"
|
||||
)
|
||||
|
||||
def deployable_files(self) -> list[str]:
|
||||
"""Return files tracked by Git or untracked and not ignored."""
|
||||
output = subprocess.check_output(
|
||||
["git", "ls-files", "--cached", "--others", "--exclude-standard"], # noqa: S607
|
||||
cwd=self.path,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return [line for line in output.splitlines() if line]
|
||||
|
||||
def origin_url(self) -> str | None:
|
||||
"""Get the Git repository's remote URL."""
|
||||
try:
|
||||
|
||||
@@ -1,20 +1,77 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
pyproject = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return True
|
||||
if not isinstance(pyproject, dict):
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
return declared_type != "flow"
|
||||
|
||||
|
||||
# Be mindful about changing this.
|
||||
# on some environments we don't use this command but instead uv sync directly
|
||||
# so if you expect this to support more things you will need to replicate it there
|
||||
# ask @joaomdmoura if you are unsure
|
||||
def install_crew(proxy_options: list[str]) -> None:
|
||||
def install_crew(
|
||||
proxy_options: list[str],
|
||||
*,
|
||||
raise_on_error: bool = False,
|
||||
install_project: bool | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Install the crew by running the UV command to lock and install.
|
||||
"""
|
||||
try:
|
||||
command = ["uv", "sync", *proxy_options]
|
||||
if install_project is None:
|
||||
install_project = not _is_json_crew_project()
|
||||
|
||||
command = ["uv", "sync"]
|
||||
if not install_project and "--no-install-project" not in proxy_options:
|
||||
command.append("--no-install-project")
|
||||
command.extend(proxy_options)
|
||||
|
||||
# Inject tool repository credentials so uv can authenticate
|
||||
# against private package indexes (e.g. crewai tool repository).
|
||||
@@ -22,11 +79,21 @@ def install_crew(proxy_options: list[str]) -> None:
|
||||
# project depends on tools from a private index.
|
||||
env = build_env_with_all_tool_credentials()
|
||||
|
||||
subprocess.run(command, check=True, capture_output=False, text=True, env=env) # noqa: S603
|
||||
subprocess.run( # noqa: S603
|
||||
command,
|
||||
check=True,
|
||||
capture_output=False,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"An error occurred while running the crew: {e}", err=True)
|
||||
click.echo(e.output, err=True)
|
||||
if raise_on_error:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
if raise_on_error:
|
||||
raise
|
||||
|
||||
@@ -1,12 +1,95 @@
|
||||
"""Re-export of ``crewai_core.plus_api.PlusAPI``.
|
||||
|
||||
Kept as a stable import path for the CLI; new code should import from
|
||||
``crewai_core.plus_api`` directly.
|
||||
"""
|
||||
"""CrewAI CLI API client extensions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from crewai_core.plus_api import PlusAPI as PlusAPI
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, cast
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from crewai_core.plus_api import PlusAPI as _CorePlusAPI
|
||||
import httpx
|
||||
|
||||
|
||||
HttpMethod = Literal["GET", "POST", "PATCH", "DELETE"]
|
||||
|
||||
|
||||
class PlusAPI(_CorePlusAPI):
|
||||
"""CLI API client.
|
||||
|
||||
The ZIP deployment methods live here as well as in newer crewai-core
|
||||
versions so editable CLI installs still work when an older crewai-core is
|
||||
present in the runtime environment.
|
||||
"""
|
||||
|
||||
def _make_multipart_request(
|
||||
self,
|
||||
method: HttpMethod,
|
||||
endpoint: str,
|
||||
*,
|
||||
zip_file_path: str | Path,
|
||||
data: dict[str, str] | None = None,
|
||||
timeout: float | None = None,
|
||||
verify: bool = True,
|
||||
) -> httpx.Response:
|
||||
"""Send an authenticated multipart request containing a project ZIP."""
|
||||
url = urljoin(self.base_url, endpoint)
|
||||
headers = dict(cast(dict[str, str], self.headers))
|
||||
headers.pop("Content-Type", None)
|
||||
path = Path(zip_file_path)
|
||||
request_kwargs: dict[str, Any] = {"headers": headers}
|
||||
if data is not None:
|
||||
request_kwargs["data"] = data
|
||||
if timeout is not None:
|
||||
request_kwargs["timeout"] = timeout
|
||||
|
||||
with (
|
||||
path.open("rb") as file_handle,
|
||||
httpx.Client(trust_env=False, verify=verify) as client,
|
||||
):
|
||||
files = {
|
||||
"zip_file": (path.name, file_handle, "application/zip"),
|
||||
}
|
||||
return client.request(method, url, files=files, **request_kwargs)
|
||||
|
||||
def create_crew_from_zip(
|
||||
self,
|
||||
zip_file_path: str | Path,
|
||||
*,
|
||||
name: str | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> httpx.Response:
|
||||
"""Create a crew deployment from a local project ZIP archive."""
|
||||
data: dict[str, str] = {}
|
||||
if name:
|
||||
data["name"] = name
|
||||
if env:
|
||||
data.update({f"env[{key}]": value for key, value in env.items()})
|
||||
return self._make_multipart_request(
|
||||
"POST",
|
||||
f"{self.CREWS_RESOURCE}/zip",
|
||||
zip_file_path=zip_file_path,
|
||||
data=data or None,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
def update_crew_from_zip(
|
||||
self,
|
||||
uuid: str,
|
||||
zip_file_path: str | Path,
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> httpx.Response:
|
||||
"""Update an existing crew deployment from a local project ZIP archive."""
|
||||
data: dict[str, str] = {}
|
||||
if env:
|
||||
data.update({f"env[{key}]": value for key, value in env.items()})
|
||||
return self._make_multipart_request(
|
||||
"POST",
|
||||
f"{self.CREWS_RESOURCE}/{uuid}/zip_update",
|
||||
zip_file_path=zip_file_path,
|
||||
data=data or None,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["PlusAPI"]
|
||||
|
||||
@@ -1,25 +1,475 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from enum import Enum
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
from crewai.project.json_loader import find_crew_json_file
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
from packaging import version
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
read_toml,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_version
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
|
||||
class CrewType(Enum):
|
||||
STANDARD = "standard"
|
||||
FLOW = "flow"
|
||||
|
||||
|
||||
def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
"""Run the crew or flow by running a command in the UV environment.
|
||||
# Must accept the same names as the kickoff interpolation pattern in
|
||||
# crewai.utilities.string_utils (_VARIABLE_PATTERN), including hyphens —
|
||||
# otherwise placeholders are interpolated at runtime but never prompted for.
|
||||
_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"
|
||||
_JSON_CREW_RUNNER_CODE = """
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
Starting from version 0.103.0, this command can be used to run both
|
||||
standard crews and flows. For flows, it detects the type from pyproject.toml
|
||||
and automatically runs the appropriate command.
|
||||
source_dir = os.environ.get("CREWAI_RUNNER_SOURCE_DIR")
|
||||
if source_dir:
|
||||
sys.path.insert(0, source_dir)
|
||||
|
||||
package_dir = Path(os.environ["CREWAI_CLI_RUNNER_PACKAGE_DIR"])
|
||||
package_spec = importlib.util.spec_from_file_location(
|
||||
"crewai_cli",
|
||||
package_dir / "__init__.py",
|
||||
submodule_search_locations=[str(package_dir)],
|
||||
)
|
||||
if package_spec is None or package_spec.loader is None:
|
||||
raise ImportError(f"Cannot load CrewAI CLI package from {package_dir}")
|
||||
|
||||
package = importlib.util.module_from_spec(package_spec)
|
||||
sys.modules["crewai_cli"] = package
|
||||
package_spec.loader.exec_module(package)
|
||||
|
||||
module_path = package_dir / "run_crew.py"
|
||||
module_spec = importlib.util.spec_from_file_location("crewai_cli.run_crew", module_path)
|
||||
if module_spec is None or module_spec.loader is None:
|
||||
raise ImportError(f"Cannot load CrewAI CLI runner from {module_path}")
|
||||
|
||||
module = importlib.util.module_from_spec(module_spec)
|
||||
sys.modules["crewai_cli.run_crew"] = module
|
||||
module_spec.loader.exec_module(module)
|
||||
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
|
||||
module._run_json_crew(
|
||||
trained_agents_file=os.getenv(CREWAI_TRAINED_AGENTS_FILE_ENV)
|
||||
)
|
||||
""".strip()
|
||||
|
||||
|
||||
def _has_json_crew() -> bool:
|
||||
"""Check if this is a JSON-defined crew project.
|
||||
|
||||
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 declared_type != "flow"
|
||||
|
||||
|
||||
def _extract_input_placeholders(text: str | None) -> set[str]:
|
||||
if not text:
|
||||
return set()
|
||||
return set(_INPUT_PLACEHOLDER_RE.findall(text))
|
||||
|
||||
|
||||
def _missing_input_names(crew: Any, inputs: dict[str, Any]) -> list[str]:
|
||||
"""Return input placeholders used by a crew but not provided as defaults."""
|
||||
placeholders: set[str] = set()
|
||||
|
||||
for agent in getattr(crew, "agents", []) or []:
|
||||
placeholders.update(_extract_input_placeholders(getattr(agent, "role", None)))
|
||||
placeholders.update(_extract_input_placeholders(getattr(agent, "goal", None)))
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(agent, "backstory", None))
|
||||
)
|
||||
|
||||
for task in getattr(crew, "tasks", []) or []:
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(task, "description", None))
|
||||
)
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(task, "expected_output", None))
|
||||
)
|
||||
placeholders.update(
|
||||
_extract_input_placeholders(getattr(task, "output_file", None))
|
||||
)
|
||||
|
||||
return sorted(name for name in placeholders if name not in inputs)
|
||||
|
||||
|
||||
def _prompt_for_missing_inputs(
|
||||
crew: Any, default_inputs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Ask for runtime values for placeholders that lack default inputs."""
|
||||
inputs = dict(default_inputs or {})
|
||||
missing = _missing_input_names(crew, inputs)
|
||||
if not missing:
|
||||
return inputs
|
||||
|
||||
enable_prompt_line_editing()
|
||||
|
||||
click.echo()
|
||||
click.secho(" Runtime inputs", fg="cyan", bold=True)
|
||||
click.secho(
|
||||
" Values for {placeholder} references in your agents and tasks.",
|
||||
dim=True,
|
||||
)
|
||||
|
||||
for name in missing:
|
||||
inputs[name] = click.prompt(
|
||||
click.style(f" {name}", fg="cyan"),
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
def _json_loading_status(message: str) -> AbstractContextManager[Any]:
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
if not console.is_terminal:
|
||||
return nullcontext()
|
||||
return console.status(
|
||||
Text(f" {message}", style="bold #1F7982"),
|
||||
spinner="dots",
|
||||
)
|
||||
|
||||
|
||||
def _load_json_crew(crew_path: Path) -> tuple[Any, dict[str, Any]]:
|
||||
from crewai.project.crew_loader import load_crew
|
||||
|
||||
return load_crew(crew_path)
|
||||
|
||||
|
||||
def _load_json_crew_for_tui(
|
||||
crew_path: Path,
|
||||
) -> tuple[type[Any], Any, dict[str, Any], list[str], list[str]]:
|
||||
with _json_loading_status("Preparing crew..."):
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
crew, default_inputs = _load_json_crew(crew_path)
|
||||
_prepare_json_crew_for_tui(crew)
|
||||
task_names = [
|
||||
getattr(task, "name", "") or getattr(task, "description", "")[:40] or "Task"
|
||||
for task in crew.tasks
|
||||
]
|
||||
agent_names = [
|
||||
getattr(agent, "role", "") or getattr(agent, "name", "") or "Agent"
|
||||
for agent in crew.agents
|
||||
]
|
||||
|
||||
return CrewRunApp, crew, default_inputs, task_names, agent_names
|
||||
|
||||
|
||||
def _prepare_json_crew_for_tui(crew: Any) -> None:
|
||||
"""Apply the same quiet/streaming setup used by the TUI JSON loader."""
|
||||
crew.verbose = False
|
||||
for agent in crew.agents:
|
||||
agent.verbose = False
|
||||
if hasattr(agent, "llm") and hasattr(agent.llm, "stream"):
|
||||
agent.llm.stream = True
|
||||
|
||||
|
||||
def _run_json_crew(trained_agents_file: str | None = None) -> Any:
|
||||
"""Load and run a JSON-defined crew."""
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_file = Path.cwd() / ".env"
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
# JSON crews run in-process, so export the trained-agents file directly
|
||||
# instead of forwarding it to a subprocess like classic crews do.
|
||||
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_run_app_cls, crew, default_inputs, task_names, agent_names = (
|
||||
_load_json_crew_for_tui(crew_path)
|
||||
)
|
||||
runtime_inputs = _prompt_for_missing_inputs(crew, default_inputs)
|
||||
|
||||
app = crew_run_app_cls(
|
||||
crew_name=crew.name or "Crew",
|
||||
total_tasks=len(crew.tasks),
|
||||
agent_names=agent_names,
|
||||
task_names=task_names,
|
||||
)
|
||||
app._crew = crew
|
||||
app._default_inputs = runtime_inputs
|
||||
|
||||
app.run()
|
||||
|
||||
_print_post_tui_summary(app)
|
||||
|
||||
if app._status == "failed":
|
||||
# Mirror the classic subprocess path: a failed crew must produce a
|
||||
# non-zero exit code so scripts and CI don't treat it as success.
|
||||
raise SystemExit(1)
|
||||
|
||||
if app._status not in ("completed", "failed"):
|
||||
# User quit mid-run. kickoff runs in a thread worker that cannot be
|
||||
# force-cancelled, so end the process to stop in-flight LLM and tool
|
||||
# work instead of letting it burn tokens in the background.
|
||||
click.secho("\n Run cancelled.", fg="yellow")
|
||||
sys.stdout.flush()
|
||||
os._exit(130)
|
||||
|
||||
if getattr(app, "_want_deploy", False):
|
||||
_chain_deploy()
|
||||
|
||||
return app._crew_result
|
||||
|
||||
|
||||
def _has_lockfile(project_root: Path | None = None) -> bool:
|
||||
"""Return True when the project already has a dependency lockfile."""
|
||||
return _has_uv_lockfile(project_root) or _has_poetry_lockfile(project_root)
|
||||
|
||||
|
||||
def _has_uv_lockfile(project_root: Path | None = None) -> bool:
|
||||
"""Return True when the project has a uv lockfile."""
|
||||
root = project_root or Path.cwd()
|
||||
return (root / "uv.lock").is_file()
|
||||
|
||||
|
||||
def _has_poetry_lockfile(project_root: Path | None = None) -> bool:
|
||||
"""Return True when the project has a Poetry lockfile."""
|
||||
root = project_root or Path.cwd()
|
||||
return (root / "poetry.lock").is_file()
|
||||
|
||||
|
||||
def _uses_poetry_lockfile(project_root: Path | None = None) -> bool:
|
||||
"""Return True when Poetry is the only available lock source."""
|
||||
return _has_poetry_lockfile(project_root) and not _has_uv_lockfile(project_root)
|
||||
|
||||
|
||||
def _has_project_venv(project_root: Path | None = None) -> bool:
|
||||
"""Return True when the project already has a local uv environment."""
|
||||
root = project_root or Path.cwd()
|
||||
return (root / ".venv").is_dir()
|
||||
|
||||
|
||||
def _install_json_crew_dependencies_if_needed() -> None:
|
||||
"""Prepare JSON crew dependencies without mutating existing lockfiles."""
|
||||
project_root = Path.cwd()
|
||||
if not (project_root / "pyproject.toml").is_file():
|
||||
return
|
||||
|
||||
has_uv_lockfile = _has_uv_lockfile(project_root)
|
||||
has_lockfile = has_uv_lockfile or _has_poetry_lockfile(project_root)
|
||||
if has_lockfile and _has_project_venv(project_root):
|
||||
return
|
||||
if _uses_poetry_lockfile(project_root):
|
||||
return
|
||||
|
||||
from crewai_cli.install_crew import install_crew
|
||||
|
||||
try:
|
||||
if has_uv_lockfile:
|
||||
click.echo("Syncing dependencies from lockfile...")
|
||||
install_crew(["--frozen"], raise_on_error=True)
|
||||
else:
|
||||
click.echo("Installing dependencies...")
|
||||
install_crew([], raise_on_error=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SystemExit(e.returncode) from e
|
||||
except Exception as e:
|
||||
raise SystemExit(1) from e
|
||||
|
||||
|
||||
def _find_local_crewai_source_dir() -> Path | None:
|
||||
"""Return the repo's CrewAI source dir when running from a source checkout."""
|
||||
for parent in Path(__file__).resolve().parents:
|
||||
candidate = parent / "lib" / "crewai" / "src"
|
||||
if (candidate / "crewai" / "project" / "json_loader.py").is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _json_crew_run_command(project_root: Path | None = None) -> list[str]:
|
||||
"""Return the project-environment command for running JSON crews."""
|
||||
if _uses_poetry_lockfile(project_root):
|
||||
return ["poetry", "run", "python", "-c", _JSON_CREW_RUNNER_CODE]
|
||||
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:
|
||||
"""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)
|
||||
|
||||
_install_json_crew_dependencies_if_needed()
|
||||
|
||||
command = _json_crew_run_command()
|
||||
env = build_env_with_all_tool_credentials()
|
||||
env[_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(Path(__file__).resolve().parent)
|
||||
if local_crewai_source_dir := _find_local_crewai_source_dir():
|
||||
env[_CREWAI_RUNNER_SOURCE_DIR_ENV] = str(local_crewai_source_dir)
|
||||
if trained_agents_file:
|
||||
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file
|
||||
|
||||
try:
|
||||
subprocess.run( # noqa: S603
|
||||
command,
|
||||
capture_output=False,
|
||||
text=True,
|
||||
check=True,
|
||||
env=env,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SystemExit(e.returncode) from e
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred while running the JSON crew: {e}")
|
||||
raise SystemExit(1) from e
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _chain_deploy() -> None:
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
def print_system_exit_failure(exc: SystemExit) -> None:
|
||||
if isinstance(exc.code, int):
|
||||
detail = f" with exit code {exc.code}"
|
||||
elif exc.code:
|
||||
detail = f": {exc.code}"
|
||||
else:
|
||||
detail = ""
|
||||
console.print(f"\nDeploy failed{detail}\n", style="bold red")
|
||||
|
||||
try:
|
||||
from crewai_cli.command import AuthenticationRequiredError
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
|
||||
console.print("\nStarting deployment…\n", style="bold #FF5A50")
|
||||
DeployCommand().create_crew(confirm=True, skip_validate=True)
|
||||
except AuthenticationRequiredError:
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
|
||||
console.print()
|
||||
AuthenticationCommand().login()
|
||||
try:
|
||||
DeployCommand().create_crew(confirm=True, skip_validate=True)
|
||||
except AuthenticationRequiredError:
|
||||
console.print(
|
||||
"\nDeploy failed: authentication is still required.\n",
|
||||
style="bold red",
|
||||
)
|
||||
except SystemExit as e:
|
||||
print_system_exit_failure(e)
|
||||
except Exception as e:
|
||||
console.print(f"\nDeploy failed: {e}\n", style="bold red")
|
||||
except SystemExit as e:
|
||||
print_system_exit_failure(e)
|
||||
except Exception as e:
|
||||
console.print(f"\nDeploy failed: {e}\n", style="bold red")
|
||||
|
||||
|
||||
def _print_post_tui_summary(app: CrewRunApp) -> None:
|
||||
"""Print a summary to the terminal after the Textual TUI exits."""
|
||||
import time
|
||||
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
elapsed = time.time() - app._start_time
|
||||
|
||||
out_tokens = app._output_tokens + app._live_out_tokens
|
||||
token_parts = []
|
||||
if app._input_tokens:
|
||||
token_parts.append(f"↑{app._input_tokens:,}")
|
||||
if out_tokens:
|
||||
token_parts.append(f"↓{out_tokens:,}")
|
||||
token_str = " ".join(token_parts)
|
||||
if token_str:
|
||||
token_str += " tokens"
|
||||
|
||||
crewai_red = "#FF5A50"
|
||||
crewai_teal = "#1F7982"
|
||||
|
||||
if app._status == "completed":
|
||||
summary = Text()
|
||||
summary.append(
|
||||
f" ✔ Completed {app._total_tasks} tasks",
|
||||
style=f"bold {crewai_teal}",
|
||||
)
|
||||
summary.append(f" in {elapsed:.1f}s", style="dim")
|
||||
if token_str:
|
||||
summary.append(f" {token_str}", style="dim")
|
||||
console.print(
|
||||
Panel(
|
||||
summary,
|
||||
title=f" {app._crew_name} ",
|
||||
title_align="left",
|
||||
border_style=crewai_teal,
|
||||
padding=(0, 1),
|
||||
)
|
||||
)
|
||||
if app._final_output:
|
||||
console.print()
|
||||
console.print(Text(" Final Result", style=f"bold {crewai_teal}"))
|
||||
console.print()
|
||||
console.print(Padding(Markdown(app._final_output), (0, 2)))
|
||||
elif app._status == "failed":
|
||||
content = Text()
|
||||
content.append(" ✘ Failed", style=f"bold {crewai_red}")
|
||||
content.append(f" after {elapsed:.1f}s\n", style="dim")
|
||||
if app._error:
|
||||
content.append(f"\n {app._error}\n", style=crewai_red)
|
||||
console.print(
|
||||
Panel(
|
||||
content,
|
||||
title=f" {app._crew_name} ",
|
||||
title_align="left",
|
||||
border_style=crewai_red,
|
||||
padding=(0, 1),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
"""Run the crew or flow.
|
||||
|
||||
Args:
|
||||
trained_agents_file: Optional path to a trained-agents pickle produced
|
||||
@@ -27,6 +477,11 @@ def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
``CREWAI_TRAINED_AGENTS_FILE`` so agents load suggestions from this
|
||||
file instead of the default ``trained_agents_data.pkl``.
|
||||
"""
|
||||
# JSON crew projects take precedence
|
||||
if _has_json_crew():
|
||||
_run_json_crew_in_project_env(trained_agents_file=trained_agents_file)
|
||||
return
|
||||
|
||||
crewai_version = get_crewai_version()
|
||||
min_required_version = "0.71.0"
|
||||
pyproject_data = read_toml()
|
||||
|
||||
113
lib/cli/src/crewai_cli/run_flow_definition.py
Normal file
113
lib/cli/src/crewai_cli/run_flow_definition.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def run_flow_definition(definition: str, inputs: str | None = None) -> None:
|
||||
"""Run a flow from a Flow Definition YAML/JSON string or file path."""
|
||||
try:
|
||||
from crewai.flow.flow import Flow
|
||||
from crewai.flow.flow_definition import FlowDefinition
|
||||
except ImportError as exc:
|
||||
click.echo(
|
||||
"Running flows from definitions requires the full crewai package.",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
parsed_inputs = _parse_inputs(inputs)
|
||||
definition_source = _read_definition_source(definition)
|
||||
|
||||
try:
|
||||
flow_definition = _parse_flow_definition(FlowDefinition, definition_source)
|
||||
flow = Flow.from_definition(flow_definition)
|
||||
result = flow.kickoff(inputs=parsed_inputs)
|
||||
except Exception as exc:
|
||||
click.echo(
|
||||
f"An error occurred while running the flow definition: {exc}", err=True
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
click.echo(_format_result(result))
|
||||
|
||||
|
||||
def _parse_inputs(inputs: str | None) -> dict[str, Any] | None:
|
||||
if inputs is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(inputs)
|
||||
except json.JSONDecodeError as exc:
|
||||
click.echo(f"Invalid --inputs JSON: {exc}", err=True)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
click.echo("Invalid --inputs JSON: expected an object.", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def _read_definition_source(definition: str) -> str:
|
||||
path = Path(definition).expanduser()
|
||||
try:
|
||||
is_file = path.is_file()
|
||||
except OSError as exc:
|
||||
if _looks_like_inline_definition(definition):
|
||||
return definition
|
||||
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
if is_file:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
click.echo(
|
||||
f"Unable to read --definition path {path}: {exc}",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
try:
|
||||
if path.exists():
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} is not a file.", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
except OSError as exc:
|
||||
click.echo(f"Invalid --definition path: {definition} ({exc})", err=True)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
return definition
|
||||
|
||||
|
||||
def _looks_like_inline_definition(definition: str) -> bool:
|
||||
stripped = definition.lstrip()
|
||||
return "\n" in definition or stripped.startswith(("{", "---")) or ":" in stripped
|
||||
|
||||
|
||||
def _parse_flow_definition(flow_definition_cls: type[Any], source: str) -> Any:
|
||||
if _looks_like_json(source):
|
||||
return flow_definition_cls.from_json(source)
|
||||
|
||||
return flow_definition_cls.from_yaml(source)
|
||||
|
||||
|
||||
def _looks_like_json(source: str) -> bool:
|
||||
stripped = source.lstrip()
|
||||
return stripped.startswith("{")
|
||||
|
||||
|
||||
def _format_result(result: Any) -> str:
|
||||
raw_result = getattr(result, "raw", result)
|
||||
if isinstance(raw_result, str):
|
||||
return raw_result
|
||||
|
||||
try:
|
||||
return json.dumps(raw_result, default=str)
|
||||
except TypeError:
|
||||
return str(raw_result)
|
||||
419
lib/cli/src/crewai_cli/tui_picker.py
Normal file
419
lib/cli/src/crewai_cli/tui_picker.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""Arrow-key interactive pickers for CLI prompts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
import sys
|
||||
from typing import overload
|
||||
|
||||
import click
|
||||
|
||||
|
||||
# CrewAI brand: primary=#FF5A50 (coral), teal=#1F7982
|
||||
_CORAL = "\033[38;2;255;90;80m" # #FF5A50
|
||||
_TEAL = "\033[38;2;31;121;130m" # #1F7982
|
||||
_BOLD = "\033[1m"
|
||||
_DIM = "\033[2m"
|
||||
_RESET = "\033[0m"
|
||||
_HIDE_CURSOR = "\033[?25l"
|
||||
_SHOW_CURSOR = "\033[?25h"
|
||||
|
||||
|
||||
def _is_interactive() -> bool:
|
||||
try:
|
||||
return sys.stdin.isatty() and sys.stdout.isatty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _read_key() -> str:
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
ch = msvcrt.getwch()
|
||||
if ch in ("\x00", "\xe0"):
|
||||
ch2 = msvcrt.getwch()
|
||||
return {"H": "up", "P": "down"}.get(ch2, "")
|
||||
if ch == "\r":
|
||||
return "enter"
|
||||
if ch == " ":
|
||||
return "space"
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch
|
||||
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setcbreak(fd)
|
||||
ch = sys.stdin.read(1)
|
||||
if ch == "\x1b":
|
||||
seq = sys.stdin.read(2)
|
||||
if seq == "[A":
|
||||
return "up"
|
||||
if seq == "[B":
|
||||
return "down"
|
||||
return "esc"
|
||||
if ch in ("\r", "\n"):
|
||||
return "enter"
|
||||
if ch == " ":
|
||||
return "space"
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def _clear_lines(n: int) -> None:
|
||||
sys.stdout.write(f"\033[{n}A")
|
||||
for _ in range(n):
|
||||
sys.stdout.write("\033[2K\n")
|
||||
sys.stdout.write(f"\033[{n}A")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _draw_single(labels: list[str], cursor: int, *, clear: bool = False) -> None:
|
||||
total = len(labels)
|
||||
if clear:
|
||||
sys.stdout.write(f"\033[{total}A")
|
||||
for i, label in enumerate(labels):
|
||||
if i == cursor:
|
||||
sys.stdout.write(f"\033[2K {_CORAL}→{_RESET} {_BOLD}{label}{_RESET}\n")
|
||||
else:
|
||||
sys.stdout.write(f"\033[2K {label}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _draw_multi(
|
||||
labels: list[str],
|
||||
cursor: int,
|
||||
selected: set[int],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
clear: bool = False,
|
||||
) -> None:
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
hint_text = "↑↓ navigate, space toggle, enter confirm"
|
||||
if action_indices:
|
||||
hint_text = "↑↓ navigate, space toggle, enter confirm, ▸ rows expand/collapse"
|
||||
hint = f" {_DIM}{hint_text}{_RESET}"
|
||||
total = len(labels) + 1
|
||||
if clear:
|
||||
sys.stdout.write(f"\033[{total}A")
|
||||
sys.stdout.write(f"\033[2K{hint}\n")
|
||||
for i, label in enumerate(labels):
|
||||
if i in separator_indices:
|
||||
sys.stdout.write(f"\033[2K {_TEAL}{label}{_RESET}\n")
|
||||
continue
|
||||
if i in action_indices:
|
||||
check = " "
|
||||
elif i in selected:
|
||||
check = f"{_CORAL}[x]{_RESET}"
|
||||
else:
|
||||
check = "[ ]"
|
||||
arrow = f"{_CORAL}→{_RESET} " if i == cursor else " "
|
||||
bold = f"{_BOLD}{label}{_RESET}" if i == cursor else label
|
||||
sys.stdout.write(f"\033[2K {arrow}{check} {bold}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _arrow_select_one(labels: list[str]) -> int:
|
||||
cursor = 0
|
||||
total = len(labels)
|
||||
sys.stdout.write(_HIDE_CURSOR)
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
_draw_single(labels, cursor)
|
||||
while True:
|
||||
key = _read_key()
|
||||
if key == "up" and cursor > 0:
|
||||
cursor -= 1
|
||||
_draw_single(labels, cursor, clear=True)
|
||||
elif key == "down" and cursor < total - 1:
|
||||
cursor += 1
|
||||
_draw_single(labels, cursor, clear=True)
|
||||
elif key == "enter":
|
||||
_clear_lines(total)
|
||||
return cursor
|
||||
elif key in ("esc", "q"):
|
||||
_clear_lines(total)
|
||||
return -1
|
||||
finally:
|
||||
sys.stdout.write(_SHOW_CURSOR)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _arrow_select_multi(
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> tuple[list[int], int | None]:
|
||||
total = len(labels)
|
||||
selected: set[int] = set(preselected or ())
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
if initial_cursor is not None and 0 <= initial_cursor < total:
|
||||
cursor = initial_cursor
|
||||
else:
|
||||
cursor = _first_selectable_index(total, separator_indices)
|
||||
sys.stdout.write(_HIDE_CURSOR)
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
)
|
||||
while True:
|
||||
key = _read_key()
|
||||
if key == "up":
|
||||
cursor = _next_selectable_index(cursor, -1, total, separator_indices)
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
)
|
||||
elif key == "down":
|
||||
cursor = _next_selectable_index(cursor, 1, total, separator_indices)
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
)
|
||||
elif key == "space":
|
||||
if cursor in action_indices:
|
||||
_clear_lines(total + 1)
|
||||
return sorted(selected), cursor
|
||||
selected ^= {cursor}
|
||||
_draw_multi(
|
||||
labels,
|
||||
cursor,
|
||||
selected,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
clear=True,
|
||||
)
|
||||
elif key == "enter":
|
||||
_clear_lines(total + 1)
|
||||
if cursor in action_indices:
|
||||
return sorted(selected), cursor
|
||||
return sorted(selected), None
|
||||
elif key in ("esc", "q"):
|
||||
_clear_lines(total + 1)
|
||||
return sorted(selected), None
|
||||
finally:
|
||||
sys.stdout.write(_SHOW_CURSOR)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _numbered_select(labels: list[str]) -> int:
|
||||
for idx, label in enumerate(labels, 1):
|
||||
click.echo(f" {idx}. {label}")
|
||||
click.echo()
|
||||
while True:
|
||||
choice = click.prompt(" Select", type=str, default="1")
|
||||
if choice.lower() == "q":
|
||||
return -1
|
||||
try:
|
||||
num = int(choice)
|
||||
if 1 <= num <= len(labels):
|
||||
return num - 1
|
||||
except ValueError:
|
||||
# Non-numeric input falls through to the shared error message.
|
||||
pass
|
||||
click.secho(f" Invalid choice. Enter 1-{len(labels)}.", fg="red")
|
||||
|
||||
|
||||
def _numbered_select_multi(
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
) -> tuple[list[int], int | None]:
|
||||
action_indices = action_indices or set()
|
||||
separator_indices = separator_indices or set()
|
||||
numbered_indices: list[int] = []
|
||||
for idx, label in enumerate(labels):
|
||||
if idx in separator_indices:
|
||||
click.secho(f" {label}", fg="cyan")
|
||||
continue
|
||||
numbered_indices.append(idx)
|
||||
click.echo(f" {len(numbered_indices)}. {label}")
|
||||
click.echo()
|
||||
raw = click.prompt(
|
||||
" Select (comma-separated numbers, or empty to skip)",
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
if not raw.strip():
|
||||
return sorted(preselected or ()), None
|
||||
indices: list[int] = list(preselected or ())
|
||||
for part in raw.split(","):
|
||||
with suppress(ValueError):
|
||||
num = int(part.strip())
|
||||
if 1 <= num <= len(numbered_indices):
|
||||
idx = numbered_indices[num - 1]
|
||||
if idx in action_indices:
|
||||
return sorted(set(indices)), idx
|
||||
indices.append(idx)
|
||||
return sorted(set(indices)), None
|
||||
|
||||
|
||||
def _first_selectable_index(total: int, separator_indices: set[int]) -> int:
|
||||
for idx in range(total):
|
||||
if idx not in separator_indices:
|
||||
return idx
|
||||
return 0
|
||||
|
||||
|
||||
def _next_selectable_index(
|
||||
cursor: int,
|
||||
direction: int,
|
||||
total: int,
|
||||
separator_indices: set[int],
|
||||
) -> int:
|
||||
next_cursor = cursor + direction
|
||||
while 0 <= next_cursor < total:
|
||||
if next_cursor not in separator_indices:
|
||||
return next_cursor
|
||||
next_cursor += direction
|
||||
return cursor
|
||||
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def pick(title: str, options: list[tuple[str, str]]) -> str | None:
|
||||
"""Arrow-key single-select picker.
|
||||
|
||||
Args:
|
||||
title: Header text.
|
||||
options: List of ``(value, description)`` tuples.
|
||||
|
||||
Returns:
|
||||
The *value* of the selected option, or ``None`` if cancelled.
|
||||
"""
|
||||
labels = [f"{value:<12s} {desc}" for value, desc in options]
|
||||
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan", bold=True)
|
||||
click.echo()
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
idx = _arrow_select_one(labels)
|
||||
except Exception:
|
||||
idx = _numbered_select(labels)
|
||||
else:
|
||||
idx = _numbered_select(labels)
|
||||
|
||||
if idx < 0:
|
||||
return None
|
||||
|
||||
value, _desc = options[idx]
|
||||
click.secho(f" ✔ {value}", fg="green")
|
||||
return value
|
||||
|
||||
|
||||
def pick_one(title: str, labels: list[str]) -> int:
|
||||
"""Arrow-key single-select from plain labels.
|
||||
|
||||
Returns:
|
||||
Selected index, or ``-1`` if cancelled.
|
||||
"""
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan")
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
return _arrow_select_one(labels)
|
||||
except Exception:
|
||||
return _numbered_select(labels)
|
||||
return _numbered_select(labels)
|
||||
|
||||
|
||||
@overload
|
||||
def pick_many(
|
||||
title: str,
|
||||
labels: list[str],
|
||||
*,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> list[int]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def pick_many(
|
||||
title: str,
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int],
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> tuple[list[int], int | None]: ...
|
||||
|
||||
|
||||
def pick_many(
|
||||
title: str,
|
||||
labels: list[str],
|
||||
*,
|
||||
action_indices: set[int] | None = None,
|
||||
separator_indices: set[int] | None = None,
|
||||
preselected: set[int] | None = None,
|
||||
initial_cursor: int | None = None,
|
||||
) -> list[int] | tuple[list[int], int | None]:
|
||||
"""Arrow-key multi-select with checkboxes.
|
||||
|
||||
Returns:
|
||||
Sorted list of selected indices, or ``(indices, action_index)`` when
|
||||
``action_indices`` is provided.
|
||||
"""
|
||||
click.echo()
|
||||
click.secho(f" {title}", fg="cyan")
|
||||
|
||||
if _is_interactive():
|
||||
try:
|
||||
selected, action = _arrow_select_multi(
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
initial_cursor=initial_cursor,
|
||||
)
|
||||
except Exception:
|
||||
selected, action = _numbered_select_multi(
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
)
|
||||
else:
|
||||
selected, action = _numbered_select_multi(
|
||||
labels,
|
||||
action_indices=action_indices,
|
||||
separator_indices=separator_indices,
|
||||
preselected=preselected,
|
||||
)
|
||||
if action_indices is None:
|
||||
return selected
|
||||
return selected, action
|
||||
@@ -24,6 +24,7 @@ __all__ = [
|
||||
"build_env_with_all_tool_credentials",
|
||||
"build_env_with_tool_repository_credentials",
|
||||
"copy_template",
|
||||
"enable_prompt_line_editing",
|
||||
"fetch_and_json_env_file",
|
||||
"get_project_description",
|
||||
"get_project_name",
|
||||
@@ -40,6 +41,19 @@ __all__ = [
|
||||
console = Console()
|
||||
|
||||
|
||||
def enable_prompt_line_editing() -> None:
|
||||
"""Enable cursor movement/history editing for Click text prompts when available."""
|
||||
try:
|
||||
import readline
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
try:
|
||||
readline.parse_and_bind("set editing-mode emacs")
|
||||
except Exception: # pragma: no cover - readline backends vary by platform
|
||||
return
|
||||
|
||||
|
||||
def copy_template(
|
||||
src: Path, dst: Path, name: str, class_name: str, folder_name: str
|
||||
) -> None:
|
||||
|
||||
270
lib/cli/tests/deploy/test_archive.py
Normal file
270
lib/cli/tests/deploy/test_archive.py
Normal file
@@ -0,0 +1,270 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_cli.deploy.archive import create_project_zip
|
||||
|
||||
|
||||
def test_create_project_zip_excludes_local_artifacts(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "uv.lock").write_text("# lock\n")
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "main.py").write_text("print('hello')\n")
|
||||
(tmp_path / ".env").write_text("OPENAI_API_KEY=secret\n")
|
||||
(tmp_path / ".env.example").write_text("OPENAI_API_KEY=\n")
|
||||
(tmp_path / "__pycache__").mkdir()
|
||||
(tmp_path / "__pycache__" / "main.pyc").write_bytes(b"compiled")
|
||||
(tmp_path / ".git").mkdir()
|
||||
(tmp_path / ".git" / "config").write_text("[core]\n")
|
||||
|
||||
archive_path = create_project_zip("demo", project_dir=tmp_path)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert names == {
|
||||
"pyproject.toml",
|
||||
"uv.lock",
|
||||
"src/main.py",
|
||||
".env.example",
|
||||
}
|
||||
|
||||
|
||||
def test_create_project_zip_uses_repository_file_list(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "uv.lock").write_text("# lock\n")
|
||||
(tmp_path / "ignored.txt").write_text("ignored\n")
|
||||
|
||||
class RepositoryStub:
|
||||
def deployable_files(self) -> list[str]:
|
||||
return ["pyproject.toml", "uv.lock"]
|
||||
|
||||
archive_path = create_project_zip(
|
||||
"demo",
|
||||
project_dir=tmp_path,
|
||||
repository=RepositoryStub(), # type: ignore[arg-type]
|
||||
)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert names == {"pyproject.toml", "uv.lock"}
|
||||
|
||||
|
||||
def test_create_project_zip_without_repository_uses_git_ignore_rules(
|
||||
tmp_path: Path,
|
||||
):
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / ".gitignore").write_text("node_modules/\nsecret.txt\n")
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "main.py").write_text("print('hello')\n")
|
||||
(tmp_path / "node_modules").mkdir()
|
||||
(tmp_path / "node_modules" / "package.json").write_text("{}\n")
|
||||
(tmp_path / "secret.txt").write_text("secret\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "init"],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
|
||||
pytest.skip(f"git is not available in this environment: {exc}")
|
||||
|
||||
archive_path = create_project_zip("demo", project_dir=tmp_path)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert names == {
|
||||
".gitignore",
|
||||
"pyproject.toml",
|
||||
"src/main.py",
|
||||
}
|
||||
|
||||
|
||||
def test_create_project_zip_does_not_fallback_when_repository_listing_fails(
|
||||
tmp_path: Path,
|
||||
):
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
|
||||
class RepositoryStub:
|
||||
def deployable_files(self) -> list[str]:
|
||||
raise RuntimeError("git listing failed")
|
||||
|
||||
with pytest.raises(RuntimeError, match="git listing failed"):
|
||||
create_project_zip(
|
||||
"demo",
|
||||
project_dir=tmp_path,
|
||||
repository=RepositoryStub(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def test_create_project_zip_excludes_symlinked_files(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
outside_file = tmp_path.parent / f"{tmp_path.name}-secret.txt"
|
||||
outside_file.write_text("secret\n")
|
||||
archive_path: Path | None = None
|
||||
try:
|
||||
try:
|
||||
(tmp_path / "external-secret.txt").symlink_to(outside_file)
|
||||
except OSError as exc:
|
||||
pytest.skip(f"symlinks are not supported in this environment: {exc}")
|
||||
|
||||
archive_path = create_project_zip("demo", project_dir=tmp_path)
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
finally:
|
||||
if archive_path is not None:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
outside_file.unlink(missing_ok=True)
|
||||
|
||||
assert names == {"pyproject.toml"}
|
||||
|
||||
|
||||
def test_create_project_zip_adds_json_project_wrapper(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "json_crew"
|
||||
version = "0.1.0"
|
||||
dependencies = ["crewai[tools]>=1.15"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
(tmp_path / "agents").mkdir()
|
||||
(tmp_path / "agents" / "researcher.jsonc").write_text("{}\n")
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
|
||||
archive_path = create_project_zip("json_crew", project_dir=tmp_path)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
crew_py = archive.read("src/json_crew/crew.py").decode()
|
||||
main_py = archive.read("src/json_crew/main.py").decode()
|
||||
pyproject = archive.read("pyproject.toml").decode()
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert "uv.lock" not in names
|
||||
assert "crew.jsonc" in names
|
||||
assert "agents/researcher.jsonc" in names
|
||||
assert "src/json_crew/__init__.py" in names
|
||||
assert "src/json_crew/crew.py" in names
|
||||
assert "src/json_crew/main.py" in names
|
||||
assert "src/json_crew/config/agents.yaml" in names
|
||||
assert "src/json_crew/config/tasks.yaml" in names
|
||||
assert "load_crew(_crew_path())" in crew_py
|
||||
assert "JsonCrew" in crew_py
|
||||
assert "from json_crew.crew import JsonCrew" in main_py
|
||||
assert "run_crew = \"json_crew.main:run\"" in pyproject
|
||||
|
||||
|
||||
def test_create_project_zip_updates_existing_json_project_scripts(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "json_crew"
|
||||
version = "0.1.0"
|
||||
|
||||
[project.scripts]
|
||||
json_crew = "old.module:run"
|
||||
run_crew = "old.module:run"
|
||||
custom = "custom.module:main"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
|
||||
archive_path = create_project_zip("json_crew", project_dir=tmp_path)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
pyproject = archive.read("pyproject.toml").decode()
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert 'json_crew = "json_crew.main:run"' in pyproject
|
||||
assert 'run_crew = "json_crew.main:run"' in pyproject
|
||||
assert 'train = "json_crew.main:train"' in pyproject
|
||||
assert 'replay = "json_crew.main:replay"' in pyproject
|
||||
assert 'test = "json_crew.main:test"' in pyproject
|
||||
assert 'run_with_trigger = "json_crew.main:run_with_trigger"' in pyproject
|
||||
assert 'custom = "custom.module:main"' in pyproject
|
||||
assert "old.module:run" not in pyproject
|
||||
assert "[tool.crewai]" in pyproject
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_config",
|
||||
[
|
||||
'tool = "invalid"\n',
|
||||
'[tool]\ncrewai = "invalid"\n',
|
||||
],
|
||||
)
|
||||
def test_create_project_zip_adds_json_wrapper_for_malformed_tool_config(
|
||||
tmp_path: Path, tool_config: str
|
||||
):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
f"""
|
||||
[project]
|
||||
name = "json_crew"
|
||||
version = "0.1.0"
|
||||
|
||||
{tool_config}
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
|
||||
archive_path = create_project_zip("json_crew", project_dir=tmp_path)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
pyproject = archive.read("pyproject.toml").decode()
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert "src/json_crew/crew.py" in names
|
||||
assert "src/json_crew/main.py" in names
|
||||
assert "run_crew = \"json_crew.main:run\"" in pyproject
|
||||
|
||||
|
||||
def test_create_project_zip_rejects_empty_normalized_package_name(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "!!!"
|
||||
version = "0.1.0"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"Could not derive a valid Python package name",
|
||||
):
|
||||
create_project_zip("invalid", project_dir=tmp_path)
|
||||
@@ -1,16 +1,172 @@
|
||||
import sys
|
||||
import unittest
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
import json
|
||||
|
||||
import crewai_cli.deploy.main as deploy_main
|
||||
import httpx
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
from crewai_cli.deploy.validate import Severity, ValidationResult
|
||||
from crewai_cli.utils import parse_toml
|
||||
|
||||
|
||||
def test_ensure_lockfile_for_deploy_runs_install_when_lock_missing(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(proxy_options, *, raise_on_error=False):
|
||||
calls.append((proxy_options, raise_on_error))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
deploy_main._ensure_lockfile_for_deploy()
|
||||
|
||||
assert calls == [([], True)]
|
||||
|
||||
|
||||
def test_ensure_lockfile_for_deploy_skips_when_lock_exists(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "uv.lock").write_text("# lock\n")
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(proxy_options, *, raise_on_error=False):
|
||||
calls.append((proxy_options, raise_on_error))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
deploy_main._ensure_lockfile_for_deploy()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_ensure_lockfile_for_deploy_skips_without_pyproject(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(proxy_options, *, raise_on_error=False):
|
||||
calls.append((proxy_options, raise_on_error))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
deploy_main._ensure_lockfile_for_deploy()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_ensure_lockfile_for_deploy_failure_exits_nonzero(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
|
||||
def fake_install_crew(proxy_options, *, raise_on_error=False):
|
||||
raise subprocess.CalledProcessError(42, ["uv", "sync"])
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
deploy_main._ensure_lockfile_for_deploy()
|
||||
|
||||
assert exc_info.value.code == 42
|
||||
|
||||
|
||||
class _FakeDeployValidator:
|
||||
def __init__(self, results: list[ValidationResult]):
|
||||
self.results = results
|
||||
|
||||
@property
|
||||
def errors(self) -> list[ValidationResult]:
|
||||
return [
|
||||
result
|
||||
for result in self.results
|
||||
if result.severity is Severity.ERROR
|
||||
]
|
||||
|
||||
def run(self) -> list[ValidationResult]:
|
||||
return self.results
|
||||
|
||||
|
||||
def test_prepare_project_for_deploy_blocks_install_when_validation_fails(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
install_calls = []
|
||||
rendered_results = []
|
||||
missing_lockfile = ValidationResult(
|
||||
Severity.ERROR,
|
||||
"missing_lockfile",
|
||||
"Expected to find a lockfile",
|
||||
)
|
||||
invalid_config = ValidationResult(
|
||||
Severity.ERROR,
|
||||
"invalid_crew_json",
|
||||
"crew.jsonc has invalid configuration",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
deploy_main,
|
||||
"DeployValidator",
|
||||
lambda: _FakeDeployValidator([missing_lockfile, invalid_config]),
|
||||
)
|
||||
monkeypatch.setattr(deploy_main, "render_report", rendered_results.append)
|
||||
|
||||
def fake_install_crew(proxy_options, *, raise_on_error=False):
|
||||
install_calls.append((proxy_options, raise_on_error))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
assert deploy_main._prepare_project_for_deploy(skip_validate=False) is False
|
||||
|
||||
assert install_calls == []
|
||||
assert [[result.code for result in results] for results in rendered_results] == [
|
||||
["invalid_crew_json"]
|
||||
]
|
||||
|
||||
|
||||
def test_prepare_project_for_deploy_creates_missing_lock_after_validation(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
install_calls = []
|
||||
missing_lockfile = ValidationResult(
|
||||
Severity.ERROR,
|
||||
"missing_lockfile",
|
||||
"Expected to find a lockfile",
|
||||
)
|
||||
validators = [
|
||||
_FakeDeployValidator([missing_lockfile]),
|
||||
_FakeDeployValidator([]),
|
||||
]
|
||||
|
||||
def fake_validator():
|
||||
return validators.pop(0)
|
||||
|
||||
def fake_install_crew(proxy_options, *, raise_on_error=False):
|
||||
install_calls.append((proxy_options, raise_on_error))
|
||||
(tmp_path / "uv.lock").write_text("# lock\n")
|
||||
|
||||
monkeypatch.setattr(deploy_main, "DeployValidator", fake_validator)
|
||||
monkeypatch.setattr(deploy_main, "render_report", lambda results: None)
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
assert deploy_main._prepare_project_for_deploy(skip_validate=False) is True
|
||||
|
||||
assert install_calls == [([], True)]
|
||||
assert validators == []
|
||||
|
||||
|
||||
class TestDeployCommand(unittest.TestCase):
|
||||
@patch("crewai_cli.command.get_auth_token")
|
||||
@patch("crewai_cli.deploy.main.get_project_name")
|
||||
@@ -28,19 +184,25 @@ class TestDeployCommand(unittest.TestCase):
|
||||
self.mock_get_auth_token.return_value = "test_token"
|
||||
self.mock_get_project_name.return_value = "test_project"
|
||||
|
||||
self.deploy_command = DeployCommand()
|
||||
self.deploy_command = deploy_main.DeployCommand()
|
||||
self.mock_client = self.deploy_command.plus_api_client
|
||||
|
||||
def test_init_success(self):
|
||||
self.assertEqual(self.deploy_command.project_name, "test_project")
|
||||
self.mock_plus_api.assert_called_once_with(api_key="test_token")
|
||||
|
||||
@patch("builtins.input")
|
||||
def test_confirm_zip_input_only_confirms_env_vars(self, mock_input):
|
||||
self.deploy_command._confirm_zip_input({"MODEL": "openai/gpt-5"}, False)
|
||||
|
||||
mock_input.assert_called_once_with("Press Enter to continue with 1 env vars: MODEL")
|
||||
|
||||
@patch("crewai_cli.command.get_auth_token")
|
||||
def test_init_failure(self, mock_get_auth_token):
|
||||
mock_get_auth_token.side_effect = Exception("Auth failed")
|
||||
|
||||
with self.assertRaises(SystemExit):
|
||||
DeployCommand()
|
||||
deploy_main.DeployCommand()
|
||||
|
||||
def test_validate_response_successful_response(self):
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
@@ -123,8 +285,15 @@ class TestDeployCommand(unittest.TestCase):
|
||||
)
|
||||
self.assertIn("2023-01-01 - INFO: Test log", fake_out.getvalue())
|
||||
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info")
|
||||
def test_deploy_with_uuid(self, mock_display):
|
||||
def test_deploy_with_uuid(self, mock_display, mock_repository):
|
||||
mock_repository.return_value.origin_url.return_value = (
|
||||
"https://github.com/test/repo.git"
|
||||
)
|
||||
mock_repository.return_value.create_initial_commit_if_needed.return_value = (
|
||||
False
|
||||
)
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||
@@ -135,8 +304,15 @@ class TestDeployCommand(unittest.TestCase):
|
||||
self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid")
|
||||
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info")
|
||||
def test_deploy_with_project_name(self, mock_display):
|
||||
def test_deploy_with_project_name(self, mock_display, mock_repository):
|
||||
mock_repository.return_value.origin_url.return_value = (
|
||||
"https://github.com/test/repo.git"
|
||||
)
|
||||
mock_repository.return_value.create_initial_commit_if_needed.return_value = (
|
||||
False
|
||||
)
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||
@@ -147,16 +323,142 @@ class TestDeployCommand(unittest.TestCase):
|
||||
self.mock_client.deploy_by_name.assert_called_once_with("test_project")
|
||||
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info")
|
||||
def test_deploy_with_remote_keeps_remote_path_when_fetch_fails(
|
||||
self, mock_display, mock_repository, mock_create_project_zip
|
||||
):
|
||||
repository = mock_repository.return_value
|
||||
repository.origin_url.return_value = "https://github.com/test/repo.git"
|
||||
repository.fetch.side_effect = ValueError("fetch failed")
|
||||
repository.create_initial_commit_if_needed.return_value = False
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||
self.mock_client.deploy_by_name.return_value = mock_response
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command.deploy(skip_validate=True)
|
||||
output = fake_out.getvalue()
|
||||
|
||||
mock_repository.assert_called_once_with(fetch=False)
|
||||
repository.fetch.assert_called_once_with()
|
||||
self.assertIn("Continuing with remote deployment", output)
|
||||
self.mock_client.deploy_by_name.assert_called_once_with("test_project")
|
||||
self.mock_client.update_crew_from_zip.assert_not_called()
|
||||
mock_create_project_zip.assert_not_called()
|
||||
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info")
|
||||
def test_deploy_with_remote_keeps_remote_path_when_initial_commit_fails(
|
||||
self, mock_display, mock_repository, mock_create_project_zip
|
||||
):
|
||||
repository = mock_repository.return_value
|
||||
repository.origin_url.return_value = "https://github.com/test/repo.git"
|
||||
repository.create_initial_commit_if_needed.side_effect = RuntimeError(
|
||||
"commit failed"
|
||||
)
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||
self.mock_client.deploy_by_name.return_value = mock_response
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command.deploy(skip_validate=True)
|
||||
output = fake_out.getvalue()
|
||||
|
||||
mock_repository.assert_called_once_with(fetch=False)
|
||||
repository.fetch.assert_called_once_with()
|
||||
self.assertIn("Continuing with remote deployment", output)
|
||||
self.mock_client.deploy_by_name.assert_called_once_with("test_project")
|
||||
self.mock_client.update_crew_from_zip.assert_not_called()
|
||||
mock_create_project_zip.assert_not_called()
|
||||
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository.origin_url")
|
||||
@patch("builtins.input")
|
||||
def test_create_crew(self, mock_input, mock_git_origin_url, mock_fetch_env):
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info")
|
||||
def test_deploy_with_uuid_without_remote_updates_from_zip(
|
||||
self, mock_display, mock_repository, mock_fetch_env, mock_create_project_zip
|
||||
):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
mock_git_origin_url.return_value = "https://github.com/test/repo.git"
|
||||
mock_repository.return_value.origin_url.return_value = None
|
||||
mock_repository.return_value.create_initial_commit_if_needed.return_value = (
|
||||
False
|
||||
)
|
||||
mock_create_project_zip.return_value = Path("/tmp/test_project.zip")
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"uuid": "test-uuid"}
|
||||
self.mock_client.update_crew_from_zip.return_value = mock_response
|
||||
|
||||
self.deploy_command.deploy(uuid="test-uuid", skip_validate=True)
|
||||
|
||||
self.mock_client.update_crew_from_zip.assert_called_once_with(
|
||||
"test-uuid",
|
||||
Path("/tmp/test_project.zip"),
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
self.mock_client.deploy_by_uuid.assert_not_called()
|
||||
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info")
|
||||
def test_deploy_with_project_name_without_remote_updates_from_zip(
|
||||
self, mock_display, mock_repository, mock_fetch_env, mock_create_project_zip
|
||||
):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
mock_repository.return_value.origin_url.return_value = None
|
||||
mock_repository.return_value.create_initial_commit_if_needed.return_value = (
|
||||
False
|
||||
)
|
||||
mock_create_project_zip.return_value = Path("/tmp/test_project.zip")
|
||||
status_response = MagicMock()
|
||||
status_response.status_code = 200
|
||||
status_response.is_success = True
|
||||
status_response.json.return_value = {"uuid": "test-uuid"}
|
||||
update_response = MagicMock()
|
||||
update_response.status_code = 200
|
||||
update_response.json.return_value = {"uuid": "test-uuid"}
|
||||
self.mock_client.crew_status_by_name.return_value = status_response
|
||||
self.mock_client.update_crew_from_zip.return_value = update_response
|
||||
|
||||
self.deploy_command.deploy(skip_validate=True)
|
||||
|
||||
self.mock_client.crew_status_by_name.assert_called_once_with("test_project")
|
||||
self.mock_client.update_crew_from_zip.assert_called_once_with(
|
||||
"test-uuid",
|
||||
Path("/tmp/test_project.zip"),
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
self.mock_client.deploy_by_name.assert_not_called()
|
||||
mock_display.assert_called_once_with({"uuid": "test-uuid"})
|
||||
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
@patch("builtins.input")
|
||||
@pytest.mark.timeout(180)
|
||||
def test_create_crew(self, mock_input, mock_repository, mock_fetch_env):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
mock_repository.return_value.origin_url.return_value = (
|
||||
"https://github.com/test/repo.git"
|
||||
)
|
||||
mock_repository.return_value.create_initial_commit_if_needed.return_value = (
|
||||
False
|
||||
)
|
||||
mock_input.return_value = ""
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "new-uuid", "status": "created"}
|
||||
self.mock_client.create_crew.return_value = mock_response
|
||||
|
||||
@@ -165,6 +467,129 @@ class TestDeployCommand(unittest.TestCase):
|
||||
self.assertIn("Deployment created successfully!", fake_out.getvalue())
|
||||
self.assertIn("new-uuid", fake_out.getvalue())
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
def test_create_crew_without_git_repo_initializes_and_uses_zip(
|
||||
self, mock_repository, mock_fetch_env, mock_create_project_zip
|
||||
):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
mock_repository.side_effect = ValueError("not a Git repository")
|
||||
initialized_repository = MagicMock()
|
||||
initialized_repository.origin_url.return_value = None
|
||||
mock_repository.initialize.return_value = initialized_repository
|
||||
mock_create_project_zip.return_value = Path("/tmp/test_project.zip")
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "zip-uuid", "status": "created"}
|
||||
self.mock_client.create_crew_from_zip.return_value = mock_response
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command.create_crew(confirm=True, skip_validate=True)
|
||||
output = fake_out.getvalue()
|
||||
|
||||
self.assertIn("Initialized a local Git repository", output)
|
||||
self.assertIn("Deploying from a ZIP upload", output)
|
||||
mock_repository.initialize.assert_called_once_with()
|
||||
mock_create_project_zip.assert_called_once_with(
|
||||
"test_project", repository=initialized_repository
|
||||
)
|
||||
self.mock_client.create_crew_from_zip.assert_called_once_with(
|
||||
Path("/tmp/test_project.zip"),
|
||||
name="test_project",
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
self.mock_client.create_crew.assert_not_called()
|
||||
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
def test_prepare_git_repository_returns_repo_when_init_commit_fails(
|
||||
self, mock_repository
|
||||
):
|
||||
recovered_repository = MagicMock()
|
||||
mock_repository.side_effect = [
|
||||
ValueError("not a Git repository"),
|
||||
recovered_repository,
|
||||
]
|
||||
mock_repository.initialize.side_effect = RuntimeError("commit failed")
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
repository = self.deploy_command._prepare_git_repository()
|
||||
|
||||
self.assertIs(repository, recovered_repository)
|
||||
self.assertIn("Git auto-setup did not complete", fake_out.getvalue())
|
||||
mock_repository.initialize.assert_called_once_with()
|
||||
self.assertEqual(mock_repository.call_count, 2)
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
def test_create_crew_without_remote_uses_zip(
|
||||
self, mock_repository, mock_fetch_env, mock_create_project_zip
|
||||
):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
mock_repository.return_value.origin_url.return_value = None
|
||||
mock_repository.return_value.create_initial_commit_if_needed.return_value = (
|
||||
False
|
||||
)
|
||||
mock_create_project_zip.return_value = Path("/tmp/test_project.zip")
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "zip-uuid", "status": "created"}
|
||||
self.mock_client.create_crew_from_zip.return_value = mock_response
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command.create_crew(confirm=True, skip_validate=True)
|
||||
output = fake_out.getvalue()
|
||||
|
||||
self.assertIn("No origin remote found.", output)
|
||||
self.assertIn("Deploying from a ZIP upload", output)
|
||||
mock_create_project_zip.assert_called_once_with(
|
||||
"test_project", repository=mock_repository.return_value
|
||||
)
|
||||
self.mock_client.create_crew_from_zip.assert_called_once_with(
|
||||
Path("/tmp/test_project.zip"),
|
||||
name="test_project",
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
self.mock_client.create_crew.assert_not_called()
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
def test_create_crew_without_remote_uses_git_file_list_when_commit_fails(
|
||||
self, mock_repository, mock_fetch_env, mock_create_project_zip
|
||||
):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
repository = mock_repository.return_value
|
||||
repository.origin_url.return_value = None
|
||||
repository.create_initial_commit_if_needed.side_effect = RuntimeError(
|
||||
"commit failed"
|
||||
)
|
||||
mock_create_project_zip.return_value = Path("/tmp/test_project.zip")
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "zip-uuid", "status": "created"}
|
||||
self.mock_client.create_crew_from_zip.return_value = mock_response
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command.create_crew(confirm=True, skip_validate=True)
|
||||
output = fake_out.getvalue()
|
||||
|
||||
self.assertIn("Continuing with ZIP deployment using Git", output)
|
||||
self.assertIn("file listing", output)
|
||||
mock_create_project_zip.assert_called_once_with(
|
||||
"test_project", repository=repository
|
||||
)
|
||||
self.mock_client.create_crew_from_zip.assert_called_once_with(
|
||||
Path("/tmp/test_project.zip"),
|
||||
name="test_project",
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
self.mock_client.create_crew.assert_not_called()
|
||||
|
||||
def test_list_crews(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
|
||||
@@ -110,6 +110,45 @@ def _run_without_import_check(root: Path) -> DeployValidator:
|
||||
return v
|
||||
|
||||
|
||||
def _scaffold_json_crew(root: Path, *, task_agent: str = "researcher") -> None:
|
||||
(root / "pyproject.toml").write_text(_make_pyproject(name="json_crew"))
|
||||
(root / "uv.lock").write_text("# dummy uv lockfile\n")
|
||||
agents_dir = root / "agents"
|
||||
agents_dir.mkdir()
|
||||
(agents_dir / "researcher.jsonc").write_text(
|
||||
dedent(
|
||||
"""
|
||||
{
|
||||
"role": "Researcher",
|
||||
"goal": "Research things",
|
||||
"backstory": "Experienced researcher",
|
||||
"llm": "openai/gpt-4o-mini"
|
||||
}
|
||||
"""
|
||||
).strip()
|
||||
+ "\n"
|
||||
)
|
||||
(root / "crew.jsonc").write_text(
|
||||
dedent(
|
||||
f"""
|
||||
{{
|
||||
"name": "json_crew",
|
||||
"agents": ["researcher"],
|
||||
"tasks": [
|
||||
{{
|
||||
"name": "research",
|
||||
"description": "Research https://example.com/a//b",
|
||||
"expected_output": "Findings",
|
||||
"agent": "{task_agent}"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
"""
|
||||
).strip()
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"project_name, expected",
|
||||
[
|
||||
@@ -129,6 +168,38 @@ def test_valid_standard_crew_project_passes(tmp_path: Path) -> None:
|
||||
assert v.ok, f"expected clean run, got {v.results}"
|
||||
|
||||
|
||||
def test_valid_json_crew_project_passes(tmp_path: Path) -> None:
|
||||
_scaffold_json_crew(tmp_path)
|
||||
v = DeployValidator(project_root=tmp_path)
|
||||
v.run()
|
||||
assert "invalid_crew_json" not in _codes(v)
|
||||
|
||||
|
||||
def test_json_task_agent_mismatch_is_error(tmp_path: Path) -> None:
|
||||
_scaffold_json_crew(tmp_path, task_agent="missing_agent")
|
||||
v = DeployValidator(project_root=tmp_path)
|
||||
v.run()
|
||||
finding = next(r for r in v.results if r.code == "invalid_crew_json")
|
||||
assert finding.severity is Severity.ERROR
|
||||
assert "missing_agent" in finding.detail
|
||||
|
||||
|
||||
def test_json_runtime_fields_are_deploy_errors(tmp_path: Path) -> None:
|
||||
_scaffold_json_crew(tmp_path)
|
||||
crew_path = tmp_path / "crew.jsonc"
|
||||
crew_path.write_text(
|
||||
crew_path.read_text().replace(
|
||||
'"name": "json_crew",',
|
||||
'"name": "json_crew",\n "id": "00000000-0000-4000-8000-000000000000",',
|
||||
)
|
||||
)
|
||||
v = DeployValidator(project_root=tmp_path)
|
||||
v.run()
|
||||
finding = next(r for r in v.results if r.code == "invalid_crew_json")
|
||||
assert finding.severity is Severity.ERROR
|
||||
assert "runtime-only" in finding.detail
|
||||
|
||||
|
||||
def test_missing_pyproject_errors(tmp_path: Path) -> None:
|
||||
v = _run_without_import_check(tmp_path)
|
||||
assert "missing_pyproject" in _codes(v)
|
||||
@@ -410,7 +481,7 @@ def test_modern_crewai_pin_does_not_warn(tmp_path: Path) -> None:
|
||||
|
||||
def test_create_crew_aborts_on_validation_error(tmp_path: Path) -> None:
|
||||
"""`crewai deploy create` must not contact the API when validation fails."""
|
||||
from unittest.mock import MagicMock, patch as mock_patch
|
||||
from unittest.mock import patch as mock_patch
|
||||
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
|
||||
@@ -419,11 +490,38 @@ def test_create_crew_aborts_on_validation_error(tmp_path: Path) -> None:
|
||||
mock_patch("crewai_cli.deploy.main.get_project_name", return_value="p"),
|
||||
mock_patch("crewai_cli.command.PlusAPI") as mock_api,
|
||||
mock_patch(
|
||||
"crewai_cli.deploy.main.validate_project"
|
||||
) as mock_validate,
|
||||
"crewai_cli.deploy.main._prepare_project_for_deploy",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
mock_validate.return_value = MagicMock(ok=False)
|
||||
cmd = DeployCommand()
|
||||
cmd.create_crew()
|
||||
assert not cmd.plus_api_client.create_crew.called
|
||||
del mock_api # silence unused-var lint
|
||||
del mock_api # silence unused-var lint
|
||||
|
||||
|
||||
def test_is_json_crew_defers_to_declared_flow_type(tmp_path):
|
||||
"""A flow project with a stray crew.jsonc must validate as a flow."""
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "demo"\nversion = "0.1.0"\n\n'
|
||||
'[tool.crewai]\ntype = "flow"\n'
|
||||
)
|
||||
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is False
|
||||
|
||||
|
||||
def test_is_json_crew_true_for_declared_crew_type(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
|
||||
|
||||
|
||||
def test_is_json_crew_true_without_pyproject(tmp_path):
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
|
||||
assert DeployValidator(project_root=tmp_path)._is_json_crew is True
|
||||
|
||||
@@ -13,6 +13,7 @@ from crewai_cli.cli import (
|
||||
flow_add_crew,
|
||||
login,
|
||||
reset_memories,
|
||||
run,
|
||||
test,
|
||||
train,
|
||||
version,
|
||||
@@ -93,9 +94,9 @@ def test_version_command_with_tools(runner):
|
||||
def test_test_default_iterations(evaluate_crew, runner):
|
||||
result = runner.invoke(test)
|
||||
|
||||
evaluate_crew.assert_called_once_with(3, "gpt-4o-mini", trained_agents_file=None)
|
||||
evaluate_crew.assert_called_once_with(3, "gpt-5.4-mini", trained_agents_file=None)
|
||||
assert result.exit_code == 0
|
||||
assert "Testing the crew for 3 iterations with model gpt-4o-mini" in result.output
|
||||
assert "Testing the crew for 3 iterations with model gpt-5.4-mini" in result.output
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.evaluate_crew")
|
||||
@@ -119,6 +120,43 @@ def test_test_invalid_string_iterations(evaluate_crew, runner):
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
def test_run_uses_project_runner_by_default(run_crew, runner):
|
||||
result = runner.invoke(run)
|
||||
|
||||
assert result.exit_code == 0
|
||||
run_crew.assert_called_once_with(trained_agents_file=None)
|
||||
assert "experimental" not in result.output.lower()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_flow_definition")
|
||||
def test_run_with_definition_uses_definition_runner(run_flow_definition, runner):
|
||||
result = runner.invoke(
|
||||
run,
|
||||
["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (
|
||||
"Warning: `crewai run --definition` is experimental and may change without notice."
|
||||
in result.output
|
||||
)
|
||||
run_flow_definition.assert_called_once_with(
|
||||
definition="flow.yaml", inputs='{"topic":"AI"}'
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
@mock.patch("crewai_cli.cli.run_flow_definition")
|
||||
def test_run_rejects_inputs_without_definition(run_flow_definition, run_crew, runner):
|
||||
result = runner.invoke(run, ["--inputs", '{"topic":"AI"}'])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "Error: --inputs requires --definition" in result.output
|
||||
run_flow_definition.assert_not_called()
|
||||
run_crew.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.AuthenticationCommand")
|
||||
def test_login(command, runner):
|
||||
mock_auth = command.return_value
|
||||
|
||||
@@ -6,6 +6,8 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
import crewai_cli.create_json_crew as json_crew
|
||||
import crewai_cli.tui_picker as tui_picker
|
||||
from crewai_cli.create_crew import create_crew, create_folder_structure
|
||||
|
||||
|
||||
@@ -345,3 +347,468 @@ def test_env_vars_are_uppercased_in_env_file(
|
||||
env_file_path = crew_path / ".env"
|
||||
content = env_file_path.read_text()
|
||||
assert "MODEL=" in content
|
||||
|
||||
|
||||
def test_json_wizard_defaults_to_sequential_and_memory_enabled(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
json_crew,
|
||||
"_wizard_agent",
|
||||
lambda **_: {
|
||||
"name": "researcher",
|
||||
"role": "Researcher",
|
||||
"goal": "Research",
|
||||
"backstory": "Researcher",
|
||||
"llm": "openai/gpt-5.5",
|
||||
"tools": [],
|
||||
"planning": False,
|
||||
"allow_delegation": False,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
json_crew,
|
||||
"_wizard_task",
|
||||
lambda **_: {
|
||||
"name": "research_task",
|
||||
"description": "Research",
|
||||
"expected_output": "Findings",
|
||||
"agent": "researcher",
|
||||
"context": [],
|
||||
},
|
||||
)
|
||||
|
||||
def confirm(label: str, default: bool = False) -> bool:
|
||||
if label == "Enable crew memory?":
|
||||
return default
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(json_crew, "_confirm", confirm)
|
||||
monkeypatch.setattr(json_crew.click, "prompt", lambda *_, **__: "")
|
||||
monkeypatch.setattr(
|
||||
json_crew,
|
||||
"pick_one",
|
||||
lambda *_args, **_kwargs: pytest.fail("process should not be prompted"),
|
||||
)
|
||||
|
||||
_agents, _tasks, settings = json_crew._wizard_agents_and_tasks(
|
||||
skip_provider=True,
|
||||
default_llm="openai/gpt-5.5",
|
||||
)
|
||||
|
||||
assert settings == {"process": "sequential", "memory": True, "inputs": {}}
|
||||
|
||||
|
||||
def test_json_wizard_shows_interpolation_hint(capsys):
|
||||
json_crew._show_interpolation_hint("tasks")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "{placeholder}" in output
|
||||
assert "dynamic values" in output
|
||||
assert "{topic}" not in output
|
||||
assert "Description >" not in output
|
||||
assert '"description"' not in output
|
||||
|
||||
|
||||
def test_json_wizard_text_prompt_uses_full_prompt_for_readline(monkeypatch):
|
||||
prompts: list[str] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
json_crew, "_readline_safe_prompt", lambda prompt: f"safe:{prompt}"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"builtins.input", lambda prompt: prompts.append(prompt) or "Draft content"
|
||||
)
|
||||
|
||||
assert json_crew._prompt_text("Goal", spacing_before=False) == "Draft content"
|
||||
assert len(prompts) == 1
|
||||
assert prompts[0].startswith("safe:")
|
||||
assert "Goal" in prompts[0]
|
||||
assert " > " in prompts[0]
|
||||
|
||||
|
||||
def test_json_wizard_tool_picker_prioritizes_common_tools(monkeypatch):
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
return [1, 3], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
|
||||
tools = json_crew._select_tools()
|
||||
|
||||
assert tools == ["SerperDevTool", "DirectoryReadTool"]
|
||||
assert len(picker_calls) == 1
|
||||
labels = picker_calls[0][1]
|
||||
assert 0 in picker_calls[0][2]["separator_indices"]
|
||||
assert labels[0] == "── Common tools ──"
|
||||
assert labels[1].strip().endswith("SerperDevTool")
|
||||
assert labels[2].strip().endswith("ScrapeWebsiteTool")
|
||||
assert labels[3].strip().endswith("DirectoryReadTool")
|
||||
assert labels[4].strip().endswith("FileReadTool")
|
||||
assert labels[5].strip().endswith("FileWriterTool")
|
||||
assert labels[1].index("Google search") < labels[1].index("SerperDevTool")
|
||||
assert "More tools" not in labels
|
||||
|
||||
|
||||
def test_json_wizard_tool_picker_collapses_categories_by_default(monkeypatch):
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
return [], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
|
||||
json_crew._select_tools()
|
||||
|
||||
labels = picker_calls[0][1]
|
||||
action_indices = picker_calls[0][2]["action_indices"]
|
||||
# Categories show as collapsed action rows, not separators with tools
|
||||
assert any(label.startswith("▸ Search & Research") for label in labels)
|
||||
assert any(label.startswith("▸ Web Scraping") for label in labels)
|
||||
assert not any(label.strip().endswith("BraveSearchTool") for label in labels)
|
||||
assert len(action_indices) >= 4
|
||||
# Only the common tools section is visible beyond the category rows
|
||||
assert len(labels) == 1 + 5 + len(action_indices)
|
||||
|
||||
|
||||
def test_json_wizard_tool_picker_expands_one_category_at_a_time(monkeypatch):
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
|
||||
def find_category_row(labels: list[str], category: str) -> int:
|
||||
return next(
|
||||
idx for idx, label in enumerate(labels) if category in label
|
||||
)
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
call_num = len(picker_calls)
|
||||
if call_num == 1:
|
||||
return [], find_category_row(labels, "Search & Research")
|
||||
if call_num == 2:
|
||||
# Search & Research is expanded; select BraveSearchTool and
|
||||
# expand Web Scraping instead
|
||||
brave = next(
|
||||
idx
|
||||
for idx, label in enumerate(labels)
|
||||
if label.strip().endswith("BraveSearchTool")
|
||||
)
|
||||
return [brave], find_category_row(labels, "Web Scraping")
|
||||
return [], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
|
||||
tools = json_crew._select_tools()
|
||||
|
||||
assert tools == ["BraveSearchTool"]
|
||||
assert len(picker_calls) == 3
|
||||
# Second render: Search & Research expanded, others collapsed
|
||||
labels2 = picker_calls[1][1]
|
||||
assert any(label.startswith("▾ Search & Research") for label in labels2)
|
||||
assert any(label.strip().endswith("BraveSearchTool") for label in labels2)
|
||||
assert any(label.startswith("▸ Web Scraping") for label in labels2)
|
||||
# Third render: Web Scraping expanded, Search & Research collapsed again
|
||||
labels3 = picker_calls[2][1]
|
||||
assert any(label.startswith("▸ Search & Research") for label in labels3)
|
||||
assert any(label.startswith("▾ Web Scraping") for label in labels3)
|
||||
assert not any(label.strip().endswith("BraveSearchTool") for label in labels3)
|
||||
# The collapsed Search & Research row reports its selection count
|
||||
assert any(
|
||||
"Search & Research" in label and "1 selected" in label for label in labels3
|
||||
)
|
||||
# Cursor returns to the toggled category row
|
||||
assert picker_calls[2][2]["initial_cursor"] == next(
|
||||
idx for idx, label in enumerate(labels3) if "Web Scraping" in label
|
||||
)
|
||||
|
||||
|
||||
def test_json_wizard_tool_picker_preserves_selection_across_renders(monkeypatch):
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
call_num = len(picker_calls)
|
||||
if call_num == 1:
|
||||
# Select a common tool, then expand a category
|
||||
category_row = next(
|
||||
idx for idx, label in enumerate(labels) if "Web Scraping" in label
|
||||
)
|
||||
return [1], category_row
|
||||
# Confirm without touching anything else
|
||||
return sorted(kwargs["preselected"]), None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
|
||||
tools = json_crew._select_tools()
|
||||
|
||||
# The common-tool selection survived the expand re-render via preselected
|
||||
assert tools == ["SerperDevTool"]
|
||||
assert 1 in picker_calls[1][2]["preselected"]
|
||||
|
||||
|
||||
def test_json_wizard_tool_picker_lists_builtin_tools_across_categories(monkeypatch):
|
||||
picker_calls: list[tuple[str, list[str], dict[str, object]]] = []
|
||||
expanded_labels: list[str] = []
|
||||
|
||||
def pick_many(title: str, labels: list[str], **kwargs):
|
||||
picker_calls.append((title, labels, kwargs))
|
||||
expanded_labels.extend(labels)
|
||||
action_indices = sorted(kwargs["action_indices"])
|
||||
call_num = len(picker_calls)
|
||||
if call_num <= len(action_indices):
|
||||
# Expand the n-th category (indices shift between renders, so
|
||||
# recompute from this render's action rows)
|
||||
return [], action_indices[call_num - 1]
|
||||
return [], None
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_many", pick_many)
|
||||
|
||||
json_crew._select_tools()
|
||||
|
||||
tool_names = {
|
||||
label.rsplit(maxsplit=1)[-1]
|
||||
for label in expanded_labels
|
||||
if not label.startswith(("▸", "▾", "──"))
|
||||
}
|
||||
|
||||
assert {
|
||||
"DirectorySearchTool",
|
||||
"MDXSearchTool",
|
||||
"XMLSearchTool",
|
||||
"YoutubeVideoSearchTool",
|
||||
"S3ReaderTool",
|
||||
"E2BExecTool",
|
||||
"TavilyResearchTool",
|
||||
"SerplyNewsSearchTool",
|
||||
"BrowserbaseLoadTool",
|
||||
"PatronusEvalTool",
|
||||
}.issubset(tool_names)
|
||||
assert {
|
||||
"MCPServerAdapter",
|
||||
"MongoDBVectorSearchConfig",
|
||||
"ScrapegraphScrapeToolSchema",
|
||||
"SnowflakeConfig",
|
||||
}.isdisjoint(tool_names)
|
||||
|
||||
|
||||
def test_multi_picker_skips_separator_on_initial_cursor(monkeypatch):
|
||||
cursors: list[int] = []
|
||||
|
||||
monkeypatch.setattr(tui_picker, "_read_key", lambda: "enter")
|
||||
monkeypatch.setattr(
|
||||
tui_picker,
|
||||
"_draw_multi",
|
||||
lambda _labels, cursor, *_args, **_kwargs: cursors.append(cursor),
|
||||
)
|
||||
monkeypatch.setattr(tui_picker, "_clear_lines", lambda *_args, **_kwargs: None)
|
||||
|
||||
assert tui_picker._arrow_select_multi(
|
||||
["── Common tools ──", "Google search via Serper API SerperDevTool"],
|
||||
separator_indices={0},
|
||||
) == ([], None)
|
||||
assert cursors == [1]
|
||||
|
||||
|
||||
def test_json_wizard_agent_attribute_prompts_are_compact(monkeypatch):
|
||||
prompt_calls: list[tuple[str, bool]] = []
|
||||
prompt_values = {
|
||||
"Role": "Senior Dev Rel",
|
||||
"Goal": "Draft content",
|
||||
"Backstory": "Knows developer communities",
|
||||
}
|
||||
|
||||
def prompt_text(
|
||||
label: str,
|
||||
default: str = "",
|
||||
*,
|
||||
spacing_before: bool = True,
|
||||
) -> str:
|
||||
prompt_calls.append((label, spacing_before))
|
||||
return prompt_values[label]
|
||||
|
||||
monkeypatch.setattr(json_crew, "_prompt_text", prompt_text)
|
||||
monkeypatch.setattr(json_crew, "_select_model", lambda: "openai/gpt-5.5")
|
||||
monkeypatch.setattr(json_crew, "pick_many", lambda *_args, **_kwargs: ([], None))
|
||||
monkeypatch.setattr(json_crew, "_confirm", lambda *_args, **_kwargs: False)
|
||||
|
||||
agent = json_crew._wizard_agent(agent_num=1, existing_names=[])
|
||||
|
||||
assert agent is not None
|
||||
assert prompt_calls == [
|
||||
("Role", False),
|
||||
("Goal", False),
|
||||
("Backstory", False),
|
||||
]
|
||||
|
||||
|
||||
def test_json_wizard_task_attribute_prompts_are_compact(monkeypatch):
|
||||
prompt_calls: list[tuple[str, bool]] = []
|
||||
prompt_values = {
|
||||
"Description": "Research latest release",
|
||||
"Expected output": "Release summary",
|
||||
}
|
||||
|
||||
def prompt_text(
|
||||
label: str,
|
||||
default: str = "",
|
||||
*,
|
||||
spacing_before: bool = True,
|
||||
) -> str:
|
||||
prompt_calls.append((label, spacing_before))
|
||||
return prompt_values[label]
|
||||
|
||||
monkeypatch.setattr(json_crew, "_prompt_text", prompt_text)
|
||||
|
||||
task = json_crew._wizard_task(
|
||||
task_num=1,
|
||||
agent_names=["senior_dev_rel"],
|
||||
prior_task_names=[],
|
||||
)
|
||||
|
||||
assert task is not None
|
||||
assert prompt_calls == [
|
||||
("Description", False),
|
||||
("Expected output", False),
|
||||
]
|
||||
|
||||
|
||||
def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
with mock.patch(
|
||||
"crewai_cli.create_json_crew._wizard_agents_and_tasks"
|
||||
) as mock_wizard:
|
||||
mock_wizard.return_value = (
|
||||
[
|
||||
{
|
||||
"name": "researcher",
|
||||
"role": "Researcher",
|
||||
"goal": "Research",
|
||||
"backstory": "Researcher",
|
||||
"llm": "openai/gpt-5.5",
|
||||
"tools": [],
|
||||
"planning": False,
|
||||
"allow_delegation": False,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "research_task",
|
||||
"description": "Research",
|
||||
"expected_output": "Findings",
|
||||
"agent": "researcher",
|
||||
"context": [],
|
||||
}
|
||||
],
|
||||
{"process": "sequential", "memory": False, "inputs": {}},
|
||||
)
|
||||
|
||||
json_crew.create_json_crew("JSON Crew", provider="openai", skip_provider=True)
|
||||
|
||||
mock_wizard.assert_called_once_with(
|
||||
skip_provider=True,
|
||||
default_llm="openai/gpt-5.5",
|
||||
)
|
||||
assert (tmp_path / "json_crew" / "crew.jsonc").exists()
|
||||
assert not (tmp_path / "json_crew" / "tests").exists()
|
||||
assert not (tmp_path / "json_crew" / "config.jsonc").exists()
|
||||
|
||||
crew_template = (tmp_path / "json_crew" / "crew.jsonc").read_text()
|
||||
assert (
|
||||
'"guardrail": "Every factual claim needs context support."'
|
||||
in crew_template
|
||||
)
|
||||
assert '"guardrails": [' in crew_template
|
||||
assert '"guardrail_max_retries": 2' in crew_template
|
||||
assert "Docs: https://docs.crewai.com/concepts/tasks" in crew_template
|
||||
assert '"output_pydantic": null' in crew_template
|
||||
assert '"type": "ConditionalTask"' in crew_template
|
||||
assert '"condition": { "python": "my_project.conditions.should_run" }' in (
|
||||
crew_template
|
||||
)
|
||||
assert '"output_json": { "python": "my_project.models.ReportOutput" }' in (
|
||||
crew_template
|
||||
)
|
||||
assert (
|
||||
'"converter_cls": { "python": "my_project.converters.CustomConverter" }'
|
||||
in crew_template
|
||||
)
|
||||
assert '"markdown": false' in crew_template
|
||||
assert '"input_files": { "brief": "data/brief.txt" }' in crew_template
|
||||
assert "Docs: https://docs.crewai.com/concepts/crews" in crew_template
|
||||
assert "manager_agent can reference an agents/<name>.jsonc file" in crew_template
|
||||
assert '"manager_agent": "researcher"' in crew_template
|
||||
assert (
|
||||
'"before_kickoff_callbacks": [{"python": '
|
||||
'"my_project.callbacks.before_kickoff"}]'
|
||||
) in crew_template
|
||||
assert (
|
||||
'"after_kickoff_callbacks": [{"python": '
|
||||
'"my_project.callbacks.after_kickoff"}]'
|
||||
) in crew_template
|
||||
assert '"output_log_file": "crew.log"' in crew_template
|
||||
assert "Crew-level LLM fields also accept object form" in crew_template
|
||||
assert '"chat_llm": {"model": "llama3", "provider": "ollama"' in (
|
||||
crew_template
|
||||
)
|
||||
assert "Use {placeholder} in agent or task text" in crew_template
|
||||
assert "`crewai run` prompts for any placeholders" in crew_template
|
||||
assert "Use {placeholder} inputs here" in crew_template
|
||||
|
||||
agent_template = (
|
||||
tmp_path / "json_crew" / "agents" / "researcher.jsonc"
|
||||
).read_text()
|
||||
assert "You can use {placeholder} inputs in role, goal, or backstory" in (
|
||||
agent_template
|
||||
)
|
||||
assert '"role": "Senior {industry} Researcher"' in agent_template
|
||||
assert '"type": {"python": "my_project.agents.CustomAgent"}' in agent_template
|
||||
assert "Optional agent-level guardrail" in agent_template
|
||||
assert "Python refs must point to module-level functions/classes" in agent_template
|
||||
assert (
|
||||
'"step_callback": {"python": "my_project.callbacks.on_agent_step"}'
|
||||
in agent_template
|
||||
)
|
||||
assert '"guardrail_max_retries": 2' in agent_template
|
||||
assert "Docs: https://docs.crewai.com/concepts/agents" in agent_template
|
||||
assert '"reasoning": true' in agent_template
|
||||
assert "For custom endpoints or deployment-based providers" in agent_template
|
||||
assert '"deployment_name": "my-deployment", "provider": "azure"' in (
|
||||
agent_template
|
||||
)
|
||||
assert '"planning_config": {' in agent_template
|
||||
assert '"llm": {"model": "deepseek-chat", "provider": "deepseek"}' in (
|
||||
agent_template
|
||||
)
|
||||
assert '"knowledge_sources": []' in agent_template
|
||||
|
||||
|
||||
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") == (
|
||||
"anthropic/claude-custom"
|
||||
)
|
||||
assert json_crew._default_model_for_provider("unknown") is None
|
||||
|
||||
|
||||
def test_json_wizard_task_reprompts_on_cancelled_agent_pick(monkeypatch):
|
||||
"""Esc on the agent picker must reprompt, not silently assign agent 0."""
|
||||
prompts = iter(["Do the research", "A report"])
|
||||
monkeypatch.setattr(json_crew, "_prompt_text", lambda *a, **k: next(prompts))
|
||||
|
||||
pick_calls: list[str] = []
|
||||
picks = iter([-1, 1])
|
||||
|
||||
def fake_pick_one(title: str, labels: list[str]) -> int:
|
||||
pick_calls.append(title)
|
||||
return next(picks)
|
||||
|
||||
monkeypatch.setattr(json_crew, "pick_one", fake_pick_one)
|
||||
|
||||
task = json_crew._wizard_task(
|
||||
task_num=1,
|
||||
agent_names=["first_agent", "second_agent"],
|
||||
prior_task_names=[],
|
||||
)
|
||||
|
||||
assert len(pick_calls) == 2
|
||||
assert task["agent"] == "second_agent"
|
||||
|
||||
823
lib/cli/tests/test_crew_run_tui.py
Normal file
823
lib/cli/tests/test_crew_run_tui.py
Normal file
@@ -0,0 +1,823 @@
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.observation_events import (
|
||||
GoalAchievedEarlyEvent,
|
||||
PlanRefinementEvent,
|
||||
PlanReplanTriggeredEvent,
|
||||
PlanStepCompletedEvent,
|
||||
PlanStepStartedEvent,
|
||||
StepObservationCompletedEvent,
|
||||
StepObservationFailedEvent,
|
||||
StepObservationStartedEvent,
|
||||
)
|
||||
from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai_cli.command import AuthenticationRequiredError
|
||||
from crewai_cli import run_crew
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
|
||||
def _app_with_plan() -> CrewRunApp:
|
||||
app = CrewRunApp()
|
||||
app._plan = {
|
||||
"plan": "Demo plan",
|
||||
"steps": [
|
||||
{"step_number": 1, "description": "First"},
|
||||
{"step_number": 2, "description": "Second"},
|
||||
{"step_number": 3, "description": "Third"},
|
||||
],
|
||||
}
|
||||
app._plan_step_status = {1: "pending", 2: "pending", 3: "pending"}
|
||||
return app
|
||||
|
||||
|
||||
def _log_entry(name: str) -> dict:
|
||||
now = time.time()
|
||||
return {
|
||||
"tool_name": name,
|
||||
"status": "success",
|
||||
"args": None,
|
||||
"result": f"{name} result",
|
||||
"error": None,
|
||||
"start_time": now,
|
||||
"duration": 1.0,
|
||||
"task_idx": 1,
|
||||
}
|
||||
|
||||
|
||||
def _emit_event(event: object) -> None:
|
||||
future = crewai_event_bus.emit(None, event)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
|
||||
def test_chain_deploy_skips_validation_after_auth_retry(monkeypatch) -> None:
|
||||
create_calls: list[dict[str, object]] = []
|
||||
login_calls: list[bool] = []
|
||||
|
||||
class FakeDeployCommand:
|
||||
attempts = 0
|
||||
|
||||
def create_crew(self, **kwargs) -> None:
|
||||
create_calls.append(kwargs)
|
||||
FakeDeployCommand.attempts += 1
|
||||
if FakeDeployCommand.attempts == 1:
|
||||
raise AuthenticationRequiredError
|
||||
|
||||
class FakeAuthenticationCommand:
|
||||
def login(self) -> None:
|
||||
login_calls.append(True)
|
||||
|
||||
monkeypatch.setattr("crewai_cli.deploy.main.DeployCommand", FakeDeployCommand)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.authentication.main.AuthenticationCommand",
|
||||
FakeAuthenticationCommand,
|
||||
)
|
||||
|
||||
run_crew._chain_deploy()
|
||||
|
||||
assert create_calls == [
|
||||
{"confirm": True, "skip_validate": True},
|
||||
{"confirm": True, "skip_validate": True},
|
||||
]
|
||||
assert login_calls == [True]
|
||||
|
||||
|
||||
def test_chain_deploy_does_not_login_for_deploy_exit(monkeypatch, capsys) -> None:
|
||||
create_calls: list[dict[str, object]] = []
|
||||
login_calls: list[bool] = []
|
||||
|
||||
class FakeDeployCommand:
|
||||
def create_crew(self, **kwargs) -> None:
|
||||
create_calls.append(kwargs)
|
||||
raise SystemExit(42)
|
||||
|
||||
class FakeAuthenticationCommand:
|
||||
def login(self) -> None:
|
||||
login_calls.append(True)
|
||||
|
||||
monkeypatch.setattr("crewai_cli.deploy.main.DeployCommand", FakeDeployCommand)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.authentication.main.AuthenticationCommand",
|
||||
FakeAuthenticationCommand,
|
||||
)
|
||||
|
||||
run_crew._chain_deploy()
|
||||
|
||||
assert create_calls == [{"confirm": True, "skip_validate": True}]
|
||||
assert login_calls == []
|
||||
assert "Deploy failed with exit code 42" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_plan_step_status_updates_only_the_explicit_step() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
app._set_plan_step_status(2, "done")
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "done",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_step_observation_events_update_the_explicit_step() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
StepObservationStartedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
step_description="Second",
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "active",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
StepObservationCompletedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
step_description="Second",
|
||||
step_completed_successfully=True,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "done",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_plan_step_lifecycle_events_update_the_explicit_step() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
PlanStepStartedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
step_description="Second",
|
||||
)
|
||||
)
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "active",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
_emit_event(
|
||||
PlanStepCompletedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
step_description="Second",
|
||||
success=True,
|
||||
result="done",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "done",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_failed_plan_step_lifecycle_event_marks_exact_step_failed() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
PlanStepCompletedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
step_description="Second",
|
||||
success=False,
|
||||
error="Step failed",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "failed",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_tool_usage_events_do_not_advance_plan_steps() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
ToolUsageStartedEvent(tool_name="search", tool_args={"query": "CrewAI"}),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
now = datetime.now()
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
ToolUsageFinishedEvent(
|
||||
tool_name="search",
|
||||
tool_args={"query": "CrewAI"},
|
||||
started_at=now,
|
||||
finished_at=now,
|
||||
output="result",
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "pending",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_next_tool_does_not_mark_unfinished_tool_successful() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
ToolUsageStartedEvent(tool_name="search", tool_args={"query": "CrewAI"}),
|
||||
)
|
||||
_emit_event(
|
||||
ToolUsageStartedEvent(tool_name="scrape", tool_args={"url": "https://x"}),
|
||||
)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._log_entries[0]["status"] == "timeout"
|
||||
assert app._log_entries[0]["result"] is None
|
||||
assert app._log_entries[0]["error"] == (
|
||||
"No result received before the next tool started"
|
||||
)
|
||||
assert app._log_entries[1]["status"] == "running"
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "pending",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_internal_reasoning_function_call_is_hidden_from_activity_log() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
ToolUsageStartedEvent(
|
||||
tool_name="create_reasoning_plan",
|
||||
tool_args={"plan": "Plan", "steps": [], "ready": True},
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
now = datetime.now()
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
ToolUsageFinishedEvent(
|
||||
tool_name="create_reasoning_plan",
|
||||
tool_args={"plan": "Plan", "steps": [], "ready": True},
|
||||
started_at=now,
|
||||
finished_at=now,
|
||||
output='{"plan":"Plan","steps":[],"ready":true}',
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
ToolUsageErrorEvent(
|
||||
tool_name="create_reasoning_plan",
|
||||
tool_args={"plan": "Plan", "steps": [], "ready": True},
|
||||
error="internal planning fallback",
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._log_entries == []
|
||||
assert app._current_task_steps == []
|
||||
|
||||
|
||||
def test_tool_failure_does_not_override_successful_plan_step_completion() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
PlanStepStartedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=1,
|
||||
step_description="First",
|
||||
)
|
||||
)
|
||||
_emit_event(
|
||||
ToolUsageStartedEvent(
|
||||
tool_name="search_the_internet_with_serper",
|
||||
tool_args={"search_query": "CrewAI release"},
|
||||
plan_step_number=1,
|
||||
plan_step_description="First",
|
||||
)
|
||||
)
|
||||
_emit_event(
|
||||
ToolUsageErrorEvent(
|
||||
tool_name="search_the_internet_with_serper",
|
||||
tool_args={"search_query": "CrewAI release"},
|
||||
plan_step_number=1,
|
||||
plan_step_description="First",
|
||||
error="No results",
|
||||
)
|
||||
)
|
||||
_emit_event(
|
||||
PlanStepCompletedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=1,
|
||||
step_description="First",
|
||||
success=True,
|
||||
result="Recovered with another source",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "done",
|
||||
2: "pending",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_tool_event_step_metadata_is_stored_in_activity_log() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
ToolUsageStartedEvent(
|
||||
tool_name="search_the_internet_with_serper",
|
||||
tool_args={"search_query": "CrewAI release"},
|
||||
plan_step_number=2,
|
||||
plan_step_description="Second",
|
||||
)
|
||||
)
|
||||
now = datetime.now()
|
||||
_emit_event(
|
||||
ToolUsageFinishedEvent(
|
||||
tool_name="search_the_internet_with_serper",
|
||||
tool_args={"search_query": "CrewAI release"},
|
||||
plan_step_number=2,
|
||||
plan_step_description="Second",
|
||||
started_at=now,
|
||||
finished_at=now,
|
||||
output="Found official source",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._log_entries[0]["plan_step_number"] == 2
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "pending",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_starting_next_tool_does_not_infer_plan_step_progress() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
_emit_event(
|
||||
ToolUsageStartedEvent(
|
||||
tool_name="search_the_internet_with_serper",
|
||||
tool_args={"search_query": "CrewAI release"},
|
||||
)
|
||||
)
|
||||
_emit_event(
|
||||
ToolUsageErrorEvent(
|
||||
tool_name="search_the_internet_with_serper",
|
||||
tool_args={"search_query": "CrewAI release"},
|
||||
error="No results",
|
||||
)
|
||||
)
|
||||
_emit_event(
|
||||
ToolUsageStartedEvent(
|
||||
tool_name="read_website_content",
|
||||
tool_args={"url": "https://example.com"},
|
||||
)
|
||||
)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._log_entries[0]["status"] == "error"
|
||||
assert app._log_entries[1]["status"] == "running"
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "pending",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_crew_done_does_not_mark_unfinished_tool_successful() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
app._plan_step_status = {1: "failed", 2: "done", 3: "pending"}
|
||||
app._log_entries = [
|
||||
{
|
||||
"tool_name": "search",
|
||||
"status": "running",
|
||||
"args": '{"query": "CrewAI"}',
|
||||
"result": None,
|
||||
"error": None,
|
||||
"start_time": time.time() - 2,
|
||||
"duration": None,
|
||||
"task_idx": 1,
|
||||
}
|
||||
]
|
||||
|
||||
app._on_crew_done("final output")
|
||||
await pilot.pause()
|
||||
|
||||
assert app._log_entries[0]["status"] == "timeout"
|
||||
assert app._log_entries[0]["result"] is None
|
||||
assert app._log_entries[0]["error"] == "No result received before crew completed"
|
||||
assert app._plan_step_status == {1: "failed", 2: "done", 3: "done"}
|
||||
|
||||
|
||||
def test_streamed_step_observation_updates_named_step_only() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
updated = app._try_parse_step_observation(
|
||||
'{"step_completed_successfully":true,'
|
||||
'"key_information_learned":"Step 2 succeeded with the official source."}'
|
||||
)
|
||||
|
||||
assert updated is True
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "done",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_failed_streamed_step_observation_marks_named_step_failed() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
updated = app._try_parse_step_observation(
|
||||
'{"step_completed_successfully":false,'
|
||||
'"key_information_learned":"Step 2 failed because the tool failed."}'
|
||||
)
|
||||
|
||||
assert updated is True
|
||||
assert app._plan_step_status == {
|
||||
1: "pending",
|
||||
2: "failed",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_streamed_goal_achieved_observation_collapses_remaining_steps_done() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
updated = app._try_parse_step_observation(
|
||||
'{"step_number":2,'
|
||||
'"step_completed_successfully":true,'
|
||||
'"key_information_learned":"Goal is already satisfied.",'
|
||||
'"goal_already_achieved":true}'
|
||||
)
|
||||
|
||||
assert updated is True
|
||||
assert app._plan_step_status == {
|
||||
1: "done",
|
||||
2: "done",
|
||||
3: "done",
|
||||
}
|
||||
|
||||
|
||||
def test_task_completion_collapses_pending_plan_steps_but_preserves_failed() -> None:
|
||||
app = _app_with_plan()
|
||||
app._plan_step_status = {1: "failed", 2: "done", 3: "pending"}
|
||||
|
||||
app._collapse_plan_on_task_done()
|
||||
|
||||
assert app._plan_step_status == {1: "failed", 2: "done", 3: "done"}
|
||||
|
||||
|
||||
def test_observation_failure_collapses_to_done_because_executor_continues() -> None:
|
||||
app = _app_with_plan()
|
||||
app._plan_step_status = {1: "done", 2: "active", 3: "pending"}
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
StepObservationFailedEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
step_description="Second",
|
||||
error="observer timeout",
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "done",
|
||||
2: "done",
|
||||
3: "pending",
|
||||
}
|
||||
|
||||
|
||||
def test_goal_achieved_event_collapses_remaining_steps_done() -> None:
|
||||
app = _app_with_plan()
|
||||
app._plan_step_status = {1: "done", 2: "active", 3: "pending"}
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
GoalAchievedEarlyEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
steps_completed=2,
|
||||
steps_remaining=1,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "done",
|
||||
2: "done",
|
||||
3: "done",
|
||||
}
|
||||
|
||||
|
||||
def test_replan_event_keeps_old_plan_until_next_streamed_plan_replaces_it() -> None:
|
||||
app = _app_with_plan()
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
PlanReplanTriggeredEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
replan_reason="Need updated sources",
|
||||
replan_count=1,
|
||||
completed_steps_preserved=1,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan is not None
|
||||
assert app._plan_step_status == {1: "pending", 2: "pending", 3: "pending"}
|
||||
assert app._awaiting_replan is True
|
||||
|
||||
app._try_parse_plan(
|
||||
'{"plan":"Updated plan","steps":['
|
||||
'{"step_number":1,"description":"Updated first"},'
|
||||
'{"step_number":2,"description":"Updated second"}]}'
|
||||
)
|
||||
|
||||
assert app._plan == {
|
||||
"plan": "Updated plan",
|
||||
"steps": [
|
||||
{"step_number": 1, "description": "Updated first"},
|
||||
{"step_number": 2, "description": "Updated second"},
|
||||
],
|
||||
}
|
||||
assert app._plan_step_status == {1: "pending", 2: "pending"}
|
||||
assert app._awaiting_replan is False
|
||||
|
||||
|
||||
def test_plan_refinement_updates_descriptions_without_new_statuses() -> None:
|
||||
app = _app_with_plan()
|
||||
app._plan_step_status = {1: "done", 2: "active", 3: "pending"}
|
||||
app._subscribe()
|
||||
try:
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
PlanRefinementEvent(
|
||||
agent_role="Agent",
|
||||
step_number=2,
|
||||
refined_step_count=1,
|
||||
refinements=["Step 3: Write the final answer from verified facts"],
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
assert app._plan_step_status == {
|
||||
1: "done",
|
||||
2: "done",
|
||||
3: "pending",
|
||||
}
|
||||
assert app._plan["steps"][2]["description"] == (
|
||||
"Write the final answer from verified facts"
|
||||
)
|
||||
|
||||
|
||||
def test_step_observation_json_is_hidden_from_streaming_text() -> None:
|
||||
app = _app_with_plan()
|
||||
|
||||
assert (
|
||||
app._strip_step_observation_json(
|
||||
'Visible before {"step_completed_successfully":true,'
|
||||
'"key_information_learned":"Step 2 succeeded."} visible after'
|
||||
)
|
||||
== "Visible before visible after"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completed_run_keeps_activity_log_keyboard_navigation_active() -> None:
|
||||
app = CrewRunApp()
|
||||
|
||||
async with app.run_test(size=(100, 40)) as pilot:
|
||||
app._log_entries = [_log_entry("search"), _log_entry("scrape")]
|
||||
|
||||
app._on_crew_done("final output")
|
||||
await pilot.pause()
|
||||
|
||||
assert app.focused is app.query_one("#log-panel")
|
||||
|
||||
await pilot.press("down", "enter")
|
||||
await pilot.pause()
|
||||
|
||||
assert app._log_cursor == 1
|
||||
assert app._log_expanded == {1}
|
||||
|
||||
await pilot.press("up")
|
||||
await pilot.pause()
|
||||
|
||||
assert app._log_cursor == 0
|
||||
|
||||
|
||||
class _FakeTask:
|
||||
fingerprint = None
|
||||
|
||||
def __init__(self, task_id: str, name: str) -> None:
|
||||
self.id = task_id
|
||||
self.name = name
|
||||
self.description = name
|
||||
|
||||
|
||||
def test_async_task_completion_marks_the_right_sidebar_row() -> None:
|
||||
"""Overlapping tasks: completing task 1 while task 2 runs must not
|
||||
mark task 2 done, and starting task 2 must not mark task 1 done."""
|
||||
from crewai.events.types.task_events import TaskCompletedEvent, TaskStartedEvent
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
|
||||
app = CrewRunApp(total_tasks=2, task_names=["first", "second"])
|
||||
app._subscribe()
|
||||
try:
|
||||
task1 = _FakeTask("id-1", "first")
|
||||
task2 = _FakeTask("id-2", "second")
|
||||
|
||||
for task in (task1, task2):
|
||||
future = crewai_event_bus.emit(
|
||||
None, TaskStartedEvent(context=None, task=task)
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
# Both started: neither prematurely done
|
||||
assert app._task_statuses == {1: "active", 2: "active"}
|
||||
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
TaskCompletedEvent(
|
||||
output=TaskOutput(description="first", raw="done", agent="a"),
|
||||
task=task1,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
assert app._task_statuses == {1: "done", 2: "active"}
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
|
||||
|
||||
def test_pop_task_state_falls_back_to_current_task() -> None:
|
||||
app = CrewRunApp(total_tasks=2, task_names=["first", "second"])
|
||||
app._current_task_idx = 2
|
||||
app._current_task_desc = "second"
|
||||
|
||||
class _Evt:
|
||||
task = None
|
||||
task_name = "unknown"
|
||||
|
||||
state = app._pop_task_state(_Evt())
|
||||
assert state["idx"] == 2
|
||||
assert state["desc"] == "second"
|
||||
|
||||
|
||||
def test_overlapping_task_logs_keep_their_own_state() -> None:
|
||||
"""Task 1 completing after task 2 started must log its own description,
|
||||
agent, and output — and must not steal or reset task 2's stream state."""
|
||||
from crewai.events.types.task_events import TaskCompletedEvent, TaskStartedEvent
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
|
||||
app = CrewRunApp(total_tasks=2, task_names=["first", "second"])
|
||||
app._subscribe()
|
||||
try:
|
||||
task1 = _FakeTask("id-1", "first")
|
||||
task2 = _FakeTask("id-2", "second")
|
||||
|
||||
for task in (task1, task2):
|
||||
future = crewai_event_bus.emit(
|
||||
None, TaskStartedEvent(context=None, task=task)
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
# Task 2 is current and has streamed state in flight
|
||||
app._task_full_output = "task two streaming output"
|
||||
app._current_task_steps = [{"type": "llm", "summary": "thinking"}]
|
||||
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
TaskCompletedEvent(
|
||||
output=TaskOutput(
|
||||
description="first", raw="task one result", agent="a1"
|
||||
),
|
||||
task=task1,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
# Task 1's entry carries its own identity and output
|
||||
entry1 = app._task_logs[-1]
|
||||
assert entry1["idx"] == 1
|
||||
assert entry1["desc"] == "first"
|
||||
assert entry1["output"] == "task one result"
|
||||
assert entry1["steps"] == []
|
||||
|
||||
# Task 2's in-flight stream state was not consumed or reset
|
||||
assert app._task_full_output == "task two streaming output"
|
||||
assert app._current_task_steps == [{"type": "llm", "summary": "thinking"}]
|
||||
|
||||
future = crewai_event_bus.emit(
|
||||
None,
|
||||
TaskCompletedEvent(
|
||||
output=TaskOutput(
|
||||
description="second", raw="task two result", agent="a2"
|
||||
),
|
||||
task=task2,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
future.result(timeout=5)
|
||||
|
||||
entry2 = app._task_logs[-1]
|
||||
assert entry2["idx"] == 2
|
||||
assert entry2["desc"] == "second"
|
||||
assert entry2["output"] == "task two streaming output"
|
||||
assert any(step.get("summary") == "thinking" for step in entry2["steps"])
|
||||
finally:
|
||||
app._unsubscribe()
|
||||
@@ -31,6 +31,18 @@ def test_is_git_not_installed(fp):
|
||||
Repository(path=".")
|
||||
|
||||
|
||||
def test_fetch_failure_raises_value_error(fp):
|
||||
fp.register(["git", "--version"], stdout="git version 2.30.0\n")
|
||||
fp.register(["git", "rev-parse", "--is-inside-work-tree"], stdout="true\n")
|
||||
fp.register(["git", "fetch"], returncode=128, stderr="remote unavailable\n")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"Git fetch failed with exit code 128 for command \['git', 'fetch'\]: remote unavailable",
|
||||
):
|
||||
Repository(path=".")
|
||||
|
||||
|
||||
def test_status(fp, repository):
|
||||
fp.register(
|
||||
["git", "status", "--branch", "--porcelain"],
|
||||
@@ -99,3 +111,45 @@ def test_origin_url(fp, repository):
|
||||
stdout="https://github.com/user/repo.git\n",
|
||||
)
|
||||
assert repository.origin_url() == "https://github.com/user/repo.git"
|
||||
|
||||
|
||||
def test_initialize_creates_initial_commit(fp, tmp_path):
|
||||
fp.register(["git", "--version"], stdout="git version 2.30.0\n")
|
||||
fp.register(["git", "init"], stdout="")
|
||||
fp.register(["git", "--version"], stdout="git version 2.30.0\n")
|
||||
fp.register(["git", "rev-parse", "--is-inside-work-tree"], stdout="true\n")
|
||||
fp.register(["git", "rev-parse", "--verify", "HEAD"], returncode=1)
|
||||
fp.register(["git", "add", "."], stdout="")
|
||||
fp.register(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.name=CrewAI",
|
||||
"-c",
|
||||
"user.email=deploy@crewai.com",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"Initial crew",
|
||||
],
|
||||
stdout="",
|
||||
)
|
||||
|
||||
repo = Repository.initialize(path=str(tmp_path))
|
||||
|
||||
assert repo.path == str(tmp_path)
|
||||
exclude_file = tmp_path / ".git" / "info" / "exclude"
|
||||
exclude_text = exclude_file.read_text()
|
||||
assert ".env" in exclude_text
|
||||
assert "!.env.example" in exclude_text
|
||||
assert "!.env.sample" in exclude_text
|
||||
assert "__pycache__/" in exclude_text
|
||||
|
||||
|
||||
def test_deployable_files_uses_git_excludes(fp, repository):
|
||||
fp.register(
|
||||
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
|
||||
stdout="pyproject.toml\nsrc/main.py\n",
|
||||
)
|
||||
|
||||
assert repository.deployable_files() == ["pyproject.toml", "src/main.py"]
|
||||
|
||||
102
lib/cli/tests/test_install_crew.py
Normal file
102
lib/cli/tests/test_install_crew.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
import crewai_cli.install_crew as install_crew_module
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _tool_credentials(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
install_crew_module,
|
||||
"build_env_with_all_tool_credentials",
|
||||
lambda: {"CREWAI_TEST": "1"},
|
||||
)
|
||||
|
||||
|
||||
def test_install_crew_json_project_skips_project_install(
|
||||
fp, monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "json_crew"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
fp.register(["uv", "sync", "--no-install-project"], stdout="")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
|
||||
def test_install_crew_json_project_with_python_package_installs_project(
|
||||
fp, monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "hybrid-crew"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
package_dir = tmp_path / "src" / "hybrid_crew"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "crew.py").write_text("class HybridCrew: ...\n")
|
||||
fp.register(["uv", "sync"], stdout="")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
|
||||
def test_install_crew_flow_project_installs_project(fp, monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "flow_project"
|
||||
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
""".strip()
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
fp.register(["uv", "sync"], stdout="")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
|
||||
def test_install_crew_classic_project_installs_project(
|
||||
fp, monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'classic'\n")
|
||||
fp.register(["uv", "sync"], stdout="")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
|
||||
def test_install_crew_install_project_false_adds_no_install_project(fp):
|
||||
fp.register(["uv", "sync", "--no-install-project", "--frozen"], stdout="")
|
||||
|
||||
install_crew_module.install_crew(["--frozen"], install_project=False)
|
||||
|
||||
|
||||
def test_install_crew_reraises_sync_failure_when_requested(fp):
|
||||
fp.register(["uv", "sync"], returncode=1, stderr="sync failed\n")
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
install_crew_module.install_crew([], raise_on_error=True)
|
||||
|
||||
|
||||
def test_install_crew_swallows_sync_failure_by_default(fp):
|
||||
fp.register(["uv", "sync"], returncode=1, stderr="sync failed\n")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
@@ -292,6 +292,36 @@ class TestPlusAPI(unittest.TestCase):
|
||||
"POST", "/crewai_plus/api/v1/crews", json=payload
|
||||
)
|
||||
|
||||
@patch("crewai_cli.plus_api.PlusAPI._make_multipart_request")
|
||||
def test_create_crew_from_zip(self, mock_make_multipart_request):
|
||||
self.api.create_crew_from_zip(
|
||||
"/tmp/test.zip",
|
||||
name="test_crew",
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
mock_make_multipart_request.assert_called_once_with(
|
||||
"POST",
|
||||
"/crewai_plus/api/v1/crews/zip",
|
||||
zip_file_path="/tmp/test.zip",
|
||||
data={"name": "test_crew", "env[ENV_VAR]": "value"},
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
@patch("crewai_cli.plus_api.PlusAPI._make_multipart_request")
|
||||
def test_update_crew_from_zip(self, mock_make_multipart_request):
|
||||
self.api.update_crew_from_zip(
|
||||
"test_uuid",
|
||||
"/tmp/test.zip",
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
mock_make_multipart_request.assert_called_once_with(
|
||||
"POST",
|
||||
"/crewai_plus/api/v1/crews/test_uuid/zip_update",
|
||||
zip_file_path="/tmp/test.zip",
|
||||
data={"env[ENV_VAR]": "value"},
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
@patch("crewai_core.plus_api.Settings")
|
||||
@patch.dict(os.environ, {"CREWAI_PLUS_URL": ""})
|
||||
def test_custom_base_url(self, mock_settings_class):
|
||||
|
||||
470
lib/cli/tests/test_run_crew.py
Normal file
470
lib/cli/tests/test_run_crew.py
Normal file
@@ -0,0 +1,470 @@
|
||||
"""Tests for crewai_cli.run_crew JSON crew handling."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
|
||||
import crewai_cli.run_crew as run_crew_module
|
||||
|
||||
|
||||
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)
|
||||
called: dict = {}
|
||||
|
||||
def fake_run_json_crew_in_project_env(trained_agents_file=None):
|
||||
called["trained_agents_file"] = trained_agents_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_run_json_crew_in_project_env",
|
||||
fake_run_json_crew_in_project_env,
|
||||
)
|
||||
|
||||
run_crew_module.run_crew(trained_agents_file="some.pkl")
|
||||
|
||||
assert called == {"trained_agents_file": "some.pkl"}
|
||||
|
||||
|
||||
def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path: Path):
|
||||
"""JSON crew runs should execute inside the project uv environment."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
install_calls = []
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_install_json_crew_dependencies_if_needed",
|
||||
lambda: install_calls.append(True),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"build_env_with_all_tool_credentials",
|
||||
lambda: {"EXISTING": "value"},
|
||||
)
|
||||
|
||||
def fake_subprocess_run(command, **kwargs):
|
||||
subprocess_calls.append((command, kwargs))
|
||||
|
||||
monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run)
|
||||
|
||||
run_crew_module._run_json_crew_in_project_env(
|
||||
trained_agents_file="trained.pkl"
|
||||
)
|
||||
|
||||
expected_env = {
|
||||
"EXISTING": "value",
|
||||
run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV: str(
|
||||
Path(run_crew_module.__file__).resolve().parent
|
||||
),
|
||||
CREWAI_TRAINED_AGENTS_FILE_ENV: "trained.pkl",
|
||||
}
|
||||
if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir():
|
||||
expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str(
|
||||
local_crewai_source_dir
|
||||
)
|
||||
|
||||
assert install_calls == [True]
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"--no-sync",
|
||||
"python",
|
||||
"-c",
|
||||
run_crew_module._JSON_CREW_RUNNER_CODE,
|
||||
],
|
||||
{
|
||||
"capture_output": False,
|
||||
"text": True,
|
||||
"check": True,
|
||||
"env": expected_env,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_json_run_uses_poetry_run_for_poetry_lock_without_uv_lock(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "poetry.lock").write_text("# lock\n")
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_install_json_crew_dependencies_if_needed",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"build_env_with_all_tool_credentials",
|
||||
lambda: {},
|
||||
)
|
||||
subprocess_calls = []
|
||||
|
||||
def fake_subprocess_run(command, **kwargs):
|
||||
subprocess_calls.append((command, kwargs))
|
||||
|
||||
monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run)
|
||||
|
||||
run_crew_module._run_json_crew_in_project_env()
|
||||
|
||||
expected_env = {
|
||||
run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV: str(
|
||||
Path(run_crew_module.__file__).resolve().parent
|
||||
),
|
||||
}
|
||||
if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir():
|
||||
expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str(
|
||||
local_crewai_source_dir
|
||||
)
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
[
|
||||
"poetry",
|
||||
"run",
|
||||
"python",
|
||||
"-c",
|
||||
run_crew_module._JSON_CREW_RUNNER_CODE,
|
||||
],
|
||||
{
|
||||
"capture_output": False,
|
||||
"text": True,
|
||||
"check": True,
|
||||
"env": expected_env,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_json_runner_code_loads_current_cli_package_over_project_env(tmp_path: Path):
|
||||
old_parent = tmp_path / "old"
|
||||
old_pkg = old_parent / "crewai_cli"
|
||||
old_pkg.mkdir(parents=True)
|
||||
(old_pkg / "__init__.py").write_text("")
|
||||
(old_pkg / "run_crew.py").write_text("raise ImportError('old package used')\n")
|
||||
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 / "json_loader.py").write_text(
|
||||
"raise ImportError('old crewai used')\n"
|
||||
)
|
||||
|
||||
current_pkg = tmp_path / "current" / "crewai_cli"
|
||||
current_pkg.mkdir(parents=True)
|
||||
marker = tmp_path / "marker.txt"
|
||||
(current_pkg / "__init__.py").write_text("")
|
||||
(current_pkg / "run_crew.py").write_text(
|
||||
"from pathlib import Path\n"
|
||||
"from crewai.project.json_loader import SOURCE\n"
|
||||
"def _run_json_crew(trained_agents_file=None):\n"
|
||||
f" Path({str(marker)!r}).write_text(SOURCE + ':' + (trained_agents_file or ''))\n"
|
||||
)
|
||||
current_crewai_project = tmp_path / "current_crewai_src" / "crewai" / "project"
|
||||
current_crewai_project.mkdir(parents=True)
|
||||
(tmp_path / "current_crewai_src" / "crewai" / "__init__.py").write_text("")
|
||||
(current_crewai_project / "__init__.py").write_text("")
|
||||
(current_crewai_project / "json_loader.py").write_text("SOURCE = 'current'\n")
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = str(old_parent)
|
||||
env[run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(current_pkg)
|
||||
env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str(
|
||||
tmp_path / "current_crewai_src"
|
||||
)
|
||||
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = "trained.pkl"
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", run_crew_module._JSON_CREW_RUNNER_CODE],
|
||||
check=True,
|
||||
env=env,
|
||||
cwd=tmp_path,
|
||||
)
|
||||
|
||||
assert marker.read_text() == "current:trained.pkl"
|
||||
|
||||
|
||||
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):
|
||||
called["trained_agents_file"] = trained_agents_file
|
||||
return "result"
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_run_json_crew", fake_run_json_crew)
|
||||
|
||||
assert (
|
||||
run_crew_module._run_json_crew_in_project_env(
|
||||
trained_agents_file="trained.pkl"
|
||||
)
|
||||
== "result"
|
||||
)
|
||||
assert called == {"trained_agents_file": "trained.pkl"}
|
||||
|
||||
|
||||
def test_json_project_env_run_failure_exits_nonzero(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_install_json_crew_dependencies_if_needed", lambda: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "build_env_with_all_tool_credentials", lambda: {}
|
||||
)
|
||||
|
||||
def fake_subprocess_run(command, **kwargs):
|
||||
raise subprocess.CalledProcessError(7, command)
|
||||
|
||||
monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_crew_module._run_json_crew_in_project_env()
|
||||
|
||||
assert exc_info.value.code == 7
|
||||
|
||||
|
||||
def test_json_run_installs_dependencies_when_pyproject_has_no_lockfile(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
"""JSON crew runs should lock/sync project dependencies only when needed."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
calls.append((proxy_options, raise_on_error, install_project))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == [([], True, None)]
|
||||
|
||||
|
||||
def test_json_run_syncs_frozen_when_uv_lock_exists_without_venv(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "uv.lock").write_text("# lock\n")
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
calls.append((proxy_options, raise_on_error, install_project))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == [(["--frozen"], True, None)]
|
||||
|
||||
|
||||
def test_json_run_skips_uv_sync_when_only_poetry_lock_exists_without_venv(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "poetry.lock").write_text("# lock\n")
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
calls.append((proxy_options, raise_on_error, install_project))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lockfile", ["uv.lock", "poetry.lock"])
|
||||
def test_json_run_skips_dependency_install_when_lockfile_and_venv_exist(
|
||||
monkeypatch, tmp_path: Path, lockfile: str
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / lockfile).write_text("# lock\n")
|
||||
(tmp_path / ".venv").mkdir()
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
calls.append((proxy_options, raise_on_error, install_project))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_json_run_skips_dependency_install_without_pyproject(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
calls.append((proxy_options, raise_on_error))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_json_run_install_failure_exits_nonzero(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
raise subprocess.CalledProcessError(42, ["uv", "sync"])
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert exc_info.value.code == 42
|
||||
|
||||
|
||||
def test_run_json_crew_exports_trained_agents_env(monkeypatch, tmp_path: Path):
|
||||
"""JSON crews run in-process, so the pickle path must land in the env var."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv(CREWAI_TRAINED_AGENTS_FILE_ENV, raising=False)
|
||||
|
||||
try:
|
||||
# No crew.json(c) in tmp_path: the loader fails *after* the env var
|
||||
# export, which is the part under test.
|
||||
with pytest.raises(FileNotFoundError):
|
||||
run_crew_module._run_json_crew(trained_agents_file="some.pkl")
|
||||
assert os.environ[CREWAI_TRAINED_AGENTS_FILE_ENV] == "some.pkl"
|
||||
finally:
|
||||
os.environ.pop(CREWAI_TRAINED_AGENTS_FILE_ENV, None)
|
||||
|
||||
|
||||
def test_run_json_crew_leaves_env_untouched_without_flag(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv(CREWAI_TRAINED_AGENTS_FILE_ENV, raising=False)
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
run_crew_module._run_json_crew()
|
||||
|
||||
assert CREWAI_TRAINED_AGENTS_FILE_ENV not in os.environ
|
||||
|
||||
|
||||
def test_missing_input_names_accepts_hyphenated_placeholders():
|
||||
"""The prompt regex must accept the same names kickoff interpolation does."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
crew = SimpleNamespace(
|
||||
agents=[
|
||||
SimpleNamespace(
|
||||
role="Researcher", goal="Cover {my-topic}", backstory=""
|
||||
)
|
||||
],
|
||||
tasks=[
|
||||
SimpleNamespace(
|
||||
description="Write about {my-topic} for {target-audience}",
|
||||
expected_output="Post",
|
||||
output_file=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
assert run_crew_module._missing_input_names(crew, {}) == [
|
||||
"my-topic",
|
||||
"target-audience",
|
||||
]
|
||||
|
||||
|
||||
def _patch_tui_run(monkeypatch, status: str):
|
||||
"""Stub the TUI pieces of _run_json_crew so only exit handling runs."""
|
||||
|
||||
class FakeApp:
|
||||
def __init__(self, **kwargs):
|
||||
self._status = status
|
||||
self._crew_result = "result" if status == "completed" else None
|
||||
self._want_deploy = False
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
crew = SimpleNamespace(name="Demo", tasks=[], agents=[])
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "find_crew_json_file", lambda: Path("crew.jsonc")
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_load_json_crew_for_tui",
|
||||
lambda _path: (FakeApp, crew, {}, [], []),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_prompt_for_missing_inputs", lambda _crew, inputs: inputs
|
||||
)
|
||||
monkeypatch.setattr(run_crew_module, "_print_post_tui_summary", lambda _app: None)
|
||||
|
||||
|
||||
def test_run_json_crew_failed_status_exits_nonzero(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_patch_tui_run(monkeypatch, status="failed")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_crew_module._run_json_crew()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
def test_run_json_crew_completed_status_returns_result(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_patch_tui_run(monkeypatch, status="completed")
|
||||
|
||||
assert run_crew_module._run_json_crew() == "result"
|
||||
|
||||
|
||||
def test_has_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
|
||||
|
||||
|
||||
def test_has_json_crew_true_for_declared_crew_type(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
|
||||
|
||||
|
||||
def test_has_json_crew_true_without_pyproject(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
|
||||
assert run_crew_module._has_json_crew() is True
|
||||
156
lib/cli/tests/test_run_flow_definition.py
Normal file
156
lib/cli/tests/test_run_flow_definition.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from crewai_cli.run_flow_definition import run_flow_definition
|
||||
|
||||
|
||||
class _FakeFlow:
|
||||
def __init__(self, definition):
|
||||
self.definition = definition
|
||||
|
||||
def kickoff(self, inputs=None):
|
||||
return {
|
||||
"flow": self.definition["name"],
|
||||
"inputs": inputs or {},
|
||||
}
|
||||
|
||||
|
||||
class _FakeFlowFactory:
|
||||
@classmethod
|
||||
def from_definition(cls, definition):
|
||||
return _FakeFlow(definition)
|
||||
|
||||
|
||||
class _FakeFlowDefinition:
|
||||
@classmethod
|
||||
def from_yaml(cls, source):
|
||||
return yaml.safe_load(source)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, source):
|
||||
return json.loads(source)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_flow_runtime(monkeypatch):
|
||||
crewai_module = types.ModuleType("crewai")
|
||||
flow_package = types.ModuleType("crewai.flow")
|
||||
flow_module = types.ModuleType("crewai.flow.flow")
|
||||
flow_definition_module = types.ModuleType("crewai.flow.flow_definition")
|
||||
|
||||
flow_module.Flow = _FakeFlowFactory
|
||||
flow_definition_module.FlowDefinition = _FakeFlowDefinition
|
||||
|
||||
monkeypatch.setitem(sys.modules, "crewai", crewai_module)
|
||||
monkeypatch.setitem(sys.modules, "crewai.flow", flow_package)
|
||||
monkeypatch.setitem(sys.modules, "crewai.flow.flow", flow_module)
|
||||
monkeypatch.setitem(
|
||||
sys.modules, "crewai.flow.flow_definition", flow_definition_module
|
||||
)
|
||||
|
||||
|
||||
def _captured_json(capsys):
|
||||
return json.loads(capsys.readouterr().out)
|
||||
|
||||
|
||||
def test_run_flow_definition_reads_definition_file(
|
||||
tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
run_flow_definition(str(definition_path), '{"topic":"AI"}')
|
||||
|
||||
assert _captured_json(capsys) == {
|
||||
"flow": "TestFlow",
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("definition_source", "expected_flow_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"schema: crewai.flow/v1\nname: InlineFlow\n",
|
||||
"InlineFlow",
|
||||
id="inline-yaml",
|
||||
),
|
||||
pytest.param(
|
||||
'{"schema":"crewai.flow/v1","name":"InlineJsonFlow"}',
|
||||
"InlineJsonFlow",
|
||||
id="inline-json",
|
||||
),
|
||||
pytest.param(
|
||||
'{"schema":"crewai.flow/v1","name":"' + ("JsonFlow" * 500) + '"}',
|
||||
"JsonFlow" * 500,
|
||||
id="large-inline-json",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_run_flow_definition_accepts_inline_definitions(
|
||||
definition_source, expected_flow_name, capsys, fake_flow_runtime
|
||||
):
|
||||
run_flow_definition(definition_source)
|
||||
|
||||
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("filename", "definition_source", "expected_flow_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"flow.yaml",
|
||||
"schema: crewai.flow/v1\nname: YamlFileFlow\n",
|
||||
"YamlFileFlow",
|
||||
id="yaml-file",
|
||||
),
|
||||
pytest.param(
|
||||
"flow.json",
|
||||
'{"schema":"crewai.flow/v1","name":"JsonFlow"}',
|
||||
"JsonFlow",
|
||||
id="json-file",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_run_flow_definition_accepts_definition_files(
|
||||
filename, definition_source, expected_flow_name, tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / filename
|
||||
definition_path.write_text(definition_source)
|
||||
|
||||
run_flow_definition(str(definition_path))
|
||||
|
||||
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
|
||||
|
||||
|
||||
def test_run_flow_definition_rejects_non_object_inputs(fake_flow_runtime, capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
run_flow_definition("name: TestFlow", '["not", "an", "object"]')
|
||||
|
||||
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_run_flow_definition_reports_unreadable_file(
|
||||
monkeypatch, tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
def raise_permission_error(self, *args, **kwargs):
|
||||
raise PermissionError("no access")
|
||||
|
||||
monkeypatch.setattr("pathlib.Path.read_text", raise_permission_error)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_flow_definition(str(definition_path))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "Unable to read --definition path" in err
|
||||
assert str(definition_path) in err
|
||||
assert "no access" in err
|
||||
@@ -157,14 +157,16 @@ def test_install_api_error(mock_get, capsys, tool_command):
|
||||
mock_get.assert_called_once_with("error-tool")
|
||||
|
||||
|
||||
@patch("crewai_cli.tools.main.git.Repository.fetch")
|
||||
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=False)
|
||||
def test_publish_when_not_in_sync(mock_is_synced, mock_fetch, capsys, tool_command):
|
||||
@patch("crewai_cli.tools.main.git.Repository")
|
||||
def test_publish_when_not_in_sync(mock_repository, capsys, tool_command):
|
||||
mock_repository.return_value.is_synced.return_value = False
|
||||
|
||||
with raises(SystemExit):
|
||||
tool_command.publish(is_public=True)
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Local changes need to be resolved before publishing" in output
|
||||
mock_repository.return_value.is_synced.assert_called_once_with()
|
||||
|
||||
|
||||
@patch("crewai_cli.tools.main.get_project_name", return_value="sample-tool")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Final, Literal, TypedDict, cast
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -190,6 +191,36 @@ class PlusAPI:
|
||||
with httpx.Client(trust_env=False, verify=verify) as client:
|
||||
return client.request(method, url, **request_kwargs)
|
||||
|
||||
def _make_multipart_request(
|
||||
self,
|
||||
method: HttpMethod,
|
||||
endpoint: str,
|
||||
*,
|
||||
zip_file_path: str | Path,
|
||||
data: dict[str, str] | None = None,
|
||||
timeout: float | None = None,
|
||||
verify: bool = True,
|
||||
) -> httpx.Response:
|
||||
"""Send an authenticated multipart request containing a project ZIP."""
|
||||
url = urljoin(self.base_url, endpoint)
|
||||
headers = dict(cast(dict[str, str], self.headers))
|
||||
headers.pop("Content-Type", None)
|
||||
path = Path(zip_file_path)
|
||||
request_kwargs: dict[str, Any] = {"headers": headers}
|
||||
if data is not None:
|
||||
request_kwargs["data"] = data
|
||||
if timeout is not None:
|
||||
request_kwargs["timeout"] = timeout
|
||||
|
||||
with (
|
||||
path.open("rb") as file_handle,
|
||||
httpx.Client(trust_env=False, verify=verify) as client,
|
||||
):
|
||||
files = {
|
||||
"zip_file": (path.name, file_handle, "application/zip"),
|
||||
}
|
||||
return client.request(method, url, files=files, **request_kwargs)
|
||||
|
||||
def login_to_tool_repository(
|
||||
self, user_identifier: str | None = None
|
||||
) -> httpx.Response:
|
||||
@@ -312,6 +343,46 @@ class PlusAPI:
|
||||
def create_crew(self, payload: CreateCrewPayload) -> httpx.Response:
|
||||
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
|
||||
|
||||
def create_crew_from_zip(
|
||||
self,
|
||||
zip_file_path: str | Path,
|
||||
*,
|
||||
name: str | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> httpx.Response:
|
||||
"""Create a crew deployment from a local project ZIP archive."""
|
||||
data: dict[str, str] = {}
|
||||
if name:
|
||||
data["name"] = name
|
||||
if env:
|
||||
data.update({f"env[{key}]": value for key, value in env.items()})
|
||||
return self._make_multipart_request(
|
||||
"POST",
|
||||
f"{self.CREWS_RESOURCE}/zip",
|
||||
zip_file_path=zip_file_path,
|
||||
data=data or None,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
def update_crew_from_zip(
|
||||
self,
|
||||
uuid: str,
|
||||
zip_file_path: str | Path,
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
) -> httpx.Response:
|
||||
"""Update an existing crew deployment from a local project ZIP archive."""
|
||||
data: dict[str, str] = {}
|
||||
if env:
|
||||
data.update({f"env[{key}]": value for key, value in env.items()})
|
||||
return self._make_multipart_request(
|
||||
"POST",
|
||||
f"{self.CREWS_RESOURCE}/{uuid}/zip_update",
|
||||
zip_file_path=zip_file_path,
|
||||
data=data or None,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
def get_organizations(self) -> httpx.Response:
|
||||
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from crewai_core import (
|
||||
user_data,
|
||||
version,
|
||||
)
|
||||
import pytest
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
import pytest
|
||||
|
||||
|
||||
def test_version_returns_string() -> None:
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
import inspect
|
||||
import json
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, BinaryIO, Protocol, cast, runtime_checkable
|
||||
@@ -23,6 +24,9 @@ from typing_extensions import TypeIs
|
||||
from crewai_files.core.constants import DEFAULT_MAX_FILE_SIZE_BYTES, MAGIC_BUFFER_SIZE
|
||||
|
||||
|
||||
OCTET_STREAM = "application/octet-stream"
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AsyncReadable(Protocol):
|
||||
"""Protocol for async readable streams."""
|
||||
@@ -56,13 +60,51 @@ class _AsyncReadableValidator:
|
||||
ValidatedAsyncReadable = Annotated[AsyncReadable, _AsyncReadableValidator()]
|
||||
|
||||
|
||||
def _fallback_content_type(filename: str | None) -> str:
|
||||
"""Get content type from filename extension or return default."""
|
||||
def _detect_content_type_from_bytes(data: bytes) -> str | None:
|
||||
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
return "image/png"
|
||||
if data.startswith(b"\xff\xd8\xff"):
|
||||
return "image/jpeg"
|
||||
if data.startswith(b"%PDF-"):
|
||||
return "application/pdf"
|
||||
|
||||
try:
|
||||
decoded = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
stripped = decoded.lstrip()
|
||||
if stripped.startswith(("{", "[")):
|
||||
try:
|
||||
json.loads(decoded)
|
||||
return "application/json"
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if "\x00" not in decoded:
|
||||
return "text/plain"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _fallback_content_type(filename: str | None, data: bytes | None = None) -> str:
|
||||
"""Get content type from filename extension, then content sniffing.
|
||||
|
||||
The extension lookup runs first so specific types like ``text/csv`` or
|
||||
``application/xml`` are not degraded to generic sniffed types such as
|
||||
``text/plain``; byte sniffing only covers extensionless/unknown names.
|
||||
"""
|
||||
if filename:
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
if mime_type:
|
||||
return mime_type
|
||||
return "application/octet-stream"
|
||||
|
||||
if data:
|
||||
content_type = _detect_content_type_from_bytes(data)
|
||||
if content_type:
|
||||
return content_type
|
||||
|
||||
return OCTET_STREAM
|
||||
|
||||
|
||||
def generate_filename(content_type: str) -> str:
|
||||
@@ -97,9 +139,19 @@ def detect_content_type(data: bytes, filename: str | None = None) -> str:
|
||||
import magic
|
||||
|
||||
result: str = magic.from_buffer(data[:MAGIC_BUFFER_SIZE], mime=True)
|
||||
return result
|
||||
if result != OCTET_STREAM:
|
||||
return result
|
||||
return _fallback_content_type(filename, data)
|
||||
except ImportError:
|
||||
return _fallback_content_type(filename)
|
||||
return _fallback_content_type(filename, data)
|
||||
|
||||
|
||||
def _read_magic_header(path: Path) -> bytes | None:
|
||||
try:
|
||||
with path.open("rb") as file:
|
||||
return file.read(MAGIC_BUFFER_SIZE)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def detect_content_type_from_path(path: Path, filename: str | None = None) -> str:
|
||||
@@ -115,13 +167,16 @@ def detect_content_type_from_path(path: Path, filename: str | None = None) -> st
|
||||
Returns:
|
||||
The detected MIME type.
|
||||
"""
|
||||
fallback_filename = filename or path.name
|
||||
try:
|
||||
import magic
|
||||
|
||||
result: str = magic.from_file(str(path), mime=True)
|
||||
return result
|
||||
if result != OCTET_STREAM:
|
||||
return result
|
||||
return _fallback_content_type(fallback_filename, _read_magic_header(path))
|
||||
except ImportError:
|
||||
return _fallback_content_type(filename or path.name)
|
||||
return _fallback_content_type(fallback_filename, _read_magic_header(path))
|
||||
|
||||
|
||||
class _BinaryIOValidator:
|
||||
|
||||
@@ -129,6 +129,20 @@ class FileResolver:
|
||||
"""
|
||||
return constraints is not None and constraints.supports_url_references
|
||||
|
||||
@classmethod
|
||||
def _should_resolve_as_url_reference(
|
||||
cls,
|
||||
file: FileInput,
|
||||
provider: ProviderType,
|
||||
constraints: ProviderConstraints | None,
|
||||
) -> bool:
|
||||
"""Check if the provider can accept the current URL source directly."""
|
||||
if not cls._is_url_source(file) or not cls._supports_url(constraints):
|
||||
return False
|
||||
|
||||
provider_lower = provider.lower()
|
||||
return "bedrock" not in provider_lower and "aws" not in provider_lower
|
||||
|
||||
@staticmethod
|
||||
def _resolve_as_url(file: FileInput) -> UrlReference:
|
||||
"""Resolve a URL source as UrlReference.
|
||||
@@ -159,7 +173,7 @@ class FileResolver:
|
||||
"""
|
||||
constraints = get_constraints_for_provider(provider)
|
||||
|
||||
if self._is_url_source(file) and self._supports_url(constraints):
|
||||
if self._should_resolve_as_url_reference(file, provider, constraints):
|
||||
return self._resolve_as_url(file)
|
||||
|
||||
context = self._build_file_context(file)
|
||||
@@ -424,7 +438,7 @@ class FileResolver:
|
||||
"""
|
||||
constraints = get_constraints_for_provider(provider)
|
||||
|
||||
if self._is_url_source(file) and self._supports_url(constraints):
|
||||
if self._should_resolve_as_url_reference(file, provider, constraints):
|
||||
return self._resolve_as_url(file)
|
||||
|
||||
context = self._build_file_context(file)
|
||||
|
||||
@@ -63,7 +63,7 @@ spider-client = [
|
||||
"spider-client>=0.1.25",
|
||||
]
|
||||
scrapegraph-py = [
|
||||
"scrapegraph-py>=1.9.0",
|
||||
"scrapegraph-py>=1.9.0,<2",
|
||||
]
|
||||
linkup-sdk = [
|
||||
"linkup-sdk>=0.2.2",
|
||||
|
||||
@@ -22,6 +22,31 @@ logger = logging.getLogger(__name__)
|
||||
_UNSAFE_PATHS_ENV = "CREWAI_TOOLS_ALLOW_UNSAFE_PATHS"
|
||||
|
||||
|
||||
def format_path_for_display(path: str, base_dir: str | None = None) -> str:
|
||||
"""Return a path label that does not expose absolute directory prefixes."""
|
||||
if base_dir is None:
|
||||
base_dir = os.getcwd()
|
||||
|
||||
try:
|
||||
resolved_base = os.path.realpath(base_dir)
|
||||
resolved_path = os.path.realpath(
|
||||
os.path.join(resolved_base, path) if not os.path.isabs(path) else path
|
||||
)
|
||||
if os.path.commonpath([resolved_base, resolved_path]) == resolved_base:
|
||||
return os.path.relpath(resolved_path, resolved_base)
|
||||
except (OSError, ValueError) as exc:
|
||||
logger.debug("Falling back to basename for display path formatting: %s", exc)
|
||||
|
||||
return os.path.basename(os.path.realpath(path)) or "[redacted path]"
|
||||
|
||||
|
||||
def format_error_for_display(error: Exception) -> str:
|
||||
"""Return exception details without OS-added absolute path context."""
|
||||
if isinstance(error, OSError):
|
||||
return error.strerror or error.__class__.__name__
|
||||
return str(error)
|
||||
|
||||
|
||||
def _is_escape_hatch_enabled() -> bool:
|
||||
"""Check if the unsafe paths escape hatch is enabled."""
|
||||
return os.environ.get(_UNSAFE_PATHS_ENV, "").lower() in ("true", "1", "yes")
|
||||
@@ -66,8 +91,8 @@ def validate_file_path(path: str, base_dir: str | None = None) -> str:
|
||||
prefix = resolved_base if resolved_base.endswith(os.sep) else resolved_base + os.sep
|
||||
if not resolved_path.startswith(prefix) and resolved_path != resolved_base:
|
||||
raise ValueError(
|
||||
f"Path '{path}' resolves to '{resolved_path}' which is outside "
|
||||
f"the allowed directory '{resolved_base}'. "
|
||||
f"Path '{format_path_for_display(resolved_path, resolved_base)}' is "
|
||||
f"outside the allowed directory. "
|
||||
f"Set {_UNSAFE_PATHS_ENV}=true to bypass this check."
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ from typing import Any
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai_tools.security.safe_path import validate_file_path
|
||||
from crewai_tools.security.safe_path import (
|
||||
format_error_for_display,
|
||||
format_path_for_display,
|
||||
validate_file_path,
|
||||
)
|
||||
|
||||
|
||||
class FileReadToolSchema(BaseModel):
|
||||
@@ -58,8 +62,9 @@ class FileReadTool(BaseTool):
|
||||
**kwargs: Additional keyword arguments passed to BaseTool.
|
||||
"""
|
||||
if file_path is not None:
|
||||
display_path = format_path_for_display(file_path)
|
||||
kwargs["description"] = (
|
||||
f"A tool that reads file content. The default file is {file_path}, but you can provide a different 'file_path' parameter to read another file. You can also specify 'start_line' and 'line_count' to read specific parts of the file."
|
||||
f"A tool that reads file content. The default file is {display_path}, but you can provide a different 'file_path' parameter to read another file. You can also specify 'start_line' and 'line_count' to read specific parts of the file."
|
||||
)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
@@ -78,7 +83,12 @@ class FileReadTool(BaseTool):
|
||||
if file_path is None:
|
||||
return "Error: No file path provided. Please provide a file path either in the constructor or as an argument."
|
||||
|
||||
file_path = validate_file_path(file_path)
|
||||
try:
|
||||
file_path = validate_file_path(file_path)
|
||||
except ValueError as e:
|
||||
return f"Error: Invalid file path: {e!s}"
|
||||
|
||||
display_path = format_path_for_display(file_path)
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
if start_line == 1 and line_count is None:
|
||||
@@ -98,8 +108,11 @@ class FileReadTool(BaseTool):
|
||||
|
||||
return "".join(selected_lines)
|
||||
except FileNotFoundError:
|
||||
return f"Error: File not found at path: {file_path}"
|
||||
return f"Error: File not found at path: {display_path}"
|
||||
except PermissionError:
|
||||
return f"Error: Permission denied when trying to read file: {file_path}"
|
||||
return f"Error: Permission denied when trying to read file: {display_path}"
|
||||
except Exception as e:
|
||||
return f"Error: Failed to read file {file_path}. {e!s}"
|
||||
return (
|
||||
f"Error: Failed to read file {display_path}. "
|
||||
f"{format_error_for_display(e)}"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,11 @@ from typing import Any
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.security.safe_path import (
|
||||
format_error_for_display,
|
||||
format_path_for_display,
|
||||
)
|
||||
|
||||
|
||||
def strtobool(val: str | bool) -> bool:
|
||||
if isinstance(val, bool):
|
||||
@@ -44,6 +49,9 @@ class FileWriterTool(BaseTool):
|
||||
# itself, since that is not a valid file target.
|
||||
real_directory = Path(directory).resolve()
|
||||
real_filepath = Path(filepath).resolve()
|
||||
display_filepath = format_path_for_display(
|
||||
str(real_filepath), str(real_directory)
|
||||
)
|
||||
if (
|
||||
not real_filepath.is_relative_to(real_directory)
|
||||
or real_filepath == real_directory
|
||||
@@ -56,15 +64,18 @@ class FileWriterTool(BaseTool):
|
||||
kwargs["overwrite"] = strtobool(kwargs["overwrite"])
|
||||
|
||||
if os.path.exists(real_filepath) and not kwargs["overwrite"]:
|
||||
return f"File {real_filepath} already exists and overwrite option was not passed."
|
||||
return f"File {display_filepath} already exists and overwrite option was not passed."
|
||||
|
||||
mode = "w" if kwargs["overwrite"] else "x"
|
||||
with open(real_filepath, mode) as file:
|
||||
file.write(kwargs["content"])
|
||||
return f"Content successfully written to {real_filepath}"
|
||||
return f"Content successfully written to {display_filepath}"
|
||||
except FileExistsError:
|
||||
return f"File {real_filepath} already exists and overwrite option was not passed."
|
||||
return f"File {display_filepath} already exists and overwrite option was not passed."
|
||||
except KeyError as e:
|
||||
return f"An error occurred while accessing key: {e!s}"
|
||||
except Exception as e:
|
||||
return f"An error occurred while writing to the file: {e!s}"
|
||||
return (
|
||||
"An error occurred while writing to the file: "
|
||||
f"{format_error_for_display(e)}"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
from crewai_tools import FileReadTool
|
||||
@@ -6,21 +5,16 @@ from crewai_tools import FileReadTool
|
||||
|
||||
def test_file_read_tool_constructor():
|
||||
"""Test FileReadTool initialization with file_path."""
|
||||
test_file = "/tmp/test_file.txt"
|
||||
test_content = "Hello, World!"
|
||||
with open(test_file, "w") as f:
|
||||
f.write(test_content)
|
||||
test_file = "test_file.txt"
|
||||
|
||||
tool = FileReadTool(file_path=test_file)
|
||||
assert tool.file_path == test_file
|
||||
assert "test_file.txt" in tool.description
|
||||
|
||||
os.remove(test_file)
|
||||
|
||||
|
||||
def test_file_read_tool_run():
|
||||
"""Test FileReadTool _run method with file_path at runtime."""
|
||||
test_file = "/tmp/test_file.txt"
|
||||
test_file = "test_file.txt"
|
||||
test_content = "Hello, World!"
|
||||
|
||||
# Use mock_open to mock file operations
|
||||
@@ -36,18 +30,18 @@ def test_file_read_tool_error_handling():
|
||||
result = tool._run()
|
||||
assert "Error: No file path provided" in result
|
||||
|
||||
result = tool._run(file_path="/nonexistent/file.txt")
|
||||
result = tool._run(file_path="nonexistent/file.txt")
|
||||
assert "Error: File not found at path:" in result
|
||||
|
||||
with patch("builtins.open", side_effect=PermissionError()):
|
||||
result = tool._run(file_path="/tmp/no_permission.txt")
|
||||
result = tool._run(file_path="no_permission.txt")
|
||||
assert "Error: Permission denied" in result
|
||||
|
||||
|
||||
def test_file_read_tool_constructor_and_run():
|
||||
"""Test FileReadTool using both constructor and runtime file paths."""
|
||||
test_file1 = "/tmp/test1.txt"
|
||||
test_file2 = "/tmp/test2.txt"
|
||||
test_file1 = "test1.txt"
|
||||
test_file2 = "test2.txt"
|
||||
content1 = "File 1 content"
|
||||
content2 = "File 2 content"
|
||||
|
||||
@@ -64,7 +58,7 @@ def test_file_read_tool_constructor_and_run():
|
||||
|
||||
def test_file_read_tool_chunk_reading():
|
||||
"""Test FileReadTool reading specific chunks of a file."""
|
||||
test_file = "/tmp/multiline_test.txt"
|
||||
test_file = "multiline_test.txt"
|
||||
lines = [
|
||||
"Line 1\n",
|
||||
"Line 2\n",
|
||||
@@ -104,7 +98,7 @@ def test_file_read_tool_chunk_reading():
|
||||
|
||||
def test_file_read_tool_chunk_error_handling():
|
||||
"""Test error handling for chunk reading."""
|
||||
test_file = "/tmp/short_test.txt"
|
||||
test_file = "short_test.txt"
|
||||
lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
|
||||
file_content = "".join(lines)
|
||||
|
||||
@@ -122,7 +116,7 @@ def test_file_read_tool_chunk_error_handling():
|
||||
|
||||
def test_file_read_tool_zero_or_negative_start_line():
|
||||
"""Test that start_line values of 0 or negative read from the start of the file."""
|
||||
test_file = "/tmp/negative_test.txt"
|
||||
test_file = "negative_test.txt"
|
||||
lines = ["Line 1\n", "Line 2\n", "Line 3\n", "Line 4\n", "Line 5\n"]
|
||||
file_content = "".join(lines)
|
||||
|
||||
@@ -150,3 +144,45 @@ def test_file_read_tool_zero_or_negative_start_line():
|
||||
result = tool._run(file_path=test_file, start_line=-10, line_count=2)
|
||||
expected = "".join(lines[0:2]) # Should read first 2 lines
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_file_read_tool_error_messages_do_not_disclose_absolute_paths(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""FileReadTool should redact absolute prefixes from user-visible errors."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
tool = FileReadTool()
|
||||
target = tmp_path / "secret.txt"
|
||||
|
||||
result = tool._run(file_path=str(target))
|
||||
assert "secret.txt" in result
|
||||
assert str(tmp_path) not in result
|
||||
|
||||
target.touch()
|
||||
with patch("builtins.open", side_effect=PermissionError()):
|
||||
result = tool._run(file_path=str(target))
|
||||
assert "secret.txt" in result
|
||||
assert str(tmp_path) not in result
|
||||
|
||||
with patch(
|
||||
"builtins.open",
|
||||
side_effect=OSError(5, "Input/output error", str(target)),
|
||||
):
|
||||
result = tool._run(file_path=str(target))
|
||||
assert "secret.txt" in result
|
||||
assert str(tmp_path) not in result
|
||||
|
||||
|
||||
def test_file_read_tool_invalid_path_error_does_not_disclose_workspace(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Validation errors should not echo the resolved workspace path."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
outside = tmp_path.parent / "outside.txt"
|
||||
|
||||
result = FileReadTool()._run(file_path=str(outside))
|
||||
|
||||
assert "Invalid file path" in result
|
||||
assert "outside.txt" in result
|
||||
assert str(tmp_path) not in result
|
||||
assert str(tmp_path.parent) not in result
|
||||
|
||||
@@ -47,6 +47,8 @@ def test_basic_file_write(tool, temp_env):
|
||||
assert os.path.exists(path)
|
||||
assert read_file(path) == temp_env["test_content"]
|
||||
assert "successfully written" in result
|
||||
assert temp_env["test_file"] in result
|
||||
assert temp_env["temp_dir"] not in result
|
||||
|
||||
|
||||
def test_directory_creation(tool, temp_env):
|
||||
@@ -62,6 +64,8 @@ def test_directory_creation(tool, temp_env):
|
||||
assert os.path.exists(new_dir)
|
||||
assert os.path.exists(path)
|
||||
assert "successfully written" in result
|
||||
assert temp_env["test_file"] in result
|
||||
assert new_dir not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -134,6 +138,8 @@ def test_file_exists_error_handling(tool, temp_env, overwrite):
|
||||
)
|
||||
|
||||
assert "already exists and overwrite option was not passed" in result
|
||||
assert temp_env["test_file"] in result
|
||||
assert temp_env["temp_dir"] not in result
|
||||
assert read_file(path) == "Pre-existing content"
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from crewai_tools.security.safe_path import (
|
||||
format_path_for_display,
|
||||
validate_directory_path,
|
||||
validate_file_path,
|
||||
validate_url,
|
||||
@@ -66,6 +67,37 @@ class TestValidateFilePath:
|
||||
result = validate_file_path("/etc/passwd", str(tmp_path))
|
||||
assert result == os.path.realpath("/etc/passwd")
|
||||
|
||||
def test_rejection_message_redacts_absolute_prefixes(self, tmp_path):
|
||||
outside = tmp_path.parent / "outside.txt"
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
validate_file_path(str(outside), str(tmp_path))
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "outside.txt" in message
|
||||
assert str(tmp_path) not in message
|
||||
assert str(tmp_path.parent) not in message
|
||||
|
||||
|
||||
class TestFormatPathForDisplay:
|
||||
"""Tests for user-visible path labels."""
|
||||
|
||||
def test_returns_relative_path_inside_base(self, tmp_path):
|
||||
nested_file = tmp_path / "nested" / "file.txt"
|
||||
nested_file.parent.mkdir()
|
||||
nested_file.touch()
|
||||
|
||||
result = format_path_for_display(str(nested_file), str(tmp_path))
|
||||
|
||||
assert result == os.path.join("nested", "file.txt")
|
||||
|
||||
def test_redacts_absolute_prefix_outside_base(self, tmp_path):
|
||||
outside_file = tmp_path.parent / "outside.txt"
|
||||
|
||||
result = format_path_for_display(str(outside_file), str(tmp_path))
|
||||
|
||||
assert result == "outside.txt"
|
||||
|
||||
|
||||
class TestValidateDirectoryPath:
|
||||
"""Tests for validate_directory_path."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user