mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-23 17:18:09 +00:00
Compare commits
14 Commits
1.14.8a1
...
luzk/remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8068b61994 | ||
|
|
2eb4e3a236 | ||
|
|
221dfdb08e | ||
|
|
720a4c7216 | ||
|
|
4b2ce00a09 | ||
|
|
0391febc6c | ||
|
|
4cbfbdb232 | ||
|
|
9db2d44766 | ||
|
|
cf04181190 | ||
|
|
854c67d21c | ||
|
|
f48a6389f1 | ||
|
|
bc2c2a858c | ||
|
|
fa89ac428e | ||
|
|
b0816e00b6 |
16
conftest.py
16
conftest.py
@@ -134,17 +134,21 @@ def bedrock_host_matcher(r1: Request, r2: Request) -> bool: # type: ignore[no-a
|
||||
)
|
||||
|
||||
|
||||
def _patched_make_vcr_request(httpx_request: Any, **kwargs: Any) -> Any:
|
||||
def _patched_make_vcr_request(
|
||||
httpx_request: Any, real_request_body: Any = None, **kwargs: Any
|
||||
) -> Any:
|
||||
"""Patched version of VCR's _make_vcr_request that handles binary content.
|
||||
|
||||
The original implementation fails on binary request bodies (like file uploads)
|
||||
because it assumes all content can be decoded as UTF-8.
|
||||
"""
|
||||
raw_body = httpx_request.read()
|
||||
try:
|
||||
body = raw_body.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
body = base64.b64encode(raw_body).decode("ascii")
|
||||
raw_body = real_request_body if real_request_body is not None else httpx_request.read()
|
||||
body: Any = raw_body
|
||||
if isinstance(raw_body, bytes):
|
||||
try:
|
||||
body = raw_body.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
body = base64.b64encode(raw_body).decode("ascii")
|
||||
uri = str(httpx_request.url)
|
||||
headers = dict(httpx_request.headers)
|
||||
return Request(httpx_request.method, uri, body, headers)
|
||||
|
||||
@@ -398,6 +398,7 @@
|
||||
"pages": [
|
||||
"edge/en/enterprise/features/automations",
|
||||
"edge/en/enterprise/features/crew-studio",
|
||||
"edge/en/enterprise/features/merged-step-card",
|
||||
"edge/en/enterprise/features/marketplace",
|
||||
"edge/en/enterprise/features/agent-repositories",
|
||||
"edge/en/enterprise/features/tools-and-integrations",
|
||||
@@ -515,6 +516,7 @@
|
||||
"edge/en/enterprise/guides/update-crew",
|
||||
"edge/en/enterprise/guides/enable-crew-studio",
|
||||
"edge/en/enterprise/guides/capture_telemetry_logs",
|
||||
"edge/en/enterprise/guides/datadog",
|
||||
"edge/en/enterprise/guides/azure-openai-setup",
|
||||
"edge/en/enterprise/guides/vertex-ai-workload-identity-setup",
|
||||
"edge/en/enterprise/guides/tool-repository",
|
||||
@@ -921,6 +923,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/en/enterprise/features/automations",
|
||||
"v1.14.7/en/enterprise/features/crew-studio",
|
||||
"v1.14.7/en/enterprise/features/merged-step-card",
|
||||
"v1.14.7/en/enterprise/features/marketplace",
|
||||
"v1.14.7/en/enterprise/features/agent-repositories",
|
||||
"v1.14.7/en/enterprise/features/tools-and-integrations",
|
||||
@@ -8547,6 +8550,7 @@
|
||||
"pages": [
|
||||
"edge/pt-BR/enterprise/features/automations",
|
||||
"edge/pt-BR/enterprise/features/crew-studio",
|
||||
"edge/pt-BR/enterprise/features/merged-step-card",
|
||||
"edge/pt-BR/enterprise/features/marketplace",
|
||||
"edge/pt-BR/enterprise/features/agent-repositories",
|
||||
"edge/pt-BR/enterprise/features/tools-and-integrations",
|
||||
@@ -8647,6 +8651,7 @@
|
||||
"edge/pt-BR/enterprise/guides/update-crew",
|
||||
"edge/pt-BR/enterprise/guides/enable-crew-studio",
|
||||
"edge/pt-BR/enterprise/guides/capture_telemetry_logs",
|
||||
"edge/pt-BR/enterprise/guides/datadog",
|
||||
"edge/pt-BR/enterprise/guides/azure-openai-setup",
|
||||
"edge/pt-BR/enterprise/guides/tool-repository",
|
||||
"edge/pt-BR/enterprise/guides/custom-mcp-server",
|
||||
@@ -9047,6 +9052,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/pt-BR/enterprise/features/automations",
|
||||
"v1.14.7/pt-BR/enterprise/features/crew-studio",
|
||||
"v1.14.7/pt-BR/enterprise/features/merged-step-card",
|
||||
"v1.14.7/pt-BR/enterprise/features/marketplace",
|
||||
"v1.14.7/pt-BR/enterprise/features/agent-repositories",
|
||||
"v1.14.7/pt-BR/enterprise/features/tools-and-integrations",
|
||||
@@ -16410,6 +16416,7 @@
|
||||
"pages": [
|
||||
"edge/ko/enterprise/features/automations",
|
||||
"edge/ko/enterprise/features/crew-studio",
|
||||
"edge/ko/enterprise/features/merged-step-card",
|
||||
"edge/ko/enterprise/features/marketplace",
|
||||
"edge/ko/enterprise/features/agent-repositories",
|
||||
"edge/ko/enterprise/features/tools-and-integrations",
|
||||
@@ -16510,6 +16517,7 @@
|
||||
"edge/ko/enterprise/guides/update-crew",
|
||||
"edge/ko/enterprise/guides/enable-crew-studio",
|
||||
"edge/ko/enterprise/guides/capture_telemetry_logs",
|
||||
"edge/ko/enterprise/guides/datadog",
|
||||
"edge/ko/enterprise/guides/azure-openai-setup",
|
||||
"edge/ko/enterprise/guides/tool-repository",
|
||||
"edge/ko/enterprise/guides/custom-mcp-server",
|
||||
@@ -16922,6 +16930,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/ko/enterprise/features/automations",
|
||||
"v1.14.7/ko/enterprise/features/crew-studio",
|
||||
"v1.14.7/ko/enterprise/features/merged-step-card",
|
||||
"v1.14.7/ko/enterprise/features/marketplace",
|
||||
"v1.14.7/ko/enterprise/features/agent-repositories",
|
||||
"v1.14.7/ko/enterprise/features/tools-and-integrations",
|
||||
@@ -24465,6 +24474,7 @@
|
||||
"pages": [
|
||||
"edge/ar/enterprise/features/automations",
|
||||
"edge/ar/enterprise/features/crew-studio",
|
||||
"edge/ar/enterprise/features/merged-step-card",
|
||||
"edge/ar/enterprise/features/marketplace",
|
||||
"edge/ar/enterprise/features/agent-repositories",
|
||||
"edge/ar/enterprise/features/tools-and-integrations",
|
||||
@@ -24565,6 +24575,7 @@
|
||||
"edge/ar/enterprise/guides/update-crew",
|
||||
"edge/ar/enterprise/guides/enable-crew-studio",
|
||||
"edge/ar/enterprise/guides/capture_telemetry_logs",
|
||||
"edge/ar/enterprise/guides/datadog",
|
||||
"edge/ar/enterprise/guides/azure-openai-setup",
|
||||
"edge/ar/enterprise/guides/tool-repository",
|
||||
"edge/ar/enterprise/guides/custom-mcp-server",
|
||||
@@ -24977,6 +24988,7 @@
|
||||
"pages": [
|
||||
"v1.14.7/ar/enterprise/features/automations",
|
||||
"v1.14.7/ar/enterprise/features/crew-studio",
|
||||
"v1.14.7/ar/enterprise/features/merged-step-card",
|
||||
"v1.14.7/ar/enterprise/features/marketplace",
|
||||
"v1.14.7/ar/enterprise/features/agent-repositories",
|
||||
"v1.14.7/ar/enterprise/features/tools-and-integrations",
|
||||
|
||||
@@ -4,6 +4,27 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="18 يونيو 2026">
|
||||
## v1.14.8a2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة إجراء عميل واحد إلى تعريفات التدفق
|
||||
- التحقق من تعبيرات CEL للتدفق عند تحميل التعريف
|
||||
|
||||
### الوثائق
|
||||
- إضافة دليل تكامل Datadog مع لوحة عمليات قابلة للاستيراد
|
||||
- تحديث اللقطة وسجل التغييرات للإصدار v1.14.8a1
|
||||
|
||||
## المساهمون
|
||||
|
||||
@joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="18 يونيو 2026">
|
||||
## v1.14.8a1
|
||||
|
||||
@@ -43,7 +64,7 @@ mode: "wide"
|
||||
- تنفيذ أدوات تشغيل تعريف التدفق بدون كود Python
|
||||
- دفع التغذية الراجعة البشرية من تعريف التدفق
|
||||
- توصيل التكوين والاستمرارية من FlowDefinition إلى وقت التشغيل
|
||||
- إضافة `crewai run --definition` التجريبية للتدفقات
|
||||
- إضافة `crewai run --definition` للتدفقات التصريحية
|
||||
- دعم تراجع نشر ZIP وتشغيل مشاريع الطاقم بتنسيق JSON
|
||||
- تقديم الطواقم بتنسيق JSON أولاً
|
||||
|
||||
|
||||
@@ -959,7 +959,7 @@ source .venv/bin/activate
|
||||
بعد تفعيل البيئة الافتراضية، يمكنك تشغيل التدفق بتنفيذ أحد الأوامر التالية:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
أو
|
||||
@@ -1160,10 +1160,4 @@ crewai run
|
||||
|
||||
يكتشف هذا الأمر تلقائيًا ما إذا كان مشروعك تدفقًا (بناءً على إعداد `type = "flow"` في pyproject.toml الخاص بك) ويشغّله وفقًا لذلك. هذه هي الطريقة الموصى بها لتشغيل التدفقات من سطر الأوامر.
|
||||
|
||||
للتوافق مع الإصدارات السابقة، يمكنك أيضًا استخدام:
|
||||
|
||||
```shell
|
||||
crewai flow kickoff
|
||||
```
|
||||
|
||||
ومع ذلك، فإن أمر `crewai run` هو الطريقة المفضلة الآن لأنه يعمل لكل من فرق Crew والتدفقات.
|
||||
أمر `crewai flow kickoff` القديم deprecated. استخدم `crewai run` لكل من فرق Crew والتدفقات.
|
||||
|
||||
87
docs/edge/ar/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/ar/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: بطاقة واحدة لكل خطوة
|
||||
description: "كل خطوة على لوحة Studio هي بطاقة واحدة تجمع بين المهمة والوكيل الذي ينفّذها."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:
|
||||
|
||||
- **المهمة** — ماذا تفعل (الاسم، الوصف، المخرجات المتوقعة، وتنسيق الاستجابة).
|
||||
- **الوكيل** — من ينفّذها (الوكيل المُعيَّن ونموذجه وأدواته).
|
||||
|
||||
الوكيل ليس مشاركًا مستقلاً في سير العمل لديك — بل هو سمة من سمات المهمة: *أي وكيل ينفّذ هذا العمل.* وضع المهمة والوكيل في بطاقة واحدة يجعل هذه العلاقة واضحة، ويحوّل أتمتتك إلى سلسلة واحدة من وحدات العمل من اليسار إلى اليمين يسهل قراءتها بنظرة واحدة.
|
||||
|
||||
<Frame caption="بطاقة واحدة لكل خطوة: المهمة مع ملخص للوكيل المُعيَّن في التذييل.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## على اللوحة
|
||||
|
||||
تعرض كل بطاقة مطوية ما يلي:
|
||||
|
||||
- **اسم المهمة ووصفها** في الأعلى.
|
||||
- **تذييل يلخّص الوكيل المُعيَّن** — الصورة الرمزية والاسم والنموذج والأدوات.
|
||||
|
||||
لا توجد عقدة وكيل منفصلة ولا حافة عمودية من الوكيل ← المهمة. تتصل خطواتك مباشرةً ببعضها البعض بالترتيب الذي تُنفَّذ به.
|
||||
|
||||
## في المحرّر
|
||||
|
||||
افتح بطاقة لتحريرها. العرض الموسّع هو البطاقة نفسها في حالة مفصّلة — وليس شاشة مختلفة — منظّمة في قسمين موسومين بوضوح.
|
||||
|
||||
<Frame caption="المحرّر الموسّع: قسم المهمة مفتوح، والوكيل ملخّص أسفله.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### المهمة — ماذا تفعل
|
||||
|
||||
مفتوحة افتراضيًا، لأنها ما تحرّره عادةً:
|
||||
|
||||
- **الاسم**
|
||||
- **الوصف**
|
||||
- **المخرجات المتوقعة**
|
||||
- **تنسيق الاستجابة** — يظهر هنا لأنه يتحكم تحديدًا في ما تقرأه الخطوات اللاحقة (مثل التوجيه) من هذه الخطوة.
|
||||
|
||||
### الوكيل — من ينفّذها
|
||||
|
||||
يُعرض الوكيل المُعيَّن كملخّص — **الاسم والنموذج والأدوات في سطر واحد**. ويُحفَظ إعداده الأعمق خلف قسمين قابلين للطي:
|
||||
|
||||
- **الدور والهدف والخلفية**
|
||||
- **إعدادات الوكيل** — الاستدلال، الحد الأقصى لمحاولات الاستدلال، السماح بالتفويض، الحد الأقصى للتكرارات، وإعدادات LLM.
|
||||
|
||||
<Tip>
|
||||
الإعداد الكامل للوكيل — الدور، الهدف، الخلفية، النموذج، الأدوات، إعدادات LLM، وكامل كتلة إعدادات الوكيل — موجود خلف القسمين القابلين للطي **الدور والهدف والخلفية** و**إعدادات الوكيل**، منظّمًا حسب عدد مرّات تحريرك له.
|
||||
</Tip>
|
||||
|
||||
## التبديل مقابل تحرير الوكيل
|
||||
|
||||
هناك طريقتان متمايزتان للتعامل مع الوكيل في البطاقة، وكل منهما تؤدي وظيفة مختلفة:
|
||||
|
||||
- **التبديل (Swap)** يعيد تعيين *أي* وكيل ينفّذ هذه المهمة. استخدم عنصر التحكم **تبديل** لاختيار وكيل مختلف من هذا المشروع، أو اختيار واحد من مستودع الوكلاء، أو إنشاء وكيل جديد. هذا مقصور على نطاق المهمة.
|
||||
- **تحرير** الوكيل — بفتح **الدور والهدف والخلفية** أو **إعدادات الوكيل** — يغيّر الوكيل *نفسه*.
|
||||
|
||||
<Frame caption="التبديل يغيّر الوكيل الذي ينفّذ المهمة.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**الوكلاء قابلون لإعادة الاستخدام ومشتركون.** يمكن للوكيل نفسه تنفيذ أكثر من مهمة عبر مشروعك. تحرير دور الوكيل أو خلفيته أو إعداداته يحدّث ذلك الوكيل **في كل مكان يُستخدم فيه** — وليس فقط في البطاقة التي فتحتها. إذا أردت تطبيق تغيير على خطوة واحدة فقط، فقم **بالتبديل** إلى وكيل مختلف بدلاً من تحرير الوكيل المشترك.
|
||||
</Warning>
|
||||
|
||||
## ذات صلة
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ar/enterprise/features/crew-studio" icon="pencil">
|
||||
أنشئ الأتمتة بمساعدة الذكاء الاصطناعي ومحرّر مرئي.
|
||||
</Card>
|
||||
<Card title="مستودعات الوكلاء" href="/ar/enterprise/features/agent-repositories" icon="users">
|
||||
إدارة الوكلاء وإعادة استخدامهم عبر أتمتتك.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -9,6 +9,10 @@ mode: "wide"
|
||||
|
||||
تتبع بيانات القياس [اتفاقيات OpenTelemetry GenAI الدلالية](https://opentelemetry.io/docs/specs/semconv/gen-ai/) بالإضافة إلى سمات خاصة بـ CrewAI.
|
||||
|
||||
<Tip>
|
||||
تُعدّ OpenTelemetry **مسار المراقبة الموصى به** — محايدة تجاه الموردين، وتعمل مع أي خلفية متوافقة مع OTLP (Grafana, Honeycomb, NewRelic، أو مجمّعك الخاص). إذا كنت تستخدم Datadog تحديدًا، فراجع دليل [تكامل Datadog](./datadog) المخصص، الذي يغطي كلًا من مسار وكيل Datadog واستيعاب OTLP من Datadog.
|
||||
</Tip>
|
||||
|
||||
## المتطلبات المسبقة
|
||||
|
||||
<CardGroup cols={2}>
|
||||
@@ -41,17 +45,7 @@ mode: "wide"
|
||||
<Frame></Frame>
|
||||
</Tab>
|
||||
<Tab title="Datadog">
|
||||
- **Datadog Site Domain** — مضيف OTLP لموقع Datadog الخاص بك فقط، دون بروتوكول أو مسار. يقوم CrewAI ببناء نقطة نهاية HTTPS OTLP الكاملة نيابةً عنك. استخدم المضيف المطابق لـ [موقع Datadog](https://docs.datadoghq.com/getting_started/site/) الخاص بك:
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — مفتاح واجهة برمجة تطبيقات Datadog الخاص بك. راجع [كيفية إنشاء واحد](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
|
||||
يصدّر تكامل Datadog **التتبعات**.
|
||||
|
||||
<Frame></Frame>
|
||||
لإعداد Datadog، راجع دليل [تكامل Datadog](./datadog) المخصص — فهو يغطي كلًا من مسار وكيل Datadog (الموصى به، أرخص لحجم السجلات الكبير) واستيعاب OTLP من Datadog، مع خطوات تهيئة كاملة للمجمّع.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
280
docs/edge/ar/enterprise/guides/datadog.mdx
Normal file
280
docs/edge/ar/enterprise/guides/datadog.mdx
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: "تكامل Datadog"
|
||||
description: "راقب عمليات نشر CrewAI AMP المُستضافة ذاتيًا في Datadog عبر وكيل Datadog أو استيعاب OTLP من Datadog — يوفر كلا المسارين نفس الواجهات المهيكلة لاستيراد لوحة معلومات العمليات الجاهزة."
|
||||
icon: "dog"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**الترجمة قيد التقدم** — يتم عرض المحتوى باللغة الإنجليزية.
|
||||
</Note>
|
||||
|
||||
CrewAI ships first-class support for Datadog: two log-ingestion paths, a JSON log schema designed for cheap indexing, and a ready-made operations dashboard you can import in under five minutes.
|
||||
|
||||
<Note>
|
||||
For vendor-neutral observability via any OTLP backend (Grafana, Honeycomb, your own collector), see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Note>
|
||||
|
||||
## Choose a path
|
||||
|
||||
CrewAI supports two log-ingestion paths to Datadog — both are first-class and produce the same structured facets that power the dashboard. Pick the one that fits your infrastructure.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
The Datadog Agent runs alongside your CrewAI containers (typically as a DaemonSet on Kubernetes) and tails their stdout. Each log event ships as a single billable line with structured attributes — see the [log schema reference](#log-schema-reference) for the full field contract.
|
||||
|
||||
**Setup:**
|
||||
1. Run the Datadog Agent next to your CrewAI containers — see [Datadog's deployment docs](https://docs.datadoghq.com/agent/) for Kubernetes, ECS, or VM setup. Enable log collection (`logs_enabled: true`) and container log collection (`logs_config.container_collect_all: true`).
|
||||
2. Confirm logs arrive in Datadog Logs with the JSON fields parsed — see [Verify ingestion](#verify-ingestion).
|
||||
|
||||
**Pick this path if** you already operate Datadog Agents (e.g. for infrastructure metrics), or your log volume makes per-event ingestion cost a real concern — collapsing tracebacks into single events keeps Agent ingestion cheap at scale.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
CrewAI AMP exports OpenTelemetry traffic directly to Datadog's OTLP endpoint with no Agent required. Logs and traces ride a single export pipeline configured in AMP's UI, using the same protocol you'd use for any other OTLP backend.
|
||||
|
||||
**Setup:**
|
||||
1. In CrewAI AMP, go to **Settings → OpenTelemetry Collectors → Add Collector** and pick **Datadog**.
|
||||
2. Configure the connection:
|
||||
- **Datadog Site Domain** — your Datadog site's OTLP host only, no protocol or path. CrewAI builds the full HTTPS OTLP endpoint for you. Use the host that matches your [Datadog site](https://docs.datadoghq.com/getting_started/site/):
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — your Datadog API key. See [how to create one](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
3. The Datadog template provisions **both signals at once** — when you save, AMP creates a traces collector at `/v1/traces` and a logs collector at `/v1/logs`, both sharing the same Datadog OTLP host and API key. You'll see them as two separate rows in your OTel collectors list.
|
||||
4. *(optional)* Click **Test Connection** to verify CrewAI can reach the endpoint with the credentials you provided. Then click **Save** — both collectors are created in one step.
|
||||
|
||||
<Frame></Frame>
|
||||
|
||||
**Pick this path if** you'd rather not operate a Datadog Agent, you already use OTLP for traces and want one export pipeline, or you may later want to fan out the same telemetry to other backends (Grafana, Honeycomb, etc.) without changing your application setup.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Either path lands the same structured facets in Datadog (`@automation_id`, `@kickoff_id`, `@execution_id`, `@automation_name`, `@crewai_version`, `@exception.type`, `@gen_ai.*`), so the dashboard works identically with either choice.
|
||||
|
||||
## Log schema reference
|
||||
|
||||
<Info>
|
||||
This schema applies to the **Datadog Agent path** — structured stdout JSON logs emitted by every CrewAI worker container. Logs delivered via the **Datadog OTLP intake** use OpenTelemetry attribute names and may differ; see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Info>
|
||||
|
||||
Every log event is emitted as a **single JSON object per line** to stdout, with internal newlines escaped. The format is plain JSON — Datadog parses it natively, and the same payload is also consumable by Splunk, Loki, Elasticsearch, and CloudWatch without custom log pipelines.
|
||||
|
||||
### Why JSON output
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Lower ingestion cost" icon="dollar-sign">
|
||||
Most managed log backends bill per event. A Python traceback in text format is counted as one event per line — 30+ events for a single error. JSON output collapses each traceback into a single event with the stack trace as an escaped string field.
|
||||
</Card>
|
||||
<Card title="Structured search" icon="magnifying-glass">
|
||||
Search by `@automation_id`, `@exception.type`, `@kickoff_id` instead of grepping free-text. Build dashboards on typed facets without parser configuration.
|
||||
</Card>
|
||||
<Card title="APM ↔ logs correlation" icon="link">
|
||||
Every event carries `trace_id` and `span_id` when fired inside a recording span, so backends auto-link logs to traces.
|
||||
</Card>
|
||||
<Card title="Stable contract" icon="file-shield">
|
||||
The `schema` field gates compatibility — within `v1`, fields are added but never renamed or removed.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Example events
|
||||
|
||||
A single info-level log inside an active automation kickoff:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:23.482914Z",
|
||||
"level": "INFO",
|
||||
"logger": "crewai_enterprise.utilities.pii_redaction",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "PII tracking state reset (engines preserved)",
|
||||
"automation_id": "12",
|
||||
"task_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow"
|
||||
}
|
||||
```
|
||||
|
||||
An error with a Python exception is collapsed into a single event with the traceback as a string:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:31.218450Z",
|
||||
"level": "ERROR",
|
||||
"logger": "api.tasks.flow_run_task",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "Flow execution failed",
|
||||
"automation_id": "12",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow",
|
||||
"exception": {
|
||||
"type": "ValueError",
|
||||
"message": "Topic cannot be empty",
|
||||
"stacktrace": "Traceback (most recent call last):\n File \"/app/flow.py\", line 42, in summarize\n ...\nValueError: Topic cannot be empty\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without JSON output, that same error would produce ~25 separate log events (one per traceback line) — all of which the backend would bill and index individually.
|
||||
|
||||
### Schema v1 fields
|
||||
|
||||
Within the `v1` schema, fields are only added, never renamed or removed. New fields will appear as soon as a deployment is upgraded.
|
||||
|
||||
| Field | Type | Always present | Source |
|
||||
|-------|------|----------------|--------|
|
||||
| `schema` | string | Yes | Constant `"v1"`. Increment indicates a breaking schema change. |
|
||||
| `ts` | string (ISO-8601 UTC, microseconds) | Yes | Record creation time, e.g. `2026-06-17T16:14:23.482914Z`. |
|
||||
| `level` | string | Yes | Python log level name: `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`. |
|
||||
| `logger` | string | Yes | Dotted logger name, e.g. `api.tasks.flow_run_task`. |
|
||||
| `crewai_version` | string | Yes (when `crewai` package metadata is resolvable) | Installed `crewai` package version, e.g. `"1.14.7"`. |
|
||||
| `msg` | string | Yes | Rendered log message (after `%`-formatting / `{}`-formatting). |
|
||||
| `automation_id` | string | When `CREWAI_PLUS_ID` env var is set | Numeric deployment ID (AMP provisions this on every container). |
|
||||
| `task_id` | string | On Celery worker logs | Celery task UUID, or `"no-task"` for non-task contexts. |
|
||||
| `kickoff_id` | string | Inside an automation kickoff | UUID of the current kickoff. |
|
||||
| `execution_id` | string | Inside an automation kickoff | UUID of the current sub-execution. Equal to `kickoff_id` at the top level; differs for nested flow methods that spawn sub-executions. |
|
||||
| `automation_name` | string | Inside an automation kickoff | Human-readable automation/flow name, e.g. `"research_flow"`. |
|
||||
| `trace_id` | string (32-hex) | Inside a recording OpenTelemetry span | Hex trace ID. Omitted when no span is active. |
|
||||
| `span_id` | string (16-hex) | Inside a recording OpenTelemetry span | Hex span ID. Omitted when no span is active. |
|
||||
| `exception` | object | When the log record has `exc_info` | `{type, message, stacktrace}` — full traceback as a single escaped string. |
|
||||
|
||||
<Tip>
|
||||
Any additional `extra={...}` kwargs passed to a logger call appear as top-level JSON fields verbatim. Reserved field names above always win to keep the schema stable.
|
||||
</Tip>
|
||||
|
||||
### Stability promise
|
||||
|
||||
The `schema` field declares the contract. Within `v1`, CrewAI commits to:
|
||||
|
||||
- **Never removing a field** that customers may have built queries or dashboards against.
|
||||
- **Never renaming a field** in place — renames happen via a schema bump (e.g. `v2`), with the old name kept as a deprecated alias for at least one release cycle.
|
||||
- **Adding new fields** at any time. Consumers should ignore unknown top-level keys.
|
||||
|
||||
When a `v2` is introduced, both the `schema` field and the migration guide will be published in advance, and `v1` will continue to be emitted for one release cycle so dashboards and queries have time to migrate.
|
||||
|
||||
## Prerequisite: promote facets
|
||||
|
||||
Datadog auto-discovers fields the first time it sees them but doesn't make them queryable in widgets until they're promoted to **facets**. This is a one-time setup in your Datadog account.
|
||||
|
||||
<Steps>
|
||||
<Step title="Search for a CrewAI log">
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and search `service:crewai*`. You should see at least one log event.
|
||||
</Step>
|
||||
<Step title="Promote each field">
|
||||
Click any log entry to open the right-hand details panel. For each field below, hover the field name → click the gear icon → **Create facet**.
|
||||
|
||||
- `automation_id`, `automation_name`, `execution_id`, `kickoff_id`, `task_id`
|
||||
- `crewai_version`, `model_id`
|
||||
- `exception.type`, `exception.message`
|
||||
|
||||
Skip any field that already shows a star icon next to its name — that means it's already a facet. The `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and `gen_ai.request.model` facets are typically promoted automatically by Datadog's LLM Observability auto-discovery, but verify they exist before importing the dashboard.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Import the dashboard
|
||||
|
||||
<Steps>
|
||||
<Step title="Download the dashboard JSON">
|
||||
Save [`datadog_dashboard.json`](https://raw.githubusercontent.com/crewAIInc/crewAI/main/docs/edge/en/enterprise/guides/datadog_dashboard.json) to your machine.
|
||||
</Step>
|
||||
<Step title="Open the import dialog in Datadog">
|
||||
Navigate to **Dashboards → New Dashboard**. Click the **gear icon** in the top right of the empty dashboard and select **Import Dashboard JSON**.
|
||||
</Step>
|
||||
<Step title="Paste or upload the JSON">
|
||||
Paste the contents of `datadog_dashboard.json` into the import dialog (or drag the file in). Click **Import**.
|
||||
|
||||
Datadog creates the dashboard immediately and lands you on it. The first load may show empty widgets for a few seconds while queries execute against the time range.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
Datadog's [Dashboard API](https://docs.datadoghq.com/api/latest/dashboards/#create-a-new-dashboard) accepts the same JSON via `POST /api/v1/dashboard`. Use it if you manage dashboards through Terraform, Pulumi, or CI.
|
||||
</Tip>
|
||||
|
||||
## What you get
|
||||
|
||||
The dashboard is organized into four sections plus a placeholder for a custom drill-down widget:
|
||||
|
||||
| Section | Widgets | Useful for |
|
||||
|---------|---------|------------|
|
||||
| **Header** | Total Executions · Error Rate (%) · Active Automations · CrewAI Versions in Use | At-a-glance health for the last hour. Error Rate is conditionally formatted (green ≤ 5%, yellow ≤ 10%, red > 10%). |
|
||||
| **Throughput** | Executions per Hour by Automation (top 10, stacked bars) | Spotting traffic shifts, surfacing busy automations, validating that a rollout didn't change baseline volume. |
|
||||
| **Errors** | Errors by Exception Type (top 5, stacked bars) · Top Exception Types by Count (toplist) | Triaging failures — which exception types are spiking, which automations they're hitting. |
|
||||
| **Cost** | Total Tokens per Hour by Model (input + output, stacked area) | Tracking LLM token spend by model. Useful for catching cost regressions when an automation switches model or starts looping. |
|
||||
| **Drill-Down** | _(empty placeholder)_ | See [Customization](#customize) for adding a recent-errors log stream here. |
|
||||
|
||||
Three template variables at the top of the dashboard re-scope every widget at once:
|
||||
|
||||
- **`$automation`** — filter to a single automation by name.
|
||||
- **`$version`** — filter to a single `crewai` SDK version (useful for comparing pre- and post-upgrade behavior).
|
||||
- **`$service`** — filter to a specific Datadog `service` tag (useful when multiple CrewAI deployments share one Datadog account).
|
||||
|
||||
## Verify ingestion
|
||||
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and run a query that matches your ingestion path:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
Search `service:crewai* @schema:v1`. You should see structured logs with the JSON fields parsed into Datadog facets. Pick a recent event and verify it has `@automation_id`, `@kickoff_id`, `@execution_id`, `@crewai_version`, and (when running inside a span) `@trace_id` / `@span_id` populated.
|
||||
|
||||
If nothing appears, confirm the Datadog Agent is tailing container stdout and that the deployment is running a recent enough CrewAI Enterprise build.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
Search `source:otlp service:crewai*`. OTLP attributes land with their OpenTelemetry names (`automation_id`, `crewai.kickoff.id`, etc.) rather than the stdout JSON keys, but they map to the same dashboard facets after [facet promotion](#prerequisite-promote-facets).
|
||||
|
||||
If nothing appears, verify the collector endpoint is correct (`/v1/logs` for logs, `/v1/traces` for traces) and **Test Connection** succeeded when the collector was saved.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Customize
|
||||
|
||||
The dashboard ships with deliberate gaps so you can extend it without uninstalling and re-importing.
|
||||
|
||||
### Add a Recent Errors log stream
|
||||
|
||||
The **Drill-Down** section is intentionally empty. Add a Log Stream widget to it for an inline view of recent failures:
|
||||
|
||||
1. Edit the dashboard and click **+ Add Widgets** inside the Drill-Down group.
|
||||
2. Drag in a **Log Stream** widget.
|
||||
3. Set the filter query to `status:error $automation $version $service`.
|
||||
4. Choose columns: `@timestamp`, `@automation_name`, `@exception.type`, `@exception.message`, `@execution_id`.
|
||||
5. Sort by most recent, limit to 25 entries.
|
||||
|
||||
Clicking any row jumps to Logs Explorer with the same filter pre-applied.
|
||||
|
||||
### Add p95 latency
|
||||
|
||||
Logs don't include execution duration by default. Two ways to add a latency widget:
|
||||
|
||||
- **From APM traces** — if you also export OTLP traces to Datadog, add a Timeseries widget with data source **Traces**, query `service:crewai*`, aggregation `p95 of @duration`. Datadog APM auto-tracks span duration.
|
||||
- **From metric extraction** — extract a `flow.duration_ms` metric from logs via [Datadog's log-to-metric pipeline](https://docs.datadoghq.com/logs/log_configuration/logs_to_metrics/), then chart it like any other metric. Useful if you don't run APM.
|
||||
|
||||
### Re-scope to multiple deployments
|
||||
|
||||
The `$service` template variable defaults to `*` and will catch every CrewAI deployment in your Datadog account. Change the default to a specific service name in **Configure → Template Variables** if you want the dashboard to focus on one deployment by default.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| All widgets show "No data" | Facets aren't promoted | Re-do the [Promote facets](#prerequisite-promote-facets) step. Datadog won't query against an un-promoted field. |
|
||||
| Error Rate widget shows `NaN` | No executions in the time window | Either no traffic, or `@execution_id` isn't faceted. Expand the time range and re-check facets. |
|
||||
| Throughput chart is flat at the same value | Logs aren't reaching Datadog | Search `service:crewai*` in Logs Explorer. If nothing shows, verify the Datadog Agent is running (Agent path) or the OTel collector endpoint is correct (OTLP path). |
|
||||
| `crewai_version` shows fewer values than expected | Some containers predate the structured-logs work | The `crewai_version` field was added alongside JSON output. Older deployments (pre-structured-logs AMP builds) won't emit it. Upgrade those deployments to pick up the field. See the [log schema reference](#log-schema-reference) for the full field contract. |
|
||||
| Template variables don't filter widgets | The widget's filter line doesn't reference the template variable | Edit the widget and confirm the search includes `$automation $version $service`. |
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="OpenTelemetry Export" icon="magnifying-glass-chart" href="./capture_telemetry_logs">
|
||||
Vendor-neutral observability for non-Datadog stacks (Grafana, Honeycomb, your own collector) — or as a Datadog complement when you want to fan out telemetry to multiple backends.
|
||||
</Card>
|
||||
<Card title="Datadog Log Search Syntax" icon="magnifying-glass" href="https://docs.datadoghq.com/logs/explorer/search_syntax/">
|
||||
Reference for customizing widget queries against the structured facets above.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -172,7 +172,7 @@ crewai install
|
||||
## الخطوة 8: تشغيل Flow
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
عند تشغيل هذا الأمر، ستشاهد Flow يعمل:
|
||||
|
||||
@@ -4,6 +4,27 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Jun 18, 2026">
|
||||
## v1.14.8a2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add single agent action to Flow definitions
|
||||
- Validate flow CEL expressions at definition load time
|
||||
|
||||
### Documentation
|
||||
- Add Datadog integration guide with importable operations dashboard
|
||||
- Update snapshot and changelog for v1.14.8a1
|
||||
|
||||
## Contributors
|
||||
|
||||
@joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Jun 18, 2026">
|
||||
## v1.14.8a1
|
||||
|
||||
@@ -43,7 +64,7 @@ mode: "wide"
|
||||
- Implement Flow definition run tools without Python code
|
||||
- Drive human feedback from the flow definition
|
||||
- Wire config and persistence from FlowDefinition into the runtime
|
||||
- Add experimental `crewai run --definition` for flows
|
||||
- Add `crewai run --definition` for declarative flows
|
||||
- Support ZIP deployment fallback and JSON crew project env runs
|
||||
- Introduce JSON first crews
|
||||
|
||||
|
||||
@@ -956,13 +956,13 @@ Once all of the dependencies are installed, you need to activate the virtual env
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
After activating the virtual environment, you can run the flow by executing one of the following commands:
|
||||
After activating the virtual environment, you can run the flow with the CrewAI CLI:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
or
|
||||
You can also run the project script directly:
|
||||
|
||||
```bash
|
||||
uv run kickoff
|
||||
@@ -1160,10 +1160,4 @@ crewai run
|
||||
|
||||
This command automatically detects if your project is a flow (based on the `type = "flow"` setting in your pyproject.toml) and runs it accordingly. This is the recommended way to run flows from the command line.
|
||||
|
||||
For backward compatibility, you can also use:
|
||||
|
||||
```shell
|
||||
crewai flow kickoff
|
||||
```
|
||||
|
||||
However, the `crewai run` command is now the preferred method as it works for both crews and flows.
|
||||
The legacy `crewai flow kickoff` command is deprecated. Use `crewai run` for both crews and flows.
|
||||
|
||||
@@ -39,6 +39,7 @@ The Enterprise Tools Repository includes:
|
||||
- **Error Handling**: Incorporates robust error handling mechanisms to ensure smooth operation.
|
||||
- **Caching Mechanism**: Features intelligent caching to optimize performance and reduce redundant operations.
|
||||
- **Asynchronous Support**: Handles both synchronous and asynchronous tools, enabling non-blocking operations.
|
||||
- **Typed Outputs**: Uses optional Pydantic models to give agents clear JSON fields while direct Python calls still receive the tool's normal return value.
|
||||
|
||||
## Using CrewAI Tools
|
||||
|
||||
@@ -184,6 +185,55 @@ class MyCustomTool(BaseTool):
|
||||
return "Tool's result"
|
||||
```
|
||||
|
||||
### Typed Tool Outputs
|
||||
|
||||
When a tool returns structured data, define a Pydantic output model. This gives the agent field names it can trust, such as `sku`, `quantity`, or `needs_reorder`.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent a JSON string based on the output model.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str
|
||||
quantity: int
|
||||
needs_reorder: bool
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Checks current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
tool = InventoryTool()
|
||||
|
||||
# Direct calls receive the raw Pydantic object.
|
||||
result = tool.run(sku="SKU-123")
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
To send Markdown or another short text format to the agent, override `format_output_for_agent`. Direct calls to `tool.run(...)` still return the normal Python value.
|
||||
|
||||
```python Code
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Checks current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = InventoryResult.model_validate(raw_result)
|
||||
status = "reorder needed" if result.needs_reorder else "stock is healthy"
|
||||
return f"{result.sku}: {result.quantity} units. {status}."
|
||||
```
|
||||
|
||||
If you do not override `format_output_for_agent`, typed outputs are sent to the agent as JSON. Plain string results work as before.
|
||||
|
||||
## Asynchronous Tool Support
|
||||
|
||||
CrewAI supports asynchronous tools, allowing you to implement tools that perform non-blocking operations like network requests, file I/O, or other async operations without blocking the main execution thread.
|
||||
|
||||
87
docs/edge/en/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/en/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: One Card per Step
|
||||
description: "Each step on the Studio canvas is a single card that combines the task and the agent that performs it."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:
|
||||
|
||||
- **The task** — what to do (name, description, expected output, and response format).
|
||||
- **The agent** — who does it (the assigned agent, its model, and its tools).
|
||||
|
||||
An agent isn't an independent participant in your workflow — it's an attribute of the task: *which agent performs this work.* Putting the task and its agent on one card makes that relationship explicit and turns your automation into a single, left-to-right chain of work units that's easier to read at a glance.
|
||||
|
||||
<Frame caption="One card per step: the task with its assigned agent summarized in the footer.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## On the canvas
|
||||
|
||||
Each collapsed card shows:
|
||||
|
||||
- The **task name and description** at the top.
|
||||
- A **footer summarizing the assigned agent** — avatar, name, model, and tools.
|
||||
|
||||
There's no separate agent node and no vertical agent → task edge. Your steps connect directly to one another in the order they run.
|
||||
|
||||
## In the editor
|
||||
|
||||
Open a card to edit it. The expanded view is the same card in a detailed state — not a different screen — organized into two clearly labeled sections.
|
||||
|
||||
<Frame caption="The expanded editor: the task section open, the agent summarized below it.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### The task — what to do
|
||||
|
||||
Open by default, since this is what you usually edit:
|
||||
|
||||
- **Name**
|
||||
- **Description**
|
||||
- **Expected Output**
|
||||
- **Response Format** — surfaced here because it controls exactly what downstream steps (such as routing) read from this step.
|
||||
|
||||
### The agent — who does it
|
||||
|
||||
The assigned agent is shown as a summary — **name, model, and tools inline**. Its deeper configuration is preserved behind two disclosures:
|
||||
|
||||
- **Role, goal & backstory**
|
||||
- **Agent settings** — reasoning, max reasoning attempts, allow delegation, max iterations, and LLM settings.
|
||||
|
||||
<Tip>
|
||||
An agent's full configuration — Role, Goal, Backstory, Model, Tools, LLM Settings, and the complete Agent Settings block — lives behind the **Role, goal & backstory** and **Agent settings** disclosures, organized by how often you edit it.
|
||||
</Tip>
|
||||
|
||||
## Swapping vs. editing the agent
|
||||
|
||||
There are two distinct ways to work with the agent on a card, and they do different things:
|
||||
|
||||
- **Swap** reassigns *which* agent performs this task. Use the **Swap** control to pick a different agent from this project, choose one from your Agent Repository, or create a new agent. This is scoped to the task.
|
||||
- **Editing** the agent — opening **Role, goal & backstory** or **Agent settings** — changes the agent *itself*.
|
||||
|
||||
<Frame caption="Swap changes which agent performs the task.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Agents are reusable and shared.** The same agent can perform more than one task across your project. Editing an agent's role, backstory, or settings updates that agent **everywhere it's used** — not just on the card you opened. If you want a change to apply to only one step, **Swap** in a different agent instead of editing the shared one.
|
||||
</Warning>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/en/enterprise/features/crew-studio" icon="pencil">
|
||||
Build automations with AI assistance and a visual editor.
|
||||
</Card>
|
||||
<Card title="Agent Repositories" href="/en/enterprise/features/agent-repositories" icon="users">
|
||||
Manage and reuse agents across your automations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -9,6 +9,10 @@ CrewAI AMP can export OpenTelemetry **traces** and **logs** from your deployment
|
||||
|
||||
Telemetry data follows the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) plus additional CrewAI-specific attributes.
|
||||
|
||||
<Tip>
|
||||
OpenTelemetry is the **recommended observability path** — vendor-neutral, works with any OTLP-compatible backend (Grafana, Honeycomb, NewRelic, your own collector). If you specifically use Datadog, see the dedicated [Datadog Integration](./datadog) guide which covers both the Datadog Agent path and Datadog's OTLP intake.
|
||||
</Tip>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<CardGroup cols={2}>
|
||||
@@ -41,17 +45,7 @@ Telemetry data follows the [OpenTelemetry GenAI semantic conventions](https://op
|
||||
<Frame></Frame>
|
||||
</Tab>
|
||||
<Tab title="Datadog">
|
||||
- **Datadog Site Domain** — Your Datadog site's OTLP host only, with no protocol or path. CrewAI builds the full HTTPS OTLP endpoint for you. Use the host that matches your [Datadog site](https://docs.datadoghq.com/getting_started/site/):
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — Your Datadog API key. See [how to create one](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
|
||||
The Datadog integration exports **traces**.
|
||||
|
||||
<Frame></Frame>
|
||||
For Datadog setup, see the dedicated [Datadog Integration](./datadog) guide — it covers both the Datadog Agent path (recommended, cheaper for log volume) and Datadog's OTLP intake with full collector configuration steps.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
276
docs/edge/en/enterprise/guides/datadog.mdx
Normal file
276
docs/edge/en/enterprise/guides/datadog.mdx
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
title: "Datadog Integration"
|
||||
description: "Monitor self-hosted CrewAI AMP deployments in Datadog via the Datadog Agent or Datadog's OTLP intake — either path lands the same structured facets so you can import the ready-made operations dashboard."
|
||||
icon: "dog"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
CrewAI ships first-class support for Datadog: two log-ingestion paths, a JSON log schema designed for cheap indexing, and a ready-made operations dashboard you can import in under five minutes.
|
||||
|
||||
<Note>
|
||||
For vendor-neutral observability via any OTLP backend (Grafana, Honeycomb, your own collector), see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Note>
|
||||
|
||||
## Choose a path
|
||||
|
||||
CrewAI supports two log-ingestion paths to Datadog — both are first-class and produce the same structured facets that power the dashboard. Pick the one that fits your infrastructure.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
The Datadog Agent runs alongside your CrewAI containers (typically as a DaemonSet on Kubernetes) and tails their stdout. Each log event ships as a single billable line with structured attributes — see the [log schema reference](#log-schema-reference) for the full field contract.
|
||||
|
||||
**Setup:**
|
||||
1. Run the Datadog Agent next to your CrewAI containers — see [Datadog's deployment docs](https://docs.datadoghq.com/agent/) for Kubernetes, ECS, or VM setup. Enable log collection (`logs_enabled: true`) and container log collection (`logs_config.container_collect_all: true`).
|
||||
2. Confirm logs arrive in Datadog Logs with the JSON fields parsed — see [Verify ingestion](#verify-ingestion).
|
||||
|
||||
**Pick this path if** you already operate Datadog Agents (e.g. for infrastructure metrics), or your log volume makes per-event ingestion cost a real concern — collapsing tracebacks into single events keeps Agent ingestion cheap at scale.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
CrewAI AMP exports OpenTelemetry traffic directly to Datadog's OTLP endpoint with no Agent required. Logs and traces ride a single export pipeline configured in AMP's UI, using the same protocol you'd use for any other OTLP backend.
|
||||
|
||||
**Setup:**
|
||||
1. In CrewAI AMP, go to **Settings → OpenTelemetry Collectors → Add Collector** and pick **Datadog**.
|
||||
2. Configure the connection:
|
||||
- **Datadog Site Domain** — your Datadog site's OTLP host only, no protocol or path. CrewAI builds the full HTTPS OTLP endpoint for you. Use the host that matches your [Datadog site](https://docs.datadoghq.com/getting_started/site/):
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — your Datadog API key. See [how to create one](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
3. The Datadog template provisions **both signals at once** — when you save, AMP creates a traces collector at `/v1/traces` and a logs collector at `/v1/logs`, both sharing the same Datadog OTLP host and API key. You'll see them as two separate rows in your OTel collectors list.
|
||||
4. *(optional)* Click **Test Connection** to verify CrewAI can reach the endpoint with the credentials you provided. Then click **Save** — both collectors are created in one step.
|
||||
|
||||
<Frame></Frame>
|
||||
|
||||
**Pick this path if** you'd rather not operate a Datadog Agent, you already use OTLP for traces and want one export pipeline, or you may later want to fan out the same telemetry to other backends (Grafana, Honeycomb, etc.) without changing your application setup.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Either path lands the same structured facets in Datadog (`@automation_id`, `@kickoff_id`, `@execution_id`, `@automation_name`, `@crewai_version`, `@exception.type`, `@gen_ai.*`), so the dashboard works identically with either choice.
|
||||
|
||||
## Log schema reference
|
||||
|
||||
<Info>
|
||||
This schema applies to the **Datadog Agent path** — structured stdout JSON logs emitted by every CrewAI worker container. Logs delivered via the **Datadog OTLP intake** use OpenTelemetry attribute names and may differ; see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Info>
|
||||
|
||||
Every log event is emitted as a **single JSON object per line** to stdout, with internal newlines escaped. The format is plain JSON — Datadog parses it natively, and the same payload is also consumable by Splunk, Loki, Elasticsearch, and CloudWatch without custom log pipelines.
|
||||
|
||||
### Why JSON output
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Lower ingestion cost" icon="dollar-sign">
|
||||
Most managed log backends bill per event. A Python traceback in text format is counted as one event per line — 30+ events for a single error. JSON output collapses each traceback into a single event with the stack trace as an escaped string field.
|
||||
</Card>
|
||||
<Card title="Structured search" icon="magnifying-glass">
|
||||
Search by `@automation_id`, `@exception.type`, `@kickoff_id` instead of grepping free-text. Build dashboards on typed facets without parser configuration.
|
||||
</Card>
|
||||
<Card title="APM ↔ logs correlation" icon="link">
|
||||
Every event carries `trace_id` and `span_id` when fired inside a recording span, so backends auto-link logs to traces.
|
||||
</Card>
|
||||
<Card title="Stable contract" icon="file-shield">
|
||||
The `schema` field gates compatibility — within `v1`, fields are added but never renamed or removed.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Example events
|
||||
|
||||
A single info-level log inside an active automation kickoff:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:23.482914Z",
|
||||
"level": "INFO",
|
||||
"logger": "crewai_enterprise.utilities.pii_redaction",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "PII tracking state reset (engines preserved)",
|
||||
"automation_id": "12",
|
||||
"task_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow"
|
||||
}
|
||||
```
|
||||
|
||||
An error with a Python exception is collapsed into a single event with the traceback as a string:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:31.218450Z",
|
||||
"level": "ERROR",
|
||||
"logger": "api.tasks.flow_run_task",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "Flow execution failed",
|
||||
"automation_id": "12",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow",
|
||||
"exception": {
|
||||
"type": "ValueError",
|
||||
"message": "Topic cannot be empty",
|
||||
"stacktrace": "Traceback (most recent call last):\n File \"/app/flow.py\", line 42, in summarize\n ...\nValueError: Topic cannot be empty\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without JSON output, that same error would produce ~25 separate log events (one per traceback line) — all of which the backend would bill and index individually.
|
||||
|
||||
### Schema v1 fields
|
||||
|
||||
Within the `v1` schema, fields are only added, never renamed or removed. New fields will appear as soon as a deployment is upgraded.
|
||||
|
||||
| Field | Type | Always present | Source |
|
||||
|-------|------|----------------|--------|
|
||||
| `schema` | string | Yes | Constant `"v1"`. Increment indicates a breaking schema change. |
|
||||
| `ts` | string (ISO-8601 UTC, microseconds) | Yes | Record creation time, e.g. `2026-06-17T16:14:23.482914Z`. |
|
||||
| `level` | string | Yes | Python log level name: `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`. |
|
||||
| `logger` | string | Yes | Dotted logger name, e.g. `api.tasks.flow_run_task`. |
|
||||
| `crewai_version` | string | Yes (when `crewai` package metadata is resolvable) | Installed `crewai` package version, e.g. `"1.14.7"`. |
|
||||
| `msg` | string | Yes | Rendered log message (after `%`-formatting / `{}`-formatting). |
|
||||
| `automation_id` | string | When `CREWAI_PLUS_ID` env var is set | Numeric deployment ID (AMP provisions this on every container). |
|
||||
| `task_id` | string | On Celery worker logs | Celery task UUID, or `"no-task"` for non-task contexts. |
|
||||
| `kickoff_id` | string | Inside an automation kickoff | UUID of the current kickoff. |
|
||||
| `execution_id` | string | Inside an automation kickoff | UUID of the current sub-execution. Equal to `kickoff_id` at the top level; differs for nested flow methods that spawn sub-executions. |
|
||||
| `automation_name` | string | Inside an automation kickoff | Human-readable automation/flow name, e.g. `"research_flow"`. |
|
||||
| `trace_id` | string (32-hex) | Inside a recording OpenTelemetry span | Hex trace ID. Omitted when no span is active. |
|
||||
| `span_id` | string (16-hex) | Inside a recording OpenTelemetry span | Hex span ID. Omitted when no span is active. |
|
||||
| `exception` | object | When the log record has `exc_info` | `{type, message, stacktrace}` — full traceback as a single escaped string. |
|
||||
|
||||
<Tip>
|
||||
Any additional `extra={...}` kwargs passed to a logger call appear as top-level JSON fields verbatim. Reserved field names above always win to keep the schema stable.
|
||||
</Tip>
|
||||
|
||||
### Stability promise
|
||||
|
||||
The `schema` field declares the contract. Within `v1`, CrewAI commits to:
|
||||
|
||||
- **Never removing a field** that customers may have built queries or dashboards against.
|
||||
- **Never renaming a field** in place — renames happen via a schema bump (e.g. `v2`), with the old name kept as a deprecated alias for at least one release cycle.
|
||||
- **Adding new fields** at any time. Consumers should ignore unknown top-level keys.
|
||||
|
||||
When a `v2` is introduced, both the `schema` field and the migration guide will be published in advance, and `v1` will continue to be emitted for one release cycle so dashboards and queries have time to migrate.
|
||||
|
||||
## Prerequisite: promote facets
|
||||
|
||||
Datadog auto-discovers fields the first time it sees them but doesn't make them queryable in widgets until they're promoted to **facets**. This is a one-time setup in your Datadog account.
|
||||
|
||||
<Steps>
|
||||
<Step title="Search for a CrewAI log">
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and search `service:crewai*`. You should see at least one log event.
|
||||
</Step>
|
||||
<Step title="Promote each field">
|
||||
Click any log entry to open the right-hand details panel. For each field below, hover the field name → click the gear icon → **Create facet**.
|
||||
|
||||
- `automation_id`, `automation_name`, `execution_id`, `kickoff_id`, `task_id`
|
||||
- `crewai_version`, `model_id`
|
||||
- `exception.type`, `exception.message`
|
||||
|
||||
Skip any field that already shows a star icon next to its name — that means it's already a facet. The `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and `gen_ai.request.model` facets are typically promoted automatically by Datadog's LLM Observability auto-discovery, but verify they exist before importing the dashboard.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Import the dashboard
|
||||
|
||||
<Steps>
|
||||
<Step title="Download the dashboard JSON">
|
||||
Save [`datadog_dashboard.json`](https://raw.githubusercontent.com/crewAIInc/crewAI/main/docs/edge/en/enterprise/guides/datadog_dashboard.json) to your machine.
|
||||
</Step>
|
||||
<Step title="Open the import dialog in Datadog">
|
||||
Navigate to **Dashboards → New Dashboard**. Click the **gear icon** in the top right of the empty dashboard and select **Import Dashboard JSON**.
|
||||
</Step>
|
||||
<Step title="Paste or upload the JSON">
|
||||
Paste the contents of `datadog_dashboard.json` into the import dialog (or drag the file in). Click **Import**.
|
||||
|
||||
Datadog creates the dashboard immediately and lands you on it. The first load may show empty widgets for a few seconds while queries execute against the time range.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
Datadog's [Dashboard API](https://docs.datadoghq.com/api/latest/dashboards/#create-a-new-dashboard) accepts the same JSON via `POST /api/v1/dashboard`. Use it if you manage dashboards through Terraform, Pulumi, or CI.
|
||||
</Tip>
|
||||
|
||||
## What you get
|
||||
|
||||
The dashboard is organized into four sections plus a placeholder for a custom drill-down widget:
|
||||
|
||||
| Section | Widgets | Useful for |
|
||||
|---------|---------|------------|
|
||||
| **Header** | Total Executions · Error Rate (%) · Active Automations · CrewAI Versions in Use | At-a-glance health for the last hour. Error Rate is conditionally formatted (green ≤ 5%, yellow ≤ 10%, red > 10%). |
|
||||
| **Throughput** | Executions per Hour by Automation (top 10, stacked bars) | Spotting traffic shifts, surfacing busy automations, validating that a rollout didn't change baseline volume. |
|
||||
| **Errors** | Errors by Exception Type (top 5, stacked bars) · Top Exception Types by Count (toplist) | Triaging failures — which exception types are spiking, which automations they're hitting. |
|
||||
| **Cost** | Total Tokens per Hour by Model (input + output, stacked area) | Tracking LLM token spend by model. Useful for catching cost regressions when an automation switches model or starts looping. |
|
||||
| **Drill-Down** | _(empty placeholder)_ | See [Customization](#customize) for adding a recent-errors log stream here. |
|
||||
|
||||
Three template variables at the top of the dashboard re-scope every widget at once:
|
||||
|
||||
- **`$automation`** — filter to a single automation by name.
|
||||
- **`$version`** — filter to a single `crewai` SDK version (useful for comparing pre- and post-upgrade behavior).
|
||||
- **`$service`** — filter to a specific Datadog `service` tag (useful when multiple CrewAI deployments share one Datadog account).
|
||||
|
||||
## Verify ingestion
|
||||
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and run a query that matches your ingestion path:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
Search `service:crewai* @schema:v1`. You should see structured logs with the JSON fields parsed into Datadog facets. Pick a recent event and verify it has `@automation_id`, `@kickoff_id`, `@execution_id`, `@crewai_version`, and (when running inside a span) `@trace_id` / `@span_id` populated.
|
||||
|
||||
If nothing appears, confirm the Datadog Agent is tailing container stdout and that the deployment is running a recent enough CrewAI Enterprise build.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
Search `source:otlp service:crewai*`. OTLP attributes land with their OpenTelemetry names (`automation_id`, `crewai.kickoff.id`, etc.) rather than the stdout JSON keys, but they map to the same dashboard facets after [facet promotion](#prerequisite-promote-facets).
|
||||
|
||||
If nothing appears, verify the collector endpoint is correct (`/v1/logs` for logs, `/v1/traces` for traces) and **Test Connection** succeeded when the collector was saved.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Customize
|
||||
|
||||
The dashboard ships with deliberate gaps so you can extend it without uninstalling and re-importing.
|
||||
|
||||
### Add a Recent Errors log stream
|
||||
|
||||
The **Drill-Down** section is intentionally empty. Add a Log Stream widget to it for an inline view of recent failures:
|
||||
|
||||
1. Edit the dashboard and click **+ Add Widgets** inside the Drill-Down group.
|
||||
2. Drag in a **Log Stream** widget.
|
||||
3. Set the filter query to `status:error $automation $version $service`.
|
||||
4. Choose columns: `@timestamp`, `@automation_name`, `@exception.type`, `@exception.message`, `@execution_id`.
|
||||
5. Sort by most recent, limit to 25 entries.
|
||||
|
||||
Clicking any row jumps to Logs Explorer with the same filter pre-applied.
|
||||
|
||||
### Add p95 latency
|
||||
|
||||
Logs don't include execution duration by default. Two ways to add a latency widget:
|
||||
|
||||
- **From APM traces** — if you also export OTLP traces to Datadog, add a Timeseries widget with data source **Traces**, query `service:crewai*`, aggregation `p95 of @duration`. Datadog APM auto-tracks span duration.
|
||||
- **From metric extraction** — extract a `flow.duration_ms` metric from logs via [Datadog's log-to-metric pipeline](https://docs.datadoghq.com/logs/log_configuration/logs_to_metrics/), then chart it like any other metric. Useful if you don't run APM.
|
||||
|
||||
### Re-scope to multiple deployments
|
||||
|
||||
The `$service` template variable defaults to `*` and will catch every CrewAI deployment in your Datadog account. Change the default to a specific service name in **Configure → Template Variables** if you want the dashboard to focus on one deployment by default.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| All widgets show "No data" | Facets aren't promoted | Re-do the [Promote facets](#prerequisite-promote-facets) step. Datadog won't query against an un-promoted field. |
|
||||
| Error Rate widget shows `NaN` | No executions in the time window | Either no traffic, or `@execution_id` isn't faceted. Expand the time range and re-check facets. |
|
||||
| Throughput chart is flat at the same value | Logs aren't reaching Datadog | Search `service:crewai*` in Logs Explorer. If nothing shows, verify the Datadog Agent is running (Agent path) or the OTel collector endpoint is correct (OTLP path). |
|
||||
| `crewai_version` shows fewer values than expected | Some containers predate the structured-logs work | The `crewai_version` field was added alongside JSON output. Older deployments (pre-structured-logs AMP builds) won't emit it. Upgrade those deployments to pick up the field. See the [log schema reference](#log-schema-reference) for the full field contract. |
|
||||
| Template variables don't filter widgets | The widget's filter line doesn't reference the template variable | Edit the widget and confirm the search includes `$automation $version $service`. |
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="OpenTelemetry Export" icon="magnifying-glass-chart" href="./capture_telemetry_logs">
|
||||
Vendor-neutral observability for non-Datadog stacks (Grafana, Honeycomb, your own collector) — or as a Datadog complement when you want to fan out telemetry to multiple backends.
|
||||
</Card>
|
||||
<Card title="Datadog Log Search Syntax" icon="magnifying-glass" href="https://docs.datadoghq.com/logs/explorer/search_syntax/">
|
||||
Reference for customizing widget queries against the structured facets above.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
582
docs/edge/en/enterprise/guides/datadog_dashboard.json
Normal file
582
docs/edge/en/enterprise/guides/datadog_dashboard.json
Normal file
@@ -0,0 +1,582 @@
|
||||
{
|
||||
"title": "crewAI -- Operations",
|
||||
"description": "Monitoring dashboard for self-hosted crewAI deployments running structured JSON logs. Tracks executions, errors, token usage, and automation health.",
|
||||
"widgets": [
|
||||
{
|
||||
"id": 8810001,
|
||||
"definition": {
|
||||
"title": "Header",
|
||||
"background_color": "vivid_blue",
|
||||
"show_title": true,
|
||||
"type": "group",
|
||||
"layout_type": "ordered",
|
||||
"widgets": [
|
||||
{
|
||||
"id": 9910001,
|
||||
"definition": {
|
||||
"title": "Total Executions",
|
||||
"time": {
|
||||
"live_span": "1h"
|
||||
},
|
||||
"type": "query_value",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "scalar",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "cardinality",
|
||||
"metric": "@execution_id"
|
||||
},
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"autoscale": true,
|
||||
"precision": 0
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 3,
|
||||
"height": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9910002,
|
||||
"definition": {
|
||||
"title": "Error Rate (%)",
|
||||
"time": {
|
||||
"live_span": "1h"
|
||||
},
|
||||
"type": "query_value",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "scalar",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "status:error $automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "count"
|
||||
},
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query2",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "cardinality",
|
||||
"metric": "@execution_id"
|
||||
},
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1 / query2 * 100"
|
||||
}
|
||||
],
|
||||
"conditional_formats": [
|
||||
{
|
||||
"comparator": ">",
|
||||
"value": 10,
|
||||
"palette": "white_on_red"
|
||||
},
|
||||
{
|
||||
"comparator": ">",
|
||||
"value": 5,
|
||||
"palette": "white_on_yellow"
|
||||
},
|
||||
{
|
||||
"comparator": ">=",
|
||||
"value": 0,
|
||||
"palette": "white_on_green"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"autoscale": false,
|
||||
"custom_unit": "%",
|
||||
"precision": 2
|
||||
},
|
||||
"layout": {
|
||||
"x": 3,
|
||||
"y": 0,
|
||||
"width": 3,
|
||||
"height": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9910003,
|
||||
"definition": {
|
||||
"title": "Active Automations",
|
||||
"time": {
|
||||
"live_span": "1h"
|
||||
},
|
||||
"type": "query_value",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "scalar",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "cardinality",
|
||||
"metric": "@automation_id"
|
||||
},
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"autoscale": true,
|
||||
"precision": 0
|
||||
},
|
||||
"layout": {
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"width": 3,
|
||||
"height": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9910004,
|
||||
"definition": {
|
||||
"title": "CrewAI Versions in Use",
|
||||
"time": {
|
||||
"live_span": "1h"
|
||||
},
|
||||
"type": "query_value",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "scalar",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "cardinality",
|
||||
"metric": "@crewai_version"
|
||||
},
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"autoscale": true,
|
||||
"precision": 0
|
||||
},
|
||||
"layout": {
|
||||
"x": 9,
|
||||
"y": 0,
|
||||
"width": 3,
|
||||
"height": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8820001,
|
||||
"definition": {
|
||||
"title": "Throughput",
|
||||
"background_color": "vivid_green",
|
||||
"show_title": true,
|
||||
"type": "group",
|
||||
"layout_type": "ordered",
|
||||
"widgets": [
|
||||
{
|
||||
"id": 9920001,
|
||||
"definition": {
|
||||
"title": "Executions per Hour by Automation (top 10)",
|
||||
"show_legend": false,
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "cardinality",
|
||||
"metric": "@execution_id",
|
||||
"interval": 3600000
|
||||
},
|
||||
"group_by": [
|
||||
{
|
||||
"facet": "@automation_name",
|
||||
"limit": 10,
|
||||
"sort": {
|
||||
"aggregation": "cardinality",
|
||||
"metric": "@execution_id",
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1"
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"palette": "semantic"
|
||||
},
|
||||
"display_type": "bars"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"width": 12,
|
||||
"height": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8830001,
|
||||
"definition": {
|
||||
"title": "Errors",
|
||||
"background_color": "vivid_orange",
|
||||
"show_title": true,
|
||||
"type": "group",
|
||||
"layout_type": "ordered",
|
||||
"widgets": [
|
||||
{
|
||||
"id": 9930001,
|
||||
"definition": {
|
||||
"title": "Errors by Exception Type (top 5)",
|
||||
"show_legend": false,
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "status:error $automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "count"
|
||||
},
|
||||
"group_by": [
|
||||
{
|
||||
"facet": "@exception.type",
|
||||
"limit": 5,
|
||||
"sort": {
|
||||
"aggregation": "count",
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1"
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"palette": "warm"
|
||||
},
|
||||
"display_type": "bars"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9930002,
|
||||
"definition": {
|
||||
"title": "Top Exception Types by Count",
|
||||
"type": "toplist",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "scalar",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "status:error $automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "count"
|
||||
},
|
||||
"group_by": [
|
||||
{
|
||||
"facet": "@exception.type",
|
||||
"limit": 10,
|
||||
"sort": {
|
||||
"aggregation": "count",
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1"
|
||||
}
|
||||
],
|
||||
"sort": {
|
||||
"count": 10,
|
||||
"order_by": [
|
||||
{
|
||||
"type": "formula",
|
||||
"index": 0,
|
||||
"order": "desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"palette": "datadog16"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 7,
|
||||
"width": 12,
|
||||
"height": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8840001,
|
||||
"definition": {
|
||||
"title": "Cost",
|
||||
"background_color": "vivid_purple",
|
||||
"show_title": true,
|
||||
"type": "group",
|
||||
"layout_type": "ordered",
|
||||
"widgets": [
|
||||
{
|
||||
"id": 9940001,
|
||||
"definition": {
|
||||
"title": "Total Tokens per Hour by Model (input + output)",
|
||||
"show_legend": false,
|
||||
"type": "timeseries",
|
||||
"requests": [
|
||||
{
|
||||
"response_format": "timeseries",
|
||||
"queries": [
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query1",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "sum",
|
||||
"metric": "@gen_ai.usage.input_tokens",
|
||||
"interval": 3600000
|
||||
},
|
||||
"group_by": [
|
||||
{
|
||||
"facet": "@gen_ai.request.model",
|
||||
"limit": 10,
|
||||
"sort": {
|
||||
"aggregation": "sum",
|
||||
"metric": "@gen_ai.usage.input_tokens",
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data_source": "logs",
|
||||
"name": "query2",
|
||||
"search": {
|
||||
"query": "$automation $version $service"
|
||||
},
|
||||
"compute": {
|
||||
"aggregation": "sum",
|
||||
"metric": "@gen_ai.usage.output_tokens",
|
||||
"interval": 3600000
|
||||
},
|
||||
"group_by": [
|
||||
{
|
||||
"facet": "@gen_ai.request.model",
|
||||
"limit": 10,
|
||||
"sort": {
|
||||
"aggregation": "sum",
|
||||
"metric": "@gen_ai.usage.output_tokens",
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formulas": [
|
||||
{
|
||||
"formula": "query1 + query2",
|
||||
"alias": "Total Tokens"
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"palette": "cool"
|
||||
},
|
||||
"display_type": "area"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 12,
|
||||
"height": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 11,
|
||||
"width": 12,
|
||||
"height": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8850002,
|
||||
"definition": {
|
||||
"title": "Drill-Down",
|
||||
"background_color": "gray",
|
||||
"show_title": true,
|
||||
"type": "group",
|
||||
"layout_type": "ordered",
|
||||
"widgets": []
|
||||
},
|
||||
"layout": {
|
||||
"x": 0,
|
||||
"y": 15,
|
||||
"width": 12,
|
||||
"height": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"template_variables": [
|
||||
{
|
||||
"name": "automation",
|
||||
"prefix": "@automation_name",
|
||||
"available_values": [],
|
||||
"default": "*"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"prefix": "@crewai_version",
|
||||
"available_values": [],
|
||||
"default": "*"
|
||||
},
|
||||
{
|
||||
"name": "service",
|
||||
"prefix": "service",
|
||||
"available_values": [],
|
||||
"default": "*"
|
||||
}
|
||||
],
|
||||
"layout_type": "ordered",
|
||||
"notify_list": [],
|
||||
"pause_auto_refresh": false,
|
||||
"reflow_type": "fixed",
|
||||
"tags": [
|
||||
"ai:created_with_ai"
|
||||
]
|
||||
}
|
||||
@@ -395,7 +395,7 @@ crewai install
|
||||
Now it's time to see your flow in action! Run it using the CrewAI CLI:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
When you run this command, you'll see your flow spring to life:
|
||||
|
||||
@@ -65,7 +65,7 @@ Regardless of which approach you use, your tool must:
|
||||
- Have a **`description`** — tells the agent when and how to use the tool. This directly affects how well agents use your tool, so be clear and specific.
|
||||
- Implement **`_run`** (BaseTool) or provide a **function body** (@tool) — the synchronous execution logic.
|
||||
- Use **type annotations** on all parameters and return values.
|
||||
- Return a **string** result (or something that can be meaningfully converted to one).
|
||||
- Return a **string** result, or define an optional Pydantic output schema for structured results.
|
||||
|
||||
### Optional: Async Support
|
||||
|
||||
@@ -104,6 +104,67 @@ class TranslateInput(BaseModel):
|
||||
|
||||
Explicit schemas are recommended for published tools — they produce better agent behavior and clearer documentation for your users.
|
||||
|
||||
### Optional: Typed Outputs with `result_schema`
|
||||
|
||||
If your tool returns structured data, define a Pydantic output model. This is a good default for published tools because users and agents can rely on named fields.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent JSON based on the output model.
|
||||
|
||||
CrewAI can infer the output schema from a Pydantic return annotation:
|
||||
|
||||
```python
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GeolocateResult(BaseModel):
|
||||
latitude: float = Field(..., description="Latitude in decimal degrees.")
|
||||
longitude: float = Field(..., description="Longitude in decimal degrees.")
|
||||
|
||||
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
|
||||
def _run(self, address: str) -> GeolocateResult:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
|
||||
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
|
||||
```
|
||||
|
||||
Set `result_schema` explicitly when your tool returns a dictionary:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
result_schema: type[BaseModel] = GeolocateResult
|
||||
|
||||
def _run(self, address: str) -> dict[str, float]:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return {"latitude": 38.8977, "longitude": -77.0365}
|
||||
return {"latitude": 40.7128, "longitude": -74.0060}
|
||||
```
|
||||
|
||||
If agents should receive a short text summary instead of JSON, override `format_output_for_agent` on your `BaseTool` subclass.
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
|
||||
def _run(self, address: str) -> GeolocateResult:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
|
||||
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = GeolocateResult.model_validate(raw_result)
|
||||
return f"Latitude {result.latitude}, longitude {result.longitude}"
|
||||
```
|
||||
|
||||
The override only changes what the agent sees. Direct users of your package still receive the normal value from `tool.run(...)`.
|
||||
|
||||
### Optional: Environment Variables
|
||||
|
||||
If your tool requires API keys or other configuration, declare them with `env_vars` so users know what to set:
|
||||
@@ -241,4 +302,4 @@ agent = Agent(
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
@@ -53,6 +53,111 @@ def my_simple_tool(question: str) -> str:
|
||||
return "Tool output"
|
||||
```
|
||||
|
||||
### Best Practice: Define Typed Outputs
|
||||
|
||||
When a tool returns structured data, define a Pydantic output model. This helps the agent read the result as clear fields instead of guessing from plain text.
|
||||
|
||||
Typed outputs are useful for results with stable fields, such as IDs, status values, scores, prices, or lists. Plain strings are still fine for short prose results.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses a typed tool, CrewAI sends the agent JSON based on the output model.
|
||||
|
||||
#### Return a Pydantic Model
|
||||
|
||||
CrewAI infers the output schema when your `BaseTool` has a Pydantic return annotation.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
quantity: int = Field(description="Units available.")
|
||||
needs_reorder: bool = Field(description="Whether the item should be reordered.")
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Check current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
tool = InventoryTool()
|
||||
result = tool.run(sku="SKU-123")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
When an agent calls `InventoryTool`, it receives JSON like this:
|
||||
|
||||
```json
|
||||
{"sku":"SKU-123","quantity":14,"needs_reorder":false}
|
||||
```
|
||||
|
||||
#### Use `result_schema` with Dictionary Results
|
||||
|
||||
If your tool returns a dictionary, set `result_schema` explicitly. You can do this on a `BaseTool` subclass or with the `@tool` decorator:
|
||||
|
||||
```python Code
|
||||
from crewai.tools import tool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ProductResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
name: str = Field(description="The product name.")
|
||||
in_stock: bool = Field(description="Whether the product is available.")
|
||||
|
||||
@tool("Product Lookup", result_schema=ProductResult)
|
||||
def product_lookup(sku: str) -> dict[str, object]:
|
||||
"""Look up product availability by SKU."""
|
||||
catalog = {
|
||||
"SKU-123": ("Noise-canceling headset", True),
|
||||
"SKU-456": ("USB-C dock", False),
|
||||
}
|
||||
name, in_stock = catalog.get(sku, ("Unknown product", False))
|
||||
return {
|
||||
"sku": sku,
|
||||
"name": name,
|
||||
"in_stock": in_stock,
|
||||
}
|
||||
```
|
||||
|
||||
#### Customize the Text Sent to the Agent
|
||||
|
||||
By default, typed tool outputs are sent to the agent as JSON. If the agent should receive a short summary instead, subclass `BaseTool` and override `format_output_for_agent`.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
quantity: int = Field(description="Units available.")
|
||||
needs_reorder: bool = Field(description="Whether the item should be reordered.")
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Check current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = InventoryResult.model_validate(raw_result)
|
||||
status = "reorder needed" if result.needs_reorder else "stock is healthy"
|
||||
return f"{result.sku}: {result.quantity} units. {status}."
|
||||
|
||||
tool = InventoryTool()
|
||||
result = tool.run(sku="SKU-123")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
The override only changes what the agent sees. Direct calls to `tool.run(...)` still return the normal Python value.
|
||||
|
||||
### Defining a Cache Function for the Tool
|
||||
|
||||
To optimize tool performance with caching, define custom caching strategies using the `cache_function` attribute.
|
||||
|
||||
@@ -195,9 +195,12 @@ class ToolCallHookContext:
|
||||
agent: Agent | None # Agent executing
|
||||
task: Task | None # Current task
|
||||
crew: Crew | None # Crew instance
|
||||
tool_result: str | None # Tool result (after hooks)
|
||||
tool_result: str | None # Agent-facing result string (after hooks)
|
||||
raw_tool_result: Any | None # Raw Python result (after hooks)
|
||||
```
|
||||
|
||||
For typed tool outputs, `tool_result` is the string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. `raw_tool_result` is the original Python value returned by the tool.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Safety and Validation
|
||||
|
||||
@@ -60,9 +60,12 @@ class ToolCallHookContext:
|
||||
agent: Agent | BaseAgent | None # Agent executing the tool
|
||||
task: Task | None # Current task
|
||||
crew: Crew | None # Crew instance
|
||||
tool_result: str | None # Tool result (after hooks only)
|
||||
tool_result: str | None # Agent-facing result string (after hooks only)
|
||||
raw_tool_result: Any | None # Raw Python result (after hooks only)
|
||||
```
|
||||
|
||||
For typed tool outputs, `tool_result` is the string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. Use `raw_tool_result` when your hook needs the typed object or dictionary.
|
||||
|
||||
### Modifying Tool Inputs
|
||||
|
||||
**Important:** Always modify tool inputs in-place:
|
||||
|
||||
@@ -4,6 +4,27 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 6월 18일">
|
||||
## v1.14.8a2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Flow 정의에 단일 에이전트 작업 추가
|
||||
- 정의 로드 시 흐름 CEL 표현식 검증
|
||||
|
||||
### 문서
|
||||
- 가져올 수 있는 운영 대시보드와 함께 Datadog 통합 가이드 추가
|
||||
- v1.14.8a1의 스냅샷 및 변경 로그 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 6월 18일">
|
||||
## v1.14.8a1
|
||||
|
||||
@@ -43,7 +64,7 @@ mode: "wide"
|
||||
- Python 코드 없이 Flow 정의 실행 도구 구현
|
||||
- Flow 정의에서 인간 피드백 유도
|
||||
- FlowDefinition의 구성 및 지속성을 런타임에 연결
|
||||
- 흐름을 위한 실험적 `crewai run --definition` 추가
|
||||
- 선언적 흐름을 위한 `crewai run --definition` 추가
|
||||
- ZIP 배포 대체 및 JSON 크루 프로젝트 환경 실행 지원
|
||||
- JSON 우선 크루 도입
|
||||
|
||||
|
||||
@@ -951,7 +951,7 @@ source .venv/bin/activate
|
||||
가상 환경을 활성화한 후, 아래 명령어 중 하나를 실행하여 플로우를 실행할 수 있습니다:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
또는
|
||||
@@ -1054,10 +1054,4 @@ crewai run
|
||||
|
||||
이 명령어는 프로젝트가 pyproject.toml의 `type = "flow"` 설정을 기반으로 flow인지 자동으로 감지하여 해당 방식으로 실행합니다. 명령줄에서 flow를 실행하는 권장 방법입니다.
|
||||
|
||||
하위 호환성을 위해 다음 명령어도 사용할 수 있습니다:
|
||||
|
||||
```shell
|
||||
crewai flow kickoff
|
||||
```
|
||||
|
||||
하지만 `crewai run` 명령어가 이제 crew와 flow 모두에 작동하므로 더욱 선호되는 방법입니다.
|
||||
레거시 `crewai flow kickoff` 명령어는 deprecated되었습니다. crew와 flow 모두 `crewai run`을 사용하세요.
|
||||
|
||||
87
docs/edge/ko/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/ko/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 단계당 하나의 카드
|
||||
description: "Studio 캔버스의 각 단계는 작업과 이를 수행하는 에이전트를 하나로 결합한 단일 카드입니다."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
|
||||
</Note>
|
||||
|
||||
## 개요
|
||||
|
||||
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:
|
||||
|
||||
- **작업(Task)** — 무엇을 할지(이름, 설명, 예상 출력, 응답 형식).
|
||||
- **에이전트(Agent)** — 누가 수행하는지(할당된 에이전트, 모델, 도구).
|
||||
|
||||
에이전트는 워크플로의 독립적인 참여자가 아니라 작업의 속성, 즉 *이 작업을 어떤 에이전트가 수행하는지*를 나타냅니다. 작업과 에이전트를 하나의 카드에 두면 이 관계가 명확해지고, 자동화가 왼쪽에서 오른쪽으로 이어지는 단일 작업 단위 체인이 되어 한눈에 읽기 쉬워집니다.
|
||||
|
||||
<Frame caption="단계당 하나의 카드: 작업과 푸터에 요약된 할당 에이전트.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## 캔버스에서
|
||||
|
||||
접힌 각 카드는 다음을 표시합니다:
|
||||
|
||||
- 상단의 **작업 이름과 설명**.
|
||||
- **할당된 에이전트를 요약한 푸터** — 아바타, 이름, 모델, 도구.
|
||||
|
||||
별도의 에이전트 노드나 에이전트 → 작업 세로 연결선이 없습니다. 각 단계는 실행 순서대로 서로 직접 연결됩니다.
|
||||
|
||||
## 에디터에서
|
||||
|
||||
카드를 열어 편집합니다. 확장된 보기는 다른 화면이 아니라 동일한 카드의 상세 상태이며, 명확하게 구분된 두 개의 섹션으로 구성됩니다.
|
||||
|
||||
<Frame caption="확장된 에디터: 작업 섹션이 열려 있고 그 아래에 에이전트가 요약되어 있습니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### 작업 — 무엇을 할지
|
||||
|
||||
가장 자주 편집하는 항목이므로 기본적으로 열려 있습니다:
|
||||
|
||||
- **이름**
|
||||
- **설명**
|
||||
- **예상 출력**
|
||||
- **응답 형식** — 다운스트림 단계(예: 라우팅)가 이 단계에서 무엇을 읽을지 정확히 제어하므로 여기에 표시됩니다.
|
||||
|
||||
### 에이전트 — 누가 수행하는지
|
||||
|
||||
할당된 에이전트는 요약으로 표시됩니다 — **이름, 모델, 도구가 인라인으로** 표시됩니다. 더 깊은 구성은 두 개의 접이식 섹션 뒤에 보존됩니다:
|
||||
|
||||
- **역할, 목표 및 배경 스토리**
|
||||
- **에이전트 설정** — 추론, 최대 추론 시도 횟수, 위임 허용, 최대 반복 횟수, LLM 설정.
|
||||
|
||||
<Tip>
|
||||
에이전트의 전체 구성 — 역할, 목표, 배경 스토리, 모델, 도구, LLM 설정 및 전체 에이전트 설정 블록 — 은 **역할, 목표 및 배경 스토리**와 **에이전트 설정** 접이식 섹션 뒤에 편집 빈도에 따라 정리되어 있습니다.
|
||||
</Tip>
|
||||
|
||||
## 에이전트 교체 vs. 편집
|
||||
|
||||
카드에서 에이전트를 다루는 방식은 두 가지로 구분되며, 각각 다른 작업을 수행합니다:
|
||||
|
||||
- **교체(Swap)** 는 *어떤* 에이전트가 이 작업을 수행할지 재할당합니다. **교체** 컨트롤을 사용하여 이 프로젝트의 다른 에이전트를 선택하거나, 에이전트 저장소에서 선택하거나, 새 에이전트를 만들 수 있습니다. 이는 작업 범위로 한정됩니다.
|
||||
- 에이전트 **편집** — **역할, 목표 및 배경 스토리** 또는 **에이전트 설정** 을 여는 것 — 은 에이전트 *자체*를 변경합니다.
|
||||
|
||||
<Frame caption="교체는 작업을 수행할 에이전트를 변경합니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**에이전트는 재사용 가능하며 공유됩니다.** 동일한 에이전트가 프로젝트 전반에서 둘 이상의 작업을 수행할 수 있습니다. 에이전트의 역할, 배경 스토리 또는 설정을 편집하면 열어 본 카드뿐만 아니라 **해당 에이전트가 사용되는 모든 곳**에서 업데이트됩니다. 변경 사항을 하나의 단계에만 적용하려면 공유 에이전트를 편집하지 말고 다른 에이전트로 **교체**하세요.
|
||||
</Warning>
|
||||
|
||||
## 관련 항목
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ko/enterprise/features/crew-studio" icon="pencil">
|
||||
AI 지원과 비주얼 에디터로 자동화를 구축합니다.
|
||||
</Card>
|
||||
<Card title="에이전트 저장소" href="/ko/enterprise/features/agent-repositories" icon="users">
|
||||
자동화 전반에서 에이전트를 관리하고 재사용합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -9,6 +9,10 @@ CrewAI AMP는 배포에서 OpenTelemetry **트레이스**와 **로그**를 자
|
||||
|
||||
텔레메트리 데이터는 [OpenTelemetry GenAI 시맨틱 규칙](https://opentelemetry.io/docs/specs/semconv/gen-ai/)과 추가적인 CrewAI 전용 속성을 따릅니다.
|
||||
|
||||
<Tip>
|
||||
OpenTelemetry는 **권장되는 관측 가능성 경로**입니다 — 벤더 중립적이며, OTLP 호환 백엔드(Grafana, Honeycomb, NewRelic, 자체 수집기)에서 작동합니다. Datadog을 사용하는 경우, Datadog Agent 경로와 Datadog의 OTLP 수집을 모두 다루는 전용 [Datadog 통합](./datadog) 가이드를 참조하세요.
|
||||
</Tip>
|
||||
|
||||
## 사전 요구 사항
|
||||
|
||||
<CardGroup cols={2}>
|
||||
@@ -41,17 +45,7 @@ CrewAI AMP는 배포에서 OpenTelemetry **트레이스**와 **로그**를 자
|
||||
<Frame></Frame>
|
||||
</Tab>
|
||||
<Tab title="Datadog">
|
||||
- **Datadog Site Domain** — Datadog 사이트의 OTLP 호스트만 입력합니다 (프로토콜이나 경로 제외). CrewAI가 전체 HTTPS OTLP 엔드포인트를 자동으로 구성합니다. [Datadog 사이트](https://docs.datadoghq.com/getting_started/site/)에 맞는 호스트를 사용하세요:
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — Datadog API 키입니다. [키 생성 방법](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys)을 참고하세요.
|
||||
|
||||
Datadog 통합은 **트레이스**를 내보냅니다.
|
||||
|
||||
<Frame></Frame>
|
||||
Datadog 설정은 전용 [Datadog 통합](./datadog) 가이드를 참조하세요 — Datadog Agent 경로(권장, 로그 볼륨에 더 저렴)와 Datadog의 OTLP 수집을 모두 다루며, 수집기 구성 단계를 완전히 설명합니다.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
280
docs/edge/ko/enterprise/guides/datadog.mdx
Normal file
280
docs/edge/ko/enterprise/guides/datadog.mdx
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: "Datadog 통합"
|
||||
description: "Datadog Agent 또는 Datadog의 OTLP 수집을 통해 자체 호스팅 CrewAI AMP 배포를 Datadog에서 모니터링하세요 — 두 경로 모두 동일한 구조화된 패싯을 생성하므로 기성 운영 대시보드를 가져올 수 있습니다."
|
||||
icon: "dog"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**번역 진행 중** — 콘텐츠가 영어로 표시됩니다.
|
||||
</Note>
|
||||
|
||||
CrewAI ships first-class support for Datadog: two log-ingestion paths, a JSON log schema designed for cheap indexing, and a ready-made operations dashboard you can import in under five minutes.
|
||||
|
||||
<Note>
|
||||
For vendor-neutral observability via any OTLP backend (Grafana, Honeycomb, your own collector), see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Note>
|
||||
|
||||
## Choose a path
|
||||
|
||||
CrewAI supports two log-ingestion paths to Datadog — both are first-class and produce the same structured facets that power the dashboard. Pick the one that fits your infrastructure.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
The Datadog Agent runs alongside your CrewAI containers (typically as a DaemonSet on Kubernetes) and tails their stdout. Each log event ships as a single billable line with structured attributes — see the [log schema reference](#log-schema-reference) for the full field contract.
|
||||
|
||||
**Setup:**
|
||||
1. Run the Datadog Agent next to your CrewAI containers — see [Datadog's deployment docs](https://docs.datadoghq.com/agent/) for Kubernetes, ECS, or VM setup. Enable log collection (`logs_enabled: true`) and container log collection (`logs_config.container_collect_all: true`).
|
||||
2. Confirm logs arrive in Datadog Logs with the JSON fields parsed — see [Verify ingestion](#verify-ingestion).
|
||||
|
||||
**Pick this path if** you already operate Datadog Agents (e.g. for infrastructure metrics), or your log volume makes per-event ingestion cost a real concern — collapsing tracebacks into single events keeps Agent ingestion cheap at scale.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
CrewAI AMP exports OpenTelemetry traffic directly to Datadog's OTLP endpoint with no Agent required. Logs and traces ride a single export pipeline configured in AMP's UI, using the same protocol you'd use for any other OTLP backend.
|
||||
|
||||
**Setup:**
|
||||
1. In CrewAI AMP, go to **Settings → OpenTelemetry Collectors → Add Collector** and pick **Datadog**.
|
||||
2. Configure the connection:
|
||||
- **Datadog Site Domain** — your Datadog site's OTLP host only, no protocol or path. CrewAI builds the full HTTPS OTLP endpoint for you. Use the host that matches your [Datadog site](https://docs.datadoghq.com/getting_started/site/):
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — your Datadog API key. See [how to create one](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
3. The Datadog template provisions **both signals at once** — when you save, AMP creates a traces collector at `/v1/traces` and a logs collector at `/v1/logs`, both sharing the same Datadog OTLP host and API key. You'll see them as two separate rows in your OTel collectors list.
|
||||
4. *(optional)* Click **Test Connection** to verify CrewAI can reach the endpoint with the credentials you provided. Then click **Save** — both collectors are created in one step.
|
||||
|
||||
<Frame></Frame>
|
||||
|
||||
**Pick this path if** you'd rather not operate a Datadog Agent, you already use OTLP for traces and want one export pipeline, or you may later want to fan out the same telemetry to other backends (Grafana, Honeycomb, etc.) without changing your application setup.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Either path lands the same structured facets in Datadog (`@automation_id`, `@kickoff_id`, `@execution_id`, `@automation_name`, `@crewai_version`, `@exception.type`, `@gen_ai.*`), so the dashboard works identically with either choice.
|
||||
|
||||
## Log schema reference
|
||||
|
||||
<Info>
|
||||
This schema applies to the **Datadog Agent path** — structured stdout JSON logs emitted by every CrewAI worker container. Logs delivered via the **Datadog OTLP intake** use OpenTelemetry attribute names and may differ; see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Info>
|
||||
|
||||
Every log event is emitted as a **single JSON object per line** to stdout, with internal newlines escaped. The format is plain JSON — Datadog parses it natively, and the same payload is also consumable by Splunk, Loki, Elasticsearch, and CloudWatch without custom log pipelines.
|
||||
|
||||
### Why JSON output
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Lower ingestion cost" icon="dollar-sign">
|
||||
Most managed log backends bill per event. A Python traceback in text format is counted as one event per line — 30+ events for a single error. JSON output collapses each traceback into a single event with the stack trace as an escaped string field.
|
||||
</Card>
|
||||
<Card title="Structured search" icon="magnifying-glass">
|
||||
Search by `@automation_id`, `@exception.type`, `@kickoff_id` instead of grepping free-text. Build dashboards on typed facets without parser configuration.
|
||||
</Card>
|
||||
<Card title="APM ↔ logs correlation" icon="link">
|
||||
Every event carries `trace_id` and `span_id` when fired inside a recording span, so backends auto-link logs to traces.
|
||||
</Card>
|
||||
<Card title="Stable contract" icon="file-shield">
|
||||
The `schema` field gates compatibility — within `v1`, fields are added but never renamed or removed.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Example events
|
||||
|
||||
A single info-level log inside an active automation kickoff:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:23.482914Z",
|
||||
"level": "INFO",
|
||||
"logger": "crewai_enterprise.utilities.pii_redaction",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "PII tracking state reset (engines preserved)",
|
||||
"automation_id": "12",
|
||||
"task_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow"
|
||||
}
|
||||
```
|
||||
|
||||
An error with a Python exception is collapsed into a single event with the traceback as a string:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:31.218450Z",
|
||||
"level": "ERROR",
|
||||
"logger": "api.tasks.flow_run_task",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "Flow execution failed",
|
||||
"automation_id": "12",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow",
|
||||
"exception": {
|
||||
"type": "ValueError",
|
||||
"message": "Topic cannot be empty",
|
||||
"stacktrace": "Traceback (most recent call last):\n File \"/app/flow.py\", line 42, in summarize\n ...\nValueError: Topic cannot be empty\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without JSON output, that same error would produce ~25 separate log events (one per traceback line) — all of which the backend would bill and index individually.
|
||||
|
||||
### Schema v1 fields
|
||||
|
||||
Within the `v1` schema, fields are only added, never renamed or removed. New fields will appear as soon as a deployment is upgraded.
|
||||
|
||||
| Field | Type | Always present | Source |
|
||||
|-------|------|----------------|--------|
|
||||
| `schema` | string | Yes | Constant `"v1"`. Increment indicates a breaking schema change. |
|
||||
| `ts` | string (ISO-8601 UTC, microseconds) | Yes | Record creation time, e.g. `2026-06-17T16:14:23.482914Z`. |
|
||||
| `level` | string | Yes | Python log level name: `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`. |
|
||||
| `logger` | string | Yes | Dotted logger name, e.g. `api.tasks.flow_run_task`. |
|
||||
| `crewai_version` | string | Yes (when `crewai` package metadata is resolvable) | Installed `crewai` package version, e.g. `"1.14.7"`. |
|
||||
| `msg` | string | Yes | Rendered log message (after `%`-formatting / `{}`-formatting). |
|
||||
| `automation_id` | string | When `CREWAI_PLUS_ID` env var is set | Numeric deployment ID (AMP provisions this on every container). |
|
||||
| `task_id` | string | On Celery worker logs | Celery task UUID, or `"no-task"` for non-task contexts. |
|
||||
| `kickoff_id` | string | Inside an automation kickoff | UUID of the current kickoff. |
|
||||
| `execution_id` | string | Inside an automation kickoff | UUID of the current sub-execution. Equal to `kickoff_id` at the top level; differs for nested flow methods that spawn sub-executions. |
|
||||
| `automation_name` | string | Inside an automation kickoff | Human-readable automation/flow name, e.g. `"research_flow"`. |
|
||||
| `trace_id` | string (32-hex) | Inside a recording OpenTelemetry span | Hex trace ID. Omitted when no span is active. |
|
||||
| `span_id` | string (16-hex) | Inside a recording OpenTelemetry span | Hex span ID. Omitted when no span is active. |
|
||||
| `exception` | object | When the log record has `exc_info` | `{type, message, stacktrace}` — full traceback as a single escaped string. |
|
||||
|
||||
<Tip>
|
||||
Any additional `extra={...}` kwargs passed to a logger call appear as top-level JSON fields verbatim. Reserved field names above always win to keep the schema stable.
|
||||
</Tip>
|
||||
|
||||
### Stability promise
|
||||
|
||||
The `schema` field declares the contract. Within `v1`, CrewAI commits to:
|
||||
|
||||
- **Never removing a field** that customers may have built queries or dashboards against.
|
||||
- **Never renaming a field** in place — renames happen via a schema bump (e.g. `v2`), with the old name kept as a deprecated alias for at least one release cycle.
|
||||
- **Adding new fields** at any time. Consumers should ignore unknown top-level keys.
|
||||
|
||||
When a `v2` is introduced, both the `schema` field and the migration guide will be published in advance, and `v1` will continue to be emitted for one release cycle so dashboards and queries have time to migrate.
|
||||
|
||||
## Prerequisite: promote facets
|
||||
|
||||
Datadog auto-discovers fields the first time it sees them but doesn't make them queryable in widgets until they're promoted to **facets**. This is a one-time setup in your Datadog account.
|
||||
|
||||
<Steps>
|
||||
<Step title="Search for a CrewAI log">
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and search `service:crewai*`. You should see at least one log event.
|
||||
</Step>
|
||||
<Step title="Promote each field">
|
||||
Click any log entry to open the right-hand details panel. For each field below, hover the field name → click the gear icon → **Create facet**.
|
||||
|
||||
- `automation_id`, `automation_name`, `execution_id`, `kickoff_id`, `task_id`
|
||||
- `crewai_version`, `model_id`
|
||||
- `exception.type`, `exception.message`
|
||||
|
||||
Skip any field that already shows a star icon next to its name — that means it's already a facet. The `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and `gen_ai.request.model` facets are typically promoted automatically by Datadog's LLM Observability auto-discovery, but verify they exist before importing the dashboard.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Import the dashboard
|
||||
|
||||
<Steps>
|
||||
<Step title="Download the dashboard JSON">
|
||||
Save [`datadog_dashboard.json`](https://raw.githubusercontent.com/crewAIInc/crewAI/main/docs/edge/en/enterprise/guides/datadog_dashboard.json) to your machine.
|
||||
</Step>
|
||||
<Step title="Open the import dialog in Datadog">
|
||||
Navigate to **Dashboards → New Dashboard**. Click the **gear icon** in the top right of the empty dashboard and select **Import Dashboard JSON**.
|
||||
</Step>
|
||||
<Step title="Paste or upload the JSON">
|
||||
Paste the contents of `datadog_dashboard.json` into the import dialog (or drag the file in). Click **Import**.
|
||||
|
||||
Datadog creates the dashboard immediately and lands you on it. The first load may show empty widgets for a few seconds while queries execute against the time range.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
Datadog's [Dashboard API](https://docs.datadoghq.com/api/latest/dashboards/#create-a-new-dashboard) accepts the same JSON via `POST /api/v1/dashboard`. Use it if you manage dashboards through Terraform, Pulumi, or CI.
|
||||
</Tip>
|
||||
|
||||
## What you get
|
||||
|
||||
The dashboard is organized into four sections plus a placeholder for a custom drill-down widget:
|
||||
|
||||
| Section | Widgets | Useful for |
|
||||
|---------|---------|------------|
|
||||
| **Header** | Total Executions · Error Rate (%) · Active Automations · CrewAI Versions in Use | At-a-glance health for the last hour. Error Rate is conditionally formatted (green ≤ 5%, yellow ≤ 10%, red > 10%). |
|
||||
| **Throughput** | Executions per Hour by Automation (top 10, stacked bars) | Spotting traffic shifts, surfacing busy automations, validating that a rollout didn't change baseline volume. |
|
||||
| **Errors** | Errors by Exception Type (top 5, stacked bars) · Top Exception Types by Count (toplist) | Triaging failures — which exception types are spiking, which automations they're hitting. |
|
||||
| **Cost** | Total Tokens per Hour by Model (input + output, stacked area) | Tracking LLM token spend by model. Useful for catching cost regressions when an automation switches model or starts looping. |
|
||||
| **Drill-Down** | _(empty placeholder)_ | See [Customization](#customize) for adding a recent-errors log stream here. |
|
||||
|
||||
Three template variables at the top of the dashboard re-scope every widget at once:
|
||||
|
||||
- **`$automation`** — filter to a single automation by name.
|
||||
- **`$version`** — filter to a single `crewai` SDK version (useful for comparing pre- and post-upgrade behavior).
|
||||
- **`$service`** — filter to a specific Datadog `service` tag (useful when multiple CrewAI deployments share one Datadog account).
|
||||
|
||||
## Verify ingestion
|
||||
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and run a query that matches your ingestion path:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
Search `service:crewai* @schema:v1`. You should see structured logs with the JSON fields parsed into Datadog facets. Pick a recent event and verify it has `@automation_id`, `@kickoff_id`, `@execution_id`, `@crewai_version`, and (when running inside a span) `@trace_id` / `@span_id` populated.
|
||||
|
||||
If nothing appears, confirm the Datadog Agent is tailing container stdout and that the deployment is running a recent enough CrewAI Enterprise build.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
Search `source:otlp service:crewai*`. OTLP attributes land with their OpenTelemetry names (`automation_id`, `crewai.kickoff.id`, etc.) rather than the stdout JSON keys, but they map to the same dashboard facets after [facet promotion](#prerequisite-promote-facets).
|
||||
|
||||
If nothing appears, verify the collector endpoint is correct (`/v1/logs` for logs, `/v1/traces` for traces) and **Test Connection** succeeded when the collector was saved.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Customize
|
||||
|
||||
The dashboard ships with deliberate gaps so you can extend it without uninstalling and re-importing.
|
||||
|
||||
### Add a Recent Errors log stream
|
||||
|
||||
The **Drill-Down** section is intentionally empty. Add a Log Stream widget to it for an inline view of recent failures:
|
||||
|
||||
1. Edit the dashboard and click **+ Add Widgets** inside the Drill-Down group.
|
||||
2. Drag in a **Log Stream** widget.
|
||||
3. Set the filter query to `status:error $automation $version $service`.
|
||||
4. Choose columns: `@timestamp`, `@automation_name`, `@exception.type`, `@exception.message`, `@execution_id`.
|
||||
5. Sort by most recent, limit to 25 entries.
|
||||
|
||||
Clicking any row jumps to Logs Explorer with the same filter pre-applied.
|
||||
|
||||
### Add p95 latency
|
||||
|
||||
Logs don't include execution duration by default. Two ways to add a latency widget:
|
||||
|
||||
- **From APM traces** — if you also export OTLP traces to Datadog, add a Timeseries widget with data source **Traces**, query `service:crewai*`, aggregation `p95 of @duration`. Datadog APM auto-tracks span duration.
|
||||
- **From metric extraction** — extract a `flow.duration_ms` metric from logs via [Datadog's log-to-metric pipeline](https://docs.datadoghq.com/logs/log_configuration/logs_to_metrics/), then chart it like any other metric. Useful if you don't run APM.
|
||||
|
||||
### Re-scope to multiple deployments
|
||||
|
||||
The `$service` template variable defaults to `*` and will catch every CrewAI deployment in your Datadog account. Change the default to a specific service name in **Configure → Template Variables** if you want the dashboard to focus on one deployment by default.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| All widgets show "No data" | Facets aren't promoted | Re-do the [Promote facets](#prerequisite-promote-facets) step. Datadog won't query against an un-promoted field. |
|
||||
| Error Rate widget shows `NaN` | No executions in the time window | Either no traffic, or `@execution_id` isn't faceted. Expand the time range and re-check facets. |
|
||||
| Throughput chart is flat at the same value | Logs aren't reaching Datadog | Search `service:crewai*` in Logs Explorer. If nothing shows, verify the Datadog Agent is running (Agent path) or the OTel collector endpoint is correct (OTLP path). |
|
||||
| `crewai_version` shows fewer values than expected | Some containers predate the structured-logs work | The `crewai_version` field was added alongside JSON output. Older deployments (pre-structured-logs AMP builds) won't emit it. Upgrade those deployments to pick up the field. See the [log schema reference](#log-schema-reference) for the full field contract. |
|
||||
| Template variables don't filter widgets | The widget's filter line doesn't reference the template variable | Edit the widget and confirm the search includes `$automation $version $service`. |
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="OpenTelemetry Export" icon="magnifying-glass-chart" href="./capture_telemetry_logs">
|
||||
Vendor-neutral observability for non-Datadog stacks (Grafana, Honeycomb, your own collector) — or as a Datadog complement when you want to fan out telemetry to multiple backends.
|
||||
</Card>
|
||||
<Card title="Datadog Log Search Syntax" icon="magnifying-glass" href="https://docs.datadoghq.com/logs/explorer/search_syntax/">
|
||||
Reference for customizing widget queries against the structured facets above.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -393,7 +393,7 @@ crewai install
|
||||
이제 여러분의 flow가 실제로 작동하는 모습을 볼 차례입니다! CrewAI CLI를 사용하여 flow를 실행하세요:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
이 명령어를 실행하면 flow가 다음과 같이 작동하는 것을 확인할 수 있습니다:
|
||||
|
||||
@@ -4,6 +4,27 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="18 jun 2026">
|
||||
## v1.14.8a2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.8a2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar ação de agente único às definições de Fluxo
|
||||
- Validar expressões CEL de fluxo no momento do carregamento da definição
|
||||
|
||||
### Documentação
|
||||
- Adicionar guia de integração do Datadog com painel de operações importável
|
||||
- Atualizar snapshot e changelog para v1.14.8a1
|
||||
|
||||
## Contributors
|
||||
|
||||
@joaomdmoura, @lucasgomide, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="18 jun 2026">
|
||||
## v1.14.8a1
|
||||
|
||||
@@ -43,7 +64,7 @@ mode: "wide"
|
||||
- Implementar ferramentas de execução de definição de fluxo sem código Python
|
||||
- Conduzir feedback humano a partir da definição de fluxo
|
||||
- Conectar configuração e persistência do FlowDefinition ao tempo de execução
|
||||
- Adicionar `crewai run --definition` experimental para fluxos
|
||||
- Adicionar `crewai run --definition` para fluxos declarativos
|
||||
- Suportar fallback de implantação ZIP e execuções de projeto de equipe em JSON
|
||||
- Introduzir equipes em JSON primeiro
|
||||
|
||||
|
||||
@@ -948,7 +948,7 @@ source .venv/bin/activate
|
||||
Com o ambiente ativado, execute o flow usando um dos comandos:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
ou
|
||||
@@ -1052,10 +1052,4 @@ crewai run
|
||||
|
||||
O comando detecta automaticamente se seu projeto é um flow (com base na configuração `type = "flow"` no pyproject.toml) e executa conforme o esperado. Esse é o método recomendado para executar flows pelo terminal.
|
||||
|
||||
Por compatibilidade retroativa, também é possível usar:
|
||||
|
||||
```shell
|
||||
crewai flow kickoff
|
||||
```
|
||||
|
||||
No entanto, o comando `crewai run` é agora o preferido, pois funciona tanto para crews quanto para flows.
|
||||
O comando legado `crewai flow kickoff` está deprecated. Use `crewai run` para crews e flows.
|
||||
|
||||
87
docs/edge/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
87
docs/edge/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Um Card por Etapa
|
||||
description: "Cada etapa no canvas do Studio é um único card que combina a tarefa e o agente que a executa."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
|
||||
</Note>
|
||||
|
||||
## Visão geral
|
||||
|
||||
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:
|
||||
|
||||
- **A tarefa** — o que fazer (nome, descrição, saída esperada e formato da resposta).
|
||||
- **O agente** — quem faz (o agente atribuído, seu modelo e suas ferramentas).
|
||||
|
||||
Um agente não é um participante independente do seu fluxo de trabalho — ele é um atributo da tarefa: *qual agente executa este trabalho.* Colocar a tarefa e seu agente em um único card torna essa relação explícita e transforma sua automação em uma única cadeia de unidades de trabalho, da esquerda para a direita, mais fácil de ler em uma olhada.
|
||||
|
||||
<Frame caption="Um card por etapa: a tarefa com o agente atribuído resumido no rodapé.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## No canvas
|
||||
|
||||
Cada card recolhido mostra:
|
||||
|
||||
- O **nome e a descrição da tarefa** no topo.
|
||||
- Um **rodapé resumindo o agente atribuído** — avatar, nome, modelo e ferramentas.
|
||||
|
||||
Não há nó de agente separado nem aresta vertical de agente → tarefa. Suas etapas se conectam diretamente umas às outras na ordem em que são executadas.
|
||||
|
||||
## No editor
|
||||
|
||||
Abra um card para editá-lo. A visão expandida é o mesmo card em um estado detalhado — não uma tela diferente — organizada em duas seções claramente identificadas.
|
||||
|
||||
<Frame caption="O editor expandido: a seção da tarefa aberta, com o agente resumido abaixo.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### A tarefa — o que fazer
|
||||
|
||||
Aberta por padrão, já que é o que você costuma editar:
|
||||
|
||||
- **Nome**
|
||||
- **Descrição**
|
||||
- **Saída Esperada**
|
||||
- **Formato da Resposta** — exibido aqui porque controla exatamente o que as etapas seguintes (como o roteamento) leem desta etapa.
|
||||
|
||||
### O agente — quem faz
|
||||
|
||||
O agente atribuído é mostrado como um resumo — **nome, modelo e ferramentas em linha**. Sua configuração mais detalhada é preservada por trás de duas seções recolhíveis:
|
||||
|
||||
- **Papel, objetivo e história**
|
||||
- **Configurações do agente** — raciocínio, máximo de tentativas de raciocínio, permitir delegação, máximo de iterações e configurações de LLM.
|
||||
|
||||
<Tip>
|
||||
A configuração completa de um agente — Papel, Objetivo, História, Modelo, Ferramentas, Configurações de LLM e todo o bloco de Configurações do agente — fica por trás das seções recolhíveis **Papel, objetivo e história** e **Configurações do agente**, organizada pela frequência com que você a edita.
|
||||
</Tip>
|
||||
|
||||
## Trocar vs. editar o agente
|
||||
|
||||
Há duas maneiras distintas de trabalhar com o agente em um card, e elas fazem coisas diferentes:
|
||||
|
||||
- **Trocar** reatribui *qual* agente executa esta tarefa. Use o controle **Trocar** para escolher um agente diferente deste projeto, selecionar um do seu Repositório de Agentes ou criar um novo agente. Isso tem escopo limitado à tarefa.
|
||||
- **Editar** o agente — abrindo **Papel, objetivo e história** ou **Configurações do agente** — altera o agente *em si*.
|
||||
|
||||
<Frame caption="Trocar muda qual agente executa a tarefa.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Os agentes são reutilizáveis e compartilhados.** O mesmo agente pode executar mais de uma tarefa em todo o seu projeto. Editar o papel, a história ou as configurações de um agente atualiza esse agente **em todos os lugares onde ele é usado** — não apenas no card que você abriu. Se quiser que uma alteração se aplique a apenas uma etapa, **Troque** por um agente diferente em vez de editar o agente compartilhado.
|
||||
</Warning>
|
||||
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/pt-BR/enterprise/features/crew-studio" icon="pencil">
|
||||
Crie automações com assistência de IA e um editor visual.
|
||||
</Card>
|
||||
<Card title="Repositórios de Agentes" href="/pt-BR/enterprise/features/agent-repositories" icon="users">
|
||||
Gerencie e reutilize agentes em suas automações.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -9,6 +9,10 @@ O CrewAI AMP pode exportar **traces** e **logs** do OpenTelemetry das suas impla
|
||||
|
||||
Os dados de telemetria seguem as [convenções semânticas GenAI do OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/gen-ai/) além de atributos adicionais específicos do CrewAI.
|
||||
|
||||
<Tip>
|
||||
OpenTelemetry é o **caminho de observabilidade recomendado** — neutro em relação a fornecedores, funciona com qualquer backend compatível com OTLP (Grafana, Honeycomb, NewRelic, seu próprio coletor). Se você usa especificamente o Datadog, veja o guia dedicado [Integração com Datadog](./datadog), que cobre tanto o caminho do Datadog Agent quanto o ingest OTLP do Datadog.
|
||||
</Tip>
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
@@ -41,17 +45,7 @@ Os dados de telemetria seguem as [convenções semânticas GenAI do OpenTelemetr
|
||||
<Frame></Frame>
|
||||
</Tab>
|
||||
<Tab title="Datadog">
|
||||
- **Datadog Site Domain** — Apenas o host OTLP do seu site Datadog, sem protocolo ou caminho. O CrewAI monta o endpoint HTTPS OTLP completo para você. Use o host correspondente ao seu [site Datadog](https://docs.datadoghq.com/getting_started/site/):
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — Sua chave de API do Datadog. Veja [como criar uma](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
|
||||
A integração com o Datadog exporta **traces**.
|
||||
|
||||
<Frame></Frame>
|
||||
Para configurar o Datadog, veja o guia dedicado [Integração com Datadog](./datadog) — ele cobre tanto o caminho do Datadog Agent (recomendado, mais barato para volumes altos de log) quanto o ingest OTLP do Datadog, com os passos completos de configuração do coletor.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
280
docs/edge/pt-BR/enterprise/guides/datadog.mdx
Normal file
280
docs/edge/pt-BR/enterprise/guides/datadog.mdx
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: "Integração com Datadog"
|
||||
description: "Monitore implantações CrewAI AMP auto-hospedadas no Datadog via Datadog Agent ou ingest OTLP do Datadog — ambos os caminhos entregam as mesmas facetas estruturadas para importar o dashboard de operações pronto."
|
||||
icon: "dog"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
**Tradução em andamento** — conteúdo exibido em inglês.
|
||||
</Note>
|
||||
|
||||
CrewAI ships first-class support for Datadog: two log-ingestion paths, a JSON log schema designed for cheap indexing, and a ready-made operations dashboard you can import in under five minutes.
|
||||
|
||||
<Note>
|
||||
For vendor-neutral observability via any OTLP backend (Grafana, Honeycomb, your own collector), see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Note>
|
||||
|
||||
## Choose a path
|
||||
|
||||
CrewAI supports two log-ingestion paths to Datadog — both are first-class and produce the same structured facets that power the dashboard. Pick the one that fits your infrastructure.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
The Datadog Agent runs alongside your CrewAI containers (typically as a DaemonSet on Kubernetes) and tails their stdout. Each log event ships as a single billable line with structured attributes — see the [log schema reference](#log-schema-reference) for the full field contract.
|
||||
|
||||
**Setup:**
|
||||
1. Run the Datadog Agent next to your CrewAI containers — see [Datadog's deployment docs](https://docs.datadoghq.com/agent/) for Kubernetes, ECS, or VM setup. Enable log collection (`logs_enabled: true`) and container log collection (`logs_config.container_collect_all: true`).
|
||||
2. Confirm logs arrive in Datadog Logs with the JSON fields parsed — see [Verify ingestion](#verify-ingestion).
|
||||
|
||||
**Pick this path if** you already operate Datadog Agents (e.g. for infrastructure metrics), or your log volume makes per-event ingestion cost a real concern — collapsing tracebacks into single events keeps Agent ingestion cheap at scale.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
CrewAI AMP exports OpenTelemetry traffic directly to Datadog's OTLP endpoint with no Agent required. Logs and traces ride a single export pipeline configured in AMP's UI, using the same protocol you'd use for any other OTLP backend.
|
||||
|
||||
**Setup:**
|
||||
1. In CrewAI AMP, go to **Settings → OpenTelemetry Collectors → Add Collector** and pick **Datadog**.
|
||||
2. Configure the connection:
|
||||
- **Datadog Site Domain** — your Datadog site's OTLP host only, no protocol or path. CrewAI builds the full HTTPS OTLP endpoint for you. Use the host that matches your [Datadog site](https://docs.datadoghq.com/getting_started/site/):
|
||||
- `otlp.datadoghq.com` (US1)
|
||||
- `otlp.us3.datadoghq.com` (US3)
|
||||
- `otlp.us5.datadoghq.com` (US5)
|
||||
- `otlp.datadoghq.eu` (EU1)
|
||||
- `otlp.ap1.datadoghq.com` (AP1)
|
||||
- **API Key** — your Datadog API key. See [how to create one](https://docs.datadoghq.com/account_management/api-app-keys/#api-keys).
|
||||
3. The Datadog template provisions **both signals at once** — when you save, AMP creates a traces collector at `/v1/traces` and a logs collector at `/v1/logs`, both sharing the same Datadog OTLP host and API key. You'll see them as two separate rows in your OTel collectors list.
|
||||
4. *(optional)* Click **Test Connection** to verify CrewAI can reach the endpoint with the credentials you provided. Then click **Save** — both collectors are created in one step.
|
||||
|
||||
<Frame></Frame>
|
||||
|
||||
**Pick this path if** you'd rather not operate a Datadog Agent, you already use OTLP for traces and want one export pipeline, or you may later want to fan out the same telemetry to other backends (Grafana, Honeycomb, etc.) without changing your application setup.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Either path lands the same structured facets in Datadog (`@automation_id`, `@kickoff_id`, `@execution_id`, `@automation_name`, `@crewai_version`, `@exception.type`, `@gen_ai.*`), so the dashboard works identically with either choice.
|
||||
|
||||
## Log schema reference
|
||||
|
||||
<Info>
|
||||
This schema applies to the **Datadog Agent path** — structured stdout JSON logs emitted by every CrewAI worker container. Logs delivered via the **Datadog OTLP intake** use OpenTelemetry attribute names and may differ; see [OpenTelemetry Export](./capture_telemetry_logs).
|
||||
</Info>
|
||||
|
||||
Every log event is emitted as a **single JSON object per line** to stdout, with internal newlines escaped. The format is plain JSON — Datadog parses it natively, and the same payload is also consumable by Splunk, Loki, Elasticsearch, and CloudWatch without custom log pipelines.
|
||||
|
||||
### Why JSON output
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Lower ingestion cost" icon="dollar-sign">
|
||||
Most managed log backends bill per event. A Python traceback in text format is counted as one event per line — 30+ events for a single error. JSON output collapses each traceback into a single event with the stack trace as an escaped string field.
|
||||
</Card>
|
||||
<Card title="Structured search" icon="magnifying-glass">
|
||||
Search by `@automation_id`, `@exception.type`, `@kickoff_id` instead of grepping free-text. Build dashboards on typed facets without parser configuration.
|
||||
</Card>
|
||||
<Card title="APM ↔ logs correlation" icon="link">
|
||||
Every event carries `trace_id` and `span_id` when fired inside a recording span, so backends auto-link logs to traces.
|
||||
</Card>
|
||||
<Card title="Stable contract" icon="file-shield">
|
||||
The `schema` field gates compatibility — within `v1`, fields are added but never renamed or removed.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Example events
|
||||
|
||||
A single info-level log inside an active automation kickoff:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:23.482914Z",
|
||||
"level": "INFO",
|
||||
"logger": "crewai_enterprise.utilities.pii_redaction",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "PII tracking state reset (engines preserved)",
|
||||
"automation_id": "12",
|
||||
"task_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow"
|
||||
}
|
||||
```
|
||||
|
||||
An error with a Python exception is collapsed into a single event with the traceback as a string:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "v1",
|
||||
"ts": "2026-06-17T16:14:31.218450Z",
|
||||
"level": "ERROR",
|
||||
"logger": "api.tasks.flow_run_task",
|
||||
"crewai_version": "1.14.7",
|
||||
"msg": "Flow execution failed",
|
||||
"automation_id": "12",
|
||||
"kickoff_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"execution_id": "0843a930-b306-464b-89c8-bfafa78cc711",
|
||||
"automation_name": "research_flow",
|
||||
"exception": {
|
||||
"type": "ValueError",
|
||||
"message": "Topic cannot be empty",
|
||||
"stacktrace": "Traceback (most recent call last):\n File \"/app/flow.py\", line 42, in summarize\n ...\nValueError: Topic cannot be empty\n"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without JSON output, that same error would produce ~25 separate log events (one per traceback line) — all of which the backend would bill and index individually.
|
||||
|
||||
### Schema v1 fields
|
||||
|
||||
Within the `v1` schema, fields are only added, never renamed or removed. New fields will appear as soon as a deployment is upgraded.
|
||||
|
||||
| Field | Type | Always present | Source |
|
||||
|-------|------|----------------|--------|
|
||||
| `schema` | string | Yes | Constant `"v1"`. Increment indicates a breaking schema change. |
|
||||
| `ts` | string (ISO-8601 UTC, microseconds) | Yes | Record creation time, e.g. `2026-06-17T16:14:23.482914Z`. |
|
||||
| `level` | string | Yes | Python log level name: `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`. |
|
||||
| `logger` | string | Yes | Dotted logger name, e.g. `api.tasks.flow_run_task`. |
|
||||
| `crewai_version` | string | Yes (when `crewai` package metadata is resolvable) | Installed `crewai` package version, e.g. `"1.14.7"`. |
|
||||
| `msg` | string | Yes | Rendered log message (after `%`-formatting / `{}`-formatting). |
|
||||
| `automation_id` | string | When `CREWAI_PLUS_ID` env var is set | Numeric deployment ID (AMP provisions this on every container). |
|
||||
| `task_id` | string | On Celery worker logs | Celery task UUID, or `"no-task"` for non-task contexts. |
|
||||
| `kickoff_id` | string | Inside an automation kickoff | UUID of the current kickoff. |
|
||||
| `execution_id` | string | Inside an automation kickoff | UUID of the current sub-execution. Equal to `kickoff_id` at the top level; differs for nested flow methods that spawn sub-executions. |
|
||||
| `automation_name` | string | Inside an automation kickoff | Human-readable automation/flow name, e.g. `"research_flow"`. |
|
||||
| `trace_id` | string (32-hex) | Inside a recording OpenTelemetry span | Hex trace ID. Omitted when no span is active. |
|
||||
| `span_id` | string (16-hex) | Inside a recording OpenTelemetry span | Hex span ID. Omitted when no span is active. |
|
||||
| `exception` | object | When the log record has `exc_info` | `{type, message, stacktrace}` — full traceback as a single escaped string. |
|
||||
|
||||
<Tip>
|
||||
Any additional `extra={...}` kwargs passed to a logger call appear as top-level JSON fields verbatim. Reserved field names above always win to keep the schema stable.
|
||||
</Tip>
|
||||
|
||||
### Stability promise
|
||||
|
||||
The `schema` field declares the contract. Within `v1`, CrewAI commits to:
|
||||
|
||||
- **Never removing a field** that customers may have built queries or dashboards against.
|
||||
- **Never renaming a field** in place — renames happen via a schema bump (e.g. `v2`), with the old name kept as a deprecated alias for at least one release cycle.
|
||||
- **Adding new fields** at any time. Consumers should ignore unknown top-level keys.
|
||||
|
||||
When a `v2` is introduced, both the `schema` field and the migration guide will be published in advance, and `v1` will continue to be emitted for one release cycle so dashboards and queries have time to migrate.
|
||||
|
||||
## Prerequisite: promote facets
|
||||
|
||||
Datadog auto-discovers fields the first time it sees them but doesn't make them queryable in widgets until they're promoted to **facets**. This is a one-time setup in your Datadog account.
|
||||
|
||||
<Steps>
|
||||
<Step title="Search for a CrewAI log">
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and search `service:crewai*`. You should see at least one log event.
|
||||
</Step>
|
||||
<Step title="Promote each field">
|
||||
Click any log entry to open the right-hand details panel. For each field below, hover the field name → click the gear icon → **Create facet**.
|
||||
|
||||
- `automation_id`, `automation_name`, `execution_id`, `kickoff_id`, `task_id`
|
||||
- `crewai_version`, `model_id`
|
||||
- `exception.type`, `exception.message`
|
||||
|
||||
Skip any field that already shows a star icon next to its name — that means it's already a facet. The `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and `gen_ai.request.model` facets are typically promoted automatically by Datadog's LLM Observability auto-discovery, but verify they exist before importing the dashboard.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Import the dashboard
|
||||
|
||||
<Steps>
|
||||
<Step title="Download the dashboard JSON">
|
||||
Save [`datadog_dashboard.json`](https://raw.githubusercontent.com/crewAIInc/crewAI/main/docs/edge/en/enterprise/guides/datadog_dashboard.json) to your machine.
|
||||
</Step>
|
||||
<Step title="Open the import dialog in Datadog">
|
||||
Navigate to **Dashboards → New Dashboard**. Click the **gear icon** in the top right of the empty dashboard and select **Import Dashboard JSON**.
|
||||
</Step>
|
||||
<Step title="Paste or upload the JSON">
|
||||
Paste the contents of `datadog_dashboard.json` into the import dialog (or drag the file in). Click **Import**.
|
||||
|
||||
Datadog creates the dashboard immediately and lands you on it. The first load may show empty widgets for a few seconds while queries execute against the time range.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
Datadog's [Dashboard API](https://docs.datadoghq.com/api/latest/dashboards/#create-a-new-dashboard) accepts the same JSON via `POST /api/v1/dashboard`. Use it if you manage dashboards through Terraform, Pulumi, or CI.
|
||||
</Tip>
|
||||
|
||||
## What you get
|
||||
|
||||
The dashboard is organized into four sections plus a placeholder for a custom drill-down widget:
|
||||
|
||||
| Section | Widgets | Useful for |
|
||||
|---------|---------|------------|
|
||||
| **Header** | Total Executions · Error Rate (%) · Active Automations · CrewAI Versions in Use | At-a-glance health for the last hour. Error Rate is conditionally formatted (green ≤ 5%, yellow ≤ 10%, red > 10%). |
|
||||
| **Throughput** | Executions per Hour by Automation (top 10, stacked bars) | Spotting traffic shifts, surfacing busy automations, validating that a rollout didn't change baseline volume. |
|
||||
| **Errors** | Errors by Exception Type (top 5, stacked bars) · Top Exception Types by Count (toplist) | Triaging failures — which exception types are spiking, which automations they're hitting. |
|
||||
| **Cost** | Total Tokens per Hour by Model (input + output, stacked area) | Tracking LLM token spend by model. Useful for catching cost regressions when an automation switches model or starts looping. |
|
||||
| **Drill-Down** | _(empty placeholder)_ | See [Customization](#customize) for adding a recent-errors log stream here. |
|
||||
|
||||
Three template variables at the top of the dashboard re-scope every widget at once:
|
||||
|
||||
- **`$automation`** — filter to a single automation by name.
|
||||
- **`$version`** — filter to a single `crewai` SDK version (useful for comparing pre- and post-upgrade behavior).
|
||||
- **`$service`** — filter to a specific Datadog `service` tag (useful when multiple CrewAI deployments share one Datadog account).
|
||||
|
||||
## Verify ingestion
|
||||
|
||||
Open [Logs Explorer](https://app.datadoghq.com/logs) and run a query that matches your ingestion path:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Datadog Agent">
|
||||
Search `service:crewai* @schema:v1`. You should see structured logs with the JSON fields parsed into Datadog facets. Pick a recent event and verify it has `@automation_id`, `@kickoff_id`, `@execution_id`, `@crewai_version`, and (when running inside a span) `@trace_id` / `@span_id` populated.
|
||||
|
||||
If nothing appears, confirm the Datadog Agent is tailing container stdout and that the deployment is running a recent enough CrewAI Enterprise build.
|
||||
</Tab>
|
||||
<Tab title="Datadog OTLP intake">
|
||||
Search `source:otlp service:crewai*`. OTLP attributes land with their OpenTelemetry names (`automation_id`, `crewai.kickoff.id`, etc.) rather than the stdout JSON keys, but they map to the same dashboard facets after [facet promotion](#prerequisite-promote-facets).
|
||||
|
||||
If nothing appears, verify the collector endpoint is correct (`/v1/logs` for logs, `/v1/traces` for traces) and **Test Connection** succeeded when the collector was saved.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Customize
|
||||
|
||||
The dashboard ships with deliberate gaps so you can extend it without uninstalling and re-importing.
|
||||
|
||||
### Add a Recent Errors log stream
|
||||
|
||||
The **Drill-Down** section is intentionally empty. Add a Log Stream widget to it for an inline view of recent failures:
|
||||
|
||||
1. Edit the dashboard and click **+ Add Widgets** inside the Drill-Down group.
|
||||
2. Drag in a **Log Stream** widget.
|
||||
3. Set the filter query to `status:error $automation $version $service`.
|
||||
4. Choose columns: `@timestamp`, `@automation_name`, `@exception.type`, `@exception.message`, `@execution_id`.
|
||||
5. Sort by most recent, limit to 25 entries.
|
||||
|
||||
Clicking any row jumps to Logs Explorer with the same filter pre-applied.
|
||||
|
||||
### Add p95 latency
|
||||
|
||||
Logs don't include execution duration by default. Two ways to add a latency widget:
|
||||
|
||||
- **From APM traces** — if you also export OTLP traces to Datadog, add a Timeseries widget with data source **Traces**, query `service:crewai*`, aggregation `p95 of @duration`. Datadog APM auto-tracks span duration.
|
||||
- **From metric extraction** — extract a `flow.duration_ms` metric from logs via [Datadog's log-to-metric pipeline](https://docs.datadoghq.com/logs/log_configuration/logs_to_metrics/), then chart it like any other metric. Useful if you don't run APM.
|
||||
|
||||
### Re-scope to multiple deployments
|
||||
|
||||
The `$service` template variable defaults to `*` and will catch every CrewAI deployment in your Datadog account. Change the default to a specific service name in **Configure → Template Variables** if you want the dashboard to focus on one deployment by default.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| All widgets show "No data" | Facets aren't promoted | Re-do the [Promote facets](#prerequisite-promote-facets) step. Datadog won't query against an un-promoted field. |
|
||||
| Error Rate widget shows `NaN` | No executions in the time window | Either no traffic, or `@execution_id` isn't faceted. Expand the time range and re-check facets. |
|
||||
| Throughput chart is flat at the same value | Logs aren't reaching Datadog | Search `service:crewai*` in Logs Explorer. If nothing shows, verify the Datadog Agent is running (Agent path) or the OTel collector endpoint is correct (OTLP path). |
|
||||
| `crewai_version` shows fewer values than expected | Some containers predate the structured-logs work | The `crewai_version` field was added alongside JSON output. Older deployments (pre-structured-logs AMP builds) won't emit it. Upgrade those deployments to pick up the field. See the [log schema reference](#log-schema-reference) for the full field contract. |
|
||||
| Template variables don't filter widgets | The widget's filter line doesn't reference the template variable | Edit the widget and confirm the search includes `$automation $version $service`. |
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="OpenTelemetry Export" icon="magnifying-glass-chart" href="./capture_telemetry_logs">
|
||||
Vendor-neutral observability for non-Datadog stacks (Grafana, Honeycomb, your own collector) — or as a Datadog complement when you want to fan out telemetry to multiple backends.
|
||||
</Card>
|
||||
<Card title="Datadog Log Search Syntax" icon="magnifying-glass" href="https://docs.datadoghq.com/logs/explorer/search_syntax/">
|
||||
Reference for customizing widget queries against the structured facets above.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -393,7 +393,7 @@ crewai install
|
||||
Agora é hora de ver seu flow em ação! Execute-o usando a CLI do CrewAI:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
crewai run
|
||||
```
|
||||
|
||||
Quando você rodar esse comando, verá seu flow ganhando vida:
|
||||
|
||||
BIN
docs/images/enterprise/merged-step-card-canvas.png
Normal file
BIN
docs/images/enterprise/merged-step-card-canvas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
BIN
docs/images/enterprise/merged-step-card-editor.png
Normal file
BIN
docs/images/enterprise/merged-step-card-editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
BIN
docs/images/enterprise/merged-step-card-swap-agent.png
Normal file
BIN
docs/images/enterprise/merged-step-card-swap-agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
87
docs/v1.14.7/ar/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/ar/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: بطاقة واحدة لكل خطوة
|
||||
description: "كل خطوة على لوحة Studio هي بطاقة واحدة تجمع بين المهمة والوكيل الذي ينفّذها."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**الإطلاق يوم الأربعاء 24 يونيو.** تنتقل لوحة Studio إلى بطاقة واحدة لكل خطوة بدلاً من عُقد منفصلة للمهمة والوكيل، وذلك لتبسيط اللوحة مع إضافتنا لوظائف جديدة قريبًا. تستمر أتمتتك الحالية في العمل دون أي تغييرات مطلوبة — تبقى جميع إعدادات المهمة والوكيل متاحة، ولكن منظّمة في بطاقة واحدة.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
على لوحة Studio، تُمثَّل كل خطوة عمل بـ **بطاقة واحدة**. تجمع البطاقة بين عنصرين كانا في السابق في عُقد منفصلة:
|
||||
|
||||
- **المهمة** — ماذا تفعل (الاسم، الوصف، المخرجات المتوقعة، وتنسيق الاستجابة).
|
||||
- **الوكيل** — من ينفّذها (الوكيل المُعيَّن ونموذجه وأدواته).
|
||||
|
||||
الوكيل ليس مشاركًا مستقلاً في سير العمل لديك — بل هو سمة من سمات المهمة: *أي وكيل ينفّذ هذا العمل.* وضع المهمة والوكيل في بطاقة واحدة يجعل هذه العلاقة واضحة، ويحوّل أتمتتك إلى سلسلة واحدة من وحدات العمل من اليسار إلى اليمين يسهل قراءتها بنظرة واحدة.
|
||||
|
||||
<Frame caption="بطاقة واحدة لكل خطوة: المهمة مع ملخص للوكيل المُعيَّن في التذييل.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## على اللوحة
|
||||
|
||||
تعرض كل بطاقة مطوية ما يلي:
|
||||
|
||||
- **اسم المهمة ووصفها** في الأعلى.
|
||||
- **تذييل يلخّص الوكيل المُعيَّن** — الصورة الرمزية والاسم والنموذج والأدوات.
|
||||
|
||||
لا توجد عقدة وكيل منفصلة ولا حافة عمودية من الوكيل ← المهمة. تتصل خطواتك مباشرةً ببعضها البعض بالترتيب الذي تُنفَّذ به.
|
||||
|
||||
## في المحرّر
|
||||
|
||||
افتح بطاقة لتحريرها. العرض الموسّع هو البطاقة نفسها في حالة مفصّلة — وليس شاشة مختلفة — منظّمة في قسمين موسومين بوضوح.
|
||||
|
||||
<Frame caption="المحرّر الموسّع: قسم المهمة مفتوح، والوكيل ملخّص أسفله.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### المهمة — ماذا تفعل
|
||||
|
||||
مفتوحة افتراضيًا، لأنها ما تحرّره عادةً:
|
||||
|
||||
- **الاسم**
|
||||
- **الوصف**
|
||||
- **المخرجات المتوقعة**
|
||||
- **تنسيق الاستجابة** — يظهر هنا لأنه يتحكم تحديدًا في ما تقرأه الخطوات اللاحقة (مثل التوجيه) من هذه الخطوة.
|
||||
|
||||
### الوكيل — من ينفّذها
|
||||
|
||||
يُعرض الوكيل المُعيَّن كملخّص — **الاسم والنموذج والأدوات في سطر واحد**. ويُحفَظ إعداده الأعمق خلف قسمين قابلين للطي:
|
||||
|
||||
- **الدور والهدف والخلفية**
|
||||
- **إعدادات الوكيل** — الاستدلال، الحد الأقصى لمحاولات الاستدلال، السماح بالتفويض، الحد الأقصى للتكرارات، وإعدادات LLM.
|
||||
|
||||
<Tip>
|
||||
الإعداد الكامل للوكيل — الدور، الهدف، الخلفية، النموذج، الأدوات، إعدادات LLM، وكامل كتلة إعدادات الوكيل — موجود خلف القسمين القابلين للطي **الدور والهدف والخلفية** و**إعدادات الوكيل**، منظّمًا حسب عدد مرّات تحريرك له.
|
||||
</Tip>
|
||||
|
||||
## التبديل مقابل تحرير الوكيل
|
||||
|
||||
هناك طريقتان متمايزتان للتعامل مع الوكيل في البطاقة، وكل منهما تؤدي وظيفة مختلفة:
|
||||
|
||||
- **التبديل (Swap)** يعيد تعيين *أي* وكيل ينفّذ هذه المهمة. استخدم عنصر التحكم **تبديل** لاختيار وكيل مختلف من هذا المشروع، أو اختيار واحد من مستودع الوكلاء، أو إنشاء وكيل جديد. هذا مقصور على نطاق المهمة.
|
||||
- **تحرير** الوكيل — بفتح **الدور والهدف والخلفية** أو **إعدادات الوكيل** — يغيّر الوكيل *نفسه*.
|
||||
|
||||
<Frame caption="التبديل يغيّر الوكيل الذي ينفّذ المهمة.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**الوكلاء قابلون لإعادة الاستخدام ومشتركون.** يمكن للوكيل نفسه تنفيذ أكثر من مهمة عبر مشروعك. تحرير دور الوكيل أو خلفيته أو إعداداته يحدّث ذلك الوكيل **في كل مكان يُستخدم فيه** — وليس فقط في البطاقة التي فتحتها. إذا أردت تطبيق تغيير على خطوة واحدة فقط، فقم **بالتبديل** إلى وكيل مختلف بدلاً من تحرير الوكيل المشترك.
|
||||
</Warning>
|
||||
|
||||
## ذات صلة
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ar/enterprise/features/crew-studio" icon="pencil">
|
||||
أنشئ الأتمتة بمساعدة الذكاء الاصطناعي ومحرّر مرئي.
|
||||
</Card>
|
||||
<Card title="مستودعات الوكلاء" href="/ar/enterprise/features/agent-repositories" icon="users">
|
||||
إدارة الوكلاء وإعادة استخدامهم عبر أتمتتك.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/v1.14.7/en/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/en/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: One Card per Step
|
||||
description: "Each step on the Studio canvas is a single card that combines the task and the agent that performs it."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Rolling out Wednesday, June 24th.** The Studio canvas is moving to one card per step instead of separate task and agent nodes, to streamline the canvas as we add new functionality soon. Your existing automations keep working with no changes needed — every task and agent setting is still available, just organized onto a single card.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
On the Studio canvas, each step of work is represented by a **single card**. The card combines two things that used to live in separate nodes:
|
||||
|
||||
- **The task** — what to do (name, description, expected output, and response format).
|
||||
- **The agent** — who does it (the assigned agent, its model, and its tools).
|
||||
|
||||
An agent isn't an independent participant in your workflow — it's an attribute of the task: *which agent performs this work.* Putting the task and its agent on one card makes that relationship explicit and turns your automation into a single, left-to-right chain of work units that's easier to read at a glance.
|
||||
|
||||
<Frame caption="One card per step: the task with its assigned agent summarized in the footer.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## On the canvas
|
||||
|
||||
Each collapsed card shows:
|
||||
|
||||
- The **task name and description** at the top.
|
||||
- A **footer summarizing the assigned agent** — avatar, name, model, and tools.
|
||||
|
||||
There's no separate agent node and no vertical agent → task edge. Your steps connect directly to one another in the order they run.
|
||||
|
||||
## In the editor
|
||||
|
||||
Open a card to edit it. The expanded view is the same card in a detailed state — not a different screen — organized into two clearly labeled sections.
|
||||
|
||||
<Frame caption="The expanded editor: the task section open, the agent summarized below it.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### The task — what to do
|
||||
|
||||
Open by default, since this is what you usually edit:
|
||||
|
||||
- **Name**
|
||||
- **Description**
|
||||
- **Expected Output**
|
||||
- **Response Format** — surfaced here because it controls exactly what downstream steps (such as routing) read from this step.
|
||||
|
||||
### The agent — who does it
|
||||
|
||||
The assigned agent is shown as a summary — **name, model, and tools inline**. Its deeper configuration is preserved behind two disclosures:
|
||||
|
||||
- **Role, goal & backstory**
|
||||
- **Agent settings** — reasoning, max reasoning attempts, allow delegation, max iterations, and LLM settings.
|
||||
|
||||
<Tip>
|
||||
An agent's full configuration — Role, Goal, Backstory, Model, Tools, LLM Settings, and the complete Agent Settings block — lives behind the **Role, goal & backstory** and **Agent settings** disclosures, organized by how often you edit it.
|
||||
</Tip>
|
||||
|
||||
## Swapping vs. editing the agent
|
||||
|
||||
There are two distinct ways to work with the agent on a card, and they do different things:
|
||||
|
||||
- **Swap** reassigns *which* agent performs this task. Use the **Swap** control to pick a different agent from this project, choose one from your Agent Repository, or create a new agent. This is scoped to the task.
|
||||
- **Editing** the agent — opening **Role, goal & backstory** or **Agent settings** — changes the agent *itself*.
|
||||
|
||||
<Frame caption="Swap changes which agent performs the task.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Agents are reusable and shared.** The same agent can perform more than one task across your project. Editing an agent's role, backstory, or settings updates that agent **everywhere it's used** — not just on the card you opened. If you want a change to apply to only one step, **Swap** in a different agent instead of editing the shared one.
|
||||
</Warning>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/en/enterprise/features/crew-studio" icon="pencil">
|
||||
Build automations with AI assistance and a visual editor.
|
||||
</Card>
|
||||
<Card title="Agent Repositories" href="/en/enterprise/features/agent-repositories" icon="users">
|
||||
Manage and reuse agents across your automations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/v1.14.7/ko/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/ko/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: 단계당 하나의 카드
|
||||
description: "Studio 캔버스의 각 단계는 작업과 이를 수행하는 에이전트를 하나로 결합한 단일 카드입니다."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**6월 24일 수요일 출시.** Studio 캔버스가 작업과 에이전트를 별도의 노드로 표시하는 대신 단계당 하나의 카드로 전환됩니다. 곧 추가될 새로운 기능을 위해 캔버스를 간소화하기 위한 변경입니다. 기존 자동화는 아무런 변경 없이 그대로 동작하며, 모든 작업 및 에이전트 설정은 단일 카드에 정리되어 그대로 사용할 수 있습니다.
|
||||
</Note>
|
||||
|
||||
## 개요
|
||||
|
||||
Studio 캔버스에서 각 작업 단계는 **하나의 카드**로 표현됩니다. 이 카드는 이전에 별도의 노드에 있던 두 가지를 결합합니다:
|
||||
|
||||
- **작업(Task)** — 무엇을 할지(이름, 설명, 예상 출력, 응답 형식).
|
||||
- **에이전트(Agent)** — 누가 수행하는지(할당된 에이전트, 모델, 도구).
|
||||
|
||||
에이전트는 워크플로의 독립적인 참여자가 아니라 작업의 속성, 즉 *이 작업을 어떤 에이전트가 수행하는지*를 나타냅니다. 작업과 에이전트를 하나의 카드에 두면 이 관계가 명확해지고, 자동화가 왼쪽에서 오른쪽으로 이어지는 단일 작업 단위 체인이 되어 한눈에 읽기 쉬워집니다.
|
||||
|
||||
<Frame caption="단계당 하나의 카드: 작업과 푸터에 요약된 할당 에이전트.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## 캔버스에서
|
||||
|
||||
접힌 각 카드는 다음을 표시합니다:
|
||||
|
||||
- 상단의 **작업 이름과 설명**.
|
||||
- **할당된 에이전트를 요약한 푸터** — 아바타, 이름, 모델, 도구.
|
||||
|
||||
별도의 에이전트 노드나 에이전트 → 작업 세로 연결선이 없습니다. 각 단계는 실행 순서대로 서로 직접 연결됩니다.
|
||||
|
||||
## 에디터에서
|
||||
|
||||
카드를 열어 편집합니다. 확장된 보기는 다른 화면이 아니라 동일한 카드의 상세 상태이며, 명확하게 구분된 두 개의 섹션으로 구성됩니다.
|
||||
|
||||
<Frame caption="확장된 에디터: 작업 섹션이 열려 있고 그 아래에 에이전트가 요약되어 있습니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### 작업 — 무엇을 할지
|
||||
|
||||
가장 자주 편집하는 항목이므로 기본적으로 열려 있습니다:
|
||||
|
||||
- **이름**
|
||||
- **설명**
|
||||
- **예상 출력**
|
||||
- **응답 형식** — 다운스트림 단계(예: 라우팅)가 이 단계에서 무엇을 읽을지 정확히 제어하므로 여기에 표시됩니다.
|
||||
|
||||
### 에이전트 — 누가 수행하는지
|
||||
|
||||
할당된 에이전트는 요약으로 표시됩니다 — **이름, 모델, 도구가 인라인으로** 표시됩니다. 더 깊은 구성은 두 개의 접이식 섹션 뒤에 보존됩니다:
|
||||
|
||||
- **역할, 목표 및 배경 스토리**
|
||||
- **에이전트 설정** — 추론, 최대 추론 시도 횟수, 위임 허용, 최대 반복 횟수, LLM 설정.
|
||||
|
||||
<Tip>
|
||||
에이전트의 전체 구성 — 역할, 목표, 배경 스토리, 모델, 도구, LLM 설정 및 전체 에이전트 설정 블록 — 은 **역할, 목표 및 배경 스토리**와 **에이전트 설정** 접이식 섹션 뒤에 편집 빈도에 따라 정리되어 있습니다.
|
||||
</Tip>
|
||||
|
||||
## 에이전트 교체 vs. 편집
|
||||
|
||||
카드에서 에이전트를 다루는 방식은 두 가지로 구분되며, 각각 다른 작업을 수행합니다:
|
||||
|
||||
- **교체(Swap)** 는 *어떤* 에이전트가 이 작업을 수행할지 재할당합니다. **교체** 컨트롤을 사용하여 이 프로젝트의 다른 에이전트를 선택하거나, 에이전트 저장소에서 선택하거나, 새 에이전트를 만들 수 있습니다. 이는 작업 범위로 한정됩니다.
|
||||
- 에이전트 **편집** — **역할, 목표 및 배경 스토리** 또는 **에이전트 설정** 을 여는 것 — 은 에이전트 *자체*를 변경합니다.
|
||||
|
||||
<Frame caption="교체는 작업을 수행할 에이전트를 변경합니다.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**에이전트는 재사용 가능하며 공유됩니다.** 동일한 에이전트가 프로젝트 전반에서 둘 이상의 작업을 수행할 수 있습니다. 에이전트의 역할, 배경 스토리 또는 설정을 편집하면 열어 본 카드뿐만 아니라 **해당 에이전트가 사용되는 모든 곳**에서 업데이트됩니다. 변경 사항을 하나의 단계에만 적용하려면 공유 에이전트를 편집하지 말고 다른 에이전트로 **교체**하세요.
|
||||
</Warning>
|
||||
|
||||
## 관련 항목
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/ko/enterprise/features/crew-studio" icon="pencil">
|
||||
AI 지원과 비주얼 에디터로 자동화를 구축합니다.
|
||||
</Card>
|
||||
<Card title="에이전트 저장소" href="/ko/enterprise/features/agent-repositories" icon="users">
|
||||
자동화 전반에서 에이전트를 관리하고 재사용합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
87
docs/v1.14.7/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
87
docs/v1.14.7/pt-BR/enterprise/features/merged-step-card.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Um Card por Etapa
|
||||
description: "Cada etapa no canvas do Studio é um único card que combina a tarefa e o agente que a executa."
|
||||
icon: "layer-group"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
{/* CLEANUP: This <Note> banner is the only time-bound content on the page. After the feature ships (Wednesday, June 24th 2026), delete the banner below — the rest of the page is evergreen present-tense docs and needs no other edits. */}
|
||||
<Note>
|
||||
**Lançamento na quarta-feira, 24 de junho.** O canvas do Studio passa a exibir um card por etapa, em vez de nós separados para tarefa e agente, para simplificar o canvas à medida que adicionamos novas funcionalidades em breve. Suas automações existentes continuam funcionando sem nenhuma alteração necessária — cada configuração de tarefa e de agente continua disponível, apenas organizada em um único card.
|
||||
</Note>
|
||||
|
||||
## Visão geral
|
||||
|
||||
No canvas do Studio, cada etapa de trabalho é representada por um **único card**. O card combina dois elementos que antes ficavam em nós separados:
|
||||
|
||||
- **A tarefa** — o que fazer (nome, descrição, saída esperada e formato da resposta).
|
||||
- **O agente** — quem faz (o agente atribuído, seu modelo e suas ferramentas).
|
||||
|
||||
Um agente não é um participante independente do seu fluxo de trabalho — ele é um atributo da tarefa: *qual agente executa este trabalho.* Colocar a tarefa e seu agente em um único card torna essa relação explícita e transforma sua automação em uma única cadeia de unidades de trabalho, da esquerda para a direita, mais fácil de ler em uma olhada.
|
||||
|
||||
<Frame caption="Um card por etapa: a tarefa com o agente atribuído resumido no rodapé.">
|
||||

|
||||
</Frame>
|
||||
|
||||
## No canvas
|
||||
|
||||
Cada card recolhido mostra:
|
||||
|
||||
- O **nome e a descrição da tarefa** no topo.
|
||||
- Um **rodapé resumindo o agente atribuído** — avatar, nome, modelo e ferramentas.
|
||||
|
||||
Não há nó de agente separado nem aresta vertical de agente → tarefa. Suas etapas se conectam diretamente umas às outras na ordem em que são executadas.
|
||||
|
||||
## No editor
|
||||
|
||||
Abra um card para editá-lo. A visão expandida é o mesmo card em um estado detalhado — não uma tela diferente — organizada em duas seções claramente identificadas.
|
||||
|
||||
<Frame caption="O editor expandido: a seção da tarefa aberta, com o agente resumido abaixo.">
|
||||

|
||||
</Frame>
|
||||
|
||||
### A tarefa — o que fazer
|
||||
|
||||
Aberta por padrão, já que é o que você costuma editar:
|
||||
|
||||
- **Nome**
|
||||
- **Descrição**
|
||||
- **Saída Esperada**
|
||||
- **Formato da Resposta** — exibido aqui porque controla exatamente o que as etapas seguintes (como o roteamento) leem desta etapa.
|
||||
|
||||
### O agente — quem faz
|
||||
|
||||
O agente atribuído é mostrado como um resumo — **nome, modelo e ferramentas em linha**. Sua configuração mais detalhada é preservada por trás de duas seções recolhíveis:
|
||||
|
||||
- **Papel, objetivo e história**
|
||||
- **Configurações do agente** — raciocínio, máximo de tentativas de raciocínio, permitir delegação, máximo de iterações e configurações de LLM.
|
||||
|
||||
<Tip>
|
||||
A configuração completa de um agente — Papel, Objetivo, História, Modelo, Ferramentas, Configurações de LLM e todo o bloco de Configurações do agente — fica por trás das seções recolhíveis **Papel, objetivo e história** e **Configurações do agente**, organizada pela frequência com que você a edita.
|
||||
</Tip>
|
||||
|
||||
## Trocar vs. editar o agente
|
||||
|
||||
Há duas maneiras distintas de trabalhar com o agente em um card, e elas fazem coisas diferentes:
|
||||
|
||||
- **Trocar** reatribui *qual* agente executa esta tarefa. Use o controle **Trocar** para escolher um agente diferente deste projeto, selecionar um do seu Repositório de Agentes ou criar um novo agente. Isso tem escopo limitado à tarefa.
|
||||
- **Editar** o agente — abrindo **Papel, objetivo e história** ou **Configurações do agente** — altera o agente *em si*.
|
||||
|
||||
<Frame caption="Trocar muda qual agente executa a tarefa.">
|
||||

|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Os agentes são reutilizáveis e compartilhados.** O mesmo agente pode executar mais de uma tarefa em todo o seu projeto. Editar o papel, a história ou as configurações de um agente atualiza esse agente **em todos os lugares onde ele é usado** — não apenas no card que você abriu. Se quiser que uma alteração se aplique a apenas uma etapa, **Troque** por um agente diferente em vez de editar o agente compartilhado.
|
||||
</Warning>
|
||||
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Studio" href="/pt-BR/enterprise/features/crew-studio" icon="pencil">
|
||||
Crie automações com assistência de IA e um editor visual.
|
||||
</Card>
|
||||
<Card title="Repositórios de Agentes" href="/pt-BR/enterprise/features/agent-repositories" icon="users">
|
||||
Gerencie e reutilize agentes em suas automações.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.8a1",
|
||||
"crewai-core==1.14.8a2",
|
||||
"click>=8.1.7,<9",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"pydantic-settings~=2.10.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.8a1"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
@@ -40,14 +40,6 @@ def replay_task_command(*args: Any, **kwargs: Any) -> Any:
|
||||
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
|
||||
|
||||
@@ -155,12 +147,18 @@ def uv(uv_args: tuple[str, ...]) -> None:
|
||||
is_flag=True,
|
||||
help="Use classic Python/YAML project structure instead of JSON",
|
||||
)
|
||||
@click.option(
|
||||
"--declarative",
|
||||
is_flag=True,
|
||||
help="Create a declarative Flow project instead of a Python Flow project",
|
||||
)
|
||||
def create(
|
||||
type: str | None,
|
||||
name: str | None,
|
||||
provider: str | None,
|
||||
skip_provider: bool = False,
|
||||
classic: bool = False,
|
||||
declarative: bool = False,
|
||||
) -> None:
|
||||
"""Create a new crew, or flow."""
|
||||
dmn_mode = is_dmn_mode_enabled()
|
||||
@@ -194,6 +192,8 @@ def create(
|
||||
if dmn_mode:
|
||||
skip_provider = True
|
||||
if type == "crew":
|
||||
if declarative:
|
||||
raise click.UsageError("--declarative can only be used with flow projects")
|
||||
if classic:
|
||||
from crewai_cli.create_crew import create_crew
|
||||
|
||||
@@ -205,7 +205,7 @@ def create(
|
||||
elif type == "flow":
|
||||
from crewai_cli.create_flow import create_flow
|
||||
|
||||
create_flow(name)
|
||||
create_flow(name, declarative=declarative)
|
||||
else:
|
||||
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
|
||||
|
||||
@@ -468,7 +468,7 @@ def memory(
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"Path to a trained-agents pickle (produced by `crewai train -f`). "
|
||||
"Crew-only: path to a trained-agents pickle (produced by `crewai train -f`). "
|
||||
"When set, agents load suggestions from this file instead of the "
|
||||
"default trained_agents_data.pkl. Equivalent to setting "
|
||||
"CREWAI_TRAINED_AGENTS_FILE."
|
||||
@@ -512,16 +512,13 @@ def install(context: click.Context) -> None:
|
||||
"--definition",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"Experimental: path to a Flow Definition YAML/JSON file, "
|
||||
"or an inline YAML/JSON string."
|
||||
),
|
||||
help="Flow-only: path to a declarative flow definition.",
|
||||
)
|
||||
@click.option(
|
||||
"--inputs",
|
||||
type=str,
|
||||
default=None,
|
||||
help='Experimental: JSON object passed to flow.kickoff(), e.g. \'{"topic":"AI"}\'.',
|
||||
help='Flow-only: JSON object passed to the declarative flow, e.g. \'{"topic":"AI"}\'.',
|
||||
)
|
||||
def run(
|
||||
trained_agents_file: str | None,
|
||||
@@ -531,16 +528,14 @@ def run(
|
||||
"""Run the Crew or Flow."""
|
||||
if inputs is not None and definition is None:
|
||||
raise click.UsageError("--inputs requires --definition")
|
||||
if trained_agents_file is not None and definition is not None:
|
||||
raise click.UsageError("--filename can only be used when running crews")
|
||||
|
||||
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)
|
||||
run_crew(
|
||||
trained_agents_file=trained_agents_file,
|
||||
definition=definition,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@@ -795,10 +790,11 @@ 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()
|
||||
click.secho(
|
||||
"The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead.",
|
||||
fg="yellow",
|
||||
)
|
||||
run_crew(trained_agents_file=None, definition=None, inputs=None)
|
||||
|
||||
|
||||
@flow.command(name="plot")
|
||||
|
||||
@@ -5,7 +5,10 @@ import click
|
||||
from crewai_core.telemetry import Telemetry
|
||||
|
||||
|
||||
def create_flow(name: str) -> None:
|
||||
DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills")
|
||||
|
||||
|
||||
def create_flow(name: str, *, declarative: bool = False) -> None:
|
||||
"""Create a new flow."""
|
||||
folder_name = name.replace(" ", "_").replace("-", "_").lower()
|
||||
class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "")
|
||||
@@ -20,6 +23,17 @@ def create_flow(name: str) -> None:
|
||||
telemetry = Telemetry()
|
||||
telemetry.flow_creation_span(class_name)
|
||||
|
||||
if declarative:
|
||||
_create_declarative_flow(name, class_name, folder_name, project_root)
|
||||
else:
|
||||
_create_python_flow(name, class_name, folder_name, project_root)
|
||||
|
||||
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
|
||||
|
||||
|
||||
def _create_python_flow(
|
||||
name: str, class_name: str, folder_name: str, project_root: Path
|
||||
) -> None:
|
||||
(project_root / "src" / folder_name).mkdir(parents=True)
|
||||
(project_root / "src" / folder_name / "crews").mkdir(parents=True)
|
||||
(project_root / "src" / folder_name / "tools").mkdir(parents=True)
|
||||
@@ -92,4 +106,41 @@ def create_flow(name: str) -> None:
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
|
||||
|
||||
def _create_declarative_flow(
|
||||
name: str, class_name: str, folder_name: str, project_root: Path
|
||||
) -> None:
|
||||
project_root.mkdir(parents=True)
|
||||
package_root = project_root / "src" / folder_name
|
||||
package_root.mkdir(parents=True)
|
||||
for folder in DECLARATIVE_FLOW_FOLDERS:
|
||||
(package_root / folder).mkdir()
|
||||
|
||||
package_dir = Path(__file__).parent
|
||||
templates_dir = package_dir / "templates" / "declarative_flow"
|
||||
|
||||
agents_md_src = package_dir / "templates" / "AGENTS.md"
|
||||
if agents_md_src.exists():
|
||||
shutil.copy2(agents_md_src, project_root / "AGENTS.md")
|
||||
|
||||
for src_file in templates_dir.rglob("*"):
|
||||
if not src_file.is_file():
|
||||
continue
|
||||
|
||||
relative_path = src_file.relative_to(templates_dir)
|
||||
dst_file = (
|
||||
project_root / relative_path
|
||||
if relative_path.name in {".gitignore", "README.md", "pyproject.toml"}
|
||||
else package_root / relative_path
|
||||
)
|
||||
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = src_file.read_text(encoding="utf-8")
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{flow_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
dst_file.write_text(content, encoding="utf-8")
|
||||
|
||||
(project_root / ".env").write_text("OPENAI_API_KEY=YOUR_API_KEY", encoding="utf-8")
|
||||
(package_root / "__init__.py").write_text("", encoding="utf-8")
|
||||
for folder in DECLARATIVE_FLOW_FOLDERS:
|
||||
(package_root / folder / ".gitkeep").write_text("", encoding="utf-8")
|
||||
|
||||
@@ -680,7 +680,7 @@ def _default_agents_and_tasks(
|
||||
]
|
||||
crew_settings = {
|
||||
"process": "sequential",
|
||||
"memory": False,
|
||||
"memory": True,
|
||||
"inputs": {},
|
||||
}
|
||||
return agents, tasks, crew_settings
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
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 = {
|
||||
@@ -38,8 +34,6 @@ _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(
|
||||
@@ -143,267 +137,7 @@ def _stage_project(root: Path, files: list[Path]) -> Path:
|
||||
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"
|
||||
|
||||
@@ -212,8 +212,16 @@ class DeployValidator:
|
||||
if crew_path is None:
|
||||
return self.results
|
||||
|
||||
agents_dir = self.project_root / "agents"
|
||||
|
||||
self._check_pyproject()
|
||||
self._check_lockfile()
|
||||
agents_dir_ok = self._check_json_agents_dir(agents_dir)
|
||||
|
||||
project = None
|
||||
try:
|
||||
project = validate_crew_project(crew_path, self.project_root / "agents")
|
||||
if agents_dir_ok:
|
||||
project = validate_crew_project(crew_path, agents_dir)
|
||||
except JSONProjectValidationError as e:
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
@@ -232,15 +240,27 @@ class DeployValidator:
|
||||
)
|
||||
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)
|
||||
if project is not None:
|
||||
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
|
||||
self._check_version_vs_lockfile()
|
||||
|
||||
return self.results
|
||||
|
||||
def _check_json_agents_dir(self, agents_dir: Path) -> bool:
|
||||
if agents_dir.is_dir():
|
||||
return True
|
||||
self._add(
|
||||
Severity.ERROR,
|
||||
"missing_agents_dir",
|
||||
"Cannot find agents/ directory",
|
||||
detail=(
|
||||
"JSON crew projects load agent definitions from "
|
||||
f"{agents_dir.relative_to(self.project_root)}/*.jsonc or *.json."
|
||||
),
|
||||
hint="Create agents/ and add one JSON or JSONC file per agent.",
|
||||
)
|
||||
return False
|
||||
|
||||
def _check_env_vars_json(
|
||||
self, crew_path: Path, agents_dir: Path, agent_names: list[str]
|
||||
) -> None:
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def kickoff_flow() -> None:
|
||||
"""
|
||||
Kickoff the flow by running a command in the UV environment.
|
||||
"""
|
||||
command = ["uv", "run", "kickoff"]
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
|
||||
|
||||
if result.stderr:
|
||||
click.echo(result.stderr, err=True)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"An error occurred while running the flow: {e}", err=True)
|
||||
click.echo(e.output, err=True)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
@@ -5,19 +5,27 @@ import click
|
||||
|
||||
def plot_flow() -> None:
|
||||
"""
|
||||
Plot the flow by running a command in the UV environment.
|
||||
Plot the flow from declarative config or the Python UV entrypoint.
|
||||
"""
|
||||
command = ["uv", "run", "plot"]
|
||||
from crewai_cli.run_declarative_flow import (
|
||||
configured_project_declarative_flow,
|
||||
plot_declarative_flow_in_project_env,
|
||||
)
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
|
||||
if definition := configured_project_declarative_flow():
|
||||
plot_declarative_flow_in_project_env(definition)
|
||||
else:
|
||||
command = ["uv", "run", "plot"]
|
||||
|
||||
if result.stderr:
|
||||
click.echo(result.stderr, err=True)
|
||||
try:
|
||||
subprocess.run( # noqa: S603
|
||||
command, capture_output=False, text=True, check=True
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"An error occurred while plotting the flow: {e}", err=True)
|
||||
click.echo(e.output, err=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"An error occurred while plotting the flow: {e}", err=True)
|
||||
raise SystemExit(1) from e
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
raise SystemExit(1) from e
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from enum import Enum
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -27,11 +26,6 @@ if TYPE_CHECKING:
|
||||
from crewai_cli.crew_run_tui import CrewRunApp
|
||||
|
||||
|
||||
class CrewType(Enum):
|
||||
STANDARD = "standard"
|
||||
FLOW = "flow"
|
||||
|
||||
|
||||
# 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.
|
||||
@@ -537,7 +531,11 @@ def _print_post_tui_summary(app: CrewRunApp) -> None:
|
||||
)
|
||||
|
||||
|
||||
def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
def run_crew(
|
||||
trained_agents_file: str | None = None,
|
||||
definition: str | None = None,
|
||||
inputs: str | None = None,
|
||||
) -> None:
|
||||
"""Run the crew or flow.
|
||||
|
||||
Args:
|
||||
@@ -545,15 +543,88 @@ def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
by ``crewai train -f``. When set, exported as
|
||||
``CREWAI_TRAINED_AGENTS_FILE`` so agents load suggestions from this
|
||||
file instead of the default ``trained_agents_data.pkl``.
|
||||
definition: Optional path to a declarative Flow definition.
|
||||
inputs: Optional JSON object passed to a declarative Flow.
|
||||
"""
|
||||
# JSON crew projects take precedence
|
||||
if inputs is not None and definition is None:
|
||||
raise click.UsageError("--inputs requires --definition")
|
||||
|
||||
if definition is not None:
|
||||
_run_explicit_declarative_flow(
|
||||
definition=definition,
|
||||
inputs=inputs,
|
||||
trained_agents_file=trained_agents_file,
|
||||
)
|
||||
return
|
||||
|
||||
if _has_json_crew():
|
||||
_run_json_crew_in_project_env(trained_agents_file=trained_agents_file)
|
||||
return
|
||||
|
||||
pyproject_data = read_toml()
|
||||
_warn_if_old_poetry_project(pyproject_data)
|
||||
project_type = _get_project_type(pyproject_data)
|
||||
|
||||
if project_type == "flow":
|
||||
_run_flow_project(
|
||||
pyproject_data=pyproject_data,
|
||||
trained_agents_file=trained_agents_file,
|
||||
)
|
||||
return
|
||||
|
||||
_run_classic_crew_project(
|
||||
pyproject_data=pyproject_data,
|
||||
trained_agents_file=trained_agents_file,
|
||||
)
|
||||
|
||||
|
||||
def _run_explicit_declarative_flow(
|
||||
definition: str, inputs: str | None, trained_agents_file: str | None
|
||||
) -> None:
|
||||
if trained_agents_file is not None:
|
||||
raise click.UsageError("--filename can only be used when running crews")
|
||||
|
||||
from crewai_cli.run_declarative_flow import run_declarative_flow
|
||||
|
||||
run_declarative_flow(definition=definition, inputs=inputs)
|
||||
|
||||
|
||||
def _run_flow_project(
|
||||
pyproject_data: dict[str, Any], trained_agents_file: str | None
|
||||
) -> None:
|
||||
if trained_agents_file is not None:
|
||||
raise click.UsageError("--filename can only be used when running crews")
|
||||
|
||||
from crewai_cli.run_declarative_flow import (
|
||||
configured_project_declarative_flow,
|
||||
run_declarative_flow_in_project_env,
|
||||
)
|
||||
|
||||
if definition := configured_project_declarative_flow(pyproject_data):
|
||||
run_declarative_flow_in_project_env(definition=definition)
|
||||
return
|
||||
|
||||
_execute_uv_script("kickoff", entity_type="flow")
|
||||
|
||||
|
||||
def _run_classic_crew_project(
|
||||
pyproject_data: dict[str, Any], trained_agents_file: str | None
|
||||
) -> None:
|
||||
_execute_uv_script(
|
||||
"run_crew",
|
||||
entity_type="crew",
|
||||
trained_agents_file=trained_agents_file,
|
||||
)
|
||||
|
||||
|
||||
def _get_project_type(pyproject_data: dict[str, Any]) -> str | None:
|
||||
project_type = pyproject_data.get("tool", {}).get("crewai", {}).get("type")
|
||||
return project_type if isinstance(project_type, str) else None
|
||||
|
||||
|
||||
def _warn_if_old_poetry_project(pyproject_data: dict[str, Any]) -> None:
|
||||
crewai_version = get_crewai_version()
|
||||
min_required_version = "0.71.0"
|
||||
pyproject_data = read_toml()
|
||||
|
||||
if pyproject_data.get("tool", {}).get("poetry") and (
|
||||
version.parse(crewai_version) < version.parse(min_required_version)
|
||||
@@ -564,25 +635,22 @@ def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
fg="red",
|
||||
)
|
||||
|
||||
is_flow = pyproject_data.get("tool", {}).get("crewai", {}).get("type") == "flow"
|
||||
crew_type = CrewType.FLOW if is_flow else CrewType.STANDARD
|
||||
|
||||
click.echo(f"Running the {'Flow' if is_flow else 'Crew'}")
|
||||
|
||||
execute_command(crew_type, trained_agents_file=trained_agents_file)
|
||||
|
||||
|
||||
def execute_command(
|
||||
crew_type: CrewType, trained_agents_file: str | None = None
|
||||
def _execute_uv_script(
|
||||
script_name: str,
|
||||
*,
|
||||
entity_type: str,
|
||||
trained_agents_file: str | None = None,
|
||||
) -> None:
|
||||
"""Execute the appropriate command based on crew type.
|
||||
"""Execute a project script through uv.
|
||||
|
||||
Args:
|
||||
crew_type: The type of crew to run.
|
||||
script_name: The project script to run.
|
||||
entity_type: The user-facing entity being run.
|
||||
trained_agents_file: Optional trained-agents pickle path forwarded to
|
||||
the subprocess via the ``CREWAI_TRAINED_AGENTS_FILE`` env var.
|
||||
"""
|
||||
command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"]
|
||||
command = ["uv", "run", script_name]
|
||||
|
||||
env = build_env_with_all_tool_credentials()
|
||||
if trained_agents_file:
|
||||
@@ -592,21 +660,20 @@ def execute_command(
|
||||
subprocess.run(command, capture_output=False, text=True, check=True, env=env) # noqa: S603
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
handle_error(e, crew_type)
|
||||
_handle_run_error(e, entity_type)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||
|
||||
|
||||
def handle_error(error: subprocess.CalledProcessError, crew_type: CrewType) -> None:
|
||||
def _handle_run_error(error: subprocess.CalledProcessError, entity_type: str) -> None:
|
||||
"""
|
||||
Handle subprocess errors with appropriate messaging.
|
||||
|
||||
Args:
|
||||
error: The subprocess error that occurred
|
||||
crew_type: The type of crew that was being run
|
||||
entity_type: The type of entity that was being run
|
||||
"""
|
||||
entity_type = "flow" if crew_type == CrewType.FLOW else "crew"
|
||||
click.echo(f"An error occurred while running the {entity_type}: {error}", err=True)
|
||||
|
||||
if error.output:
|
||||
|
||||
212
lib/cli/src/crewai_cli/run_declarative_flow.py
Normal file
212
lib/cli/src/crewai_cli/run_declarative_flow.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
|
||||
|
||||
def run_declarative_flow_in_project_env(
|
||||
definition: str, inputs: str | None = None
|
||||
) -> None:
|
||||
"""Run a declarative flow inside the project's Python environment."""
|
||||
if is_declarative_flow_project_env() or not _has_project_file():
|
||||
run_declarative_flow(definition=definition, inputs=inputs)
|
||||
return
|
||||
|
||||
if inputs is not None:
|
||||
raise click.UsageError("--inputs is only supported with --definition")
|
||||
|
||||
_execute_declarative_flow_command(["uv", "run", "crewai", "run"])
|
||||
|
||||
|
||||
def plot_declarative_flow_in_project_env(definition: str) -> None:
|
||||
"""Plot a declarative flow inside the project's Python environment."""
|
||||
if is_declarative_flow_project_env() or not _has_project_file():
|
||||
plot_declarative_flow(definition=definition)
|
||||
return
|
||||
|
||||
_execute_declarative_flow_command(["uv", "run", "crewai", "flow", "plot"])
|
||||
|
||||
|
||||
def run_declarative_flow(definition: str, inputs: str | None = None) -> None:
|
||||
"""Run a declarative flow from a definition path."""
|
||||
parsed_inputs = _parse_inputs(inputs)
|
||||
|
||||
try:
|
||||
flow = load_declarative_flow(definition)
|
||||
result = flow.kickoff(inputs=parsed_inputs)
|
||||
except Exception as exc:
|
||||
click.echo(
|
||||
f"An error occurred while running the declarative flow: {exc}", err=True
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
click.echo(_format_result(result))
|
||||
|
||||
|
||||
def plot_declarative_flow(definition: str) -> None:
|
||||
"""Plot a declarative flow from a definition path."""
|
||||
try:
|
||||
flow = load_declarative_flow(definition)
|
||||
flow.plot()
|
||||
except Exception as exc:
|
||||
click.echo(
|
||||
f"An error occurred while plotting the declarative flow: {exc}", err=True
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
|
||||
def load_declarative_flow(definition: str) -> Any:
|
||||
"""Load a declarative Flow instance from a definition path."""
|
||||
try:
|
||||
from crewai.flow.flow import Flow
|
||||
from crewai.flow.flow_definition import FlowDefinition
|
||||
except ImportError as exc:
|
||||
click.echo(
|
||||
"Running declarative flows requires the full crewai package.",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
definition_path = Path(definition).expanduser()
|
||||
definition_source = _read_declarative_flow_source(definition_path, definition)
|
||||
|
||||
flow_definition = _parse_declarative_flow(
|
||||
FlowDefinition,
|
||||
definition_source,
|
||||
source_path=definition_path,
|
||||
)
|
||||
return Flow.from_definition(flow_definition)
|
||||
|
||||
|
||||
def configured_project_declarative_flow(
|
||||
pyproject_data: dict[str, Any] | None = None,
|
||||
) -> str | None:
|
||||
"""Return the configured declarative flow source for flow projects."""
|
||||
if pyproject_data is None:
|
||||
try:
|
||||
from crewai_cli.utils import read_toml
|
||||
|
||||
pyproject_data = read_toml()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
crewai_config = pyproject_data.get("tool", {}).get("crewai", {})
|
||||
if crewai_config.get("type") != "flow":
|
||||
return None
|
||||
definition = crewai_config.get("definition")
|
||||
if not isinstance(definition, str):
|
||||
return None
|
||||
return definition.strip() or None
|
||||
|
||||
|
||||
def _execute_declarative_flow_command(command: list[str]) -> None:
|
||||
env = build_env_with_all_tool_credentials()
|
||||
|
||||
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 declarative flow: {e}",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1) from e
|
||||
|
||||
|
||||
def is_declarative_flow_project_env() -> bool:
|
||||
import os
|
||||
|
||||
return os.environ.get("UV_RUN_RECURSION_DEPTH") is not None
|
||||
|
||||
|
||||
def _has_project_file(project_root: Path | None = None) -> bool:
|
||||
root = project_root or Path.cwd()
|
||||
return (root / "pyproject.toml").is_file()
|
||||
|
||||
|
||||
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_declarative_flow_source(path: Path, definition: str) -> str:
|
||||
try:
|
||||
if path.is_file():
|
||||
source = _read_declarative_flow_file(path)
|
||||
elif path.exists():
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} is not a file.", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
click.echo(
|
||||
f"Invalid --definition path: {definition} does not exist.", 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 source
|
||||
|
||||
|
||||
def _read_declarative_flow_file(path: Path) -> str:
|
||||
try:
|
||||
source = 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
|
||||
return source
|
||||
|
||||
|
||||
def _parse_declarative_flow(
|
||||
flow_definition_cls: type[Any], source: str, *, source_path: Path
|
||||
) -> Any:
|
||||
if _looks_like_json(source):
|
||||
return flow_definition_cls.from_json(source, source_path=source_path)
|
||||
|
||||
return flow_definition_cls.from_yaml(source, source_path=source_path)
|
||||
|
||||
|
||||
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)
|
||||
@@ -1,113 +0,0 @@
|
||||
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)
|
||||
@@ -62,7 +62,7 @@ crewai create flow <name> --skip_provider # New flow project
|
||||
|
||||
# Running
|
||||
crewai run # Run crew or flow (auto-detects from pyproject.toml)
|
||||
crewai flow kickoff # Legacy flow execution
|
||||
crewai flow kickoff # Deprecated compatibility alias for crewai run
|
||||
|
||||
# Testing & training
|
||||
crewai test # Test crew (default: 2 iterations, gpt-4o-mini)
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a1"
|
||||
"crewai[tools]==1.14.8a2"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
5
lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore
vendored
Normal file
5
lib/cli/src/crewai_cli/templates/declarative_flow/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
.crewai/
|
||||
output/
|
||||
17
lib/cli/src/crewai_cli/templates/declarative_flow/README.md
Normal file
17
lib/cli/src/crewai_cli/templates/declarative_flow/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# {{name}} Flow
|
||||
|
||||
This project defines a declarative CrewAI Flow in `src/{{folder_name}}/flow.yaml`.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
crewai install
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
Edit the declarative flow definition at `src/{{folder_name}}/flow.yaml` to change the flow. Add reusable crews under `src/{{folder_name}}/crews/`, custom Python tools under `src/{{folder_name}}/tools/`, and shared knowledge files under `src/{{folder_name}}/knowledge/`.
|
||||
15
lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml
Normal file
15
lib/cli/src/crewai_cli/templates/declarative_flow/flow.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
schema: crewai.flow/v1
|
||||
name: {{flow_name}}
|
||||
description: A declarative CrewAI Flow.
|
||||
|
||||
state:
|
||||
type: dict
|
||||
default:
|
||||
topic: AI agents
|
||||
|
||||
methods:
|
||||
start:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: state.topic
|
||||
@@ -0,0 +1,20 @@
|
||||
[project]
|
||||
name = "{{folder_name}}"
|
||||
version = "0.1.0"
|
||||
description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a2"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/{{folder_name}}"]
|
||||
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
definition = "src/{{folder_name}}/flow.yaml"
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a1"
|
||||
"crewai[tools]==1.14.8a2"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a1"
|
||||
"crewai[tools]==1.14.8a2"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -132,7 +132,7 @@ def test_create_project_zip_excludes_symlinked_files(tmp_path: Path):
|
||||
assert names == {"pyproject.toml"}
|
||||
|
||||
|
||||
def test_create_project_zip_adds_json_project_wrapper(tmp_path: Path):
|
||||
def test_create_project_zip_preserves_json_project_shape(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
@@ -157,8 +157,6 @@ type = "crew"
|
||||
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)
|
||||
@@ -166,18 +164,50 @@ type = "crew"
|
||||
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
|
||||
assert all(not name.startswith("src/") for name in names)
|
||||
assert "run_crew" not in pyproject
|
||||
assert "json_crew =" not in pyproject
|
||||
assert "[project.scripts]" not in pyproject
|
||||
|
||||
|
||||
def test_create_project_zip_updates_existing_json_project_scripts(tmp_path: Path):
|
||||
def test_create_project_zip_keeps_json_project_root_shape(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "json_crew"
|
||||
version = "0.1.0"
|
||||
dependencies = ["crewai[tools]==1.14.8a1"]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
(tmp_path / "uv.lock").write_text("# lock\n")
|
||||
(tmp_path / "agents").mkdir()
|
||||
(tmp_path / "agents" / "foo.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())
|
||||
pyproject = archive.read("pyproject.toml").decode()
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert names == {
|
||||
"agents/foo.jsonc",
|
||||
"crew.jsonc",
|
||||
"pyproject.toml",
|
||||
"uv.lock",
|
||||
}
|
||||
assert "run_crew" not in pyproject
|
||||
assert "json_crew =" not in pyproject
|
||||
assert "[project.scripts]" not in pyproject
|
||||
|
||||
|
||||
def test_create_project_zip_does_not_rewrite_json_project_scripts(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
@@ -203,14 +233,10 @@ type = "crew"
|
||||
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 'json_crew = "old.module:run"' in pyproject
|
||||
assert 'run_crew = "old.module:run"' in pyproject
|
||||
assert 'custom = "custom.module:main"' in pyproject
|
||||
assert "old.module:run" not in pyproject
|
||||
assert pyproject.count("[project.scripts]") == 1
|
||||
assert "[tool.crewai]" in pyproject
|
||||
|
||||
|
||||
@@ -221,7 +247,7 @@ type = "crew"
|
||||
'[tool]\ncrewai = "invalid"\n',
|
||||
],
|
||||
)
|
||||
def test_create_project_zip_adds_json_wrapper_for_malformed_tool_config(
|
||||
def test_create_project_zip_preserves_json_project_with_malformed_tool_config(
|
||||
tmp_path: Path, tool_config: str
|
||||
):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
@@ -244,12 +270,13 @@ version = "0.1.0"
|
||||
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
|
||||
assert names == {"crew.jsonc", "pyproject.toml"}
|
||||
assert "run_crew" not in pyproject
|
||||
assert "json_crew =" not in pyproject
|
||||
assert "[project.scripts]" not in pyproject
|
||||
|
||||
|
||||
def test_create_project_zip_rejects_empty_normalized_package_name(tmp_path: Path):
|
||||
def test_create_project_zip_accepts_json_project_without_package_name(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
@@ -263,8 +290,15 @@ type = "crew"
|
||||
)
|
||||
(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)
|
||||
archive_path = create_project_zip("invalid", 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 names == {"crew.jsonc", "pyproject.toml"}
|
||||
assert "run_crew" not in pyproject
|
||||
assert "json_crew =" not in pyproject
|
||||
assert "[project.scripts]" not in pyproject
|
||||
|
||||
@@ -200,6 +200,41 @@ def test_json_runtime_fields_are_deploy_errors(tmp_path: Path) -> None:
|
||||
assert "runtime-only" in finding.detail
|
||||
|
||||
|
||||
def test_json_crew_requires_agents_dir_without_classic_errors(tmp_path: Path) -> None:
|
||||
_scaffold_json_crew(tmp_path)
|
||||
for path in (tmp_path / "agents").iterdir():
|
||||
path.unlink()
|
||||
(tmp_path / "agents").rmdir()
|
||||
|
||||
v = DeployValidator(project_root=tmp_path)
|
||||
v.run()
|
||||
|
||||
codes = _codes(v)
|
||||
assert "missing_agents_dir" in codes
|
||||
assert "missing_src_dir" not in codes
|
||||
assert "missing_crew_py" not in codes
|
||||
assert "missing_agents_yaml" not in codes
|
||||
assert "missing_tasks_yaml" not in codes
|
||||
|
||||
|
||||
def test_json_crew_reports_project_metadata_before_invalid_json(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_scaffold_json_crew(tmp_path)
|
||||
(tmp_path / "pyproject.toml").unlink()
|
||||
(tmp_path / "uv.lock").unlink()
|
||||
(tmp_path / "crew.jsonc").write_text('{"agents": ["researcher"], "tasks": []}\n')
|
||||
|
||||
v = DeployValidator(project_root=tmp_path)
|
||||
v.run()
|
||||
|
||||
codes = _codes(v)
|
||||
assert "missing_pyproject" in codes
|
||||
assert "missing_lockfile" in codes
|
||||
assert "invalid_crew_json" in codes
|
||||
assert "missing_src_dir" not in codes
|
||||
|
||||
|
||||
def test_missing_pyproject_errors(tmp_path: Path) -> None:
|
||||
v = _run_without_import_check(tmp_path)
|
||||
assert "missing_pyproject" in _codes(v)
|
||||
|
||||
@@ -12,6 +12,7 @@ from crewai_cli.cli import (
|
||||
deploy_remove,
|
||||
deply_status,
|
||||
flow_add_crew,
|
||||
flow_run,
|
||||
login,
|
||||
reset_memories,
|
||||
run,
|
||||
@@ -126,38 +127,75 @@ 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)
|
||||
run_crew.assert_called_once_with(
|
||||
trained_agents_file=None,
|
||||
definition=None,
|
||||
inputs=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):
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
def test_run_with_definition_uses_project_runner(run_crew, 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"}'
|
||||
run_crew.assert_called_once_with(
|
||||
trained_agents_file=None,
|
||||
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):
|
||||
def test_run_rejects_inputs_without_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.run_crew")
|
||||
def test_run_rejects_filename_with_definition(run_crew, runner):
|
||||
result = runner.invoke(run, ["--definition", "flow.yaml", "--filename", "x.pkl"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "Error: --filename can only be used when running crews" in result.output
|
||||
run_crew.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
def test_run_passes_filename_to_project_runner(run_crew, runner):
|
||||
result = runner.invoke(run, ["--filename", "trained.pkl"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
run_crew.assert_called_once_with(
|
||||
trained_agents_file="trained.pkl",
|
||||
definition=None,
|
||||
inputs=None,
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
def test_flow_kickoff_is_deprecated_and_uses_run_path(run_crew, runner):
|
||||
result = runner.invoke(flow_run)
|
||||
|
||||
assert result.exit_code == 0
|
||||
run_crew.assert_called_once_with(
|
||||
trained_agents_file=None,
|
||||
definition=None,
|
||||
inputs=None,
|
||||
)
|
||||
assert (
|
||||
"The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead."
|
||||
in result.output
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.create_json_crew.create_json_crew")
|
||||
def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner):
|
||||
result = runner.invoke(create, ["crew", "DMN Crew"], env={"CREWAI_DMN": "True"})
|
||||
@@ -166,6 +204,23 @@ def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner
|
||||
create_json_crew.assert_called_once_with("DMN Crew", None, True)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.create_flow.create_flow")
|
||||
def test_create_flow_declarative_uses_declarative_scaffold(create_flow, runner):
|
||||
result = runner.invoke(create, ["flow", "My Flow", "--declarative"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
create_flow.assert_called_once_with("My Flow", declarative=True)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.create_json_crew.create_json_crew")
|
||||
def test_create_crew_rejects_declarative_flag(create_json_crew, runner):
|
||||
result = runner.invoke(create, ["crew", "My Crew", "--declarative"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "--declarative can only be used with flow projects" in result.output
|
||||
create_json_crew.assert_not_called()
|
||||
|
||||
|
||||
def test_create_requires_type_in_dmn_mode(runner):
|
||||
result = runner.invoke(create, env={"CREWAI_DMN": "True"})
|
||||
|
||||
|
||||
@@ -712,8 +712,26 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
|
||||
default_llm="openai/gpt-5.5",
|
||||
)
|
||||
assert (tmp_path / "json_crew" / "crew.jsonc").exists()
|
||||
assert not (tmp_path / "json_crew" / "src").exists()
|
||||
assert not (tmp_path / "json_crew" / "tests").exists()
|
||||
assert not (tmp_path / "json_crew" / "config.jsonc").exists()
|
||||
generated_paths = {
|
||||
path.relative_to(tmp_path / "json_crew").as_posix()
|
||||
for path in (tmp_path / "json_crew").rglob("*")
|
||||
if path.is_file()
|
||||
}
|
||||
assert not any(
|
||||
path.endswith("/crew.py") or path == "crew.py" for path in generated_paths
|
||||
)
|
||||
assert not any(
|
||||
path.endswith("/agents.yaml") or path == "agents.yaml"
|
||||
for path in generated_paths
|
||||
)
|
||||
assert not any(
|
||||
path.endswith("/tasks.yaml") or path == "tasks.yaml"
|
||||
for path in generated_paths
|
||||
)
|
||||
assert not any(path.startswith("src/") for path in generated_paths)
|
||||
|
||||
pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text())
|
||||
dependency = pyproject["project"]["dependencies"][0]
|
||||
@@ -849,7 +867,7 @@ def test_json_create_dmn_mode_uses_non_interactive_defaults(tmp_path, monkeypatc
|
||||
crew_template = (project_root / "crew.jsonc").read_text()
|
||||
agent_template = (project_root / "agents" / "researcher.jsonc").read_text()
|
||||
|
||||
assert '"memory": false' in crew_template
|
||||
assert '"memory": true' in crew_template
|
||||
assert '"description": "Research current AI trends and write a concise summary."' in (
|
||||
crew_template
|
||||
)
|
||||
|
||||
35
lib/cli/tests/test_create_flow.py
Normal file
35
lib/cli/tests/test_create_flow.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
from pytest import MonkeyPatch
|
||||
import tomli
|
||||
|
||||
from crewai_cli.cli import crewai
|
||||
from crewai_cli.create_flow import create_flow
|
||||
|
||||
|
||||
def test_create_flow_declarative_project_can_run(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
create_flow("Research Flow", declarative=True)
|
||||
|
||||
project_root = tmp_path / "research_flow"
|
||||
assert project_root.is_dir()
|
||||
|
||||
pyproject = tomli.loads(
|
||||
(project_root / "pyproject.toml").read_text(encoding="utf-8")
|
||||
)
|
||||
assert pyproject["project"]["name"] == "research_flow"
|
||||
assert pyproject["project"]["requires-python"]
|
||||
assert pyproject["project"]["dependencies"]
|
||||
assert (project_root / pyproject["tool"]["crewai"]["definition"]).is_file()
|
||||
|
||||
monkeypatch.chdir(project_root)
|
||||
result = CliRunner().invoke(crewai, ["run"], env={"UV_RUN_RECURSION_DEPTH": "1"})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Running the Flow" not in result.output
|
||||
assert "AI agents" in result.output
|
||||
117
lib/cli/tests/test_flow_commands.py
Normal file
117
lib/cli/tests/test_flow_commands.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from crewai_cli.cli import flow_run
|
||||
import crewai_cli.plot_flow as plot_flow_module
|
||||
|
||||
|
||||
FLOW_YAML = """\
|
||||
schema: crewai.flow/v1
|
||||
name: TestFlow
|
||||
config:
|
||||
suppress_flow_events: true
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: "'AI'"
|
||||
"""
|
||||
|
||||
|
||||
def _write_flow_project(project_root: Path) -> None:
|
||||
(project_root / "flow.yaml").write_text(FLOW_YAML, encoding="utf-8")
|
||||
(project_root / "pyproject.toml").write_text(
|
||||
'[project]\nname = "demo"\n\n'
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_flow_kickoff_runs_configured_declarative_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_write_flow_project(tmp_path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
|
||||
|
||||
result = CliRunner().invoke(flow_run)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (
|
||||
"The command 'crewai flow kickoff' is deprecated. Use 'crewai run' instead."
|
||||
in result.output
|
||||
)
|
||||
assert "AI\n" in result.output
|
||||
assert "Running the Flow" not in result.output
|
||||
|
||||
|
||||
def test_plot_flow_runs_configured_declarative_definition(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
_write_flow_project(tmp_path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
|
||||
|
||||
plot_flow_module.plot_flow()
|
||||
|
||||
|
||||
def test_flow_kickoff_delegates_to_run_crew(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.cli.run_crew",
|
||||
lambda **kwargs: calls.append(kwargs),
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(flow_run)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert calls == [
|
||||
{"trained_agents_file": None, "definition": None, "inputs": None},
|
||||
]
|
||||
|
||||
|
||||
def test_plot_flow_keeps_python_entrypoint_without_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr(
|
||||
subprocess,
|
||||
"run",
|
||||
lambda command, **kwargs: subprocess_calls.append((command, kwargs)),
|
||||
)
|
||||
|
||||
plot_flow_module.plot_flow()
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
["uv", "run", "plot"],
|
||||
{"capture_output": False, "text": True, "check": True},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = " flow.yaml "\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
assert configured_project_declarative_flow() == "flow.yaml"
|
||||
@@ -568,3 +568,131 @@ def test_has_json_crew_true_without_pyproject(monkeypatch, tmp_path: Path):
|
||||
(tmp_path / "crew.jsonc").write_text("{}")
|
||||
|
||||
assert run_crew_module._has_json_crew() is True
|
||||
|
||||
|
||||
def test_run_crew_rejects_inputs_without_definition():
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
run_crew_module.run_crew(inputs='{"topic":"AI"}')
|
||||
|
||||
assert "--inputs requires --definition" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_crew_rejects_filename_with_explicit_definition():
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
run_crew_module.run_crew(
|
||||
trained_agents_file="trained.pkl",
|
||||
definition="flow.yaml",
|
||||
)
|
||||
|
||||
assert "--filename can only be used when running crews" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_crew_runs_explicit_declarative_definition(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
def fake_run_declarative_flow(definition: str, inputs: str | None = None):
|
||||
calls.append((definition, inputs))
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.run_declarative_flow",
|
||||
fake_run_declarative_flow,
|
||||
)
|
||||
|
||||
run_crew_module.run_crew(definition="flow.yaml", inputs='{"topic":"AI"}')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "experimental" not in captured.out.lower()
|
||||
assert calls == [("flow.yaml", '{"topic":"AI"}')]
|
||||
|
||||
|
||||
def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {"tool": {"crewai": {"type": "crew"}}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_execute_uv_script",
|
||||
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew(trained_agents_file="trained.pkl")
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
assert calls == [
|
||||
(
|
||||
"run_crew",
|
||||
{"entity_type": "crew", "trained_agents_file": "trained.pkl"},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {"tool": {"crewai": {"type": "flow"}}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_execute_uv_script",
|
||||
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
assert calls == [("kickoff", {"entity_type": "flow"})]
|
||||
|
||||
|
||||
def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {"tool": {"crewai": {"type": "flow"}}},
|
||||
)
|
||||
|
||||
with pytest.raises(click.UsageError) as exc_info:
|
||||
run_crew_module.run_crew(trained_agents_file="trained.pkl")
|
||||
|
||||
assert "--filename can only be used when running crews" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_crew_runs_configured_declarative_flow_project(monkeypatch, capsys):
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {
|
||||
"tool": {
|
||||
"crewai": {
|
||||
"type": "flow",
|
||||
"definition": "flow.yaml",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.run_declarative_flow_in_project_env",
|
||||
lambda definition, inputs=None: calls.append((definition, inputs)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_execute_uv_script",
|
||||
lambda *_args, **_kwargs: pytest.fail("declarative flows must not run kickoff"),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew()
|
||||
|
||||
assert capsys.readouterr().out == ""
|
||||
assert calls == [("flow.yaml", None)]
|
||||
|
||||
111
lib/cli/tests/test_run_declarative_flow.py
Normal file
111
lib/cli/tests/test_run_declarative_flow.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import crewai_cli.run_declarative_flow as run_declarative_flow_module
|
||||
|
||||
|
||||
FLOW_YAML = """\
|
||||
schema: crewai.flow/v1
|
||||
name: TestFlow
|
||||
config:
|
||||
suppress_flow_events: true
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: state.topic
|
||||
"""
|
||||
|
||||
|
||||
def test_run_declarative_flow_reads_definition_file(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(FLOW_YAML, encoding="utf-8")
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(definition_path), '{"topic":"AI"}'
|
||||
)
|
||||
|
||||
assert capsys.readouterr().out == "AI\n"
|
||||
|
||||
|
||||
def test_run_declarative_flow_rejects_non_object_inputs(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text(FLOW_YAML, encoding="utf-8")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(definition_path), '["not", "an", "object"]'
|
||||
)
|
||||
|
||||
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_run_declarative_flow_reports_missing_file(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow_module.run_declarative_flow("missing-flow.yaml")
|
||||
|
||||
assert (
|
||||
"Invalid --definition path: missing-flow.yaml does not exist."
|
||||
in capsys.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_project_env_uses_uv(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"build_env_with_all_tool_credentials",
|
||||
lambda: {"EXISTING": "value"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module.subprocess,
|
||||
"run",
|
||||
lambda command, **kwargs: subprocess_calls.append((command, kwargs)),
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow_in_project_env("flow.yaml")
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
["uv", "run", "crewai", "run"],
|
||||
{
|
||||
"capture_output": False,
|
||||
"text": True,
|
||||
"check": True,
|
||||
"env": {"EXISTING": "value"},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_process_inside_uv(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / "flow.yaml").write_text(FLOW_YAML, encoding="utf-8")
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow_in_project_env(
|
||||
"flow.yaml", '{"topic":"AI"}'
|
||||
)
|
||||
|
||||
assert capsys.readouterr().out == "AI\n"
|
||||
@@ -1,156 +0,0 @@
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.8a1"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.8a1"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests>=2.33.0,<3",
|
||||
"crewai==1.14.8a1",
|
||||
"crewai==1.14.8a2",
|
||||
"tiktoken>=0.8.0,<0.13",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
@@ -131,7 +131,7 @@ postgresql = [
|
||||
]
|
||||
bedrock = [
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"bedrock-agentcore>=0.1.0",
|
||||
"bedrock-agentcore>=1.7.0,<1.8.0",
|
||||
"playwright>=1.52.0",
|
||||
"nest-asyncio>=1.6.0",
|
||||
]
|
||||
|
||||
@@ -330,4 +330,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.14.8a1"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.8a1",
|
||||
"crewai-cli==1.14.8a1",
|
||||
"crewai-core==1.14.8a2",
|
||||
"crewai-cli==1.14.8a2",
|
||||
# Core Dependencies
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"openai>=2.30.0,<3",
|
||||
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.8a1",
|
||||
"crewai-tools==1.14.8a2",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken>=0.8.0,<0.13"
|
||||
@@ -78,8 +78,8 @@ qdrant = [
|
||||
"qdrant-client[fastembed]~=1.14.3",
|
||||
]
|
||||
aws = [
|
||||
"boto3~=1.42.79",
|
||||
"aiobotocore~=3.4.0",
|
||||
"boto3~=1.42.90",
|
||||
"aiobotocore~=3.5.0",
|
||||
]
|
||||
watson = [
|
||||
"ibm-watsonx-ai~=1.3.39",
|
||||
@@ -91,7 +91,7 @@ litellm = [
|
||||
"litellm>=1.84.0,<2",
|
||||
]
|
||||
bedrock = [
|
||||
"boto3~=1.42.79",
|
||||
"boto3~=1.42.90",
|
||||
]
|
||||
google-genai = [
|
||||
"google-genai~=1.65.0",
|
||||
|
||||
@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.14.8a1"
|
||||
__version__ = "1.14.8a2"
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
|
||||
@@ -57,6 +57,7 @@ from crewai.utilities.agent_utils import (
|
||||
convert_tools_to_openai_schema,
|
||||
enforce_rpm_limit,
|
||||
format_message_for_llm,
|
||||
format_native_tool_output_for_agent,
|
||||
get_llm_response,
|
||||
handle_agent_action_core,
|
||||
handle_context_length,
|
||||
@@ -907,19 +908,31 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
):
|
||||
max_usage_reached = True
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
output_tool = original_tool or structured_tool
|
||||
|
||||
from_cache = False
|
||||
result: str = "Tool not found"
|
||||
raw_tool_result: Any = result
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
if self.tools_handler and self.tools_handler.cache and output_tool is not None:
|
||||
cached_result = self.tools_handler.cache.read(
|
||||
tool=func_name, input=input_str
|
||||
)
|
||||
if cached_result is not None:
|
||||
result = (
|
||||
str(cached_result)
|
||||
if not isinstance(cached_result, str)
|
||||
else cached_result
|
||||
)
|
||||
raw_tool_result = cached_result
|
||||
result = format_native_tool_output_for_agent(output_tool, cached_result)
|
||||
from_cache = True
|
||||
|
||||
agent_key = getattr(self.agent, "key", "unknown") if self.agent else "unknown"
|
||||
@@ -938,18 +951,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
track_delegation_if_needed(func_name, args_dict or {}, self.task)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
hook_blocked = False
|
||||
before_hook_context = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
@@ -975,11 +976,18 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
raw_tool_result = result
|
||||
elif max_usage_reached and original_tool:
|
||||
result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore."
|
||||
elif not from_cache and func_name in available_functions:
|
||||
raw_tool_result = result
|
||||
elif (
|
||||
not from_cache
|
||||
and func_name in available_functions
|
||||
and output_tool is not None
|
||||
):
|
||||
try:
|
||||
raw_result = available_functions[func_name](**(args_dict or {}))
|
||||
raw_tool_result = raw_result
|
||||
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
should_cache = True
|
||||
@@ -996,11 +1004,10 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
|
||||
result = (
|
||||
str(raw_result) if not isinstance(raw_result, str) else raw_result
|
||||
)
|
||||
result = format_native_tool_output_for_agent(output_tool, raw_result)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool: {e}"
|
||||
raw_tool_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
crewai_event_bus.emit(
|
||||
@@ -1024,6 +1031,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
task=self.task,
|
||||
crew=self.crew,
|
||||
tool_result=result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
try:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -25,14 +26,14 @@ class ToolsHandler(BaseModel):
|
||||
def on_tool_use(
|
||||
self,
|
||||
calling: ToolCalling | InstructorToolCalling,
|
||||
output: str,
|
||||
output: Any,
|
||||
should_cache: bool = True,
|
||||
) -> None:
|
||||
"""Run when tool ends running.
|
||||
|
||||
Args:
|
||||
calling: The tool calling instance.
|
||||
output: The output from the tool execution.
|
||||
output: The raw output from the tool execution.
|
||||
should_cache: Whether to cache the tool output.
|
||||
"""
|
||||
self.last_used_tool = calling
|
||||
|
||||
@@ -373,9 +373,6 @@ To enable tracing, do any one of these:
|
||||
status: str = "running",
|
||||
) -> None:
|
||||
"""Show method status panel."""
|
||||
if not self.verbose:
|
||||
return
|
||||
|
||||
if status == "running":
|
||||
style = "yellow"
|
||||
panel_title = "🔄 Flow Method Running"
|
||||
|
||||
@@ -80,6 +80,7 @@ from crewai.utilities.agent_utils import (
|
||||
enforce_rpm_limit,
|
||||
extract_tool_call_info,
|
||||
format_message_for_llm,
|
||||
format_native_tool_output_for_agent,
|
||||
get_llm_response,
|
||||
handle_agent_action_core,
|
||||
handle_context_length,
|
||||
@@ -1905,19 +1906,32 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
):
|
||||
max_usage_reached = True
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
output_tool = original_tool or structured_tool
|
||||
|
||||
# Check cache before executing
|
||||
from_cache = False
|
||||
result = "Tool not found"
|
||||
raw_tool_result: Any = result
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
if self.tools_handler and self.tools_handler.cache and output_tool is not None:
|
||||
cached_result = self.tools_handler.cache.read(
|
||||
tool=func_name, input=input_str
|
||||
)
|
||||
if cached_result is not None:
|
||||
result = (
|
||||
str(cached_result)
|
||||
if not isinstance(cached_result, str)
|
||||
else cached_result
|
||||
)
|
||||
raw_tool_result = cached_result
|
||||
result = format_native_tool_output_for_agent(output_tool, cached_result)
|
||||
from_cache = True
|
||||
|
||||
# Emit tool usage started event
|
||||
@@ -1936,18 +1950,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
|
||||
track_delegation_if_needed(func_name, args_dict, self.task)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
if original_tool is not None:
|
||||
for structured in self.tools or []:
|
||||
if getattr(structured, "_original_tool", None) is original_tool:
|
||||
structured_tool = structured
|
||||
break
|
||||
if structured_tool is None:
|
||||
for structured in self.tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
hook_blocked = False
|
||||
before_hook_context = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
@@ -1973,12 +1975,13 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
elif not from_cache and not max_usage_reached:
|
||||
result = "Tool not found"
|
||||
raw_tool_result = result
|
||||
elif not from_cache and not max_usage_reached and output_tool is not None:
|
||||
if func_name in self._available_functions:
|
||||
try:
|
||||
tool_func = self._available_functions[func_name]
|
||||
raw_result = tool_func(**args_dict)
|
||||
raw_tool_result = raw_result
|
||||
|
||||
# Add to cache after successful execution (before string conversion)
|
||||
if self.tools_handler and self.tools_handler.cache:
|
||||
@@ -1992,14 +1995,12 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
|
||||
# Convert to string for message
|
||||
result = (
|
||||
str(raw_result)
|
||||
if not isinstance(raw_result, str)
|
||||
else raw_result
|
||||
result = format_native_tool_output_for_agent(
|
||||
output_tool, raw_result
|
||||
)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool: {e}"
|
||||
raw_tool_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
# Emit tool usage error event
|
||||
@@ -2021,6 +2022,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore."
|
||||
else:
|
||||
result = f"Tool '{func_name}' has reached its maximum usage limit and cannot be used anymore."
|
||||
raw_tool_result = result
|
||||
|
||||
# Execute after_tool_call hooks (even if blocked, to allow logging/monitoring)
|
||||
after_hook_context = ToolCallHookContext(
|
||||
@@ -2031,6 +2033,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
task=self.task,
|
||||
crew=self.crew,
|
||||
tool_result=result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,7 @@ from crewai.flow.conversation import (
|
||||
ConversationalInputs,
|
||||
)
|
||||
from crewai.flow.dsl import HumanFeedbackResult, human_feedback
|
||||
from crewai.flow.expressions import Expression
|
||||
from crewai.flow.flow import Flow, and_, listen, or_, router, start
|
||||
from crewai.flow.flow_config import flow_config
|
||||
from crewai.flow.input_provider import InputProvider, InputResponse
|
||||
@@ -26,6 +27,7 @@ __all__ = [
|
||||
"ConsoleProvider",
|
||||
"ConversationalConfig",
|
||||
"ConversationalInputs",
|
||||
"Expression",
|
||||
"Flow",
|
||||
"FlowStructure",
|
||||
"HumanFeedbackPending",
|
||||
|
||||
@@ -8,8 +8,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger
|
||||
from crewai.flow.dsl._utils import (
|
||||
P,
|
||||
R,
|
||||
_merge_flow_method_definition,
|
||||
_method_action,
|
||||
_set_flow_method_definition,
|
||||
)
|
||||
from crewai.flow.flow_definition import FlowMethodDefinition
|
||||
from crewai.flow.flow_wrappers import ListenMethod
|
||||
@@ -45,7 +45,7 @@ def listen(condition: FlowTrigger) -> FlowMethodDecorator:
|
||||
def decorator(func: Callable[P, R]) -> ListenMethod[P, R]:
|
||||
wrapper = ListenMethod(func)
|
||||
|
||||
_set_flow_method_definition(
|
||||
_merge_flow_method_definition(
|
||||
wrapper,
|
||||
FlowMethodDefinition(
|
||||
do=_method_action(func),
|
||||
|
||||
@@ -19,8 +19,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger
|
||||
from crewai.flow.dsl._utils import (
|
||||
P,
|
||||
R,
|
||||
_merge_flow_method_definition,
|
||||
_method_action,
|
||||
_set_flow_method_definition,
|
||||
)
|
||||
from crewai.flow.flow_definition import FlowMethodDefinition
|
||||
from crewai.flow.flow_wrappers import RouterMethod
|
||||
@@ -95,7 +95,7 @@ def _normalize_router_emit(value: Sequence[Any] | str) -> list[str]:
|
||||
|
||||
|
||||
def router(
|
||||
condition: FlowTrigger,
|
||||
condition: FlowTrigger | None = None,
|
||||
*,
|
||||
emit: Sequence[str] | str | None = None,
|
||||
) -> FlowMethodDecorator:
|
||||
@@ -107,6 +107,7 @@ def router(
|
||||
|
||||
Args:
|
||||
condition: Specifies when the router should execute. Can be:
|
||||
- None: no listen trigger, used when stacking with @start() or @listen()
|
||||
- str: Route label or method name that triggers this router
|
||||
- FlowCondition: Result from or_() or and_(), including nested conditions
|
||||
- Flow method reference: A method whose completion triggers this router
|
||||
@@ -146,14 +147,17 @@ def router(
|
||||
else:
|
||||
router_events = _get_router_return_events(func) or []
|
||||
|
||||
_set_flow_method_definition(
|
||||
method_definition_kwargs: dict[str, Any] = {
|
||||
"do": _method_action(func),
|
||||
"router": True,
|
||||
"emit": router_events or None,
|
||||
}
|
||||
if condition is not None:
|
||||
method_definition_kwargs["listen"] = _to_definition_condition(condition)
|
||||
|
||||
_merge_flow_method_definition(
|
||||
wrapper,
|
||||
FlowMethodDefinition(
|
||||
do=_method_action(func),
|
||||
listen=_to_definition_condition(condition),
|
||||
router=True,
|
||||
emit=router_events or None,
|
||||
),
|
||||
FlowMethodDefinition(**method_definition_kwargs),
|
||||
)
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ from crewai.flow.dsl._types import FlowMethodDecorator, FlowTrigger
|
||||
from crewai.flow.dsl._utils import (
|
||||
P,
|
||||
R,
|
||||
_merge_flow_method_definition,
|
||||
_method_action,
|
||||
_set_flow_method_definition,
|
||||
)
|
||||
from crewai.flow.flow_definition import FlowMethodDefinition
|
||||
from crewai.flow.flow_wrappers import StartMethod
|
||||
@@ -54,7 +54,7 @@ def start(
|
||||
def decorator(func: Callable[P, R]) -> StartMethod[P, R]:
|
||||
wrapper = StartMethod(func)
|
||||
|
||||
_set_flow_method_definition(
|
||||
_merge_flow_method_definition(
|
||||
wrapper,
|
||||
FlowMethodDefinition(
|
||||
do=_method_action(func),
|
||||
|
||||
@@ -106,6 +106,25 @@ def _get_flow_method_definition(method: Any) -> FlowMethodDefinition | None:
|
||||
return None
|
||||
|
||||
|
||||
def _merge_flow_method_definition(
|
||||
wrapper: FlowMethod[P, R],
|
||||
definition: FlowMethodDefinition,
|
||||
) -> None:
|
||||
existing = _get_flow_method_definition(wrapper)
|
||||
if existing is None:
|
||||
_set_flow_method_definition(wrapper, definition)
|
||||
return
|
||||
|
||||
updates = {
|
||||
field_name: getattr(definition, field_name)
|
||||
for field_name in definition.model_fields_set
|
||||
}
|
||||
_set_flow_method_definition(
|
||||
wrapper,
|
||||
existing.model_copy(deep=True, update=updates),
|
||||
)
|
||||
|
||||
|
||||
def _is_json_serializable(value: Any) -> bool:
|
||||
try:
|
||||
json.dumps(value)
|
||||
|
||||
329
lib/crewai/src/crewai/flow/expressions.py
Normal file
329
lib/crewai/src/crewai/flow/expressions.py
Normal file
@@ -0,0 +1,329 @@
|
||||
"""Runtime expression support for FlowDefinition CEL expressions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias, cast
|
||||
|
||||
from crewai.utilities.serialization import to_serializable
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.runtime import Flow
|
||||
else:
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
|
||||
_CEL_MACROS_WITH_LOCAL_BINDINGS = frozenset(
|
||||
{"all", "exists", "exists_one", "filter", "map"}
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
ExpressionData: TypeAlias = (
|
||||
str
|
||||
| int
|
||||
| float
|
||||
| bool
|
||||
| None
|
||||
| list["ExpressionData"]
|
||||
| dict[str, "ExpressionData"]
|
||||
)
|
||||
else:
|
||||
ExpressionData = TypeAliasType(
|
||||
"ExpressionData",
|
||||
str
|
||||
| int
|
||||
| float
|
||||
| bool
|
||||
| None
|
||||
| list["ExpressionData"]
|
||||
| dict[str, "ExpressionData"],
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Expression",
|
||||
"ExpressionData",
|
||||
"ExpressionError",
|
||||
]
|
||||
|
||||
|
||||
class ExpressionError(ValueError):
|
||||
"""An expression failed to parse, validate, render, or evaluate."""
|
||||
|
||||
|
||||
class Expression:
|
||||
"""CEL expression helper used for definition-time checks and runtime rendering."""
|
||||
|
||||
def __init__(
|
||||
self, value: ExpressionData, *, context: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
self.value = value
|
||||
self.context = context
|
||||
|
||||
@classmethod
|
||||
def from_flow(
|
||||
cls,
|
||||
value: ExpressionData,
|
||||
flow: Flow[Any],
|
||||
*,
|
||||
local_context: dict[str, Any] | None = None,
|
||||
) -> Expression:
|
||||
"""Build an expression with the standard Flow runtime context."""
|
||||
return cls(value, context=cls._flow_context(flow, local_context=local_context))
|
||||
|
||||
def validate_expression(
|
||||
self,
|
||||
*,
|
||||
allowed_roots: Iterable[str],
|
||||
source: str = "CEL expression",
|
||||
) -> None:
|
||||
"""Validate a full CEL expression without evaluating it."""
|
||||
allowed = frozenset(allowed_roots)
|
||||
expression = self._require_cel_source(cast(str, self.value), source=source)
|
||||
roots = self._collect_root_identifiers(
|
||||
self._compile_cel(expression, source=source)
|
||||
)
|
||||
unknown = sorted(root for root in roots if root not in allowed)
|
||||
if unknown:
|
||||
allowed_list = ", ".join(sorted(allowed))
|
||||
unknown_list = ", ".join(repr(root) for root in unknown)
|
||||
raise ExpressionError(
|
||||
f"unknown CEL root at {source}: {unknown_list}; "
|
||||
f"allowed roots: {allowed_list}. Reference flow data through one "
|
||||
"of those roots, for example state.field or outputs.step_name."
|
||||
)
|
||||
|
||||
def validate_template(
|
||||
self,
|
||||
*,
|
||||
allowed_roots: Iterable[str],
|
||||
source: str = "with block",
|
||||
) -> None:
|
||||
"""Validate nested strings fully wrapped in ``${...}`` as CEL."""
|
||||
self._validate_template_value(
|
||||
self.value, allowed_roots=allowed_roots, source=source
|
||||
)
|
||||
|
||||
def evaluate(self, context: dict[str, Any] | None = None) -> Any:
|
||||
"""Evaluate this value as a full CEL expression."""
|
||||
resolved_context = self.context if context is None else context
|
||||
return self._evaluate_cel(
|
||||
self._require_cel_source(cast(str, self.value)),
|
||||
resolved_context or {},
|
||||
)
|
||||
|
||||
def render_template(self, context: dict[str, Any] | None = None) -> Any:
|
||||
"""Evaluate nested strings fully wrapped in ``${...}`` as CEL."""
|
||||
resolved_context = self.context if context is None else context
|
||||
return self._render_template_value(self.value, resolved_context or {})
|
||||
|
||||
@staticmethod
|
||||
def _validate_template_value(
|
||||
value: ExpressionData,
|
||||
*,
|
||||
allowed_roots: Iterable[str],
|
||||
source: str,
|
||||
) -> None:
|
||||
if isinstance(value, str):
|
||||
expression = Expression._expression_marker_source(value, source=source)
|
||||
if expression is not None:
|
||||
Expression(expression).validate_expression(
|
||||
allowed_roots=allowed_roots, source=source
|
||||
)
|
||||
return
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
item_source = f"{source}.{key}" if isinstance(key, str) else source
|
||||
Expression._validate_template_value(
|
||||
item, allowed_roots=allowed_roots, source=item_source
|
||||
)
|
||||
return
|
||||
if isinstance(value, list):
|
||||
for index, item in enumerate(value):
|
||||
Expression._validate_template_value(
|
||||
item,
|
||||
allowed_roots=allowed_roots,
|
||||
source=f"{source}[{index}]",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _flow_context(
|
||||
flow: Flow[Any], local_context: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
from crewai.flow.runtime._outputs import outputs_by_name
|
||||
|
||||
local_outputs = local_context.get("outputs") if local_context else None
|
||||
outputs = outputs_by_name(
|
||||
flow._method_outputs,
|
||||
local_outputs=local_outputs,
|
||||
serialize=True,
|
||||
)
|
||||
context: dict[str, Any] = {
|
||||
"state": flow._copy_and_serialize_state(),
|
||||
"outputs": outputs,
|
||||
}
|
||||
if local_context:
|
||||
context.update(
|
||||
{
|
||||
key: to_serializable(value, max_depth=0)
|
||||
for key, value in local_context.items()
|
||||
if key not in {"outputs", "state"}
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
def _render_template_value(value: ExpressionData, context: dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
return Expression._render_template_string(value, context)
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: Expression._render_template_value(item, context)
|
||||
for key, item in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [Expression._render_template_value(item, context) for item in value]
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _render_template_string(value: str, context: dict[str, Any]) -> Any:
|
||||
expression = Expression._expression_marker_source(value)
|
||||
if expression is None:
|
||||
return value
|
||||
return Expression._evaluate_cel(expression, context)
|
||||
|
||||
@staticmethod
|
||||
def _expression_marker_source(
|
||||
value: str, *, source: str | None = None
|
||||
) -> str | None:
|
||||
"""Return CEL source when the trimmed string starts with ``${`` and ends with ``}``."""
|
||||
stripped = value.strip()
|
||||
if not stripped.startswith("${"):
|
||||
return None
|
||||
if not stripped.endswith("}"):
|
||||
return None
|
||||
|
||||
expression = stripped[2:-1].strip()
|
||||
if not expression:
|
||||
if source is None:
|
||||
raise ExpressionError("empty CEL expression in with block")
|
||||
raise ExpressionError(f"empty CEL expression at {source}")
|
||||
return expression
|
||||
|
||||
@staticmethod
|
||||
def _evaluate_cel(expression: str, context: dict[str, Any]) -> Any:
|
||||
try:
|
||||
from celpy import Environment
|
||||
from celpy.adapter import CELJSONEncoder, json_to_cel
|
||||
from celpy.evaluation import Context
|
||||
|
||||
environment = Environment()
|
||||
program = environment.program(
|
||||
Expression._compile_cel(expression, environment=environment)
|
||||
)
|
||||
result = program.evaluate(cast(Context, json_to_cel(context)))
|
||||
return json.loads(json.dumps(result, cls=CELJSONEncoder))
|
||||
except Exception as e:
|
||||
raise ExpressionError(
|
||||
f"failed to evaluate CEL expression {expression!r}: {e}"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def _compile_cel(
|
||||
expression: str,
|
||||
*,
|
||||
source: str | None = None,
|
||||
environment: Any | None = None,
|
||||
) -> Any:
|
||||
if environment is None:
|
||||
from celpy import Environment
|
||||
|
||||
environment = Environment()
|
||||
try:
|
||||
return environment.compile(expression)
|
||||
except Exception as e:
|
||||
if source is None:
|
||||
raise
|
||||
raise ExpressionError(
|
||||
f"invalid CEL expression at {source}: {expression!r}. "
|
||||
f"Check the CEL syntax. Parser details: {e}"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def _require_cel_source(value: str, *, source: str | None = None) -> str:
|
||||
expression = value.strip()
|
||||
if expression.startswith("${") and expression.endswith("}"):
|
||||
expression = expression[2:-1].strip()
|
||||
if expression:
|
||||
return expression
|
||||
if source is None:
|
||||
raise ExpressionError("empty CEL expression")
|
||||
raise ExpressionError(
|
||||
f"empty CEL expression at {source}. Provide a CEL expression such as "
|
||||
"state.topic or outputs.step_name."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _collect_root_identifiers(
|
||||
tree: Any, local_roots: frozenset[str] = frozenset()
|
||||
) -> set[str]:
|
||||
"""Collect CEL root identifiers, excluding receiver macro local variables."""
|
||||
data = getattr(tree, "data", None)
|
||||
children = list(getattr(tree, "children", []) or [])
|
||||
|
||||
if data == "ident" and children:
|
||||
name = str(children[0])
|
||||
return set() if name in local_roots else {name}
|
||||
|
||||
if data == "ident_arg":
|
||||
return Expression._collect_root_identifiers_from(
|
||||
children[1:], local_roots=local_roots
|
||||
)
|
||||
|
||||
if data == "member_dot_arg":
|
||||
roots = (
|
||||
Expression._collect_root_identifiers(children[0], local_roots)
|
||||
if children
|
||||
else set()
|
||||
)
|
||||
nested_locals = frozenset(
|
||||
{*local_roots, *Expression._receiver_macro_local_roots(children)}
|
||||
)
|
||||
roots.update(
|
||||
Expression._collect_root_identifiers_from(
|
||||
children[2:], local_roots=nested_locals
|
||||
)
|
||||
)
|
||||
return roots
|
||||
|
||||
return Expression._collect_root_identifiers_from(
|
||||
children, local_roots=local_roots
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _collect_root_identifiers_from(
|
||||
trees: Iterable[Any], *, local_roots: frozenset[str]
|
||||
) -> set[str]:
|
||||
return set().union(
|
||||
*(Expression._collect_root_identifiers(tree, local_roots) for tree in trees)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _receiver_macro_local_roots(children: list[Any]) -> set[str]:
|
||||
if len(children) < 3 or str(children[1]) not in _CEL_MACROS_WITH_LOCAL_BINDINGS:
|
||||
return set()
|
||||
exprlist = children[2]
|
||||
exprs = list(getattr(exprlist, "children", []) or [])
|
||||
if exprs and (name := Expression._single_identifier_name(exprs[0])):
|
||||
return {name}
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def _single_identifier_name(tree: Any) -> str | None:
|
||||
data = getattr(tree, "data", None)
|
||||
children = list(getattr(tree, "children", []) or [])
|
||||
if data == "ident" and children:
|
||||
return str(children[0])
|
||||
if len(children) != 1:
|
||||
return None
|
||||
return Expression._single_identifier_name(children[0])
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Flow Structure: the serializable, language-agnostic Flow contract.
|
||||
"""Flow Definition: the serializable, declarative Flow contract.
|
||||
|
||||
Defines :class:`FlowDefinition` and its sub-models — a static, textual
|
||||
Defines :class:`FlowDefinition` and its sub-models — a static, declarative
|
||||
(JSON/YAML) representation of a Flow: its methods, trigger conditions,
|
||||
state, and configuration. It is independent of the Python authoring
|
||||
layer that may have produced it and of the engine that runs it (see
|
||||
@@ -11,13 +11,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Annotated, Any, Literal, TypeAlias
|
||||
from typing import Annotated, Any, Literal, TypeAlias, cast
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
field_serializer,
|
||||
model_validator,
|
||||
)
|
||||
@@ -27,16 +29,20 @@ from crewai.flow.conversational_definition import (
|
||||
FlowConversationalDefinition,
|
||||
FlowConversationalRouterDefinition,
|
||||
)
|
||||
from crewai.project.crew_definition import CrewDefinition
|
||||
from crewai.flow.expressions import ExpressionData
|
||||
from crewai.project.crew_definition import AgentDefinition, CrewDefinition
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FlowDefinitionCondition = str | dict[str, Any]
|
||||
_STEP_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
_BASE_CEL_ROOTS = frozenset({"outputs", "state"})
|
||||
_EACH_STEP_CEL_ROOTS = frozenset({"item", "outputs", "state"})
|
||||
|
||||
__all__ = [
|
||||
"FlowActionDefinition",
|
||||
"FlowAgentActionDefinition",
|
||||
"FlowAtomicActionDefinition",
|
||||
"FlowCodeActionDefinition",
|
||||
"FlowConfigDefinition",
|
||||
@@ -353,10 +359,14 @@ class FlowCodeActionDefinition(BaseModel):
|
||||
description="Import reference for the callable, formatted as module:qualname.",
|
||||
examples=["my_project.flows:normalize_topic"],
|
||||
)
|
||||
with_: dict[str, Any] | None = Field(
|
||||
with_: dict[str, ExpressionData] | None = Field(
|
||||
default=None,
|
||||
alias="with",
|
||||
description="Keyword arguments passed to the callable after expression rendering.",
|
||||
description=(
|
||||
"Keyword arguments passed to the callable. String values are evaluated "
|
||||
"as CEL only when the trimmed value starts with ${ and ends with }; "
|
||||
"all other values are literal."
|
||||
),
|
||||
examples=[{"topic": "${state.topic}"}],
|
||||
)
|
||||
|
||||
@@ -377,10 +387,14 @@ class FlowToolActionDefinition(BaseModel):
|
||||
description="Import reference for a BaseTool class, formatted as module:qualname.",
|
||||
examples=["my_project.tools:SearchTool"],
|
||||
)
|
||||
with_: dict[str, Any] | None = Field(
|
||||
with_: dict[str, ExpressionData] | None = Field(
|
||||
default=None,
|
||||
alias="with",
|
||||
description="Tool input arguments after expression rendering.",
|
||||
description=(
|
||||
"Tool input arguments. String values are evaluated as CEL only when "
|
||||
"the trimmed value starts with ${ and ends with }; all other values "
|
||||
"are literal."
|
||||
),
|
||||
examples=[{"query": "${outputs.normalize_topic}", "limit": 5}],
|
||||
)
|
||||
|
||||
@@ -394,10 +408,19 @@ class FlowCrewActionDefinition(BaseModel):
|
||||
)
|
||||
|
||||
call: Literal["crew"] = Field(
|
||||
description="Action discriminator. Use crew to run an inline Crew definition.",
|
||||
description=(
|
||||
"Action discriminator. Use crew to run an inline or referenced Crew "
|
||||
"definition."
|
||||
),
|
||||
examples=["crew"],
|
||||
)
|
||||
with_: CrewDefinition = Field(
|
||||
from_declaration: str | None = Field(
|
||||
default=None,
|
||||
description="Path to a JSON/JSONC Crew declaration file or folder.",
|
||||
examples=["crews/research_crew"],
|
||||
)
|
||||
with_: CrewDefinition | None = Field(
|
||||
default=None,
|
||||
alias="with",
|
||||
description="Inline Crew definition to load and execute for this action.",
|
||||
examples=[
|
||||
@@ -418,7 +441,50 @@ class FlowCrewActionDefinition(BaseModel):
|
||||
"agent": "researcher",
|
||||
}
|
||||
],
|
||||
"inputs": {"topic": "${state.topic}"},
|
||||
}
|
||||
],
|
||||
)
|
||||
inputs: dict[str, ExpressionData] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Input overrides passed to the Crew. String values are evaluated as CEL "
|
||||
"only when the trimmed value starts with ${ and ends with }; all other "
|
||||
"values are literal."
|
||||
),
|
||||
examples=[{"topic": "${state.topic}"}],
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_crew_source(self) -> FlowCrewActionDefinition:
|
||||
if bool(self.from_declaration) == (self.with_ is not None):
|
||||
raise ValueError(
|
||||
"crew action requires exactly one of from_declaration or with"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class FlowAgentActionDefinition(BaseModel):
|
||||
"""A Flow method action that builds and kicks off a CrewAI agent."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
call: Literal["agent"] = Field(
|
||||
description="Action discriminator. Use agent to run an inline Agent definition.",
|
||||
examples=["agent"],
|
||||
)
|
||||
with_: AgentDefinition = Field(
|
||||
alias="with",
|
||||
description="Inline Agent definition to load and execute for this action.",
|
||||
examples=[
|
||||
{
|
||||
"role": "Analyst",
|
||||
"goal": "Answer user questions",
|
||||
"backstory": "Precise and concise.",
|
||||
"settings": {"llm": "openai/gpt-4o-mini"},
|
||||
"input": "${state.question}",
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -470,6 +536,7 @@ FlowAtomicActionDefinition: TypeAlias = Annotated[
|
||||
FlowCodeActionDefinition
|
||||
| FlowToolActionDefinition
|
||||
| FlowCrewActionDefinition
|
||||
| FlowAgentActionDefinition
|
||||
| FlowExpressionActionDefinition
|
||||
| FlowScriptActionDefinition,
|
||||
Field(discriminator="call"),
|
||||
@@ -562,6 +629,7 @@ FlowActionDefinition: TypeAlias = (
|
||||
FlowCodeActionDefinition
|
||||
| FlowToolActionDefinition
|
||||
| FlowCrewActionDefinition
|
||||
| FlowAgentActionDefinition
|
||||
| FlowExpressionActionDefinition
|
||||
| FlowScriptActionDefinition
|
||||
| FlowEachActionDefinition
|
||||
@@ -643,10 +711,12 @@ class FlowDefinition(BaseModel):
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
|
||||
_source_path: Path | None = PrivateAttr(default=None)
|
||||
|
||||
schema_: Literal["crewai.flow/v1"] = Field(
|
||||
default="crewai.flow/v1",
|
||||
alias="schema",
|
||||
description="Flow Definition schema identifier and version.",
|
||||
description="Declarative Flow schema identifier and version.",
|
||||
examples=["crewai.flow/v1"],
|
||||
)
|
||||
name: str = Field(
|
||||
@@ -696,6 +766,16 @@ class FlowDefinition(BaseModel):
|
||||
_validate_step_name(method_name, field="Flow method names")
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_cel_expressions(self) -> FlowDefinition:
|
||||
for method_name, method in self.methods.items():
|
||||
_validate_action_cel(
|
||||
method.do,
|
||||
path=f"methods.{method_name}.do",
|
||||
allowed_roots=_BASE_CEL_ROOTS,
|
||||
)
|
||||
return self
|
||||
|
||||
def to_dict(self, *, exclude_none: bool = True) -> dict[str, Any]:
|
||||
"""Serialize the definition to a JSON/YAML-ready dictionary."""
|
||||
return self.model_dump(by_alias=True, exclude_none=exclude_none, mode="json")
|
||||
@@ -713,29 +793,45 @@ class FlowDefinition(BaseModel):
|
||||
allow_unicode=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def source_path(self) -> Path | None:
|
||||
"""Original definition file path, when loaded from a file."""
|
||||
return self._source_path
|
||||
|
||||
@property
|
||||
def source_dir(self) -> Path | None:
|
||||
"""Directory used to resolve relative paths in the definition."""
|
||||
if self._source_path is None:
|
||||
return None
|
||||
return self._source_path.parent
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> FlowDefinition:
|
||||
def from_dict(
|
||||
cls, data: dict[str, Any], *, source_path: Path | None = None
|
||||
) -> FlowDefinition:
|
||||
"""Load a definition from a dictionary."""
|
||||
definition = cls.model_validate(data)
|
||||
if source_path is not None:
|
||||
definition._source_path = source_path.expanduser().resolve()
|
||||
log_flow_definition_issues(definition)
|
||||
return definition
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: str) -> FlowDefinition:
|
||||
def from_json(cls, data: str, *, source_path: Path | None = None) -> FlowDefinition:
|
||||
"""Load a definition from JSON."""
|
||||
return cls.from_dict(json.loads(data))
|
||||
return cls.from_dict(json.loads(data), source_path=source_path)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, data: str) -> FlowDefinition:
|
||||
def from_yaml(cls, data: str, *, source_path: Path | None = None) -> FlowDefinition:
|
||||
"""Load a definition from YAML."""
|
||||
loaded = yaml.safe_load(data) or {}
|
||||
if not isinstance(loaded, dict):
|
||||
raise ValueError("Flow definition YAML must contain a mapping")
|
||||
return cls.from_dict(loaded)
|
||||
return cls.from_dict(loaded, source_path=source_path)
|
||||
|
||||
@classmethod
|
||||
def json_schema(cls) -> dict[str, Any]:
|
||||
"""Return the JSON Schema for the Flow Definition contract."""
|
||||
"""Return the JSON Schema for the declarative Flow contract."""
|
||||
return cls.model_json_schema(by_alias=True)
|
||||
|
||||
|
||||
@@ -753,17 +849,78 @@ def _validate_step_list(steps: list[FlowEachStepDefinition], *, field: str) -> N
|
||||
seen.add(name)
|
||||
|
||||
|
||||
def _validate_action_cel(
|
||||
action: FlowActionDefinition,
|
||||
*,
|
||||
path: str,
|
||||
allowed_roots: frozenset[str],
|
||||
) -> None:
|
||||
from crewai.flow.expressions import Expression
|
||||
|
||||
if isinstance(action, FlowExpressionActionDefinition):
|
||||
Expression(action.expr).validate_expression(
|
||||
allowed_roots=allowed_roots, source=f"{path}.expr"
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(action, (FlowCodeActionDefinition, FlowToolActionDefinition)):
|
||||
if action.with_ is not None:
|
||||
Expression(action.with_).validate_template(
|
||||
allowed_roots=allowed_roots, source=f"{path}.with"
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(action, FlowCrewActionDefinition):
|
||||
if action.with_ is not None:
|
||||
Expression(cast(ExpressionData, action.with_.inputs)).validate_template(
|
||||
allowed_roots=allowed_roots,
|
||||
source=f"{path}.with.inputs",
|
||||
)
|
||||
if action.inputs is not None:
|
||||
Expression(cast(ExpressionData, action.inputs)).validate_template(
|
||||
allowed_roots=allowed_roots,
|
||||
source=f"{path}.inputs",
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(action, FlowAgentActionDefinition):
|
||||
Expression(cast(ExpressionData, action.with_.input)).validate_template(
|
||||
allowed_roots=allowed_roots,
|
||||
source=f"{path}.with.input",
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(action, FlowEachActionDefinition):
|
||||
Expression(action.in_).validate_expression(
|
||||
allowed_roots=_BASE_CEL_ROOTS,
|
||||
source=f"{path}.in",
|
||||
)
|
||||
for index, step in enumerate(action.do):
|
||||
step_path = f"{path}.do[{index}]"
|
||||
if step.if_ is not None:
|
||||
Expression(step.if_).validate_expression(
|
||||
allowed_roots=_EACH_STEP_CEL_ROOTS,
|
||||
source=f"{step_path}.if",
|
||||
)
|
||||
_validate_action_cel(
|
||||
step.action,
|
||||
path=f"{step_path}.action",
|
||||
allowed_roots=_EACH_STEP_CEL_ROOTS,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(action, FlowScriptActionDefinition):
|
||||
return
|
||||
|
||||
raise TypeError(
|
||||
f"no CEL validation defined for action type {type(action).__name__} at "
|
||||
f"{path}; add a branch to _validate_action_cel for it."
|
||||
)
|
||||
|
||||
|
||||
def log_flow_definition_issues(definition: FlowDefinition) -> None:
|
||||
for method_name, method in definition.methods.items():
|
||||
path = f"methods.{method_name}"
|
||||
if method.router and not method.is_start and method.listen is None:
|
||||
_log_flow_definition_issue(
|
||||
definition.name,
|
||||
code="router_without_trigger",
|
||||
severity="error",
|
||||
path=path,
|
||||
message="router: true requires either start or listen",
|
||||
)
|
||||
if method.emit and not method.router:
|
||||
_log_flow_definition_issue(
|
||||
definition.name,
|
||||
|
||||
@@ -2455,11 +2455,6 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
object.__setattr__(
|
||||
self, "_deferred_flow_started_event_id", started_event.event_id
|
||||
)
|
||||
if not self.suppress_flow_events:
|
||||
self._log_flow_event(
|
||||
f"Flow started with ID: {self.flow_id}", color="bold magenta"
|
||||
)
|
||||
|
||||
# After FlowStarted: env events must not pre-empt trace batch init
|
||||
# with implicit "crew" execution_type.
|
||||
get_env_context()
|
||||
@@ -3007,6 +3002,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
"""
|
||||
# First, handle routers repeatedly until no router triggers anymore
|
||||
router_results = []
|
||||
router_result_payloads: dict[str, Any] = {}
|
||||
router_result_to_feedback: dict[
|
||||
str, Any
|
||||
] = {} # Map outcome -> HumanFeedbackResult
|
||||
@@ -3044,6 +3040,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
router_result_str = str(router_result)
|
||||
router_result_event = FlowMethodName(router_result_str)
|
||||
router_results.append(router_result_event)
|
||||
router_result_payloads[router_result_str] = (
|
||||
self.last_human_feedback
|
||||
if self.last_human_feedback is not None
|
||||
else router_result
|
||||
)
|
||||
|
||||
if self.last_human_feedback is not None:
|
||||
router_result_to_feedback[router_result_str] = (
|
||||
@@ -3064,7 +3065,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
current_trigger, router_only=False
|
||||
)
|
||||
if listeners_triggered:
|
||||
listener_result = router_result_to_feedback.get(
|
||||
listener_result = router_result_payloads.get(
|
||||
str(current_trigger), result
|
||||
)
|
||||
racing_group = self._get_racing_group_for_listeners(
|
||||
|
||||
@@ -8,10 +8,13 @@ from collections.abc import Awaitable, Callable
|
||||
import contextvars
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||
|
||||
from crewai.flow.expressions import Expression, ExpressionData
|
||||
from crewai.flow.flow_definition import (
|
||||
FlowActionDefinition,
|
||||
FlowAgentActionDefinition,
|
||||
FlowCodeActionDefinition,
|
||||
FlowCrewActionDefinition,
|
||||
FlowEachActionDefinition,
|
||||
@@ -20,7 +23,6 @@ from crewai.flow.flow_definition import (
|
||||
FlowScriptActionDefinition,
|
||||
FlowToolActionDefinition,
|
||||
)
|
||||
from crewai.flow.runtime._expressions import evaluate_expression, render_with_block
|
||||
from crewai.flow.runtime._outputs import outputs_by_name
|
||||
from crewai.flow.runtime._refs import InvalidRefError, resolve_ref
|
||||
|
||||
@@ -67,9 +69,9 @@ class CodeAction:
|
||||
if self.definition.with_ is None:
|
||||
return self.handler(*args, **kwargs)
|
||||
return self.handler(
|
||||
**render_with_block(
|
||||
self.flow, self.definition.with_, local_context=local_context
|
||||
)
|
||||
**Expression.from_flow(
|
||||
self.definition.with_, self.flow, local_context=local_context
|
||||
).render_template()
|
||||
)
|
||||
|
||||
def _resolve_handler(self) -> Callable[..., Any]:
|
||||
@@ -95,7 +97,9 @@ class ToolAction:
|
||||
def run(self, *_args: Any, **kwargs: Any) -> Any:
|
||||
local_context = _pop_local_context(kwargs)
|
||||
return self.tool.run(
|
||||
**render_with_block(self.flow, self.kwargs, local_context=local_context)
|
||||
**Expression.from_flow(
|
||||
self.kwargs, self.flow, local_context=local_context
|
||||
).render_template()
|
||||
)
|
||||
|
||||
def _build_tool(self) -> Any:
|
||||
@@ -125,17 +129,66 @@ class CrewAction:
|
||||
self.definition = definition
|
||||
|
||||
async def run(self, *_args: Any, **kwargs: Any) -> Any:
|
||||
from crewai.project.crew_loader import load_crew_from_definition
|
||||
from crewai.project.crew_loader import load_crew, load_crew_from_definition
|
||||
|
||||
local_context = _pop_local_context(kwargs)
|
||||
crew_definition = self.definition.with_
|
||||
inputs = render_with_block(
|
||||
self.flow, crew_definition.inputs, local_context=local_context
|
||||
)
|
||||
crew, _ = load_crew_from_definition(crew_definition, source="crew action")
|
||||
if self.definition.from_declaration is not None:
|
||||
crew, default_inputs = load_crew(
|
||||
_resolve_crew_declaration(
|
||||
self.definition.from_declaration,
|
||||
base_dir=self.flow._definition.source_dir,
|
||||
)
|
||||
)
|
||||
input_template = {**default_inputs, **(self.definition.inputs or {})}
|
||||
else:
|
||||
crew_definition = self.definition.with_
|
||||
if crew_definition is None:
|
||||
raise ValueError(
|
||||
"crew action requires exactly one of from_declaration or with"
|
||||
)
|
||||
input_template = {
|
||||
**crew_definition.inputs,
|
||||
**(self.definition.inputs or {}),
|
||||
}
|
||||
crew, _ = load_crew_from_definition(crew_definition, source="crew action")
|
||||
|
||||
inputs = Expression.from_flow(
|
||||
cast(ExpressionData, input_template),
|
||||
self.flow,
|
||||
local_context=local_context,
|
||||
).render_template()
|
||||
return await crew.kickoff_async(inputs=inputs)
|
||||
|
||||
|
||||
class AgentAction:
|
||||
definition_type = FlowAgentActionDefinition
|
||||
|
||||
def __init__(self, flow: Flow[Any], definition: FlowAgentActionDefinition) -> None:
|
||||
self.flow = flow
|
||||
self.definition = definition
|
||||
|
||||
async def run(self, *_args: Any, **kwargs: Any) -> Any:
|
||||
from crewai.project.json_loader import load_agent_from_definition
|
||||
|
||||
local_context = _pop_local_context(kwargs)
|
||||
rendered_input = Expression.from_flow(
|
||||
cast(ExpressionData, self.definition.with_.input),
|
||||
self.flow,
|
||||
local_context=local_context,
|
||||
).render_template()
|
||||
if not isinstance(rendered_input, str):
|
||||
raise ValueError("agent input must render to a string")
|
||||
|
||||
agent, response_format = load_agent_from_definition(
|
||||
self.definition.with_,
|
||||
source="agent action",
|
||||
)
|
||||
return await agent.kickoff_async(
|
||||
rendered_input,
|
||||
response_format=response_format,
|
||||
)
|
||||
|
||||
|
||||
class ExpressionAction:
|
||||
definition_type = FlowExpressionActionDefinition
|
||||
|
||||
@@ -147,9 +200,9 @@ class ExpressionAction:
|
||||
|
||||
def run(self, *_args: Any, **kwargs: Any) -> Any:
|
||||
local_context = _pop_local_context(kwargs)
|
||||
return evaluate_expression(
|
||||
self.flow, self.definition.expr, local_context=local_context
|
||||
)
|
||||
return Expression.from_flow(
|
||||
self.definition.expr, self.flow, local_context=local_context
|
||||
).evaluate()
|
||||
|
||||
|
||||
class ScriptAction:
|
||||
@@ -225,7 +278,7 @@ class EachAction:
|
||||
]
|
||||
|
||||
async def run(self, *_args: Any, **_kwargs: Any) -> list[Any]:
|
||||
items = evaluate_expression(self.flow, self.definition.in_)
|
||||
items = Expression.from_flow(self.definition.in_, self.flow).evaluate()
|
||||
if not isinstance(items, list):
|
||||
raise ValueError("each.in must evaluate to an array")
|
||||
|
||||
@@ -248,7 +301,9 @@ class EachAction:
|
||||
return results
|
||||
|
||||
def _condition_matches(self, condition: str, local_context: LocalContext) -> bool:
|
||||
result = evaluate_expression(self.flow, condition, local_context=local_context)
|
||||
result = Expression.from_flow(
|
||||
condition, self.flow, local_context=local_context
|
||||
).evaluate()
|
||||
if not isinstance(result, bool):
|
||||
raise ValueError("if expression must evaluate to a boolean")
|
||||
return result
|
||||
@@ -278,6 +333,7 @@ _ACTION_TYPES: tuple[_ActionType, ...] = (
|
||||
EachAction,
|
||||
CodeAction,
|
||||
ToolAction,
|
||||
AgentAction,
|
||||
CrewAction,
|
||||
ExpressionAction,
|
||||
ScriptAction,
|
||||
@@ -322,3 +378,29 @@ def _pop_local_context(kwargs: dict[str, Any]) -> LocalContext | None:
|
||||
if not isinstance(local_context, dict):
|
||||
raise TypeError("flow definition local context must be a mapping")
|
||||
return cast(LocalContext, local_context)
|
||||
|
||||
|
||||
def _resolve_crew_declaration(
|
||||
from_declaration: str, *, base_dir: Path | None = None
|
||||
) -> Path:
|
||||
path = Path(from_declaration).expanduser()
|
||||
if base_dir is not None:
|
||||
resolved_base_dir = base_dir.expanduser().resolve()
|
||||
if not path.is_absolute():
|
||||
path = resolved_base_dir / path
|
||||
resolved_path = path.resolve()
|
||||
if not resolved_path.is_relative_to(resolved_base_dir):
|
||||
raise ValueError(
|
||||
"crew declaration path must be within the flow definition directory"
|
||||
)
|
||||
path = resolved_path
|
||||
|
||||
if not path.is_dir():
|
||||
return path
|
||||
|
||||
for name in ("crew.jsonc", "crew.json"):
|
||||
candidate = path / name
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
|
||||
return path / "crew.jsonc"
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
"""Runtime expression support for FlowDefinition CEL expressions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import pairwise
|
||||
import json
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from crewai.flow.runtime._outputs import outputs_by_name
|
||||
from crewai.utilities.serialization import to_serializable
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.runtime import Flow
|
||||
|
||||
|
||||
_EXPRESSION_PATTERN = re.compile(r"\$\{([^{}]*)\}")
|
||||
|
||||
__all__ = ["FlowExpressionError", "evaluate_expression", "render_with_block"]
|
||||
|
||||
|
||||
class FlowExpressionError(ValueError):
|
||||
"""A FlowDefinition expression failed to parse or evaluate."""
|
||||
|
||||
|
||||
def render_with_block(
|
||||
flow: Flow[Any], value: Any, local_context: dict[str, Any] | None = None
|
||||
) -> Any:
|
||||
"""Render CEL expressions inside a FlowDefinition ``with:`` payload."""
|
||||
context = _expression_context(flow, local_context=local_context)
|
||||
return _render_value(value, context)
|
||||
|
||||
|
||||
def evaluate_expression(
|
||||
flow: Flow[Any], expression: str, local_context: dict[str, Any] | None = None
|
||||
) -> Any:
|
||||
"""Evaluate a FlowDefinition CEL expression against runtime context."""
|
||||
expression = expression.strip()
|
||||
if not expression:
|
||||
raise FlowExpressionError("empty CEL expression")
|
||||
return _eval_cel(expression, _expression_context(flow, local_context=local_context))
|
||||
|
||||
|
||||
def _expression_context(
|
||||
flow: Flow[Any], local_context: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
local_outputs = local_context.get("outputs") if local_context else None
|
||||
outputs = outputs_by_name(
|
||||
flow._method_outputs,
|
||||
local_outputs=local_outputs,
|
||||
serialize=True,
|
||||
)
|
||||
context: dict[str, Any] = {
|
||||
"state": flow._copy_and_serialize_state(),
|
||||
"outputs": outputs,
|
||||
}
|
||||
if local_context:
|
||||
local_values = {
|
||||
key: to_serializable(value, max_depth=0)
|
||||
for key, value in local_context.items()
|
||||
if key not in {"outputs", "state"}
|
||||
}
|
||||
context.update(local_values)
|
||||
return context
|
||||
|
||||
|
||||
def _render_value(value: Any, context: dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
return _render_string(value, context)
|
||||
if isinstance(value, dict):
|
||||
return {key: _render_value(item, context) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_render_value(item, context) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _render_string(value: str, context: dict[str, Any]) -> Any:
|
||||
matches = list(_EXPRESSION_PATTERN.finditer(value))
|
||||
if not matches:
|
||||
_raise_for_invalid_interpolation(value)
|
||||
return value
|
||||
|
||||
_raise_for_literal_braces(value[: matches[0].start()])
|
||||
for previous, current in pairwise(matches):
|
||||
_raise_for_literal_braces(value[previous.end() : current.start()])
|
||||
_raise_for_literal_braces(value[matches[-1].end() :])
|
||||
|
||||
if len(matches) == 1 and matches[0].span() == (0, len(value)):
|
||||
expression = matches[0].group(1).strip()
|
||||
if not expression:
|
||||
raise FlowExpressionError("empty CEL expression in with block")
|
||||
return _eval_cel(expression, context)
|
||||
|
||||
rendered: list[str] = []
|
||||
position = 0
|
||||
for match in matches:
|
||||
start, end = match.span()
|
||||
literal = value[position:start]
|
||||
rendered.append(literal)
|
||||
|
||||
expression = match.group(1).strip()
|
||||
if not expression:
|
||||
raise FlowExpressionError("empty CEL expression in with block")
|
||||
result = _eval_cel(expression, context)
|
||||
rendered.append(result if isinstance(result, str) else json.dumps(result))
|
||||
position = end
|
||||
|
||||
literal = value[position:]
|
||||
rendered.append(literal)
|
||||
|
||||
return "".join(rendered)
|
||||
|
||||
|
||||
def _raise_for_invalid_interpolation(value: str) -> None:
|
||||
if "${" not in value:
|
||||
return
|
||||
raise FlowExpressionError(
|
||||
"invalid CEL interpolation in with block: expressions must be enclosed "
|
||||
"as ${...} and cannot contain braces"
|
||||
)
|
||||
|
||||
|
||||
def _raise_for_literal_braces(value: str) -> None:
|
||||
if "{" not in value and "}" not in value:
|
||||
return
|
||||
raise FlowExpressionError(
|
||||
"invalid CEL interpolation in with block: expressions must be enclosed "
|
||||
"as ${...} and cannot contain braces"
|
||||
)
|
||||
|
||||
|
||||
def _eval_cel(expression: str, context: dict[str, Any]) -> Any:
|
||||
try:
|
||||
from celpy import Environment
|
||||
from celpy.adapter import CELJSONEncoder, json_to_cel
|
||||
from celpy.evaluation import Context
|
||||
|
||||
environment = Environment()
|
||||
program = environment.program(environment.compile(expression))
|
||||
result = program.evaluate(cast(Context, json_to_cel(context)))
|
||||
return json.loads(json.dumps(result, cls=CELJSONEncoder))
|
||||
except Exception as e:
|
||||
raise FlowExpressionError(
|
||||
f"failed to evaluate CEL expression {expression!r}: {e}"
|
||||
) from e
|
||||
@@ -40,6 +40,8 @@ class ToolCallHookContext:
|
||||
crew: Crew instance (may be None)
|
||||
tool_result: Tool execution result (only set for after_tool_call hooks).
|
||||
Can be modified by returning a new string from after_tool_call hook.
|
||||
raw_tool_result: Raw Python tool execution result (only set for
|
||||
after_tool_call hooks). This is not modified by after hooks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -51,6 +53,7 @@ class ToolCallHookContext:
|
||||
task: Task | None = None,
|
||||
crew: Crew | None = None,
|
||||
tool_result: str | None = None,
|
||||
raw_tool_result: Any | None = None,
|
||||
) -> None:
|
||||
"""Initialize tool call hook context.
|
||||
|
||||
@@ -62,6 +65,7 @@ class ToolCallHookContext:
|
||||
task: Optional current task
|
||||
crew: Optional crew instance
|
||||
tool_result: Optional tool result (for after hooks)
|
||||
raw_tool_result: Optional raw tool result (for after hooks)
|
||||
"""
|
||||
self.tool_name = tool_name
|
||||
self.tool_input = tool_input
|
||||
@@ -70,6 +74,7 @@ class ToolCallHookContext:
|
||||
self.task = task
|
||||
self.crew = crew
|
||||
self.tool_result = tool_result
|
||||
self.raw_tool_result = raw_tool_result
|
||||
|
||||
def request_human_input(
|
||||
self,
|
||||
|
||||
@@ -15,16 +15,22 @@ from crewai.project.annotations import (
|
||||
)
|
||||
from crewai.project.crew_base import CrewBase
|
||||
from crewai.project.crew_definition import (
|
||||
AgentDefinition,
|
||||
CrewAgentDefinition,
|
||||
CrewDefinition,
|
||||
CrewTaskDefinition,
|
||||
PythonReferenceDefinition,
|
||||
)
|
||||
from crewai.project.crew_loader import load_crew, load_crew_and_kickoff
|
||||
from crewai.project.json_loader import load_agent, strip_jsonc_comments
|
||||
from crewai.project.json_loader import (
|
||||
load_agent,
|
||||
load_agent_from_definition,
|
||||
strip_jsonc_comments,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentDefinition",
|
||||
"CrewAgentDefinition",
|
||||
"CrewBase",
|
||||
"CrewDefinition",
|
||||
@@ -38,6 +44,7 @@ __all__ = [
|
||||
"crew",
|
||||
"llm",
|
||||
"load_agent",
|
||||
"load_agent_from_definition",
|
||||
"load_crew",
|
||||
"load_crew_and_kickoff",
|
||||
"output_json",
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentDefinition",
|
||||
"CrewAgentDefinition",
|
||||
"CrewDefinition",
|
||||
"CrewTaskDefinition",
|
||||
@@ -53,6 +54,20 @@ class CrewAgentDefinition(BaseModel):
|
||||
return value or {}
|
||||
|
||||
|
||||
class AgentDefinition(CrewAgentDefinition):
|
||||
"""Inline agent definition used by a Flow agent action."""
|
||||
|
||||
input: str
|
||||
response_format: PythonReferenceDefinition | None = None
|
||||
|
||||
@field_validator("input", mode="before")
|
||||
@classmethod
|
||||
def _validate_input(cls, value: Any) -> Any:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("agent.input must be a string")
|
||||
return value
|
||||
|
||||
|
||||
class CrewTaskDefinition(BaseModel):
|
||||
"""Task definition used by a crew definition."""
|
||||
|
||||
|
||||
@@ -207,19 +207,18 @@ def load_jsonc_file(source: str | Path) -> Any:
|
||||
return parse_jsonc(path.read_text(encoding="utf-8"), source=path)
|
||||
|
||||
|
||||
def load_agent(source: str | Path) -> Any:
|
||||
"""Load an existing ``Agent`` from a ``.json`` / ``.jsonc`` definition file."""
|
||||
path = Path(source)
|
||||
defn = _expect_object(load_jsonc_file(path), path)
|
||||
root = path.parent.parent if path.parent.name == "agents" else path.parent
|
||||
def _instantiate_agent_from_data(
|
||||
defn: dict[str, Any], source_label: str, root: Path
|
||||
) -> Any:
|
||||
"""Resolve the agent class and kwargs from definition data and instantiate it."""
|
||||
agent_class = _agent_class_from_definition(
|
||||
defn,
|
||||
f"{path}: type",
|
||||
f"{source_label}: type",
|
||||
project_root=root,
|
||||
)
|
||||
agent_kwargs = _agent_kwargs_from_definition(
|
||||
defn,
|
||||
path,
|
||||
source_label,
|
||||
agent_class=agent_class,
|
||||
project_root=root,
|
||||
)
|
||||
@@ -227,9 +226,50 @@ def load_agent(source: str | Path) -> Any:
|
||||
try:
|
||||
return agent_class(**agent_kwargs)
|
||||
except ValidationError as exc:
|
||||
raise JSONProjectError(_format_validation_error(path, exc)) from exc
|
||||
raise JSONProjectError(_format_validation_error(source_label, exc)) from exc
|
||||
except Exception as exc:
|
||||
raise JSONProjectError(f"{path}: failed to load agent: {exc}") from exc
|
||||
raise JSONProjectError(f"{source_label}: failed to load agent: {exc}") from exc
|
||||
|
||||
|
||||
def load_agent(source: str | Path) -> Any:
|
||||
"""Load an existing ``Agent`` from a ``.json`` / ``.jsonc`` definition file."""
|
||||
path = Path(source)
|
||||
defn = _expect_object(load_jsonc_file(path), path)
|
||||
root = path.parent.parent if path.parent.name == "agents" else path.parent
|
||||
return _instantiate_agent_from_data(defn, str(path), root)
|
||||
|
||||
|
||||
def load_agent_from_definition(
|
||||
definition: dict[str, Any] | Any,
|
||||
*,
|
||||
source: str | Path = "<inline agent>",
|
||||
project_root: str | Path | None = None,
|
||||
) -> tuple[Any, type[BaseModel] | None]:
|
||||
"""Load an ``Agent`` and optional kickoff response model from an inline definition."""
|
||||
from crewai.project.crew_definition import AgentDefinition
|
||||
|
||||
root = Path(project_root) if project_root is not None else Path.cwd()
|
||||
source_label = str(source)
|
||||
agent_definition = (
|
||||
definition
|
||||
if isinstance(definition, AgentDefinition)
|
||||
else AgentDefinition.model_validate(definition)
|
||||
)
|
||||
definition_data = agent_definition.model_dump(mode="python", exclude_none=True)
|
||||
response_format_ref = definition_data.pop("response_format", None)
|
||||
definition_data.pop("input", None)
|
||||
|
||||
agent = _instantiate_agent_from_data(definition_data, source_label, root)
|
||||
|
||||
response_format = None
|
||||
if response_format_ref is not None:
|
||||
response_format = _resolve_model_class(
|
||||
response_format_ref,
|
||||
f"{source_label}: response_format",
|
||||
root,
|
||||
)
|
||||
|
||||
return agent, response_format
|
||||
|
||||
|
||||
def validate_crew_project(
|
||||
|
||||
@@ -33,6 +33,8 @@ from typing_extensions import TypeIs
|
||||
from crewai.tools.structured_tool import (
|
||||
CrewStructuredTool,
|
||||
_deserialize_schema,
|
||||
_format_tool_output_for_agent,
|
||||
_infer_result_schema_from_callable,
|
||||
_serialize_schema,
|
||||
build_schema_hint,
|
||||
)
|
||||
@@ -149,6 +151,11 @@ class BaseTool(BaseModel, ABC):
|
||||
validate_default=True,
|
||||
description="The schema for the arguments that the tool accepts.",
|
||||
)
|
||||
result_schema: type[PydanticBaseModel] | None = Field(
|
||||
default=None,
|
||||
validate_default=True,
|
||||
description="The schema for the output that the tool returns.",
|
||||
)
|
||||
|
||||
@field_serializer("args_schema", when_used="json")
|
||||
def _serialize_args_schema(
|
||||
@@ -156,6 +163,12 @@ class BaseTool(BaseModel, ABC):
|
||||
) -> dict[str, Any] | None:
|
||||
return _serialize_schema(schema)
|
||||
|
||||
@field_serializer("result_schema", when_used="json")
|
||||
def _serialize_result_schema(
|
||||
self, schema: type[PydanticBaseModel] | None
|
||||
) -> dict[str, Any] | None:
|
||||
return _serialize_schema(schema)
|
||||
|
||||
description_updated: bool = Field(
|
||||
default=False, description="Flag to check if the description has been updated."
|
||||
)
|
||||
@@ -233,6 +246,17 @@ class BaseTool(BaseModel, ABC):
|
||||
|
||||
return create_model(f"{cls.__name__}Schema", **fields)
|
||||
|
||||
@field_validator("result_schema", mode="before")
|
||||
@classmethod
|
||||
def _default_result_schema(
|
||||
cls, v: type[PydanticBaseModel] | dict[str, Any] | None
|
||||
) -> type[PydanticBaseModel] | None:
|
||||
if isinstance(v, dict):
|
||||
return _deserialize_schema(v)
|
||||
if v is not None:
|
||||
return v
|
||||
return _infer_result_schema_from_callable(cls._run)
|
||||
|
||||
@field_validator("max_usage_count", mode="before")
|
||||
@classmethod
|
||||
def validate_max_usage_count(cls, v: int | None) -> int | None:
|
||||
@@ -340,6 +364,10 @@ class BaseTool(BaseModel, ABC):
|
||||
"Override _arun for async support or use run() for sync execution."
|
||||
)
|
||||
|
||||
def format_output_for_agent(self, raw_result: Any) -> str:
|
||||
"""Format a raw tool result into the string representation sent to an agent."""
|
||||
return _format_tool_output_for_agent(self, raw_result)
|
||||
|
||||
def reset_usage_count(self) -> None:
|
||||
"""Reset the current usage count to zero."""
|
||||
self.current_usage_count = 0
|
||||
@@ -369,6 +397,7 @@ class BaseTool(BaseModel, ABC):
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
args_schema=self.args_schema,
|
||||
result_schema=self.result_schema,
|
||||
func=self._run,
|
||||
result_as_answer=self.result_as_answer,
|
||||
max_usage_count=self.max_usage_count,
|
||||
@@ -390,6 +419,9 @@ class BaseTool(BaseModel, ABC):
|
||||
raise ValueError("The provided tool must have a callable 'func' attribute.")
|
||||
|
||||
args_schema = getattr(tool, "args_schema", None)
|
||||
result_schema = getattr(tool, "result_schema", None)
|
||||
if result_schema is None:
|
||||
result_schema = _infer_result_schema_from_callable(tool.func)
|
||||
|
||||
if args_schema is None:
|
||||
func_signature = signature(tool.func)
|
||||
@@ -420,6 +452,7 @@ class BaseTool(BaseModel, ABC):
|
||||
description=getattr(tool, "description", ""),
|
||||
func=tool.func,
|
||||
args_schema=args_schema,
|
||||
result_schema=result_schema,
|
||||
)
|
||||
|
||||
def _set_args_schema(self) -> None:
|
||||
@@ -568,6 +601,9 @@ class Tool(BaseTool, Generic[P, R]):
|
||||
raise ValueError("The provided tool must have a callable 'func' attribute.")
|
||||
|
||||
args_schema = getattr(tool, "args_schema", None)
|
||||
result_schema = getattr(tool, "result_schema", None)
|
||||
if result_schema is None:
|
||||
result_schema = _infer_result_schema_from_callable(tool.func)
|
||||
|
||||
if args_schema is None:
|
||||
func_signature = signature(tool.func)
|
||||
@@ -598,6 +634,7 @@ class Tool(BaseTool, Generic[P, R]):
|
||||
description=getattr(tool, "description", ""),
|
||||
func=tool.func,
|
||||
args_schema=args_schema,
|
||||
result_schema=result_schema,
|
||||
)
|
||||
|
||||
|
||||
@@ -621,6 +658,7 @@ def tool(
|
||||
name: str,
|
||||
/,
|
||||
*,
|
||||
result_schema: type[BaseModel] | None = ...,
|
||||
result_as_answer: bool = ...,
|
||||
max_usage_count: int | None = ...,
|
||||
) -> Callable[[Callable[P2, R2]], Tool[P2, R2]]: ...
|
||||
@@ -629,6 +667,7 @@ def tool(
|
||||
@overload
|
||||
def tool(
|
||||
*,
|
||||
result_schema: type[BaseModel] | None = ...,
|
||||
result_as_answer: bool = ...,
|
||||
max_usage_count: int | None = ...,
|
||||
) -> Callable[[Callable[P2, R2]], Tool[P2, R2]]: ...
|
||||
@@ -636,6 +675,7 @@ def tool(
|
||||
|
||||
def tool(
|
||||
*args: Callable[P2, R2] | str,
|
||||
result_schema: type[BaseModel] | None = None,
|
||||
result_as_answer: bool = False,
|
||||
max_usage_count: int | None = None,
|
||||
) -> Tool[P2, R2] | Callable[[Callable[P2, R2]], Tool[P2, R2]]:
|
||||
@@ -649,6 +689,7 @@ def tool(
|
||||
Args:
|
||||
*args: Either the function to decorate or a custom tool name.
|
||||
result_as_answer: If True, the tool result becomes the final agent answer.
|
||||
result_schema: Optional schema for the output that the tool returns.
|
||||
max_usage_count: Maximum times this tool can be used. None means unlimited.
|
||||
|
||||
Returns:
|
||||
@@ -690,12 +731,16 @@ def tool(
|
||||
|
||||
class_name = "".join(tool_name.split()).title()
|
||||
args_schema = create_model(class_name, **fields)
|
||||
resolved_result_schema = (
|
||||
result_schema or _infer_result_schema_from_callable(f)
|
||||
)
|
||||
|
||||
return Tool(
|
||||
name=tool_name,
|
||||
description=f.__doc__,
|
||||
func=f,
|
||||
args_schema=args_schema,
|
||||
result_schema=resolved_result_schema,
|
||||
result_as_answer=result_as_answer,
|
||||
max_usage_count=max_usage_count,
|
||||
current_usage_count=0,
|
||||
|
||||
@@ -5,7 +5,8 @@ from collections.abc import Callable
|
||||
import inspect
|
||||
import json
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, Annotated, Any, get_type_hints
|
||||
from typing import TYPE_CHECKING, Annotated, Any, cast, get_type_hints
|
||||
import warnings
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -36,6 +37,52 @@ def _deserialize_schema(v: Any) -> type[BaseModel] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _infer_result_schema_from_callable(
|
||||
func: Callable[..., Any],
|
||||
) -> type[BaseModel] | None:
|
||||
try:
|
||||
return_annotation = get_type_hints(func).get("return", inspect.Signature.empty)
|
||||
except Exception:
|
||||
return_annotation = inspect.signature(func).return_annotation
|
||||
|
||||
if isinstance(return_annotation, type) and issubclass(return_annotation, BaseModel):
|
||||
return return_annotation
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _format_tool_output_for_agent(tool: Any, raw_result: Any) -> str:
|
||||
original_tool = getattr(tool, "_original_tool", None)
|
||||
if original_tool is not None:
|
||||
return cast(str, original_tool.format_output_for_agent(raw_result))
|
||||
|
||||
result_schema = getattr(tool, "result_schema", None)
|
||||
if not (isinstance(result_schema, type) and issubclass(result_schema, BaseModel)):
|
||||
return str(raw_result)
|
||||
|
||||
try:
|
||||
validation_input = raw_result
|
||||
if isinstance(raw_result, BaseModel) and not isinstance(
|
||||
raw_result, result_schema
|
||||
):
|
||||
validation_input = raw_result.model_dump()
|
||||
|
||||
validated = result_schema.model_validate(validation_input)
|
||||
return validated.model_dump_json()
|
||||
except Exception as exc:
|
||||
warnings.warn(
|
||||
(
|
||||
f"Failed to validate or serialize output from tool "
|
||||
f"'{getattr(tool, 'name', '<unknown>')}' using result_schema "
|
||||
f"'{result_schema.__name__}': {exc.__class__.__name__}. "
|
||||
"Falling back to str(raw_result)."
|
||||
),
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return str(raw_result)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
@@ -81,6 +128,11 @@ class CrewStructuredTool(BaseModel):
|
||||
BeforeValidator(_deserialize_schema),
|
||||
PlainSerializer(_serialize_schema),
|
||||
] = Field(default=None)
|
||||
result_schema: Annotated[
|
||||
type[BaseModel] | None,
|
||||
BeforeValidator(_deserialize_schema),
|
||||
PlainSerializer(_serialize_schema),
|
||||
] = Field(default=None)
|
||||
func: Any = Field(default=None, exclude=True)
|
||||
result_as_answer: bool = Field(default=False)
|
||||
max_usage_count: int | None = Field(default=None)
|
||||
@@ -103,6 +155,7 @@ class CrewStructuredTool(BaseModel):
|
||||
description: str | None = None,
|
||||
return_direct: bool = False,
|
||||
args_schema: type[BaseModel] | None = None,
|
||||
result_schema: type[BaseModel] | None = None,
|
||||
infer_schema: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> CrewStructuredTool:
|
||||
@@ -114,6 +167,7 @@ class CrewStructuredTool(BaseModel):
|
||||
description: The description of the tool. Defaults to the function docstring
|
||||
return_direct: Whether to return the output directly
|
||||
args_schema: Optional schema for the function arguments
|
||||
result_schema: Optional schema for the function output
|
||||
infer_schema: Whether to infer the schema from the function signature
|
||||
**kwargs: Additional arguments to pass to the tool
|
||||
|
||||
@@ -149,10 +203,16 @@ class CrewStructuredTool(BaseModel):
|
||||
name=name,
|
||||
description=description,
|
||||
args_schema=schema,
|
||||
result_schema=result_schema or _infer_result_schema_from_callable(func),
|
||||
func=func,
|
||||
result_as_answer=return_direct,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def format_output_for_agent(self, raw_result: Any) -> str:
|
||||
"""Format a raw tool result into the string representation sent to an agent."""
|
||||
return _format_tool_output_for_agent(self, raw_result)
|
||||
|
||||
@staticmethod
|
||||
def _create_schema_from_function(
|
||||
name: str,
|
||||
|
||||
@@ -62,6 +62,9 @@ OPENAI_BIGGER_MODELS: list[
|
||||
]
|
||||
|
||||
|
||||
_RAW_RESULT_UNSET = object()
|
||||
|
||||
|
||||
class ToolUsageError(Exception):
|
||||
"""Exception raised for errors in the tool usage."""
|
||||
|
||||
@@ -106,6 +109,7 @@ class ToolUsage:
|
||||
self.action = action
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.fingerprint_context = fingerprint_context or {}
|
||||
self.last_raw_result: Any = _RAW_RESULT_UNSET
|
||||
|
||||
if (
|
||||
self.function_calling_llm
|
||||
@@ -120,6 +124,11 @@ class ToolUsage:
|
||||
"""Parse the tool string and return the tool calling."""
|
||||
return self._tool_calling(tool_string)
|
||||
|
||||
def get_last_raw_result(self, fallback: Any) -> Any:
|
||||
if self.last_raw_result is _RAW_RESULT_UNSET:
|
||||
return fallback
|
||||
return self.last_raw_result
|
||||
|
||||
def use(
|
||||
self, calling: ToolCalling | InstructorToolCalling, tool_string: str
|
||||
) -> str:
|
||||
@@ -231,6 +240,7 @@ class ToolUsage:
|
||||
result = I18N_DEFAULT.errors("task_repeated_usage").format(
|
||||
tool_names=self.tools_names
|
||||
)
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_repeated_usage(
|
||||
llm=self.function_calling_llm,
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
@@ -298,6 +308,7 @@ class ToolUsage:
|
||||
)
|
||||
if usage_limit_error:
|
||||
result = usage_limit_error
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
|
||||
result = self._format_result(result=result)
|
||||
elif result is None:
|
||||
@@ -359,7 +370,10 @@ class ToolUsage:
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
attempts=self._run_attempts,
|
||||
)
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
data = {
|
||||
"result": result,
|
||||
"tool_name": sanitize_tool_name(tool.name),
|
||||
@@ -421,6 +435,7 @@ class ToolUsage:
|
||||
result = ToolUsageError(
|
||||
f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
|
||||
).message
|
||||
self.last_raw_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
@@ -430,7 +445,10 @@ class ToolUsage:
|
||||
self.task.increment_tools_errors()
|
||||
should_retry = True
|
||||
else:
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
|
||||
finally:
|
||||
if started_event_emitted and not error_event_emitted:
|
||||
@@ -460,6 +478,7 @@ class ToolUsage:
|
||||
result = I18N_DEFAULT.errors("task_repeated_usage").format(
|
||||
tool_names=self.tools_names
|
||||
)
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_repeated_usage(
|
||||
llm=self.function_calling_llm,
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
@@ -529,6 +548,7 @@ class ToolUsage:
|
||||
)
|
||||
if usage_limit_error:
|
||||
result = usage_limit_error
|
||||
self.last_raw_result = result
|
||||
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
|
||||
result = self._format_result(result=result)
|
||||
elif result is None:
|
||||
@@ -590,7 +610,10 @@ class ToolUsage:
|
||||
tool_name=sanitize_tool_name(tool.name),
|
||||
attempts=self._run_attempts,
|
||||
)
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
data = {
|
||||
"result": result,
|
||||
"tool_name": sanitize_tool_name(tool.name),
|
||||
@@ -652,6 +675,7 @@ class ToolUsage:
|
||||
result = ToolUsageError(
|
||||
f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
|
||||
).message
|
||||
self.last_raw_result = result
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
@@ -661,7 +685,10 @@ class ToolUsage:
|
||||
self.task.increment_tools_errors()
|
||||
should_retry = True
|
||||
else:
|
||||
result = self._format_result(result=result)
|
||||
self.last_raw_result = result
|
||||
result = self._format_result(
|
||||
result=tool.format_output_for_agent(result)
|
||||
)
|
||||
|
||||
finally:
|
||||
if started_event_emitted and not error_event_emitted:
|
||||
|
||||
@@ -1383,6 +1383,19 @@ class NativeToolCallResult:
|
||||
tool_message: LLMMessage = field(default_factory=dict) # type: ignore[assignment]
|
||||
|
||||
|
||||
def format_native_tool_output_for_agent(tool: Any, raw_result: Any) -> str:
|
||||
"""Format native tool output when a tool explicitly defines a formatter."""
|
||||
formatter = inspect.getattr_static(tool, "format_output_for_agent", None)
|
||||
if formatter is None:
|
||||
return str(raw_result)
|
||||
|
||||
runtime_formatter = getattr(tool, "format_output_for_agent", None)
|
||||
if not callable(runtime_formatter):
|
||||
return str(raw_result)
|
||||
|
||||
return str(runtime_formatter(raw_result))
|
||||
|
||||
|
||||
def execute_single_native_tool_call(
|
||||
tool_call: Any,
|
||||
*,
|
||||
@@ -1456,18 +1469,24 @@ def execute_single_native_tool_call(
|
||||
original_tool = tool
|
||||
break
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
for structured in structured_tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
output_tool = original_tool or structured_tool
|
||||
|
||||
from_cache = False
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
result = "Tool not found"
|
||||
raw_tool_result: Any = result
|
||||
|
||||
if tools_handler and tools_handler.cache:
|
||||
if tools_handler and tools_handler.cache and output_tool is not None:
|
||||
cached_result = tools_handler.cache.read(tool=func_name, input=input_str)
|
||||
if cached_result is not None:
|
||||
result = (
|
||||
str(cached_result)
|
||||
if not isinstance(cached_result, str)
|
||||
else cached_result
|
||||
)
|
||||
raw_tool_result = cached_result
|
||||
result = format_native_tool_output_for_agent(output_tool, cached_result)
|
||||
from_cache = True
|
||||
|
||||
started_at = datetime.now()
|
||||
@@ -1486,12 +1505,6 @@ def execute_single_native_tool_call(
|
||||
|
||||
track_delegation_if_needed(func_name, args_dict, task)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = None
|
||||
for structured in structured_tools or []:
|
||||
if sanitize_tool_name(structured.name) == func_name:
|
||||
structured_tool = structured
|
||||
break
|
||||
|
||||
hook_blocked = False
|
||||
before_hook_context = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
@@ -1512,11 +1525,13 @@ def execute_single_native_tool_call(
|
||||
error_event_emitted = False
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
raw_tool_result = result
|
||||
elif not from_cache:
|
||||
if func_name in available_functions:
|
||||
if func_name in available_functions and output_tool is not None:
|
||||
try:
|
||||
tool_func = available_functions[func_name]
|
||||
raw_result = tool_func(**args_dict)
|
||||
raw_tool_result = raw_result
|
||||
|
||||
if tools_handler and tools_handler.cache:
|
||||
should_cache = True
|
||||
@@ -1529,11 +1544,10 @@ def execute_single_native_tool_call(
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
|
||||
result = (
|
||||
str(raw_result) if not isinstance(raw_result, str) else raw_result
|
||||
)
|
||||
result = format_native_tool_output_for_agent(output_tool, raw_result)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool: {e}"
|
||||
raw_tool_result = result
|
||||
if task:
|
||||
task.increment_tools_errors()
|
||||
crewai_event_bus.emit(
|
||||
@@ -1559,6 +1573,7 @@ def execute_single_native_tool_call(
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
try:
|
||||
for after_hook in get_after_tool_call_hooks():
|
||||
|
||||
@@ -116,6 +116,7 @@ async def aexecute_tool_and_check_finality(
|
||||
logger.log("error", f"Error in before_tool_call hook: {e}")
|
||||
|
||||
tool_result = await tool_usage.ause(tool_calling, agent_action.text)
|
||||
raw_tool_result = tool_usage.get_last_raw_result(tool_result)
|
||||
|
||||
after_hook_context = ToolCallHookContext(
|
||||
tool_name=sanitized_tool_name,
|
||||
@@ -125,6 +126,7 @@ async def aexecute_tool_and_check_finality(
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=tool_result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
@@ -234,6 +236,7 @@ def execute_tool_and_check_finality(
|
||||
logger.log("error", f"Error in before_tool_call hook: {e}")
|
||||
|
||||
tool_result = tool_usage.use(tool_calling, agent_action.text)
|
||||
raw_tool_result = tool_usage.get_last_raw_result(tool_result)
|
||||
|
||||
after_hook_context = ToolCallHookContext(
|
||||
tool_name=sanitized_tool_name,
|
||||
@@ -243,6 +246,7 @@ def execute_tool_and_check_finality(
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=tool_result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
|
||||
@@ -7,6 +7,7 @@ when the LLM supports it, across multiple providers.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
@@ -20,7 +21,7 @@ from crewai import Agent, Crew, Task
|
||||
from crewai.agents.parser import AgentFinish
|
||||
from crewai.events import crewai_event_bus
|
||||
from crewai.hooks import register_after_tool_call_hook, register_before_tool_call_hook
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext, clear_after_tool_call_hooks
|
||||
from crewai.llm import LLM
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
|
||||
@@ -1197,6 +1198,76 @@ class TestNativeToolCallingJsonParseError:
|
||||
|
||||
assert result["result"] == "ran: print(1)"
|
||||
|
||||
def test_typed_output_is_json_agent_text(self) -> None:
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for information"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.8)
|
||||
|
||||
tool = TypedSearchTool()
|
||||
executor = self._make_executor([tool])
|
||||
|
||||
from crewai.utilities.agent_utils import convert_tools_to_openai_schema
|
||||
|
||||
_, available_functions, _ = convert_tools_to_openai_schema([tool])
|
||||
|
||||
result = executor._execute_single_native_tool_call(
|
||||
call_id="call_typed",
|
||||
func_name="typed_search",
|
||||
func_args='{"query": "crew"}',
|
||||
available_functions=available_functions,
|
||||
)
|
||||
|
||||
assert json.loads(result["result"]) == {"query": "crew", "score": 0.8}
|
||||
|
||||
def test_typed_output_after_hook_includes_raw_tool_result(self) -> None:
|
||||
from crewai.utilities.agent_utils import convert_tools_to_openai_schema
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class TypedSearchTool(BaseTool):
|
||||
name: str = "typed_search"
|
||||
description: str = "Search for information"
|
||||
result_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.8)
|
||||
|
||||
seen_results: list[tuple[str | None, object]] = []
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> None:
|
||||
seen_results.append((context.tool_result, context.raw_tool_result))
|
||||
|
||||
tool = TypedSearchTool()
|
||||
executor = self._make_executor([tool])
|
||||
_, available_functions, _ = convert_tools_to_openai_schema([tool])
|
||||
|
||||
clear_after_tool_call_hooks()
|
||||
register_after_tool_call_hook(after_hook)
|
||||
try:
|
||||
result = executor._execute_single_native_tool_call(
|
||||
call_id="call_typed",
|
||||
func_name="typed_search",
|
||||
func_args='{"query": "crew"}',
|
||||
available_functions=available_functions,
|
||||
)
|
||||
finally:
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
assert json.loads(result["result"]) == {"query": "crew", "score": 0.8}
|
||||
assert seen_results == [
|
||||
('{"query":"crew","score":0.8}', SearchOutput(query="crew", score=0.8))
|
||||
]
|
||||
|
||||
def test_native_tool_loop_falls_back_when_provider_rejects_tools(self) -> None:
|
||||
"""Unsupported native tools errors should continue through ReAct."""
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@ import pytest
|
||||
|
||||
from crewai_cli.cli import run
|
||||
from crewai_cli.run_crew import (
|
||||
CrewType,
|
||||
_execute_uv_script,
|
||||
_load_json_crew_for_tui,
|
||||
_missing_input_names,
|
||||
_prompt_for_missing_inputs,
|
||||
execute_command,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +29,8 @@ def test_run_passes_filename_to_run_crew(run_crew_mock: mock.Mock, runner: CliRu
|
||||
|
||||
run_crew_mock.assert_called_once_with(
|
||||
trained_agents_file="my_custom_trained.pkl",
|
||||
definition=None,
|
||||
inputs=None,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
@@ -38,7 +39,11 @@ def test_run_passes_filename_to_run_crew(run_crew_mock: mock.Mock, runner: CliRu
|
||||
def test_run_without_filename_passes_none(run_crew_mock: mock.Mock, runner: CliRunner) -> None:
|
||||
result = runner.invoke(run)
|
||||
|
||||
run_crew_mock.assert_called_once_with(trained_agents_file=None)
|
||||
run_crew_mock.assert_called_once_with(
|
||||
trained_agents_file=None,
|
||||
definition=None,
|
||||
inputs=None,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@@ -50,7 +55,11 @@ def test_run_without_filename_passes_none(run_crew_mock: mock.Mock, runner: CliR
|
||||
def test_execute_command_sets_env_var_when_filename_provided(
|
||||
_build_env: mock.Mock, subprocess_run: mock.Mock
|
||||
) -> None:
|
||||
execute_command(CrewType.STANDARD, trained_agents_file="my_custom_trained.pkl")
|
||||
_execute_uv_script(
|
||||
"run_crew",
|
||||
entity_type="crew",
|
||||
trained_agents_file="my_custom_trained.pkl",
|
||||
)
|
||||
|
||||
_, kwargs = subprocess_run.call_args
|
||||
assert kwargs["env"]["CREWAI_TRAINED_AGENTS_FILE"] == "my_custom_trained.pkl"
|
||||
@@ -65,7 +74,7 @@ def test_execute_command_sets_env_var_when_filename_provided(
|
||||
def test_execute_command_omits_env_var_when_filename_absent(
|
||||
_build_env: mock.Mock, subprocess_run: mock.Mock
|
||||
) -> None:
|
||||
execute_command(CrewType.STANDARD)
|
||||
_execute_uv_script("run_crew", entity_type="crew")
|
||||
|
||||
_, kwargs = subprocess_run.call_args
|
||||
assert "CREWAI_TRAINED_AGENTS_FILE" not in kwargs["env"]
|
||||
|
||||
@@ -91,20 +91,24 @@ class TestToolCallHookContext:
|
||||
assert context.task == mock_task
|
||||
assert context.crew == mock_crew
|
||||
assert context.tool_result is None
|
||||
assert context.raw_tool_result is None
|
||||
|
||||
def test_context_with_result(self, mock_tool):
|
||||
"""Test that context includes result when provided."""
|
||||
tool_input = {"arg1": "value1"}
|
||||
tool_result = "Test tool result"
|
||||
raw_tool_result = {"value": 42}
|
||||
|
||||
context = ToolCallHookContext(
|
||||
tool_name="test_tool",
|
||||
tool_input=tool_input,
|
||||
tool=mock_tool,
|
||||
tool_result=tool_result,
|
||||
raw_tool_result=raw_tool_result,
|
||||
)
|
||||
|
||||
assert context.tool_result == tool_result
|
||||
assert context.raw_tool_result == raw_tool_result
|
||||
|
||||
def test_tool_input_is_mutable_reference(self, mock_tool):
|
||||
"""Test that modifying context.tool_input modifies the original dict."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user