Compare commits
1 Commits
joaomdmour
...
fix/reposi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
570b100b33 |
2
.github/workflows/vulnerability-scan.yml
vendored
@@ -73,13 +73,11 @@ jobs:
|
||||
--ignore-vuln PYSEC-2025-216 \
|
||||
--ignore-vuln PYSEC-2025-217 \
|
||||
--ignore-vuln PYSEC-2025-218 \
|
||||
--ignore-vuln PYSEC-2026-597 \
|
||||
--ignore-vuln GHSA-f4j7-r4q5-qw2c
|
||||
# Ignored CVEs:
|
||||
# PYSEC-2024-277 - joblib 1.5.3: disputed; NumpyArrayWrapper only used with trusted caches
|
||||
# PYSEC-2026-89 - markdown 3.10.2: DoS via malformed HTML; fix 3.8.1 — already past, advisory range is stale
|
||||
# PYSEC-2026-97 - nltk 3.9.4: arbitrary file read in filestring(); no fix available
|
||||
# PYSEC-2026-597 - nltk 3.9.4 (CVE-2026-12243): path traversal via _UNSAFE_NO_PROTOCOL_RE bypass (incomplete fix of nltk#3504); 3.9.4 is the latest release, no fix available
|
||||
# PYSEC-2025-148 - onnx 1.21.0: path traversal in save_external_data; no fix available
|
||||
# PYSEC-2025-183 - pyjwt 2.12.1: disputed weak-encryption claim; key length is application-chosen
|
||||
# PYSEC-2025-189..197 - torch 2.11.0: memory-corruption/DoS in functions only reachable via untrusted models; no fix available
|
||||
|
||||
@@ -34,7 +34,6 @@ repos:
|
||||
--ignore-vuln PYSEC-2024-277
|
||||
--ignore-vuln PYSEC-2026-89
|
||||
--ignore-vuln PYSEC-2026-97
|
||||
--ignore-vuln PYSEC-2026-597
|
||||
--ignore-vuln PYSEC-2025-148
|
||||
--ignore-vuln PYSEC-2025-183
|
||||
--ignore-vuln PYSEC-2025-189
|
||||
|
||||
@@ -34616,42 +34616,6 @@
|
||||
"suggestEdit": true
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/en/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/en/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/en/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/edge/en/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/en/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/pt-BR/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/pt-BR/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/edge/pt-BR/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/pt-BR/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/ko/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/ko/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/edge/ko/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/ko/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/ar/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/ar/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/edge/ar/enterprise/features/agent-control-plane/rules",
|
||||
"destination": "/edge/ar/enterprise/features/agent-control-plane/policies"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference",
|
||||
"destination": "/v1.15.1/en/api-reference/introduction"
|
||||
@@ -34776,4 +34740,4 @@
|
||||
"reddit": "https://www.reddit.com/r/crewAIInc/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ mode: "wide"
|
||||
|
||||
- [نظرة عامة](/ar/enterprise/features/agent-control-plane/overview)
|
||||
- **المراقبة** *(أنت هنا)*
|
||||
- [السياسات](/edge/ar/enterprise/features/agent-control-plane/policies)
|
||||
- [القواعد](/ar/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## نظرة عامة
|
||||
@@ -58,7 +58,7 @@ mode: "wide"
|
||||
| **Last execution** | الوقت المنقضي منذ آخر تنفيذ. |
|
||||
| **Health Status Breakdown** | شريط مكدّس بنسب `Critical` / `Warning` / `Healthy` لعمليات التنفيذ في النافذة. |
|
||||
| **Executions with Errors** | إجمالي عمليات التنفيذ الفاشلة في النافذة. |
|
||||
| **PII detection applied** | `Yes` إذا كان هناك تكوين PII لكل deployment أو [سياسة PII](/edge/ar/enterprise/features/agent-control-plane/policies) مطابِقة نشطة. |
|
||||
| **PII detection applied** | `Yes` إذا كان هناك تكوين PII لكل deployment أو [قاعدة PII](/ar/enterprise/features/agent-control-plane/rules) مطابِقة نشطة. |
|
||||
| **Executions** | إجمالي عمليات التنفيذ في النافذة. |
|
||||
| **Last updated** | متى أُعيد نشر الـ deployment آخر مرة. |
|
||||
| **Crew Version** | إصدار `crewai` الذي يُبلِّغ عنه الـ deployment. يشير أيقونة المعلومات بجانب الإصدارات الأقل من `1.13` إلى صفوف لا يمكنها المساهمة بالمقاييس. |
|
||||
@@ -96,8 +96,8 @@ mode: "wide"
|
||||
<Card title="Agent Control Plane — نظرة عامة" icon="book-open" href="/ar/enterprise/features/agent-control-plane/overview">
|
||||
ما هو ACP، المتطلبات، مستويات الخطط، و RBAC.
|
||||
</Card>
|
||||
<Card title="Agent Control Plane — السياسات" icon="shield-check" href="/edge/ar/enterprise/features/agent-control-plane/policies">
|
||||
طبّق سياسات PII Redaction على مستوى المؤسسة عبر العديد من الأتمتات.
|
||||
<Card title="Agent Control Plane — القواعد" icon="shield-check" href="/ar/enterprise/features/agent-control-plane/rules">
|
||||
طبّق قواعد PII Redaction على مستوى المؤسسة عبر العديد من الأتمتات.
|
||||
</Card>
|
||||
<Card title="Traces" icon="timeline" href="/ar/enterprise/features/traces">
|
||||
تعمّق في تنفيذ واحد لرؤية تفكير الوكيل واستدعاءات الأدوات واستخدام الرموز.
|
||||
|
||||
@@ -10,17 +10,17 @@ icon: "book-open"
|
||||
|
||||
- **نظرة عامة** *(أنت هنا)*
|
||||
- [المراقبة](/ar/enterprise/features/agent-control-plane/monitoring)
|
||||
- [السياسات](/edge/ar/enterprise/features/agent-control-plane/policies)
|
||||
- [القواعد](/ar/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
**Agent Control Plane** (ACP) هو مركز العمليات لكل ما يعمل لديك على CrewAI AMP. إنها شاشة واحدة — مقسّمة إلى تبويبَي **Automations** و **Policies** — تمنح فريقك القدرة على:
|
||||
**Agent Control Plane** (ACP) هو مركز العمليات لكل ما يعمل لديك على CrewAI AMP. إنها شاشة واحدة — مقسّمة إلى تبويبَي **Automations** و **Rules** — تمنح فريقك القدرة على:
|
||||
|
||||
- مراقبة **حالة (الصحة)** كل أتمتة حيّة (crew أو flow) بتفصيل `Critical` / `Warning` / `Healthy` وعدد عمليات التنفيذ.
|
||||
- تتبع **استهلاك LLM** — الرموز (tokens) والتكلفة — لكل أتمتة ولكل مزود ولكل نموذج، مع الفرق مقابل الفترة السابقة.
|
||||
- التعمّق في أي أتمتة منفردة أو مزود نماذج لرؤية المخططات الزمنية وتفصيل البيانات لكل مزود.
|
||||
- تطبيق **سياسات (Policies)** على مستوى المؤسسة (اليوم: PII Redaction) عبر العديد من الأتمتات دفعة واحدة بدلاً من تعديل كل deployment على حدة.
|
||||
- تطبيق **قواعد (Rules)** على مستوى المؤسسة (اليوم: PII Redaction) عبر العديد من الأتمتات دفعة واحدة بدلاً من تعديل كل deployment على حدة.
|
||||
|
||||
<Frame>
|
||||

|
||||
@@ -33,7 +33,7 @@ icon: "book-open"
|
||||
يجيب التبويبان عن سؤالَين مختلفَين:
|
||||
|
||||
- **Automations** — *"كيف يتصرف أسطولي الآن، وكم يكلّفني؟"* راجع [المراقبة](/ar/enterprise/features/agent-control-plane/monitoring).
|
||||
- **Policies** — *"كيف أفرض سياسة (مثل PII redaction) عبر العديد من عمليات النشر دون إعادة نشر كل واحدة؟"* راجع [السياسات](/edge/ar/enterprise/features/agent-control-plane/policies).
|
||||
- **Rules** — *"كيف أفرض سياسة (مثل PII redaction) عبر العديد من عمليات النشر دون إعادة نشر كل واحدة؟"* راجع [القواعد](/ar/enterprise/features/agent-control-plane/rules).
|
||||
|
||||
## المتطلبات
|
||||
|
||||
@@ -42,11 +42,11 @@ icon: "book-open"
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
يُشترط **خطة Enterprise أو Ultra** لإنشاء أو تعديل [السياسات](/edge/ar/enterprise/features/agent-control-plane/policies). يمكن للمؤسسات على الخطط الأدنى فتح تبويب Policies وعرض السياسات الموجودة، ولكن يُعرض المحرر للقراءة فقط مع شارة قفل "Enterprise" والتنبيه *"PII Redaction policies require an Enterprise plan."*. المراقبة (تبويب Automations) متاحة في جميع الخطط حيث يكون هذا الميزة مفعّلة.
|
||||
يُشترط **خطة Enterprise أو Ultra** لإنشاء أو تعديل [القواعد](/ar/enterprise/features/agent-control-plane/rules). يمكن للمؤسسات على الخطط الأدنى فتح تبويب Rules وعرض القواعد الموجودة، ولكن يُعرض المحرر للقراءة فقط مع شارة قفل "Enterprise" والتنبيه *"PII Redaction rules require an Enterprise plan."*. المراقبة (تبويب Automations) متاحة في جميع الخطط حيث يكون هذا الميزة مفعّلة.
|
||||
</Warning>
|
||||
|
||||
- يجب أن تكون ميزة **Agent Control Plane** مفعّلة لمؤسستك. إن لم ترها في الشريط الجانبي، اطلب من مالك الحساب تفعيلها.
|
||||
- داخل ACP، يحكم [RBAC](/ar/enterprise/features/rbac) الوصول: `read` للعرض في لوحة المعلومات والسياسات، و`manage` لإنشاء وتعديل وتشغيل/إيقاف وحذف السياسات.
|
||||
- داخل ACP، يحكم [RBAC](/ar/enterprise/features/rbac) الوصول: `read` للعرض في لوحة المعلومات والقواعد، و`manage` لإنشاء وتعديل وتشغيل/إيقاف وحذف القواعد.
|
||||
- يمكن ضبط نطاق جميع المخططات والجداول إلى **آخر 24 ساعة** أو **الأسبوع الماضي** أو **آخر 30 يوماً** عبر مُحدّد الوقت في أعلى اليمين. تقارن قيم الفرق (`↑ 8 vs yesterday`, `↓ $20.57 vs yesterday` وغيرها) النافذة المختارة بالنافذة السابقة بنفس الطول.
|
||||
|
||||
## ما يمكنك فعله هنا
|
||||
@@ -55,7 +55,7 @@ icon: "book-open"
|
||||
<Card title="المراقبة" icon="gauge" href="/ar/enterprise/features/agent-control-plane/monitoring">
|
||||
راقب صحة الأسطول وإنفاق LLM عبر بطاقات المقاييس و sankey التفاعلي وجداول لكل أتمتة ولوحات جانبية للتعمق في أي أتمتة أو مزود.
|
||||
</Card>
|
||||
<Card title="السياسات" icon="shield-check" href="/edge/ar/enterprise/features/agent-control-plane/policies">
|
||||
<Card title="القواعد" icon="shield-check" href="/ar/enterprise/features/agent-control-plane/rules">
|
||||
طبّق سياسات PII Redaction على مستوى المؤسسة بنطاق محدد بالأدوات والوسوم. تسري التغييرات في التنفيذ التالي — دون الحاجة لإعادة نشر.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -67,10 +67,10 @@ icon: "book-open"
|
||||
تعمّق في تنفيذ واحد لرؤية تفكير الوكيل واستدعاءات الأدوات واستخدام الرموز.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/ar/enterprise/features/rbac">
|
||||
أدِر من يمكنه قراءة Agent Control Plane ومن يمكنه تعديل السياسات.
|
||||
أدِر من يمكنه قراءة Agent Control Plane ومن يمكنه تعديل القواعد.
|
||||
</Card>
|
||||
<Card title="PII Redaction للـ Traces" icon="lock" href="/ar/enterprise/features/pii-trace-redactions">
|
||||
كتالوج الكيانات وضبط PII لكل deployment التي تستند إليها السياسات.
|
||||
كتالوج الكيانات وضبط PII لكل deployment التي تستند إليها القواعد.
|
||||
</Card>
|
||||
<Card title="النشر إلى AMP" icon="rocket" href="/ar/enterprise/guides/deploy-to-amp">
|
||||
انشر crew على إصدار crewAI يدعم Agent Control Plane.
|
||||
@@ -78,5 +78,5 @@ icon: "book-open"
|
||||
</CardGroup>
|
||||
|
||||
<Card title="تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
|
||||
تواصل مع فريق الدعم للمساعدة في تفسير المقاييس أو تصميم السياسات.
|
||||
تواصل مع فريق الدعم للمساعدة في تفسير المقاييس أو تصميم القواعد.
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "إعداد السياسات"
|
||||
title: "إعداد القواعد"
|
||||
description: "طبّق سياسات على مستوى المؤسسة عبر العديد من الأتمتات من مكان واحد."
|
||||
sidebarTitle: "السياسات"
|
||||
sidebarTitle: "القواعد"
|
||||
icon: "shield-check"
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -11,50 +11,50 @@ mode: "wide"
|
||||
|
||||
- [نظرة عامة](/ar/enterprise/features/agent-control-plane/overview)
|
||||
- [المراقبة](/ar/enterprise/features/agent-control-plane/monitoring)
|
||||
- **السياسات** *(أنت هنا)*
|
||||
- **القواعد** *(أنت هنا)*
|
||||
</Info>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
تتيح لك السياسات تطبيق سياسات — اليوم: **PII Redaction** — عبر العديد من الأتمتات دفعة واحدة، بدلاً من ضبط كل deployment على حدة. افتح تبويب **Policies** في [Agent Control Plane](/ar/enterprise/features/agent-control-plane/overview) لإدارتها.
|
||||
تتيح لك القواعد تطبيق سياسات — اليوم: **PII Redaction** — عبر العديد من الأتمتات دفعة واحدة، بدلاً من ضبط كل deployment على حدة. افتح تبويب **Rules** في [Agent Control Plane](/ar/enterprise/features/agent-control-plane/overview) لإدارتها.
|
||||
|
||||
<Frame>
|
||||

|
||||

|
||||
</Frame>
|
||||
|
||||
تعرض كل بطاقة سياسة الاسم والوصف و**النطاق (scope)** الذي تنطبق عليه السياسة (الأدوات والوسوم المختارة) وعدد **الأتمتات المُفعَّلة** — عمليات النشر التي تطابق النطاق حالياً. يقوم المُفتاح على اليمين بتشغيل السياسة أو إيقافها دون حذفها.
|
||||
تعرض كل بطاقة قاعدة الاسم والوصف و**النطاق (scope)** الذي تنطبق عليه القاعدة (الأدوات والوسوم المختارة) وعدد **الأتمتات المُفعَّلة** — عمليات النشر التي تطابق النطاق حالياً. يقوم المُفتاح على اليمين بتشغيل القاعدة أو إيقافها دون حذفها.
|
||||
|
||||
## المتطلبات
|
||||
|
||||
<Warning>
|
||||
يُشترط **خطة Enterprise أو Ultra** لإنشاء أو تعديل سياسات PII Redaction. يمكن للمؤسسات على الخطط الأدنى فتح تبويب Policies وعرض السياسات الموجودة، ولكن يُعرض المحرر للقراءة فقط مع شارة قفل "Enterprise" والتنبيه *"PII Redaction policies require an Enterprise plan."* — تواصل مع مالك حسابك أو المبيعات للترقية.
|
||||
يُشترط **خطة Enterprise أو Ultra** لإنشاء أو تعديل قواعد PII Redaction. يمكن للمؤسسات على الخطط الأدنى فتح تبويب Rules وعرض القواعد الموجودة، ولكن يُعرض المحرر للقراءة فقط مع شارة قفل "Enterprise" والتنبيه *"PII Redaction rules require an Enterprise plan."* — تواصل مع مالك حسابك أو المبيعات للترقية.
|
||||
</Warning>
|
||||
|
||||
- يجب أن تكون ميزة **Agent Control Plane** مفعّلة لمؤسستك. راجع [نظرة عامة — المتطلبات](/ar/enterprise/features/agent-control-plane/overview#المتطلبات).
|
||||
- تحتاج إلى صلاحية `manage` ضمن [RBAC](/ar/enterprise/features/rbac) على Agent Control Plane لإنشاء وتعديل وتشغيل/إيقاف وحذف السياسات. صلاحية `read` كافية لعرضها.
|
||||
- تُسجَّل جميع تغييرات السياسات بإصدارات للتدقيق.
|
||||
- تحتاج إلى صلاحية `manage` ضمن [RBAC](/ar/enterprise/features/rbac) على Agent Control Plane لإنشاء وتعديل وتشغيل/إيقاف وحذف القواعد. صلاحية `read` كافية لعرضها.
|
||||
- تُسجَّل جميع تغييرات القواعد بإصدارات للتدقيق.
|
||||
|
||||
## أنواع السياسات المتاحة
|
||||
## أنواع القواعد المتاحة
|
||||
|
||||
| النوع | ما تفعله |
|
||||
|------|---------------|
|
||||
| **PII Redaction** | تطبّق PII redaction على عمليات التنفيذ لكل أتمتة مطابِقة، باستخدام نفس كتالوج الكيانات و recognizers المخصصة الموثَّقة في [PII Redaction للـ Traces](/ar/enterprise/features/pii-trace-redactions). |
|
||||
|
||||
سيتم إضافة أنواع سياسات أخرى مع الوقت.
|
||||
سيتم إضافة أنواع قواعد أخرى مع الوقت.
|
||||
|
||||
## إنشاء سياسة
|
||||
## إنشاء قاعدة
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/acp-policies-new-side-panel.png" alt="لوحة تعديل سياسة جانبية بالشروط ونوع قناع PII" width="450" />
|
||||
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="لوحة تعديل قاعدة جانبية بالشروط ونوع قناع PII" width="450" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="افتح المحرر">
|
||||
انقر على **+ Create new** في أعلى يمين تبويب Policies، أو على **View Details** في بطاقة سياسة موجودة.
|
||||
انقر على **+ Create new** في أعلى يمين تبويب Rules، أو على **View Details** في بطاقة قاعدة موجودة.
|
||||
</Step>
|
||||
|
||||
<Step title="سَمِّ السياسة وصِفها">
|
||||
أعطِ السياسة اسماً واضحاً (مثل *Mask PII (CC)*) ووصفاً يشرح متى تنطبق. يظهر كلاهما على بطاقة السياسة وفي مودال Engaged Automations.
|
||||
<Step title="سَمِّ القاعدة وصِفها">
|
||||
أعطِ القاعدة اسماً واضحاً (مثل *Mask PII (CC)*) ووصفاً يشرح متى تنطبق. يظهر كلاهما على بطاقة القاعدة وفي مودال Engaged Automations.
|
||||
</Step>
|
||||
|
||||
<Step title="اختر النوع">
|
||||
@@ -62,12 +62,12 @@ mode: "wide"
|
||||
</Step>
|
||||
|
||||
<Step title="حدّد الشروط">
|
||||
تحدد الشروط الأتمتات التي تنخرط معها السياسة. كلاهما اختياري ويستخدم دلالات **مساواة المجموعات (set-equality)**:
|
||||
تحدد الشروط الأتمتات التي تنخرط معها القاعدة. كلاهما اختياري ويستخدم دلالات **مساواة المجموعات (set-equality)**:
|
||||
|
||||
- **Tools** — تنخرط فقط الأتمتات التي تتطابق مجموعة أدواتها **تطابقاً تامّاً** مع الأدوات المختارة. اختر من تطبيقات Studio و MCPs والأدوات مفتوحة المصدر وأدوات سجل Tool Repository.
|
||||
- **Automations** — تنخرط فقط الأتمتات التي تتطابق مجموعة وسومها **تطابقاً تامّاً** مع الوسوم المختارة.
|
||||
|
||||
ترك مُحدِّد فارغ يعني "بدون تصفية على هذا البعد". ترك كليهما فارغَين يعني أن السياسة تنطبق على **كل** أتمتة في المؤسسة.
|
||||
ترك مُحدِّد فارغ يعني "بدون تصفية على هذا البعد". ترك كليهما فارغَين يعني أن القاعدة تنطبق على **كل** أتمتة في المؤسسة.
|
||||
</Step>
|
||||
|
||||
<Step title="اضبط جدول PII Mask Type">
|
||||
@@ -75,30 +75,30 @@ mode: "wide"
|
||||
</Step>
|
||||
|
||||
<Step title="احفظ">
|
||||
تنطبق السياسة على عمليات التنفيذ **المستقبلية** لكل أتمتة مُفعَّلة بمجرد الحفظ. لا حاجة لإعادة النشر.
|
||||
تنطبق القاعدة على عمليات التنفيذ **المستقبلية** لكل أتمتة مُفعَّلة بمجرد الحفظ. لا حاجة لإعادة النشر.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## الأتمتات المُفعَّلة
|
||||
|
||||
انقر على **Engaged N automations** في أي بطاقة سياسة لرؤية أي عمليات النشر تطابقها السياسة حالياً بالضبط، إلى جانب آخر تنفيذ لكل منها.
|
||||
انقر على **Engaged N automations** في أي بطاقة قاعدة لرؤية أي عمليات النشر تطابقها القاعدة حالياً بالضبط، إلى جانب آخر تنفيذ لكل منها.
|
||||
|
||||
<Frame>
|
||||

|
||||

|
||||
</Frame>
|
||||
|
||||
هذه هي أسرع طريقة للتحقق من نطاق سياسة قبل تمكينها — على سبيل المثال، للتأكد من أن سياسة محدَّدة بنطاق وسم `production` لا تطابق عن طريق الخطأ deployment تجريبي.
|
||||
هذه هي أسرع طريقة للتحقق من نطاق قاعدة قبل تمكينها — على سبيل المثال، للتأكد من أن قاعدة محدَّدة بنطاق وسم `production` لا تطابق عن طريق الخطأ deployment تجريبي.
|
||||
|
||||
## سياسات على مستوى المؤسسة مقابل إعدادات لكل deployment
|
||||
## قواعد على مستوى المؤسسة مقابل إعدادات لكل deployment
|
||||
|
||||
يمكن ضبط PII Redaction في مكانين:
|
||||
|
||||
- **لكل deployment** — ضمن **Settings → PII Protection** على كل deployment على حدة ([الدليل](/ar/enterprise/features/pii-trace-redactions))
|
||||
- **على مستوى المؤسسة** — كسياسة في هذه الصفحة
|
||||
- **على مستوى المؤسسة** — كقاعدة في هذه الصفحة
|
||||
|
||||
عندما يتطابق نطاق سياسة مُفعَّلة على مستوى المؤسسة مع deployment، يُجاوز تكوين الكيانات الخاص بالسياسة **إعدادات PII المملوكة من قبل الـ deployment** لعمليات تنفيذ ذلك الـ deployment — تصبح السياسة المصدر الوحيد للحقيقة طالما هي مرتبطة. عطّل السياسة أو فُكَّ ارتباطها (أو غيِّر نطاقها بحيث لا تتطابق بعد الآن) ويعود الـ deployment إلى إعدادات PII Protection الخاصة به.
|
||||
عندما يتطابق نطاق قاعدة مُفعَّلة على مستوى المؤسسة مع deployment، يُجاوز تكوين الكيانات الخاص بالقاعدة **إعدادات PII المملوكة من قبل الـ deployment** لعمليات تنفيذ ذلك الـ deployment — تصبح القاعدة المصدر الوحيد للحقيقة طالما هي مرتبطة. عطّل القاعدة أو فُكَّ ارتباطها (أو غيِّر نطاقها بحيث لا تتطابق بعد الآن) ويعود الـ deployment إلى إعدادات PII Protection الخاصة به.
|
||||
|
||||
فضّل السياسات على مستوى المؤسسة عندما تريد فرض سياسة متسقة عبر العديد من عمليات النشر؛ احتفظ بالضبط لكل deployment للاستثناءات الفردية.
|
||||
فضّل القواعد على مستوى المؤسسة عندما تريد فرض سياسة متسقة عبر العديد من عمليات النشر؛ احتفظ بالضبط لكل deployment للاستثناءات الفردية.
|
||||
|
||||
## ذو صلة
|
||||
|
||||
@@ -113,10 +113,10 @@ mode: "wide"
|
||||
كتالوج الكيانات، recognizers المخصصة، والضبط لكل deployment.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/ar/enterprise/features/rbac">
|
||||
أدِر من يمكنه إنشاء أو تعديل السياسات.
|
||||
أدِر من يمكنه إنشاء أو تعديل القواعد.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Card title="تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
|
||||
تواصل مع فريق الدعم للمساعدة في تصميم سياسات لمؤسستك.
|
||||
تواصل مع فريق الدعم للمساعدة في تصميم قواعد لمؤسستك.
|
||||
</Card>
|
||||
@@ -11,7 +11,7 @@ mode: "wide"
|
||||
|
||||
- [Overview](/en/enterprise/features/agent-control-plane/overview)
|
||||
- **Monitoring** *(you are here)*
|
||||
- [Policies](/edge/en/enterprise/features/agent-control-plane/policies)
|
||||
- [Rules](/en/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## Overview
|
||||
@@ -58,7 +58,7 @@ The **Automations** sub-tab is the per-deployment breakdown of fleet health. Eac
|
||||
| **Last execution** | Time since the most recent run. |
|
||||
| **Health Status Breakdown** | Stacked bar of `Critical` / `Warning` / `Healthy` percentages for executions in the window. |
|
||||
| **Executions with Errors** | Total failed executions in the window. |
|
||||
| **PII detection applied** | `Yes` if a per-deployment PII config or a matching [PII policy](/edge/en/enterprise/features/agent-control-plane/policies) is active. |
|
||||
| **PII detection applied** | `Yes` if a per-deployment PII config or a matching [PII rule](/en/enterprise/features/agent-control-plane/rules) is active. |
|
||||
| **Executions** | Total executions in the window. |
|
||||
| **Last updated** | When the deployment was last re-deployed. |
|
||||
| **Crew Version** | The `crewai` version reported by the deployment. An info icon next to versions below `1.13` flags rows that can't contribute metrics. |
|
||||
@@ -96,8 +96,8 @@ Filter by **LLM provider** and sort by `Cost`, `Executions`, or `Last run`.
|
||||
<Card title="Agent Control Plane — Overview" icon="book-open" href="/en/enterprise/features/agent-control-plane/overview">
|
||||
What ACP is, requirements, plan tiers, and RBAC.
|
||||
</Card>
|
||||
<Card title="Agent Control Plane — Policies" icon="shield-check" href="/edge/en/enterprise/features/agent-control-plane/policies">
|
||||
Apply organization-wide PII Redaction policies across many automations.
|
||||
<Card title="Agent Control Plane — Rules" icon="shield-check" href="/en/enterprise/features/agent-control-plane/rules">
|
||||
Apply organization-wide PII Redaction rules across many automations.
|
||||
</Card>
|
||||
<Card title="Traces" icon="timeline" href="/en/enterprise/features/traces">
|
||||
Drill into a single execution to see agent reasoning, tool calls, and token usage.
|
||||
|
||||
@@ -10,17 +10,17 @@ icon: "book-open"
|
||||
|
||||
- **Overview** *(you are here)*
|
||||
- [Monitoring](/en/enterprise/features/agent-control-plane/monitoring)
|
||||
- [Policies](/edge/en/enterprise/features/agent-control-plane/policies)
|
||||
- [Rules](/en/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## Overview
|
||||
|
||||
The **Agent Control Plane** (ACP) is the operations hub for everything you have running on CrewAI AMP. It is a single screen — split into **Automations** and **Policies** tabs — that lets your team:
|
||||
The **Agent Control Plane** (ACP) is the operations hub for everything you have running on CrewAI AMP. It is a single screen — split into **Automations** and **Rules** tabs — that lets your team:
|
||||
|
||||
- Monitor the **health** of every live automation (crew or flow), with `Critical` / `Warning` / `Healthy` breakdowns and execution counts.
|
||||
- Track **LLM consumption** — tokens and cost — per automation, per provider, and per model, with a delta vs the previous period.
|
||||
- Drill into any single automation or model provider for time-series charts and per-provider breakdowns.
|
||||
- Apply organization-wide **Policies** (today: PII Redaction and Cost Limit) across many automations at once instead of editing each deployment individually.
|
||||
- Apply organization-wide **Rules** (today: PII Redaction and Cost Limit) across many automations at once instead of editing each deployment individually.
|
||||
|
||||
<Frame>
|
||||

|
||||
@@ -33,7 +33,7 @@ The **Agent Control Plane** (ACP) is the operations hub for everything you have
|
||||
The two tabs answer two different questions:
|
||||
|
||||
- **Automations** — *"How is my fleet behaving right now, and what is it costing me?"* See [Monitoring](/en/enterprise/features/agent-control-plane/monitoring).
|
||||
- **Policies** — *"How do I enforce a policy (e.g. PII redaction or a spend budget) across many deployments without re-deploying each one?"* See [Policies](/edge/en/enterprise/features/agent-control-plane/policies).
|
||||
- **Rules** — *"How do I enforce a policy (e.g. PII redaction or a spend budget) across many deployments without re-deploying each one?"* See [Rules](/en/enterprise/features/agent-control-plane/rules).
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -42,11 +42,11 @@ The two tabs answer two different questions:
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**Enterprise Plan or Ultra Plan** is required to create or edit **PII Redaction** [Policies](/edge/en/enterprise/features/agent-control-plane/policies). Lower-tier organizations can open the Policies tab and view existing policies, but the PII editor renders read-only with an "Enterprise" lock pill and the alert *"PII Redaction policies require an Enterprise plan."* **Cost Limit** policies and Monitoring (the Automations tab) are available on all plans where the feature is enabled.
|
||||
**Enterprise Plan or Ultra Plan** is required to create or edit **PII Redaction** [Rules](/en/enterprise/features/agent-control-plane/rules). Lower-tier organizations can open the Rules tab and view existing rules, but the PII editor renders read-only with an "Enterprise" lock pill and the alert *"PII Redaction rules require an Enterprise plan."* **Cost Limit** rules and Monitoring (the Automations tab) are available on all plans where the feature is enabled.
|
||||
</Warning>
|
||||
|
||||
- The **Agent Control Plane** feature must be enabled for your organization. If you don't see it in the sidebar, ask your account owner to request enablement.
|
||||
- Inside ACP, [RBAC](/en/enterprise/features/rbac) governs access: `read` to view the dashboard and policies, `manage` to create, edit, toggle, or delete policies.
|
||||
- Inside ACP, [RBAC](/en/enterprise/features/rbac) governs access: `read` to view the dashboard and rules, `manage` to create, edit, toggle, or delete rules.
|
||||
- All charts and tables can be scoped to the **Last 24 hours**, **Last Week**, or **Last 30 days** using the time selector at the top right. Deltas (`↑ 8 vs yesterday`, `↓ $20.57 vs yesterday`, etc.) compare the selected window against the previous one of the same length.
|
||||
|
||||
## What you can do here
|
||||
@@ -55,7 +55,7 @@ The two tabs answer two different questions:
|
||||
<Card title="Monitoring" icon="gauge" href="/en/enterprise/features/agent-control-plane/monitoring">
|
||||
Watch fleet health and LLM spend with metric cards, an interactive sankey, per-automation tables, and drill-down side panels for any automation or provider.
|
||||
</Card>
|
||||
<Card title="Policies" icon="shield-check" href="/edge/en/enterprise/features/agent-control-plane/policies">
|
||||
<Card title="Rules" icon="shield-check" href="/en/enterprise/features/agent-control-plane/rules">
|
||||
Apply organization-wide PII Redaction and Cost Limit policies scoped by tools and tags. Changes take effect on the next execution — no re-deploy required.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -67,10 +67,10 @@ The two tabs answer two different questions:
|
||||
Drill into a single execution to see agent reasoning, tool calls, and token usage.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/en/enterprise/features/rbac">
|
||||
Manage who can read the Agent Control Plane and who can edit policies.
|
||||
Manage who can read the Agent Control Plane and who can edit rules.
|
||||
</Card>
|
||||
<Card title="PII Redaction for Traces" icon="lock" href="/en/enterprise/features/pii-trace-redactions">
|
||||
Entity catalog and per-deployment PII configuration referenced by Policies.
|
||||
Entity catalog and per-deployment PII configuration referenced by Rules.
|
||||
</Card>
|
||||
<Card title="Deploy to AMP" icon="rocket" href="/en/enterprise/guides/deploy-to-amp">
|
||||
Deploy a crew on a crewAI version that supports the Agent Control Plane.
|
||||
@@ -78,5 +78,5 @@ The two tabs answer two different questions:
|
||||
</CardGroup>
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for help interpreting metrics or designing policies.
|
||||
Contact our support team for help interpreting metrics or designing rules.
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Set up the Policies"
|
||||
title: "Set up the Rules"
|
||||
description: "Apply organization-wide policies across many automations from a single place."
|
||||
sidebarTitle: "Policies"
|
||||
sidebarTitle: "Rules"
|
||||
icon: "shield-check"
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -11,39 +11,39 @@ mode: "wide"
|
||||
|
||||
- [Overview](/en/enterprise/features/agent-control-plane/overview)
|
||||
- [Monitoring](/en/enterprise/features/agent-control-plane/monitoring)
|
||||
- **Policies** *(you are here)*
|
||||
- **Rules** *(you are here)*
|
||||
</Info>
|
||||
|
||||
## Overview
|
||||
|
||||
Policies let you enforce organization-wide controls — today **PII Redaction** and **Cost Limit** — across many automations at once, instead of configuring each deployment individually. Open the **Policies** tab in the [Agent Control Plane](/en/enterprise/features/agent-control-plane/overview) to manage them.
|
||||
Rules let you apply policies — today **PII Redaction** and **Cost Limit** — across many automations at once, instead of configuring each deployment individually. Open the **Rules** tab in the [Agent Control Plane](/en/enterprise/features/agent-control-plane/overview) to manage them.
|
||||
|
||||
<Frame>
|
||||

|
||||

|
||||
</Frame>
|
||||
|
||||
Each policy card shows the name, description, the **scope** the policy applies to (selected tools and tags), and a count of **engaged automations** — deployments that currently match the scope. The toggle on the right enables or disables the policy without deleting it.
|
||||
Each rule card shows the name, description, the **scope** the rule applies to (selected tools and tags), and a count of **engaged automations** — deployments that currently match the scope. The toggle on the right enables or disables the rule without deleting it.
|
||||
|
||||
## Requirements
|
||||
|
||||
<Warning>
|
||||
**Enterprise Plan or Ultra Plan** is required to create or edit **PII Redaction** policies. Lower-tier organizations can still open the Policies tab and view existing policies, but the PII editor renders read-only with an "Enterprise" lock pill and the alert *"PII Redaction policies require an Enterprise plan."* — contact your account owner or sales to upgrade. **Cost Limit** policies are **not** plan-gated and can be created on any plan where the Agent Control Plane is enabled.
|
||||
**Enterprise Plan or Ultra Plan** is required to create or edit **PII Redaction** rules. Lower-tier organizations can still open the Rules tab and view existing rules, but the PII editor renders read-only with an "Enterprise" lock pill and the alert *"PII Redaction rules require an Enterprise plan."* — contact your account owner or sales to upgrade. **Cost Limit** rules are **not** plan-gated and can be created on any plan where the Agent Control Plane is enabled.
|
||||
</Warning>
|
||||
|
||||
- The **Agent Control Plane** feature must be enabled for your organization. See [Overview — Requirements](/en/enterprise/features/agent-control-plane/overview#requirements).
|
||||
- The `manage` [RBAC permission](/en/enterprise/features/rbac) on Agent Control Plane is required to create, edit, toggle, or delete policies. The `read` permission is enough to view them.
|
||||
- All policy changes are versioned for auditing.
|
||||
- The `manage` [RBAC permission](/en/enterprise/features/rbac) on Agent Control Plane is required to create, edit, toggle, or delete rules. The `read` permission is enough to view them.
|
||||
- All rule changes are versioned for auditing.
|
||||
|
||||
## Policy types
|
||||
## Rule types
|
||||
|
||||
Every policy is one of the types below. Open the tab for the policy you want to enforce.
|
||||
Every rule is one of the types below. Open the tab for the policy you want to enforce.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="PII Redaction">
|
||||
Applies PII redaction to executions of every matching automation, using the same entity catalog and custom recognizers documented in [PII Redaction for Traces](/en/enterprise/features/pii-trace-redactions).
|
||||
|
||||
<Warning>
|
||||
Creating or editing PII Redaction policies requires an **Enterprise** or **Ultra** plan. On lower tiers the PII editor renders read-only with an "Enterprise" lock pill.
|
||||
Creating or editing PII Redaction rules requires an **Enterprise** or **Ultra** plan. On lower tiers the PII editor renders read-only with an "Enterprise" lock pill.
|
||||
</Warning>
|
||||
|
||||
**Configuration** — in the **PII Mask Type** table, check each entity type you want covered and choose how to handle it:
|
||||
@@ -58,7 +58,7 @@ See [PII Redaction for Traces](/en/enterprise/features/pii-trace-redactions) for
|
||||
Emails the recipients you choose when a matching automation's LLM spend exceeds a budget threshold in the selected period. Available on **all plans** where the Agent Control Plane is enabled — it is not Enterprise-gated.
|
||||
|
||||
<Warning>
|
||||
Cost Limit policies are **notify-only**. They never pause, throttle, or stop a run — they only send an email so a human can decide what to do. Adjust the budget or remove the policy if you no longer want the alert.
|
||||
Cost Limit rules are **notify-only**. They never pause, throttle, or stop a run — they only send an email so a human can decide what to do. Adjust the budget or remove the rule if you no longer want the alert.
|
||||
</Warning>
|
||||
|
||||
**Configuration**
|
||||
@@ -74,7 +74,7 @@ Cost Limit policies are **notify-only**. They never pause, throttle, or stop a r
|
||||
**How spend is measured and matched**
|
||||
|
||||
- The threshold is evaluated **per automation**, not summed across the whole scope. Each engaged automation has its own running total for the period.
|
||||
- A policy can match many automations via its conditions (tools/tags), and a single automation can be covered by **multiple** Cost Limit policies at once. Each policy tracks its own budget and alert state independently — they don't merge.
|
||||
- A rule can match many automations via its conditions (tools/tags), and a single automation can be covered by **multiple** Cost Limit rules at once. Each rule tracks its own budget and alert state independently — they don't merge.
|
||||
- A background check compares each engaged automation's period-to-date spend against the threshold and sends the email when it's exceeded. Because the check runs periodically, expect a short delay between crossing the threshold and the email arriving.
|
||||
|
||||
**The alert email**
|
||||
@@ -83,76 +83,76 @@ When an automation goes over budget, recipients get an email summarizing the ove
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
More policy types will be added over time.
|
||||
More rule types will be added over time.
|
||||
|
||||
## Creating a policy
|
||||
## Creating a rule
|
||||
|
||||
<Tabs>
|
||||
<Tab title="PII Redaction">
|
||||
<Frame>
|
||||
<img src="/images/enterprise/acp-policies-new-side-panel.png" alt="New Policy side panel configured for PII Redaction with the PII mask type table" width="450" />
|
||||
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="New Rule side panel configured for PII Redaction with the PII mask type table" width="450" />
|
||||
</Frame>
|
||||
</Tab>
|
||||
<Tab title="Cost Limit">
|
||||
<Frame>
|
||||
<img src="/images/enterprise/acp-policies-edit-cost-limit.png" alt="New Policy side panel configured for Cost Limit with budget period, threshold, and recipient emails" width="450" />
|
||||
<img src="/images/enterprise/acp-rules-edit-cost-limit.png" alt="New Rule side panel configured for Cost Limit with budget period, threshold, and recipient emails" width="450" />
|
||||
</Frame>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the editor">
|
||||
Click **+ Create new** at the top-right of the Policies tab, or **View Details** on an existing policy card.
|
||||
Click **+ Create new** at the top-right of the Rules tab, or **View Details** on an existing rule card.
|
||||
</Step>
|
||||
|
||||
<Step title="Name and describe the policy">
|
||||
Give the policy a clear name (e.g. *Mask PII (CC)* or *Monthly $100 budget*) and a description explaining when it applies. Both show up on the policy card and in the Engaged Automations modal.
|
||||
<Step title="Name and describe the rule">
|
||||
Give the rule a clear name (e.g. *Mask PII (CC)* or *Monthly $100 budget*) and a description explaining when it applies. Both show up on the rule card and in the Engaged Automations modal.
|
||||
</Step>
|
||||
|
||||
<Step title="Pick the type">
|
||||
Choose **PII Redaction** or **Cost Limit**. The type determines which configuration section appears below the conditions. The type is fixed once the policy is created — to switch, create a new policy.
|
||||
Choose **PII Redaction** or **Cost Limit**. The type determines which configuration section appears below the conditions. The type is fixed once the rule is created — to switch, create a new rule.
|
||||
</Step>
|
||||
|
||||
<Step title="Set the conditions">
|
||||
Conditions decide which automations the policy engages with. Both are optional and use **set-equality** semantics:
|
||||
Conditions decide which automations the rule engages with. Both are optional and use **set-equality** semantics:
|
||||
|
||||
- **Tools** — only automations whose tool set **exactly matches** the selected tools will engage. Picks from Studio apps, MCPs, OSS tools, and Tool Repository registry tools.
|
||||
- **Automations** — only automations whose tag set **exactly matches** the selected tags will engage.
|
||||
|
||||
Leaving a picker empty means "no filter on this dimension". Leaving both empty means the policy applies to **every** automation in the organization.
|
||||
Leaving a picker empty means "no filter on this dimension". Leaving both empty means the rule applies to **every** automation in the organization.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure the type-specific section">
|
||||
The editor shows the configuration for the type you picked — the **PII Mask Type** table for PII Redaction, or the budget fields for Cost Limit. See [Policy types](#policy-types) for what each field does.
|
||||
The editor shows the configuration for the type you picked — the **PII Mask Type** table for PII Redaction, or the budget fields for Cost Limit. See [Rule types](#rule-types) for what each field does.
|
||||
</Step>
|
||||
|
||||
<Step title="Save">
|
||||
The policy applies to **future** executions of every engaged automation as soon as you save. No re-deploy is needed.
|
||||
The rule applies to **future** executions of every engaged automation as soon as you save. No re-deploy is needed.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Engaged automations
|
||||
|
||||
Click **Engaged N automations** on any policy card to see exactly which deployments the policy is currently matching, along with each one's last execution.
|
||||
Click **Engaged N automations** on any rule card to see exactly which deployments the rule is currently matching, along with each one's last execution.
|
||||
|
||||
<Frame>
|
||||

|
||||

|
||||
</Frame>
|
||||
|
||||
This is the fastest way to sanity-check a policy's scope before enabling it — for example, to confirm that a policy scoped to the `production` tag isn't accidentally matching a staging deployment.
|
||||
This is the fastest way to sanity-check a rule's scope before enabling it — for example, to confirm that a rule scoped to the `production` tag isn't accidentally matching a staging deployment.
|
||||
|
||||
## Org-wide policies vs per-deployment settings
|
||||
## Org-wide rules vs per-deployment settings
|
||||
|
||||
Both PII Redaction and Cost Limit can be configured in two places: org-wide as a Policy on this page, or per-deployment under that deployment's **Settings**. When an enabled org-wide policy's scope matches a deployment, the policy takes precedence over the deployment-owned setting while it's attached.
|
||||
Both PII Redaction and Cost Limit can be configured in two places: org-wide as a Rule on this page, or per-deployment under that deployment's **Settings**. When an enabled org-wide rule's scope matches a deployment, the rule takes precedence over the deployment-owned setting while it's attached.
|
||||
|
||||
| Policy | Per-deployment setting | What an attached org-wide policy does |
|
||||
| Policy | Per-deployment setting | What an attached org-wide rule does |
|
||||
|--------|------------------------|-------------------------------------|
|
||||
| **PII Redaction** | **Settings → PII Protection** ([guide](/en/enterprise/features/pii-trace-redactions)) | The policy's entity configuration **overrides** the deployment's PII settings for that deployment's executions. |
|
||||
| **Cost Limit** | **Settings → Cost Alerts** | The deployment's manual cost alert is **paused** and the attached cost policy(s) fire instead. The per-deployment form stays editable as a fallback. |
|
||||
| **PII Redaction** | **Settings → PII Protection** ([guide](/en/enterprise/features/pii-trace-redactions)) | The rule's entity configuration **overrides** the deployment's PII settings for that deployment's executions. |
|
||||
| **Cost Limit** | **Settings → Cost Alerts** | The deployment's manual cost alert is **paused** and the attached cost rule(s) fire instead. The per-deployment form stays editable as a fallback. |
|
||||
|
||||
Disable or detach the policy (or change its scope so it no longer matches) and the deployment falls back to its own per-deployment settings.
|
||||
Disable or detach the rule (or change its scope so it no longer matches) and the deployment falls back to its own per-deployment settings.
|
||||
|
||||
Prefer org-wide policies when you want to enforce a consistent policy across many deployments; reserve per-deployment configuration for one-off exceptions.
|
||||
Prefer org-wide rules when you want to enforce a consistent policy across many deployments; reserve per-deployment configuration for one-off exceptions.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -167,10 +167,10 @@ Prefer org-wide policies when you want to enforce a consistent policy across man
|
||||
Entity catalog, custom recognizers, and per-deployment configuration.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/en/enterprise/features/rbac">
|
||||
Manage who can create or edit policies.
|
||||
Manage who can create or edit rules.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for help designing policies for your organization.
|
||||
Contact our support team for help designing rules for your organization.
|
||||
</Card>
|
||||
@@ -11,7 +11,7 @@ mode: "wide"
|
||||
|
||||
- [개요](/ko/enterprise/features/agent-control-plane/overview)
|
||||
- **모니터링** *(현재 페이지)*
|
||||
- [정책](/edge/ko/enterprise/features/agent-control-plane/policies)
|
||||
- [규칙](/ko/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## 개요
|
||||
@@ -58,7 +58,7 @@ mode: "wide"
|
||||
| **Last execution** | 가장 최근 실행 이후 경과 시간. |
|
||||
| **Health Status Breakdown** | 윈도우 내 실행에 대한 `Critical` / `Warning` / `Healthy` 비율의 누적 막대. |
|
||||
| **Executions with Errors** | 윈도우 내 총 실패 실행 수. |
|
||||
| **PII detection applied** | deployment별 PII 설정 또는 일치하는 [PII 정책](/edge/ko/enterprise/features/agent-control-plane/policies)이 활성화된 경우 `Yes`. |
|
||||
| **PII detection applied** | deployment별 PII 설정 또는 일치하는 [PII 규칙](/ko/enterprise/features/agent-control-plane/rules)이 활성화된 경우 `Yes`. |
|
||||
| **Executions** | 윈도우 내 총 실행 수. |
|
||||
| **Last updated** | deployment의 마지막 재배포 시점. |
|
||||
| **Crew Version** | deployment가 보고한 `crewai` 버전. `1.13` 미만 버전 옆의 정보 아이콘은 메트릭에 기여할 수 없는 행을 표시합니다. |
|
||||
@@ -96,8 +96,8 @@ mode: "wide"
|
||||
<Card title="Agent Control Plane — 개요" icon="book-open" href="/ko/enterprise/features/agent-control-plane/overview">
|
||||
ACP란 무엇이며, 요구사항, 플랜 등급, RBAC에 대해.
|
||||
</Card>
|
||||
<Card title="Agent Control Plane — 정책" icon="shield-check" href="/edge/ko/enterprise/features/agent-control-plane/policies">
|
||||
조직 단위 PII Redaction 정책을 여러 자동화에 적용합니다.
|
||||
<Card title="Agent Control Plane — 규칙" icon="shield-check" href="/ko/enterprise/features/agent-control-plane/rules">
|
||||
조직 단위 PII Redaction 규칙을 여러 자동화에 적용합니다.
|
||||
</Card>
|
||||
<Card title="Traces" icon="timeline" href="/ko/enterprise/features/traces">
|
||||
개별 실행을 드릴다운하여 에이전트의 추론, 도구 호출, 토큰 사용량을 확인합니다.
|
||||
|
||||
@@ -10,17 +10,17 @@ icon: "book-open"
|
||||
|
||||
- **개요** *(현재 페이지)*
|
||||
- [모니터링](/ko/enterprise/features/agent-control-plane/monitoring)
|
||||
- [정책](/edge/ko/enterprise/features/agent-control-plane/policies)
|
||||
- [규칙](/ko/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## 개요
|
||||
|
||||
**Agent Control Plane**(ACP)은 CrewAI AMP에서 실행 중인 모든 워크로드를 위한 운영 허브입니다. **Automations**와 **Policies** 두 개의 탭으로 구성된 단일 화면에서 다음 작업을 할 수 있습니다:
|
||||
**Agent Control Plane**(ACP)은 CrewAI AMP에서 실행 중인 모든 워크로드를 위한 운영 허브입니다. **Automations**와 **Rules** 두 개의 탭으로 구성된 단일 화면에서 다음 작업을 할 수 있습니다:
|
||||
|
||||
- 모든 라이브 자동화(crew 또는 flow)의 **상태(health)**를 `Critical` / `Warning` / `Healthy` 분포와 실행 횟수로 모니터링합니다.
|
||||
- 자동화별·공급자별·모델별 **LLM 소비**(토큰 및 비용)를 추적하고, 이전 기간 대비 변화량을 확인합니다.
|
||||
- 임의의 자동화 또는 모델 공급자를 드릴다운하여 시계열 차트와 공급자별 분해를 살펴봅니다.
|
||||
- 조직 전체에 **정책(Policies)**(현재: PII Redaction)을 적용하여 각 deployment를 개별 편집하지 않고 한 번에 여러 자동화에 정책을 강제합니다.
|
||||
- 조직 전체에 **규칙(Rules)**(현재: PII Redaction)을 적용하여 각 deployment를 개별 편집하지 않고 한 번에 여러 자동화에 정책을 강제합니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
@@ -33,7 +33,7 @@ icon: "book-open"
|
||||
두 탭은 서로 다른 두 가지 질문에 답합니다:
|
||||
|
||||
- **Automations** — *"지금 내 플릿은 어떻게 동작하고 있고, 얼마나 비용이 들고 있는가?"* [모니터링](/ko/enterprise/features/agent-control-plane/monitoring)을 참고하세요.
|
||||
- **Policies** — *"정책(예: PII redaction)을 매번 재배포하지 않고 여러 deployment에 어떻게 강제할 수 있는가?"* [정책](/edge/ko/enterprise/features/agent-control-plane/policies)을 참고하세요.
|
||||
- **Rules** — *"정책(예: PII redaction)을 매번 재배포하지 않고 여러 deployment에 어떻게 강제할 수 있는가?"* [규칙](/ko/enterprise/features/agent-control-plane/rules)을 참고하세요.
|
||||
|
||||
## 요구사항
|
||||
|
||||
@@ -42,11 +42,11 @@ icon: "book-open"
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
[정책](/edge/ko/enterprise/features/agent-control-plane/policies)을 생성하거나 편집하려면 **Enterprise 또는 Ultra 플랜**이 필요합니다. 하위 플랜의 조직도 Policies 탭을 열고 기존 정책을 볼 수 있지만, 편집기는 "Enterprise" 잠금 핀과 *"PII Redaction policies require an Enterprise plan."* 경고와 함께 읽기 전용으로 표시됩니다. 모니터링(Automations 탭)은 기능이 활성화된 모든 플랜에서 사용할 수 있습니다.
|
||||
[규칙](/ko/enterprise/features/agent-control-plane/rules)을 생성하거나 편집하려면 **Enterprise 또는 Ultra 플랜**이 필요합니다. 하위 플랜의 조직도 Rules 탭을 열고 기존 규칙을 볼 수 있지만, 편집기는 "Enterprise" 잠금 핀과 *"PII Redaction rules require an Enterprise plan."* 경고와 함께 읽기 전용으로 표시됩니다. 모니터링(Automations 탭)은 기능이 활성화된 모든 플랜에서 사용할 수 있습니다.
|
||||
</Warning>
|
||||
|
||||
- **Agent Control Plane** 기능이 조직에 대해 활성화되어 있어야 합니다. 사이드바에 보이지 않으면 계정 오너에게 활성화를 요청하세요.
|
||||
- ACP 내부에서는 [RBAC](/ko/enterprise/features/rbac)가 접근 권한을 관리합니다: 대시보드 및 정책을 보려면 `read`, 정책을 생성·편집·토글·삭제하려면 `manage` 권한이 필요합니다.
|
||||
- ACP 내부에서는 [RBAC](/ko/enterprise/features/rbac)가 접근 권한을 관리합니다: 대시보드 및 규칙을 보려면 `read`, 규칙을 생성·편집·토글·삭제하려면 `manage` 권한이 필요합니다.
|
||||
- 모든 차트와 테이블은 오른쪽 상단의 시간 선택기를 통해 **지난 24시간**, **지난 1주**, **지난 30일**로 범위를 조정할 수 있습니다. 변화량(`↑ 8 vs yesterday`, `↓ $20.57 vs yesterday` 등)은 선택한 윈도우를 같은 길이의 이전 윈도우와 비교합니다.
|
||||
|
||||
## 여기에서 할 수 있는 일
|
||||
@@ -55,7 +55,7 @@ icon: "book-open"
|
||||
<Card title="모니터링" icon="gauge" href="/ko/enterprise/features/agent-control-plane/monitoring">
|
||||
메트릭 카드, 인터랙티브 sankey, 자동화별 테이블, 자동화 또는 공급자별 드릴다운 사이드 패널로 플릿 상태와 LLM 지출을 살펴봅니다.
|
||||
</Card>
|
||||
<Card title="정책" icon="shield-check" href="/edge/ko/enterprise/features/agent-control-plane/policies">
|
||||
<Card title="규칙" icon="shield-check" href="/ko/enterprise/features/agent-control-plane/rules">
|
||||
도구와 태그로 범위를 지정한 PII Redaction 정책을 조직 단위로 적용합니다. 변경 사항은 다음 실행부터 적용되며 재배포가 필요 없습니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -67,10 +67,10 @@ icon: "book-open"
|
||||
개별 실행을 드릴다운하여 에이전트의 추론, 도구 호출, 토큰 사용량을 확인합니다.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/ko/enterprise/features/rbac">
|
||||
누가 Agent Control Plane을 읽을 수 있고 누가 정책을 편집할 수 있는지 관리합니다.
|
||||
누가 Agent Control Plane을 읽을 수 있고 누가 규칙을 편집할 수 있는지 관리합니다.
|
||||
</Card>
|
||||
<Card title="Traces용 PII Redaction" icon="lock" href="/ko/enterprise/features/pii-trace-redactions">
|
||||
정책이 참조하는 엔티티 카탈로그 및 deployment 단위 PII 설정.
|
||||
규칙이 참조하는 엔티티 카탈로그 및 deployment 단위 PII 설정.
|
||||
</Card>
|
||||
<Card title="AMP에 배포" icon="rocket" href="/ko/enterprise/guides/deploy-to-amp">
|
||||
Agent Control Plane을 지원하는 crewAI 버전으로 crew를 배포합니다.
|
||||
@@ -78,5 +78,5 @@ icon: "book-open"
|
||||
</CardGroup>
|
||||
|
||||
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
|
||||
메트릭 해석 또는 정책 설계에 도움이 필요하시면 지원 팀에 문의하세요.
|
||||
메트릭 해석 또는 규칙 설계에 도움이 필요하시면 지원 팀에 문의하세요.
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "정책 설정하기"
|
||||
title: "규칙 설정하기"
|
||||
description: "한 곳에서 조직 단위 정책을 여러 자동화에 적용합니다."
|
||||
sidebarTitle: "정책"
|
||||
sidebarTitle: "규칙"
|
||||
icon: "shield-check"
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -11,50 +11,50 @@ mode: "wide"
|
||||
|
||||
- [개요](/ko/enterprise/features/agent-control-plane/overview)
|
||||
- [모니터링](/ko/enterprise/features/agent-control-plane/monitoring)
|
||||
- **정책** *(현재 페이지)*
|
||||
- **규칙** *(현재 페이지)*
|
||||
</Info>
|
||||
|
||||
## 개요
|
||||
|
||||
정책(Policies)은 각 deployment를 개별 설정하는 대신, 정책 — 현재: **PII Redaction** — 을 한 번에 여러 자동화에 적용할 수 있게 해줍니다. 관리하려면 [Agent Control Plane](/ko/enterprise/features/agent-control-plane/overview)에서 **Policies** 탭을 엽니다.
|
||||
규칙(Rules)은 각 deployment를 개별 설정하는 대신, 정책 — 현재: **PII Redaction** — 을 한 번에 여러 자동화에 적용할 수 있게 해줍니다. 관리하려면 [Agent Control Plane](/ko/enterprise/features/agent-control-plane/overview)에서 **Rules** 탭을 엽니다.
|
||||
|
||||
<Frame>
|
||||

|
||||

|
||||
</Frame>
|
||||
|
||||
각 정책 카드에는 이름, 설명, 정책이 적용되는 **범위(scope)**(선택된 도구와 태그), 그리고 현재 범위와 일치하는 deployment의 수인 **engaged automations**가 표시됩니다. 오른쪽 토글로 정책을 삭제하지 않고 활성/비활성할 수 있습니다.
|
||||
각 규칙 카드에는 이름, 설명, 규칙이 적용되는 **범위(scope)**(선택된 도구와 태그), 그리고 현재 범위와 일치하는 deployment의 수인 **engaged automations**가 표시됩니다. 오른쪽 토글로 규칙을 삭제하지 않고 활성/비활성할 수 있습니다.
|
||||
|
||||
## 요구사항
|
||||
|
||||
<Warning>
|
||||
PII Redaction 정책을 생성하거나 편집하려면 **Enterprise 또는 Ultra 플랜**이 필요합니다. 하위 플랜의 조직도 Policies 탭을 열고 기존 정책을 볼 수는 있지만, 편집기는 "Enterprise" 잠금 핀과 *"PII Redaction policies require an Enterprise plan."* 경고와 함께 읽기 전용으로 렌더링됩니다. 업그레이드하려면 계정 오너 또는 영업팀에 문의하세요.
|
||||
PII Redaction 규칙을 생성하거나 편집하려면 **Enterprise 또는 Ultra 플랜**이 필요합니다. 하위 플랜의 조직도 Rules 탭을 열고 기존 규칙을 볼 수는 있지만, 편집기는 "Enterprise" 잠금 핀과 *"PII Redaction rules require an Enterprise plan."* 경고와 함께 읽기 전용으로 렌더링됩니다. 업그레이드하려면 계정 오너 또는 영업팀에 문의하세요.
|
||||
</Warning>
|
||||
|
||||
- **Agent Control Plane** 기능이 조직에 대해 활성화되어 있어야 합니다. [개요 — 요구사항](/ko/enterprise/features/agent-control-plane/overview#요구사항)을 참고하세요.
|
||||
- 정책을 생성·편집·토글·삭제하려면 Agent Control Plane에 대한 [RBAC](/ko/enterprise/features/rbac)의 `manage` 권한이 필요합니다. 보려면 `read` 권한만으로 충분합니다.
|
||||
- 모든 정책 변경은 감사를 위해 버전 관리됩니다.
|
||||
- 규칙을 생성·편집·토글·삭제하려면 Agent Control Plane에 대한 [RBAC](/ko/enterprise/features/rbac)의 `manage` 권한이 필요합니다. 보려면 `read` 권한만으로 충분합니다.
|
||||
- 모든 규칙 변경은 감사를 위해 버전 관리됩니다.
|
||||
|
||||
## 사용 가능한 정책 유형
|
||||
## 사용 가능한 규칙 유형
|
||||
|
||||
| 유형 | 동작 |
|
||||
|------|---------------|
|
||||
| **PII Redaction** | 일치하는 모든 자동화의 실행에 PII redaction을 적용합니다. [Traces용 PII Redaction](/ko/enterprise/features/pii-trace-redactions)에 문서화된 동일한 엔티티 카탈로그와 커스텀 recognizer를 사용합니다. |
|
||||
|
||||
향후 더 많은 정책 유형이 추가될 예정입니다.
|
||||
향후 더 많은 규칙 유형이 추가될 예정입니다.
|
||||
|
||||
## 정책 만들기
|
||||
## 규칙 만들기
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/acp-policies-new-side-panel.png" alt="조건 및 PII 마스크 유형이 있는 정책 편집 사이드 패널" width="450" />
|
||||
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="조건 및 PII 마스크 유형이 있는 규칙 편집 사이드 패널" width="450" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="편집기 열기">
|
||||
Policies 탭 오른쪽 상단의 **+ Create new**를 클릭하거나, 기존 정책 카드의 **View Details**를 클릭합니다.
|
||||
Rules 탭 오른쪽 상단의 **+ Create new**를 클릭하거나, 기존 규칙 카드의 **View Details**를 클릭합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="정책 이름과 설명 작성">
|
||||
정책에 명확한 이름(예: *Mask PII (CC)*)과 적용 시점을 설명하는 description을 부여합니다. 둘 다 정책 카드와 Engaged Automations 모달에 표시됩니다.
|
||||
<Step title="규칙 이름과 설명 작성">
|
||||
규칙에 명확한 이름(예: *Mask PII (CC)*)과 적용 시점을 설명하는 description을 부여합니다. 둘 다 규칙 카드와 Engaged Automations 모달에 표시됩니다.
|
||||
</Step>
|
||||
|
||||
<Step title="유형 선택">
|
||||
@@ -62,12 +62,12 @@ mode: "wide"
|
||||
</Step>
|
||||
|
||||
<Step title="조건 설정">
|
||||
조건은 정책이 어떤 자동화에 engage 할지 결정합니다. 둘 다 선택 사항이며 **집합 동일성(set-equality)** 의미론을 사용합니다:
|
||||
조건은 규칙이 어떤 자동화에 engage 할지 결정합니다. 둘 다 선택 사항이며 **집합 동일성(set-equality)** 의미론을 사용합니다:
|
||||
|
||||
- **Tools** — 도구 집합이 선택된 도구와 **정확히 일치**하는 자동화만 engage 됩니다. Studio 앱, MCP, OSS 도구, Tool Repository registry 도구 중에서 선택합니다.
|
||||
- **Automations** — 태그 집합이 선택된 태그와 **정확히 일치**하는 자동화만 engage 됩니다.
|
||||
|
||||
피커를 비워두면 "이 차원에서 필터링하지 않음"을 의미합니다. 두 피커를 모두 비워두면 정책이 조직의 **모든** 자동화에 적용됩니다.
|
||||
피커를 비워두면 "이 차원에서 필터링하지 않음"을 의미합니다. 두 피커를 모두 비워두면 규칙이 조직의 **모든** 자동화에 적용됩니다.
|
||||
</Step>
|
||||
|
||||
<Step title="PII Mask Type 테이블 구성">
|
||||
@@ -75,30 +75,30 @@ mode: "wide"
|
||||
</Step>
|
||||
|
||||
<Step title="저장">
|
||||
저장하는 즉시 engage 된 모든 자동화의 **향후** 실행에 정책이 적용됩니다. 재배포는 필요 없습니다.
|
||||
저장하는 즉시 engage 된 모든 자동화의 **향후** 실행에 규칙이 적용됩니다. 재배포는 필요 없습니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Engaged automations
|
||||
|
||||
정책 카드의 **Engaged N automations**를 클릭하면 현재 정책이 일치시키고 있는 deployment와 각 deployment의 마지막 실행을 정확히 확인할 수 있습니다.
|
||||
규칙 카드의 **Engaged N automations**를 클릭하면 현재 규칙이 일치시키고 있는 deployment와 각 deployment의 마지막 실행을 정확히 확인할 수 있습니다.
|
||||
|
||||
<Frame>
|
||||

|
||||

|
||||
</Frame>
|
||||
|
||||
정책을 활성화하기 전에 범위를 빠르게 점검하는 가장 좋은 방법입니다. 예를 들어, `production` 태그로 범위를 지정한 정책이 의도치 않게 staging deployment를 일치시키지 않는지 확인할 수 있습니다.
|
||||
규칙을 활성화하기 전에 범위를 빠르게 점검하는 가장 좋은 방법입니다. 예를 들어, `production` 태그로 범위를 지정한 규칙이 의도치 않게 staging deployment를 일치시키지 않는지 확인할 수 있습니다.
|
||||
|
||||
## 조직 단위 정책 vs deployment 단위 설정
|
||||
## 조직 단위 규칙 vs deployment 단위 설정
|
||||
|
||||
PII Redaction은 두 곳에서 설정할 수 있습니다:
|
||||
|
||||
- **deployment 단위** — 각 deployment의 **Settings → PII Protection** ([가이드](/ko/enterprise/features/pii-trace-redactions))
|
||||
- **조직 단위** — 이 페이지의 정책
|
||||
- **조직 단위** — 이 페이지의 규칙
|
||||
|
||||
활성화된 조직 단위 정책의 범위가 어떤 deployment와 일치하면, 정책의 엔티티 구성이 그 deployment의 실행에 대해 **deployment가 소유한 PII 설정을 덮어씁니다**. 정책이 연결된 동안에는 정책이 단일 진실 공급원이 됩니다. 정책을 비활성화하거나 분리하면(또는 범위를 변경하여 더 이상 일치하지 않게 만들면) deployment는 자체 PII Protection 설정으로 되돌아갑니다.
|
||||
활성화된 조직 단위 규칙의 범위가 어떤 deployment와 일치하면, 규칙의 엔티티 구성이 그 deployment의 실행에 대해 **deployment가 소유한 PII 설정을 덮어씁니다**. 규칙이 연결된 동안에는 규칙이 단일 진실 공급원이 됩니다. 규칙을 비활성화하거나 분리하면(또는 범위를 변경하여 더 이상 일치하지 않게 만들면) deployment는 자체 PII Protection 설정으로 되돌아갑니다.
|
||||
|
||||
여러 deployment에 걸쳐 일관된 정책을 강제하고 싶을 때는 조직 단위 정책을 선호하고, 일회성 예외에 대해서는 deployment 단위 설정을 사용하세요.
|
||||
여러 deployment에 걸쳐 일관된 정책을 강제하고 싶을 때는 조직 단위 규칙을 선호하고, 일회성 예외에 대해서는 deployment 단위 설정을 사용하세요.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
@@ -113,10 +113,10 @@ PII Redaction은 두 곳에서 설정할 수 있습니다:
|
||||
엔티티 카탈로그, 커스텀 recognizer, deployment 단위 구성.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/ko/enterprise/features/rbac">
|
||||
누가 정책을 만들거나 편집할 수 있는지 관리합니다.
|
||||
누가 규칙을 만들거나 편집할 수 있는지 관리합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
|
||||
조직의 정책을 설계하는 데 도움이 필요하시면 지원 팀에 문의하세요.
|
||||
조직의 규칙을 설계하는 데 도움이 필요하시면 지원 팀에 문의하세요.
|
||||
</Card>
|
||||
@@ -11,7 +11,7 @@ mode: "wide"
|
||||
|
||||
- [Visão Geral](/pt-BR/enterprise/features/agent-control-plane/overview)
|
||||
- **Monitoramento** *(você está aqui)*
|
||||
- [Políticas](/edge/pt-BR/enterprise/features/agent-control-plane/policies)
|
||||
- [Regras](/pt-BR/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## Visão Geral
|
||||
@@ -58,7 +58,7 @@ A sub-aba **Automações** é o detalhamento por deployment da saúde da frota.
|
||||
| **Última execução** | Tempo decorrido desde a execução mais recente. |
|
||||
| **Health Status Breakdown** | Barra empilhada com percentuais de `Critical` / `Warning` / `Healthy` para as execuções na janela. |
|
||||
| **Executions with Errors** | Total de execuções com falha na janela. |
|
||||
| **PII detection applied** | `Yes` se houver configuração de PII por deployment ou uma [política de PII](/edge/pt-BR/enterprise/features/agent-control-plane/policies) correspondente ativa. |
|
||||
| **PII detection applied** | `Yes` se houver configuração de PII por deployment ou uma [regra de PII](/pt-BR/enterprise/features/agent-control-plane/rules) correspondente ativa. |
|
||||
| **Executions** | Total de execuções na janela. |
|
||||
| **Last updated** | Quando o deployment foi re-implantado pela última vez. |
|
||||
| **Crew Version** | A versão do `crewai` reportada pelo deployment. Um ícone de informação ao lado de versões abaixo de `1.13` indica linhas que não conseguem contribuir com métricas. |
|
||||
@@ -96,8 +96,8 @@ Filtre por **LLM provider** e ordene por `Cost`, `Executions` ou `Last run`.
|
||||
<Card title="Agent Control Plane — Visão Geral" icon="book-open" href="/pt-BR/enterprise/features/agent-control-plane/overview">
|
||||
O que é o ACP, requisitos, planos suportados e RBAC.
|
||||
</Card>
|
||||
<Card title="Agent Control Plane — Políticas" icon="shield-check" href="/edge/pt-BR/enterprise/features/agent-control-plane/policies">
|
||||
Aplique políticas de PII Redaction em nível de organização em muitas automações.
|
||||
<Card title="Agent Control Plane — Regras" icon="shield-check" href="/pt-BR/enterprise/features/agent-control-plane/rules">
|
||||
Aplique regras de PII Redaction em nível de organização em muitas automações.
|
||||
</Card>
|
||||
<Card title="Traces" icon="timeline" href="/pt-BR/enterprise/features/traces">
|
||||
Aprofunde em uma única execução para ver o raciocínio do agente, chamadas de ferramentas e uso de tokens.
|
||||
|
||||
@@ -10,17 +10,17 @@ icon: "book-open"
|
||||
|
||||
- **Visão Geral** *(você está aqui)*
|
||||
- [Monitoramento](/pt-BR/enterprise/features/agent-control-plane/monitoring)
|
||||
- [Políticas](/edge/pt-BR/enterprise/features/agent-control-plane/policies)
|
||||
- [Regras](/pt-BR/enterprise/features/agent-control-plane/rules)
|
||||
</Info>
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O **Agent Control Plane** (ACP) é o hub de operações para tudo que você tem rodando no CrewAI AMP. É uma tela única — dividida nas abas **Automações** e **Políticas** — que permite à sua equipe:
|
||||
O **Agent Control Plane** (ACP) é o hub de operações para tudo que você tem rodando no CrewAI AMP. É uma tela única — dividida nas abas **Automações** e **Regras** — que permite à sua equipe:
|
||||
|
||||
- Monitorar a **saúde** de cada automação ao vivo (crew ou flow), com detalhamentos `Critical` / `Warning` / `Healthy` e contagem de execuções.
|
||||
- Acompanhar o **consumo de LLM** — tokens e custo — por automação, por provedor e por modelo, com a variação em relação ao período anterior.
|
||||
- Aprofundar em qualquer automação ou provedor de modelo para ver gráficos de série temporal e detalhamentos por provedor.
|
||||
- Aplicar **Políticas** em nível de organização (hoje: PII Redaction) em muitas automações de uma só vez, em vez de editar cada deployment individualmente.
|
||||
- Aplicar **Regras** em nível de organização (hoje: PII Redaction) em muitas automações de uma só vez, em vez de editar cada deployment individualmente.
|
||||
|
||||
<Frame>
|
||||

|
||||
@@ -33,7 +33,7 @@ O **Agent Control Plane** (ACP) é o hub de operações para tudo que você tem
|
||||
As duas abas respondem a duas perguntas distintas:
|
||||
|
||||
- **Automações** — *"Como minha frota está se comportando agora e quanto está me custando?"* Veja [Monitoramento](/pt-BR/enterprise/features/agent-control-plane/monitoring).
|
||||
- **Políticas** — *"Como aplico uma política (por exemplo, PII redaction) em muitos deployments sem precisar reimplantar cada um?"* Veja [Políticas](/edge/pt-BR/enterprise/features/agent-control-plane/policies).
|
||||
- **Regras** — *"Como aplico uma política (por exemplo, PII redaction) em muitos deployments sem precisar reimplantar cada um?"* Veja [Regras](/pt-BR/enterprise/features/agent-control-plane/rules).
|
||||
|
||||
## Requisitos
|
||||
|
||||
@@ -42,11 +42,11 @@ As duas abas respondem a duas perguntas distintas:
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**Plano Enterprise ou Ultra** é necessário para criar ou editar [Políticas](/edge/pt-BR/enterprise/features/agent-control-plane/policies). Organizações em planos inferiores ainda podem abrir a aba Políticas e visualizar políticas existentes, mas o editor é renderizado em modo somente leitura, com um selo "Enterprise" de bloqueio e o alerta *"PII Redaction policies require an Enterprise plan."* O Monitoramento (a aba Automações) está disponível em todos os planos em que o recurso esteja habilitado.
|
||||
**Plano Enterprise ou Ultra** é necessário para criar ou editar [Regras](/pt-BR/enterprise/features/agent-control-plane/rules). Organizações em planos inferiores ainda podem abrir a aba Regras e visualizar regras existentes, mas o editor é renderizado em modo somente leitura, com um selo "Enterprise" de bloqueio e o alerta *"PII Redaction rules require an Enterprise plan."* O Monitoramento (a aba Automações) está disponível em todos os planos em que o recurso esteja habilitado.
|
||||
</Warning>
|
||||
|
||||
- O recurso **Agent Control Plane** precisa estar habilitado para sua organização. Se você não o vê na barra lateral, peça ao owner da conta para solicitar a habilitação.
|
||||
- Dentro do ACP, o [RBAC](/pt-BR/enterprise/features/rbac) controla o acesso: `read` para visualizar o dashboard e as políticas, `manage` para criar, editar, ligar/desligar ou excluir políticas.
|
||||
- Dentro do ACP, o [RBAC](/pt-BR/enterprise/features/rbac) controla o acesso: `read` para visualizar o dashboard e as regras, `manage` para criar, editar, ligar/desligar ou excluir regras.
|
||||
- Todos os gráficos e tabelas podem ser ajustados para **Últimas 24 horas**, **Última Semana** ou **Últimos 30 dias** usando o seletor de tempo no canto superior direito. As variações (`↑ 8 vs ontem`, `↓ $20.57 vs ontem`, etc.) comparam a janela selecionada com a janela anterior de mesma duração.
|
||||
|
||||
## O que você pode fazer aqui
|
||||
@@ -55,7 +55,7 @@ As duas abas respondem a duas perguntas distintas:
|
||||
<Card title="Monitoramento" icon="gauge" href="/pt-BR/enterprise/features/agent-control-plane/monitoring">
|
||||
Acompanhe a saúde da frota e o gasto com LLM com cards de métricas, um sankey interativo, tabelas por automação e painéis laterais de detalhamento para qualquer automação ou provedor.
|
||||
</Card>
|
||||
<Card title="Políticas" icon="shield-check" href="/edge/pt-BR/enterprise/features/agent-control-plane/policies">
|
||||
<Card title="Regras" icon="shield-check" href="/pt-BR/enterprise/features/agent-control-plane/rules">
|
||||
Aplique políticas de PII Redaction em nível de organização, com escopo por ferramentas e tags. As mudanças entram em vigor na próxima execução — sem necessidade de re-implantação.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -67,10 +67,10 @@ As duas abas respondem a duas perguntas distintas:
|
||||
Aprofunde em uma única execução para ver o raciocínio do agente, chamadas de ferramentas e uso de tokens.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/pt-BR/enterprise/features/rbac">
|
||||
Gerencie quem pode ler o Agent Control Plane e quem pode editar políticas.
|
||||
Gerencie quem pode ler o Agent Control Plane e quem pode editar regras.
|
||||
</Card>
|
||||
<Card title="PII Redaction para Traces" icon="lock" href="/pt-BR/enterprise/features/pii-trace-redactions">
|
||||
Catálogo de entidades e configuração de PII por deployment, referenciados pelas Políticas.
|
||||
Catálogo de entidades e configuração de PII por deployment, referenciados pelas Regras.
|
||||
</Card>
|
||||
<Card title="Deploy no AMP" icon="rocket" href="/pt-BR/enterprise/guides/deploy-to-amp">
|
||||
Implante uma crew em uma versão do crewAI que suporta o Agent Control Plane.
|
||||
@@ -78,5 +78,5 @@ As duas abas respondem a duas perguntas distintas:
|
||||
</CardGroup>
|
||||
|
||||
<Card title="Precisa de ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nosso time de suporte para interpretar métricas ou desenhar políticas.
|
||||
Entre em contato com nosso time de suporte para interpretar métricas ou desenhar regras.
|
||||
</Card>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
title: "Configure as Políticas"
|
||||
description: "Aplique políticas em nível de organização em muitas automações a partir de um único lugar."
|
||||
sidebarTitle: "Políticas"
|
||||
icon: "shield-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Navegação da Documentação do ACP (Beta)**
|
||||
|
||||
- [Visão Geral](/pt-BR/enterprise/features/agent-control-plane/overview)
|
||||
- [Monitoramento](/pt-BR/enterprise/features/agent-control-plane/monitoring)
|
||||
- **Políticas** *(você está aqui)*
|
||||
</Info>
|
||||
|
||||
## Visão Geral
|
||||
|
||||
As Políticas permitem aplicar políticas — hoje: **PII Redaction** — em muitas automações de uma só vez, em vez de configurar cada deployment individualmente. Abra a aba **Políticas** no [Agent Control Plane](/pt-BR/enterprise/features/agent-control-plane/overview) para gerenciá-las.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Cada card de política mostra o nome, a descrição, o **escopo** ao qual a política se aplica (ferramentas e tags selecionadas) e a contagem de **automações engajadas** — deployments que atualmente correspondem ao escopo. O toggle à direita ativa ou desativa a política sem excluí-la.
|
||||
|
||||
## Requisitos
|
||||
|
||||
<Warning>
|
||||
**Plano Enterprise ou Ultra** é necessário para criar ou editar políticas de PII Redaction. Organizações em planos inferiores ainda podem abrir a aba Políticas e visualizar políticas existentes, mas o editor é renderizado em modo somente leitura, com um selo "Enterprise" de bloqueio e o alerta *"PII Redaction policies require an Enterprise plan."* — entre em contato com o owner da sua conta ou com vendas para fazer upgrade.
|
||||
</Warning>
|
||||
|
||||
- O recurso **Agent Control Plane** precisa estar habilitado para sua organização. Veja [Visão Geral — Requisitos](/pt-BR/enterprise/features/agent-control-plane/overview#requisitos).
|
||||
- A permissão `manage` no [RBAC](/pt-BR/enterprise/features/rbac) sobre o Agent Control Plane é necessária para criar, editar, ligar/desligar ou excluir políticas. A permissão `read` é suficiente para visualizá-las.
|
||||
- Todas as mudanças de políticas são versionadas para auditoria.
|
||||
|
||||
## Tipos de política disponíveis
|
||||
|
||||
| Tipo | O que faz |
|
||||
|------|---------------|
|
||||
| **PII Redaction** | Aplica PII redaction às execuções de cada automação correspondente, usando o mesmo catálogo de entidades e recognizers customizados documentados em [PII Redaction para Traces](/pt-BR/enterprise/features/pii-trace-redactions). |
|
||||
|
||||
Mais tipos de políticas serão adicionados ao longo do tempo.
|
||||
|
||||
## Criando uma política
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/acp-policies-new-side-panel.png" alt="Painel lateral de edição de política com condições e tipo de máscara de PII" width="450" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="Abra o editor">
|
||||
Clique em **+ Create new** no canto superior direito da aba Políticas, ou em **View Details** em um card de política existente.
|
||||
</Step>
|
||||
|
||||
<Step title="Dê um nome e descreva a política">
|
||||
Dê à política um nome claro (ex.: *Mask PII (CC)*) e uma descrição explicando quando ela se aplica. Ambos aparecem no card da política e no modal de Automações Engajadas.
|
||||
</Step>
|
||||
|
||||
<Step title="Escolha o tipo">
|
||||
Hoje só **PII Redaction** está disponível.
|
||||
</Step>
|
||||
|
||||
<Step title="Defina as condições">
|
||||
As condições decidem quais automações a política engaja. Ambas são opcionais e usam a semântica de **igualdade de conjuntos**:
|
||||
|
||||
- **Tools** — apenas automações cujo conjunto de ferramentas **corresponde exatamente** às ferramentas selecionadas serão engajadas. Selecione entre apps do Studio, MCPs, ferramentas OSS e ferramentas do Tool Repository.
|
||||
- **Automations** — apenas automações cujo conjunto de tags **corresponde exatamente** às tags selecionadas serão engajadas.
|
||||
|
||||
Deixar um seletor vazio significa "sem filtro nesta dimensão". Deixar ambos vazios significa que a política se aplica a **todas** as automações da organização.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure a tabela PII Mask Type">
|
||||
Marque cada tipo de entidade que deseja cobrir e escolha **Mask** (substitui pelo rótulo da entidade, ex.: `<CREDIT_CARD>`) ou **Redact** (remove o texto correspondente por completo). Veja [PII Redaction para Traces](/pt-BR/enterprise/features/pii-trace-redactions) para o catálogo completo de entidades e como adicionar recognizers customizados em nível de organização.
|
||||
</Step>
|
||||
|
||||
<Step title="Salve">
|
||||
A política se aplica a **futuras** execuções de cada automação engajada assim que você salva. Nenhuma re-implantação é necessária.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Automações engajadas
|
||||
|
||||
Clique em **Engaged N automations** em qualquer card de política para ver exatamente quais deployments a política está correspondendo no momento, junto com a última execução de cada um.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Esta é a forma mais rápida de validar o escopo de uma política antes de habilitá-la — por exemplo, para confirmar que uma política com escopo na tag `production` não está acidentalmente correspondendo a um deployment de staging.
|
||||
|
||||
## Políticas em nível de organização vs configurações por deployment
|
||||
|
||||
A PII Redaction pode ser configurada em dois lugares:
|
||||
|
||||
- **Por deployment** — em **Settings → PII Protection** em cada deployment individual ([guia](/pt-BR/enterprise/features/pii-trace-redactions))
|
||||
- **Em nível de organização** — como uma Política nesta página
|
||||
|
||||
Quando o escopo de uma política habilitada em nível de organização corresponde a um deployment, a configuração de entidades da política **sobrescreve** as configurações de PII pertencentes ao deployment para as execuções daquele deployment — a política se torna a fonte única da verdade enquanto está vinculada. Desabilite ou desvincule a política (ou altere o escopo para que ela não corresponda mais) e o deployment volta às suas próprias configurações de PII Protection.
|
||||
|
||||
Prefira políticas em nível de organização quando quiser impor uma política consistente em muitos deployments; reserve a configuração por deployment para exceções pontuais.
|
||||
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Agent Control Plane — Visão Geral" icon="book-open" href="/pt-BR/enterprise/features/agent-control-plane/overview">
|
||||
O que é o ACP, requisitos, planos suportados e RBAC.
|
||||
</Card>
|
||||
<Card title="Agent Control Plane — Monitoramento" icon="gauge" href="/pt-BR/enterprise/features/agent-control-plane/monitoring">
|
||||
Acompanhe automações e consumo de LLM em toda a sua frota.
|
||||
</Card>
|
||||
<Card title="PII Redaction para Traces" icon="lock" href="/pt-BR/enterprise/features/pii-trace-redactions">
|
||||
Catálogo de entidades, recognizers customizados e configuração por deployment.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/pt-BR/enterprise/features/rbac">
|
||||
Gerencie quem pode criar ou editar políticas.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Card title="Precisa de ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nosso time de suporte para ajudar a desenhar políticas para a sua organização.
|
||||
</Card>
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "Configure as Regras"
|
||||
description: "Aplique políticas em nível de organização em muitas automações a partir de um único lugar."
|
||||
sidebarTitle: "Regras"
|
||||
icon: "shield-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Navegação da Documentação do ACP (Beta)**
|
||||
|
||||
- [Visão Geral](/pt-BR/enterprise/features/agent-control-plane/overview)
|
||||
- [Monitoramento](/pt-BR/enterprise/features/agent-control-plane/monitoring)
|
||||
- **Regras** *(você está aqui)*
|
||||
</Info>
|
||||
|
||||
## Visão Geral
|
||||
|
||||
As Regras permitem aplicar políticas — hoje: **PII Redaction** — em muitas automações de uma só vez, em vez de configurar cada deployment individualmente. Abra a aba **Regras** no [Agent Control Plane](/pt-BR/enterprise/features/agent-control-plane/overview) para gerenciá-las.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Cada card de regra mostra o nome, a descrição, o **escopo** ao qual a regra se aplica (ferramentas e tags selecionadas) e a contagem de **automações engajadas** — deployments que atualmente correspondem ao escopo. O toggle à direita ativa ou desativa a regra sem excluí-la.
|
||||
|
||||
## Requisitos
|
||||
|
||||
<Warning>
|
||||
**Plano Enterprise ou Ultra** é necessário para criar ou editar regras de PII Redaction. Organizações em planos inferiores ainda podem abrir a aba Regras e visualizar regras existentes, mas o editor é renderizado em modo somente leitura, com um selo "Enterprise" de bloqueio e o alerta *"PII Redaction rules require an Enterprise plan."* — entre em contato com o owner da sua conta ou com vendas para fazer upgrade.
|
||||
</Warning>
|
||||
|
||||
- O recurso **Agent Control Plane** precisa estar habilitado para sua organização. Veja [Visão Geral — Requisitos](/pt-BR/enterprise/features/agent-control-plane/overview#requisitos).
|
||||
- A permissão `manage` no [RBAC](/pt-BR/enterprise/features/rbac) sobre o Agent Control Plane é necessária para criar, editar, ligar/desligar ou excluir regras. A permissão `read` é suficiente para visualizá-las.
|
||||
- Todas as mudanças de regras são versionadas para auditoria.
|
||||
|
||||
## Tipos de regra disponíveis
|
||||
|
||||
| Tipo | O que faz |
|
||||
|------|---------------|
|
||||
| **PII Redaction** | Aplica PII redaction às execuções de cada automação correspondente, usando o mesmo catálogo de entidades e recognizers customizados documentados em [PII Redaction para Traces](/pt-BR/enterprise/features/pii-trace-redactions). |
|
||||
|
||||
Mais tipos de regras serão adicionados ao longo do tempo.
|
||||
|
||||
## Criando uma regra
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="Painel lateral de edição de regra com condições e tipo de máscara de PII" width="450" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="Abra o editor">
|
||||
Clique em **+ Create new** no canto superior direito da aba Regras, ou em **View Details** em um card de regra existente.
|
||||
</Step>
|
||||
|
||||
<Step title="Dê um nome e descreva a regra">
|
||||
Dê à regra um nome claro (ex.: *Mask PII (CC)*) e uma descrição explicando quando ela se aplica. Ambos aparecem no card da regra e no modal de Automações Engajadas.
|
||||
</Step>
|
||||
|
||||
<Step title="Escolha o tipo">
|
||||
Hoje só **PII Redaction** está disponível.
|
||||
</Step>
|
||||
|
||||
<Step title="Defina as condições">
|
||||
As condições decidem quais automações a regra engaja. Ambas são opcionais e usam a semântica de **igualdade de conjuntos**:
|
||||
|
||||
- **Tools** — apenas automações cujo conjunto de ferramentas **corresponde exatamente** às ferramentas selecionadas serão engajadas. Selecione entre apps do Studio, MCPs, ferramentas OSS e ferramentas do Tool Repository.
|
||||
- **Automations** — apenas automações cujo conjunto de tags **corresponde exatamente** às tags selecionadas serão engajadas.
|
||||
|
||||
Deixar um seletor vazio significa "sem filtro nesta dimensão". Deixar ambos vazios significa que a regra se aplica a **todas** as automações da organização.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure a tabela PII Mask Type">
|
||||
Marque cada tipo de entidade que deseja cobrir e escolha **Mask** (substitui pelo rótulo da entidade, ex.: `<CREDIT_CARD>`) ou **Redact** (remove o texto correspondente por completo). Veja [PII Redaction para Traces](/pt-BR/enterprise/features/pii-trace-redactions) para o catálogo completo de entidades e como adicionar recognizers customizados em nível de organização.
|
||||
</Step>
|
||||
|
||||
<Step title="Salve">
|
||||
A regra se aplica a **futuras** execuções de cada automação engajada assim que você salva. Nenhuma re-implantação é necessária.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Automações engajadas
|
||||
|
||||
Clique em **Engaged N automations** em qualquer card de regra para ver exatamente quais deployments a regra está correspondendo no momento, junto com a última execução de cada um.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Esta é a forma mais rápida de validar o escopo de uma regra antes de habilitá-la — por exemplo, para confirmar que uma regra com escopo na tag `production` não está acidentalmente correspondendo a um deployment de staging.
|
||||
|
||||
## Regras em nível de organização vs configurações por deployment
|
||||
|
||||
A PII Redaction pode ser configurada em dois lugares:
|
||||
|
||||
- **Por deployment** — em **Settings → PII Protection** em cada deployment individual ([guia](/pt-BR/enterprise/features/pii-trace-redactions))
|
||||
- **Em nível de organização** — como uma Regra nesta página
|
||||
|
||||
Quando o escopo de uma regra habilitada em nível de organização corresponde a um deployment, a configuração de entidades da regra **sobrescreve** as configurações de PII pertencentes ao deployment para as execuções daquele deployment — a regra se torna a fonte única da verdade enquanto está vinculada. Desabilite ou desvincule a regra (ou altere o escopo para que ela não corresponda mais) e o deployment volta às suas próprias configurações de PII Protection.
|
||||
|
||||
Prefira regras em nível de organização quando quiser impor uma política consistente em muitos deployments; reserve a configuração por deployment para exceções pontuais.
|
||||
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Agent Control Plane — Visão Geral" icon="book-open" href="/pt-BR/enterprise/features/agent-control-plane/overview">
|
||||
O que é o ACP, requisitos, planos suportados e RBAC.
|
||||
</Card>
|
||||
<Card title="Agent Control Plane — Monitoramento" icon="gauge" href="/pt-BR/enterprise/features/agent-control-plane/monitoring">
|
||||
Acompanhe automações e consumo de LLM em toda a sua frota.
|
||||
</Card>
|
||||
<Card title="PII Redaction para Traces" icon="lock" href="/pt-BR/enterprise/features/pii-trace-redactions">
|
||||
Catálogo de entidades, recognizers customizados e configuração por deployment.
|
||||
</Card>
|
||||
<Card title="RBAC" icon="users" href="/pt-BR/enterprise/features/rbac">
|
||||
Gerencie quem pode criar ou editar regras.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Card title="Precisa de ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nosso time de suporte para ajudar a desenhar regras para a sua organização.
|
||||
</Card>
|
||||
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 164 KiB |
@@ -526,8 +526,8 @@ def run(
|
||||
inputs: str | None,
|
||||
) -> None:
|
||||
"""Run the Crew or Flow."""
|
||||
# --inputs no longer requires --definition: with no override it resolves the
|
||||
# configured [tool.crewai] flow, same as a bare `crewai run`.
|
||||
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")
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from rich.text import Text
|
||||
|
||||
from crewai_cli.constants import ENV_VARS
|
||||
from crewai_cli.git import initialize_if_git_available
|
||||
from crewai_cli.model_catalog import get_provider_models
|
||||
from crewai_cli.tui_picker import pick_many, pick_one
|
||||
from crewai_cli.utils import (
|
||||
enable_prompt_line_editing,
|
||||
@@ -43,50 +42,41 @@ _PROVIDERS: list[tuple[str, str]] = [
|
||||
("watson", "IBM watsonx"),
|
||||
]
|
||||
|
||||
# Curated offline fallback / label source. The picker prefers models pulled
|
||||
# live from the vendor's own API via ``model_catalog.get_provider_models``;
|
||||
# this list is the hand-verified backstop used when no API key is available.
|
||||
# Keep entries to real, current model ids — last verified against each vendor's
|
||||
# official model docs on 2026-07-05.
|
||||
_PROVIDER_MODELS: dict[str, list[tuple[str, str]]] = {
|
||||
"openai": [
|
||||
("gpt-5.5", "GPT-5.5"),
|
||||
("gpt-5.5-pro", "GPT-5.5 Pro"),
|
||||
("gpt-5.4", "GPT-5.4"),
|
||||
("gpt-5.4-mini", "GPT-5.4 Mini"),
|
||||
("gpt-5.2", "GPT-5.2"),
|
||||
("o4-mini", "o4-mini"),
|
||||
("gpt-4.1", "GPT-4.1"),
|
||||
("gpt-4.1-mini", "GPT-4.1 Mini"),
|
||||
],
|
||||
"anthropic": [
|
||||
("claude-fable-5", "Claude Fable 5"),
|
||||
("claude-opus-4-8", "Claude Opus 4.8"),
|
||||
("claude-sonnet-5", "Claude Sonnet 5"),
|
||||
("claude-opus-4-7", "Claude Opus 4.7"),
|
||||
("claude-haiku-4-5", "Claude Haiku 4.5"),
|
||||
("claude-opus-4-6", "Claude Opus 4.6"),
|
||||
("claude-sonnet-4-6", "Claude Sonnet 4.6"),
|
||||
("claude-haiku-4-5-20251001", "Claude Haiku 4.5"),
|
||||
("claude-3-7-sonnet-20250219", "Claude 3.7 Sonnet"),
|
||||
("claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet"),
|
||||
],
|
||||
"gemini": [
|
||||
("gemini-3.5-flash", "Gemini 3.5 Flash"),
|
||||
("gemini-3.1-pro-preview", "Gemini 3.1 Pro (preview)"),
|
||||
("gemini-3-flash-preview", "Gemini 3 Flash (preview)"),
|
||||
("gemini-2.5-pro", "Gemini 2.5 Pro"),
|
||||
("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
||||
("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"),
|
||||
("gemini-3-pro-preview", "Gemini 3 Pro (preview)"),
|
||||
("gemini-2.5-pro-exp-03-25", "Gemini 2.5 Pro"),
|
||||
("gemini-2.5-flash-preview-04-17", "Gemini 2.5 Flash"),
|
||||
("gemini-2.0-flash-001", "Gemini 2.0 Flash"),
|
||||
("gemini-1.5-pro", "Gemini 1.5 Pro"),
|
||||
],
|
||||
"groq": [
|
||||
("meta-llama/llama-4-maverick-17b-128e-instruct", "Llama 4 Maverick"),
|
||||
("meta-llama/llama-4-scout-17b-16e-instruct", "Llama 4 Scout"),
|
||||
("openai/gpt-oss-120b", "GPT-OSS 120B"),
|
||||
("qwen/qwen3-32b", "Qwen3 32B"),
|
||||
("moonshotai/kimi-k2-instruct-0905", "Kimi K2"),
|
||||
("llama-3.3-70b-versatile", "Llama 3.3 70B"),
|
||||
("llama-3.1-70b-versatile", "Llama 3.1 70B"),
|
||||
("llama-3.1-8b-instant", "Llama 3.1 8B"),
|
||||
("deepseek-r1-distill-llama-70b", "DeepSeek R1 70B"),
|
||||
("mixtral-8x7b-32768", "Mixtral 8x7B"),
|
||||
],
|
||||
"ollama": [
|
||||
("llama3.3", "Llama 3.3"),
|
||||
("qwen3", "Qwen 3"),
|
||||
("llama3.1", "Llama 3.1"),
|
||||
("deepseek-r1", "DeepSeek R1"),
|
||||
("gpt-oss", "GPT-OSS"),
|
||||
("gemma3", "Gemma 3"),
|
||||
("qwen2.5", "Qwen 2.5"),
|
||||
("mistral", "Mistral"),
|
||||
],
|
||||
}
|
||||
@@ -768,9 +758,7 @@ def _select_model() -> str:
|
||||
provider_key, provider_name = _PROVIDERS[p_idx]
|
||||
click.secho(f" → {provider_name}", fg="green")
|
||||
|
||||
# Prefer the latest models pulled live from the vendor / LiteLLM; the
|
||||
# curated ``_PROVIDER_MODELS`` entry is the offline fallback and label source.
|
||||
models = get_provider_models(provider_key, _PROVIDER_MODELS.get(provider_key, []))
|
||||
models = _PROVIDER_MODELS.get(provider_key, [])
|
||||
if not models:
|
||||
custom = click.prompt(
|
||||
click.style(f" Enter model name for {provider_key}/", fg="cyan"),
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Shared interactive prompting for runtime inputs (flows and crews).
|
||||
|
||||
``crewai run`` asks the user for values that were not provided up front — for a
|
||||
declarative flow (derived from its state schema) and for a declarative (JSON)
|
||||
crew (derived from the ``{placeholder}`` references in its agents and tasks).
|
||||
Both paths go through this module so the experience is identical: the same
|
||||
header, the same per-field prompt styling, and prompt chrome on stderr so
|
||||
stdout carries only the run's result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
import difflib
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from crewai_cli.utils import enable_prompt_line_editing, is_dmn_mode_enabled
|
||||
|
||||
|
||||
def parse_inputs_json(inputs: str | None) -> dict[str, Any] | None:
|
||||
"""Parse a ``--inputs`` JSON object, exiting with a pointed error if invalid."""
|
||||
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 closest_name(key: str, candidates: Iterable[str]) -> str | None:
|
||||
"""Nearest candidate name to a likely typo, if one is close enough."""
|
||||
matches = difflib.get_close_matches(key, list(candidates), n=1, cutoff=0.7)
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def is_interactive() -> bool:
|
||||
"""Prompt only in an interactive terminal, never in non-interactive mode."""
|
||||
return not is_dmn_mode_enabled() and sys.stdin.isatty()
|
||||
|
||||
|
||||
def prompt_for_inputs(
|
||||
names: list[str],
|
||||
*,
|
||||
title: str,
|
||||
subtitle: str,
|
||||
describe: Callable[[str], str | None] | None = None,
|
||||
coerce: Callable[[str, str], Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prompt for each name and return ``{name: value}``.
|
||||
|
||||
``describe(name)`` returns an optional hint shown dim above the prompt (used
|
||||
by flows to surface a field's schema description). ``coerce(name, raw)``
|
||||
converts the typed string to the stored value (used by flows to coerce to
|
||||
the field's JSON-schema type); by default the raw string is kept as-is.
|
||||
|
||||
Prompt chrome is written to stderr so stdout carries only the run result.
|
||||
"""
|
||||
enable_prompt_line_editing()
|
||||
click.echo(err=True)
|
||||
click.secho(f" {title}", fg="cyan", bold=True, err=True)
|
||||
click.secho(f" {subtitle}", dim=True, err=True)
|
||||
|
||||
collected: dict[str, Any] = {}
|
||||
for name in names:
|
||||
if describe is not None and (hint := describe(name)):
|
||||
click.secho(f" {hint}", dim=True, err=True)
|
||||
raw = click.prompt(
|
||||
click.style(f" {name}", fg="cyan"),
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
collected[name] = coerce(name, raw) if coerce is not None else raw
|
||||
return collected
|
||||
@@ -1,657 +0,0 @@
|
||||
"""Dynamic model catalog for the crew-creation wizard.
|
||||
|
||||
Resolves the models to offer for a given provider using a three-tier strategy:
|
||||
|
||||
1. **Vendor API** - when the provider's API key is already present in the
|
||||
environment, query the vendor's own model-listing endpoint. This is the only
|
||||
source that reliably reflects the *latest* models (real release dates /
|
||||
display names, straight from the vendor).
|
||||
2. **Curated hardcoded fallback** - the hand-verified list baked into the
|
||||
wizard, used when no API key is available. Authoritative but frozen, so it is
|
||||
refreshed periodically.
|
||||
3. **LiteLLM feed** - the community ``model_prices_and_context_window.json`` the
|
||||
CLI already caches. Only used for providers with *no* curated list: the feed
|
||||
lags real releases badly (it can miss a vendor's newest models entirely), so
|
||||
it must never preempt the curated fallback.
|
||||
|
||||
Every tier is best-effort: any network error, timeout, missing key, or empty
|
||||
result quietly falls through to the next tier, and the caller's hardcoded list
|
||||
is always the final backstop. The picker never blocks for long — network calls
|
||||
use a short timeout and successful results are cached.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import certifi
|
||||
import httpx
|
||||
|
||||
from crewai_cli.constants import JSON_URL
|
||||
|
||||
|
||||
# ── Tunables ─────────────────────────────────────────────────────
|
||||
|
||||
#: How many models to surface per provider.
|
||||
MAX_MODELS = 8
|
||||
|
||||
#: Timeout (seconds) for any network call made while resolving models.
|
||||
_TIMEOUT = 6.0
|
||||
|
||||
#: How long a resolved (dynamic) catalog stays fresh before we refetch.
|
||||
_CATALOG_TTL = 6 * 3600
|
||||
|
||||
#: How long a fallback result is cached after a failed/empty fetch. Short, so a
|
||||
#: newly-added API key takes effect soon, but long enough to spare the picker a
|
||||
#: repeated timeout-prone network attempt on every call within one session.
|
||||
_NEGATIVE_TTL = 300
|
||||
|
||||
#: How long the shared LiteLLM feed cache stays fresh.
|
||||
_LITELLM_TTL = 24 * 3600
|
||||
|
||||
#: Env vars that may hold each provider's API key, in priority order. A
|
||||
#: provider with an empty tuple (e.g. local Ollama) needs no key. Gemini accepts
|
||||
#: either name, matching crewai's own Gemini provider.
|
||||
_PROVIDER_KEY_ENV: dict[str, tuple[str, ...]] = {
|
||||
"openai": ("OPENAI_API_KEY",),
|
||||
"anthropic": ("ANTHROPIC_API_KEY",),
|
||||
"gemini": ("GEMINI_API_KEY", "GOOGLE_API_KEY"),
|
||||
"groq": ("GROQ_API_KEY",),
|
||||
"cerebras": ("CEREBRAS_API_KEY",),
|
||||
"ollama": (),
|
||||
}
|
||||
|
||||
|
||||
def _provider_api_key(provider_key: str) -> str | None:
|
||||
"""First non-empty API key found among the provider's env vars."""
|
||||
for env in _PROVIDER_KEY_ENV.get(provider_key, ()):
|
||||
value = os.environ.get(env)
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
# Substrings that mark a model id as *not* a chat/completion model. Used to
|
||||
# filter noisy OpenAI-compatible ``/models`` listings.
|
||||
_NON_CHAT_MARKERS = (
|
||||
"embedding",
|
||||
"embed",
|
||||
"whisper",
|
||||
"tts",
|
||||
"audio",
|
||||
"transcribe",
|
||||
"realtime",
|
||||
"dall-e",
|
||||
"dalle",
|
||||
"image",
|
||||
"moderation",
|
||||
"similarity",
|
||||
"-edit",
|
||||
"davinci-002",
|
||||
"babbage-002",
|
||||
"computer-use",
|
||||
"guard",
|
||||
)
|
||||
|
||||
_ACRONYMS = {
|
||||
"gpt": "GPT",
|
||||
"ai": "AI",
|
||||
"nim": "NIM",
|
||||
"llm": "LLM",
|
||||
"hd": "HD",
|
||||
"us": "US",
|
||||
"eu": "EU",
|
||||
"oss": "OSS",
|
||||
"it": "IT",
|
||||
}
|
||||
|
||||
# Tokens with non-title-case brand capitalization.
|
||||
_BRAND_TOKENS = {
|
||||
"deepseek": "DeepSeek",
|
||||
"chatgpt": "ChatGPT",
|
||||
"qwq": "QwQ",
|
||||
}
|
||||
|
||||
|
||||
# ── Public API ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_provider_models(
|
||||
provider_key: str, fallback: list[tuple[str, str]]
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Return ``(model_id, label)`` pairs for ``provider_key``, newest first.
|
||||
|
||||
Tries the vendor API (if a key is in the environment) first, since it is the
|
||||
only reliably-fresh source. When no key is available it returns the curated
|
||||
``fallback`` verbatim — the LiteLLM feed is consulted **only** for providers
|
||||
with no curated list, because the feed lags real releases and would
|
||||
otherwise surface a staler list than the hand-verified fallback. Never
|
||||
raises: any failure degrades to the next tier.
|
||||
|
||||
Args:
|
||||
provider_key: Short provider identifier, e.g. ``"anthropic"``.
|
||||
fallback: Curated ``(model_id, label)`` pairs to use as the backstop and
|
||||
to source friendly labels for known models.
|
||||
|
||||
Returns:
|
||||
Up to :data:`MAX_MODELS` ``(model_id, label)`` pairs. Falls back to
|
||||
``fallback`` verbatim when no fresher list can be resolved.
|
||||
"""
|
||||
cached = _read_catalog_cache(provider_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
label_map = {model_id: label for model_id, label in fallback}
|
||||
|
||||
# A non-None vendor result is authoritative — even when empty (e.g. a
|
||||
# reachable Ollama with no models installed): show that rather than
|
||||
# hardcoded suggestions the crew can't actually run. The picker handles an
|
||||
# empty list by prompting for manual entry.
|
||||
vendor = _from_vendor(provider_key)
|
||||
if vendor is not None:
|
||||
result = _finalize(vendor, label_map)
|
||||
if result:
|
||||
_write_catalog_cache(provider_key, result, source="dynamic")
|
||||
return result
|
||||
|
||||
# Vendor tier unavailable. The LiteLLM feed lags real releases, so only
|
||||
# reach for it when we have no curated fallback — never override the fallback.
|
||||
entries = _from_litellm(provider_key) if not fallback else None
|
||||
result = _finalize(entries, label_map) if entries else []
|
||||
if result:
|
||||
_write_catalog_cache(provider_key, result, source="dynamic")
|
||||
return result
|
||||
|
||||
# Nothing fresher than the curated list. Cache it briefly (negative cache)
|
||||
# so a failed vendor/LiteLLM fetch isn't retried on every subsequent call.
|
||||
# Skip Ollama: it's a local, fast-failing server, so re-probing is cheap and
|
||||
# avoids serving suggestions after the server comes up within the TTL.
|
||||
if fallback and provider_key != "ollama":
|
||||
_write_catalog_cache(provider_key, fallback, source="fallback")
|
||||
return fallback
|
||||
|
||||
|
||||
# ── Tier 1: vendor APIs ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _from_vendor(provider_key: str) -> list[dict[str, Any]] | None:
|
||||
"""Fetch models from the vendor.
|
||||
|
||||
Returns the model list on a successful fetch — **including an empty list**,
|
||||
which is meaningful (e.g. a reachable Ollama server with nothing installed).
|
||||
Returns ``None`` only when the vendor tier is unavailable: no fetcher, no
|
||||
API key, or the request failed.
|
||||
"""
|
||||
fetcher = _VENDOR_FETCHERS.get(provider_key)
|
||||
if fetcher is None:
|
||||
return None
|
||||
|
||||
api_key = _provider_api_key(provider_key)
|
||||
if _PROVIDER_KEY_ENV.get(provider_key) and not api_key:
|
||||
# Provider needs a key and none is set — skip to the next tier.
|
||||
return None
|
||||
|
||||
try:
|
||||
return fetcher(api_key)
|
||||
except Exception:
|
||||
# Network error, auth failure, unexpected payload — degrade quietly.
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_openai(api_key: str | None) -> list[dict[str, Any]]:
|
||||
return _fetch_openai_compatible("https://api.openai.com/v1", api_key)
|
||||
|
||||
|
||||
def _fetch_groq(api_key: str | None) -> list[dict[str, Any]]:
|
||||
return _fetch_openai_compatible("https://api.groq.com/openai/v1", api_key)
|
||||
|
||||
|
||||
def _fetch_cerebras(api_key: str | None) -> list[dict[str, Any]]:
|
||||
return _fetch_openai_compatible("https://api.cerebras.ai/v1", api_key)
|
||||
|
||||
|
||||
def _fetch_openai_compatible(
|
||||
base_url: str, api_key: str | None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse an OpenAI-shaped ``GET /models`` response."""
|
||||
data = _http_get_json(
|
||||
f"{base_url}/models",
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
entries: list[dict[str, Any]] = []
|
||||
for item in data.get("data", []):
|
||||
model_id = item.get("id")
|
||||
if not model_id or not _is_chat_model(model_id) or _is_fine_tune(model_id):
|
||||
continue
|
||||
created = _as_float(item.get("created"))
|
||||
entries.append(_entry(model_id, _humanize(model_id), created=created))
|
||||
return entries
|
||||
|
||||
|
||||
def _fetch_anthropic(api_key: str | None) -> list[dict[str, Any]]:
|
||||
data = _http_get_json(
|
||||
"https://api.anthropic.com/v1/models",
|
||||
headers={"x-api-key": api_key or "", "anthropic-version": "2023-06-01"},
|
||||
)
|
||||
entries: list[dict[str, Any]] = []
|
||||
for item in data.get("data", []):
|
||||
model_id = item.get("id")
|
||||
if not model_id:
|
||||
continue
|
||||
label = item.get("display_name") or _humanize(model_id)
|
||||
created = _parse_iso(item.get("created_at"))
|
||||
entries.append(_entry(model_id, label, created=created))
|
||||
return entries
|
||||
|
||||
|
||||
def _fetch_gemini(api_key: str | None) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
params: dict[str, Any] = {"key": api_key or "", "pageSize": 200}
|
||||
# models.list is paginated and not guaranteed newest-first, so walk pages
|
||||
# (bounded) to see the full set — _finalize does the sort + truncation.
|
||||
for _ in range(10):
|
||||
try:
|
||||
data = _http_get_json(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models",
|
||||
params=params,
|
||||
)
|
||||
except Exception:
|
||||
# Later-page failure: keep the models already gathered. First-page
|
||||
# failure (nothing gathered yet) is a real outage — re-raise so the
|
||||
# caller falls back to the curated list rather than mistaking it for
|
||||
# a successful empty result.
|
||||
if entries:
|
||||
break
|
||||
raise
|
||||
for item in data.get("models", []):
|
||||
methods = item.get("supportedGenerationMethods") or []
|
||||
if "generateContent" not in methods:
|
||||
continue
|
||||
name = (item.get("name") or "").removeprefix("models/")
|
||||
if not name or not _is_chat_model(name) or "aqa" in name:
|
||||
continue
|
||||
label = item.get("displayName") or _humanize(name)
|
||||
# Gemini has no timestamp; rank by the version in name/version.
|
||||
version_hint = f"{name} {item.get('version') or ''}"
|
||||
entries.append(_entry(name, label, version_hint=version_hint))
|
||||
token = data.get("nextPageToken")
|
||||
if not token:
|
||||
break
|
||||
params = {"key": api_key or "", "pageSize": 200, "pageToken": token}
|
||||
return entries
|
||||
|
||||
|
||||
def _ollama_base() -> str:
|
||||
"""Resolve the Ollama server base URL from the environment.
|
||||
|
||||
Checks ``OLLAMA_API_BASE`` / ``API_BASE`` (what LiteLLM and the generated
|
||||
crew use) first, then ``OLLAMA_HOST`` (the Ollama runtime convention), so a
|
||||
user who only set ``OLLAMA_HOST`` sees models from the right server.
|
||||
"""
|
||||
base = (
|
||||
os.environ.get("OLLAMA_API_BASE")
|
||||
or os.environ.get("API_BASE")
|
||||
or os.environ.get("OLLAMA_HOST")
|
||||
or "http://localhost:11434"
|
||||
).strip()
|
||||
# OLLAMA_HOST is often scheme-less (e.g. "127.0.0.1:11434").
|
||||
if "://" not in base:
|
||||
base = f"http://{base}"
|
||||
return base.rstrip("/")
|
||||
|
||||
|
||||
def _fetch_ollama(_api_key: str | None) -> list[dict[str, Any]]:
|
||||
"""List models installed on the local Ollama server (no API key)."""
|
||||
data = _http_get_json(f"{_ollama_base()}/api/tags")
|
||||
entries: list[dict[str, Any]] = []
|
||||
for item in data.get("models", []):
|
||||
model_id = item.get("model") or item.get("name")
|
||||
if not model_id or not _is_chat_model(model_id) or _is_fine_tune(model_id):
|
||||
# /api/tags lists everything installed, including embedding models.
|
||||
continue
|
||||
# Ollama returns an ISO 8601 modified_at we can rank by.
|
||||
created = _parse_iso(item.get("modified_at"))
|
||||
entries.append(_entry(model_id, _humanize(model_id), created=created))
|
||||
return entries
|
||||
|
||||
|
||||
_VENDOR_FETCHERS: dict[str, Callable[[str | None], list[dict[str, Any]]]] = {
|
||||
"openai": _fetch_openai,
|
||||
"anthropic": _fetch_anthropic,
|
||||
"gemini": _fetch_gemini,
|
||||
"groq": _fetch_groq,
|
||||
"cerebras": _fetch_cerebras,
|
||||
"ollama": _fetch_ollama,
|
||||
}
|
||||
|
||||
|
||||
# ── Tier 2: LiteLLM feed ─────────────────────────────────────────
|
||||
|
||||
# Process-level memo so a single CLI run attempts the LiteLLM download at most
|
||||
# once — repeated picker calls otherwise each incur a multi-second timeout when
|
||||
# the feed is stale/unreachable. Reset via _reset_litellm_memo() in tests.
|
||||
_UNSET: Any = object()
|
||||
_litellm_memo: Any = _UNSET
|
||||
|
||||
|
||||
def _reset_litellm_memo() -> None:
|
||||
"""Clear the process-level LiteLLM memo (test hook)."""
|
||||
global _litellm_memo
|
||||
_litellm_memo = _UNSET
|
||||
|
||||
|
||||
def _from_litellm(provider_key: str) -> list[dict[str, Any]] | None:
|
||||
"""Build chat-model entries for ``provider_key`` from the LiteLLM feed."""
|
||||
data = _load_litellm_data()
|
||||
# A corrupt feed (non-mapping JSON root) must not crash the picker.
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
|
||||
entries: list[dict[str, Any]] = []
|
||||
for model_name, props in data.items():
|
||||
if not isinstance(props, dict):
|
||||
continue
|
||||
# `litellm_provider` can be present-but-null in the feed; coerce before
|
||||
# string ops so a null value is skipped rather than raising.
|
||||
if (props.get("litellm_provider") or "").strip().lower() != provider_key:
|
||||
continue
|
||||
if props.get("mode") != "chat":
|
||||
continue
|
||||
# LiteLLM keys are sometimes prefixed with the provider; the picker
|
||||
# re-adds ``provider/`` itself, so strip a leading one to avoid dupes.
|
||||
model_id = model_name
|
||||
if model_id.startswith(f"{provider_key}/"):
|
||||
model_id = model_id[len(provider_key) + 1 :]
|
||||
if not model_id:
|
||||
continue
|
||||
entries.append(_entry(model_id, _humanize(model_id), version_hint=model_id))
|
||||
return entries or None
|
||||
|
||||
|
||||
def _load_litellm_data() -> dict[str, Any] | None:
|
||||
"""Return the LiteLLM feed, memoized once per process (see _litellm_memo)."""
|
||||
global _litellm_memo
|
||||
if _litellm_memo is _UNSET:
|
||||
_litellm_memo = _fetch_litellm_data()
|
||||
memoized: dict[str, Any] | None = _litellm_memo
|
||||
return memoized
|
||||
|
||||
|
||||
def _fetch_litellm_data() -> dict[str, Any] | None:
|
||||
"""Read the cached LiteLLM feed, fetching it once if the cache is cold."""
|
||||
cache_file = _litellm_cache_file()
|
||||
fresh = (
|
||||
cache_file.exists()
|
||||
and (time.time() - cache_file.stat().st_mtime) < _LITELLM_TTL
|
||||
)
|
||||
if fresh:
|
||||
data = _read_json(cache_file)
|
||||
# A corrupt/non-mapping fresh cache must not block a recoverable
|
||||
# download — only short-circuit on a usable mapping.
|
||||
if isinstance(data, dict) and data:
|
||||
return data
|
||||
|
||||
try:
|
||||
data = _http_get_json(JSON_URL)
|
||||
except Exception:
|
||||
# Fall back to a stale cache if we have one, else give up on this tier.
|
||||
return _read_json(cache_file)
|
||||
|
||||
# Best-effort cache write; a failure (e.g. read-only home) is non-fatal
|
||||
# since we already hold the freshly-fetched data.
|
||||
with contextlib.suppress(OSError):
|
||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache_file.write_text(json.dumps(data), encoding="utf-8")
|
||||
return data
|
||||
|
||||
|
||||
# ── Ranking + labelling ──────────────────────────────────────────
|
||||
|
||||
|
||||
def _finalize(
|
||||
entries: list[dict[str, Any]], label_map: dict[str, str]
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Sort newest-first, dedupe, relabel with curated names, and truncate."""
|
||||
entries.sort(key=lambda e: e["sort"], reverse=True)
|
||||
seen: set[str] = set()
|
||||
out: list[tuple[str, str]] = []
|
||||
for entry in entries:
|
||||
model_id = entry["id"]
|
||||
if model_id in seen:
|
||||
continue
|
||||
seen.add(model_id)
|
||||
label = label_map.get(model_id) or entry["label"]
|
||||
out.append((model_id, label))
|
||||
if len(out) >= MAX_MODELS:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _entry(
|
||||
model_id: str,
|
||||
label: str,
|
||||
*,
|
||||
created: float = 0.0,
|
||||
version_hint: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a rankable catalog entry.
|
||||
|
||||
``sort`` is a comparable tuple ``(created, date_int, version_tuple)`` so a
|
||||
real vendor timestamp wins, then a date embedded in the id, then the numeric
|
||||
version. Types line up positionally, so entries compare cleanly.
|
||||
"""
|
||||
date_int, version = _version_key(version_hint or model_id)
|
||||
return {
|
||||
"id": model_id,
|
||||
"label": label,
|
||||
"sort": (created, date_int, version),
|
||||
}
|
||||
|
||||
|
||||
_DATE_RE = re.compile(r"(20\d{2})[-_]?(0[1-9]|1[0-2])[-_]?(0[1-9]|[12]\d|3[01])")
|
||||
_NUM_RE = re.compile(r"\d+")
|
||||
|
||||
|
||||
def _version_key(text: str) -> tuple[int, tuple[int, ...]]:
|
||||
"""Extract a ``(date_int, version_tuple)`` sort key from a model id.
|
||||
|
||||
A trailing/embedded ``YYYYMMDD`` (or ``YYYY-MM-DD``) becomes ``date_int``;
|
||||
remaining numbers become the version tuple. ``claude-opus-4-6`` → version
|
||||
``(4, 6)``; ``claude-3-5-sonnet-20241022`` → date ``20241022`` version
|
||||
``(3, 5)``.
|
||||
"""
|
||||
text = text or ""
|
||||
date_int = 0
|
||||
match = _DATE_RE.search(text)
|
||||
if match:
|
||||
date_int = int(match.group(1) + match.group(2) + match.group(3))
|
||||
text = _DATE_RE.sub(" ", text)
|
||||
version = tuple(int(n) for n in _NUM_RE.findall(text)[:4])
|
||||
return date_int, version
|
||||
|
||||
|
||||
def _is_chat_model(model_id: str) -> bool:
|
||||
"""Heuristically reject embedding/audio/image/etc. models by their id."""
|
||||
lowered = model_id.lower()
|
||||
return not any(marker in lowered for marker in _NON_CHAT_MARKERS)
|
||||
|
||||
|
||||
def _is_fine_tune(model_id: str) -> bool:
|
||||
"""A user fine-tune or training checkpoint (``ft:...`` / ``...:ckpt-step-N``).
|
||||
|
||||
These are account-specific artifacts: they clutter the picker, crowd out the
|
||||
foundation models (their recent ``created`` timestamps rank them first), and
|
||||
humanize into unreadable labels. Excluded from the auto-list; a user who
|
||||
wants one can still enter it via the picker's "Other" option.
|
||||
"""
|
||||
lowered = model_id.lower()
|
||||
return lowered.startswith("ft:") or ":ckpt" in lowered
|
||||
|
||||
|
||||
_SIZE_RE = re.compile(r"^\d+(?:\.\d+)?[bmk]$") # 8b, 70b, 1.5b, 120m, 32k
|
||||
_OSERIES_RE = re.compile(r"^o\d+$") # o1, o3, o4 — kept lowercase (OpenAI brand)
|
||||
|
||||
|
||||
def _humanize(model_id: str) -> str:
|
||||
"""Derive a readable label from a raw model id.
|
||||
|
||||
Best-effort only — vendor display names and the curated label map take
|
||||
precedence. Drops embedded dates and applies light casing so raw ids read
|
||||
cleanly: ``gpt-oss-120b`` → ``GPT OSS 120B``, ``qwen3-32b`` → ``Qwen3 32B``,
|
||||
``deepseek-r1:671b`` → ``DeepSeek R1 671B``, ``o3-mini`` → ``o3 Mini``.
|
||||
"""
|
||||
base = model_id.split("/")[-1]
|
||||
# Drop embedded release dates — they're noise in a label, and the picker
|
||||
# already shows the full model id alongside it.
|
||||
base = _DATE_RE.sub(" ", base)
|
||||
words: list[str] = []
|
||||
# Split on separators including ``:`` so Ollama tags (llama3.3:70b) read well.
|
||||
for part in re.split(r"[-_\s:]+", base):
|
||||
if not part:
|
||||
continue
|
||||
low = part.lower()
|
||||
if low in _ACRONYMS:
|
||||
words.append(_ACRONYMS[low])
|
||||
elif low in _BRAND_TOKENS:
|
||||
words.append(_BRAND_TOKENS[low])
|
||||
elif _SIZE_RE.match(low):
|
||||
words.append(low[:-1] + low[-1].upper()) # 70b -> 70B
|
||||
elif _OSERIES_RE.match(low):
|
||||
words.append(low) # o3 stays lowercase
|
||||
elif part[0].isalpha():
|
||||
# Capitalize the leading letter, preserve the rest (so a fused
|
||||
# family+version keeps its digits): qwen3 -> Qwen3, mini -> Mini.
|
||||
words.append(part[0].upper() + part[1:])
|
||||
else:
|
||||
words.append(part) # starts with a digit (4o, 4.1, 0905) — leave as-is
|
||||
return " ".join(words) or base
|
||||
|
||||
|
||||
# ── HTTP + parsing helpers ───────────────────────────────────────
|
||||
|
||||
|
||||
def _http_get_json(
|
||||
url: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""GET ``url`` and return parsed JSON, with a short timeout and TLS verify."""
|
||||
ssl_config = os.environ.get("SSL_CERT_FILE") or certifi.where()
|
||||
response = httpx.get(
|
||||
url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
timeout=_TIMEOUT,
|
||||
verify=ssl_config,
|
||||
follow_redirects=True,
|
||||
)
|
||||
response.raise_for_status()
|
||||
result: dict[str, Any] = response.json()
|
||||
return result
|
||||
|
||||
|
||||
def _parse_iso(value: Any) -> float:
|
||||
"""Parse an ISO 8601 timestamp to an epoch float; ``0.0`` on failure."""
|
||||
if not value or not isinstance(value, str):
|
||||
return 0.0
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _as_float(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any] | None:
|
||||
try:
|
||||
data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
|
||||
return data
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
# ── Caching ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _cache_dir() -> Path:
|
||||
return Path.home() / ".crewai"
|
||||
|
||||
|
||||
def _catalog_cache_file() -> Path:
|
||||
return _cache_dir() / "model_catalog_cache.json"
|
||||
|
||||
|
||||
def _litellm_cache_file() -> Path:
|
||||
# Shared with crewai_cli.provider so both flows warm the same cache.
|
||||
return _cache_dir() / "provider_cache.json"
|
||||
|
||||
|
||||
def _cache_key(provider_key: str) -> str:
|
||||
"""Cache key for a provider's resolved model list.
|
||||
|
||||
Includes the inputs that change what a fetch would return, so a cached
|
||||
entry is only reused when those inputs still match:
|
||||
|
||||
- Ollama lists models from a base URL that can change between runs.
|
||||
- Whether the vendor's API key is present flips between a live fetch and
|
||||
the negatively-cached fallback — so a key added after a no-key call is
|
||||
not shadowed by the cached fallback.
|
||||
"""
|
||||
if provider_key == "ollama":
|
||||
return f"ollama@{_ollama_base()}"
|
||||
suffix = "key" if _provider_api_key(provider_key) else "nokey"
|
||||
return f"{provider_key}#{suffix}"
|
||||
|
||||
|
||||
def _read_catalog_cache(provider_key: str) -> list[tuple[str, str]] | None:
|
||||
"""Return a fresh cached catalog for ``provider_key``, or ``None``."""
|
||||
payload = _read_json(_catalog_cache_file())
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
entry = payload.get(_cache_key(provider_key))
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
# Fallback (negative) entries expire fast; dynamic ones live the full TTL.
|
||||
ttl = _NEGATIVE_TTL if entry.get("source") == "fallback" else _CATALOG_TTL
|
||||
if (time.time() - _as_float(entry.get("ts"))) >= ttl:
|
||||
return None
|
||||
models = entry.get("models")
|
||||
if not isinstance(models, list) or not models:
|
||||
return None
|
||||
try:
|
||||
return [(str(m[0]), str(m[1])) for m in models]
|
||||
except (IndexError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _write_catalog_cache(
|
||||
provider_key: str, models: list[tuple[str, str]], *, source: str
|
||||
) -> None:
|
||||
cache_file = _catalog_cache_file()
|
||||
payload = _read_json(cache_file)
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
payload[_cache_key(provider_key)] = {
|
||||
"ts": time.time(),
|
||||
"source": source,
|
||||
"models": [[model_id, label] for model_id, label in models],
|
||||
}
|
||||
# Best-effort cache write; a failure (e.g. read-only home) is non-fatal.
|
||||
with contextlib.suppress(OSError):
|
||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache_file.write_text(json.dumps(payload), encoding="utf-8")
|
||||
@@ -12,14 +12,9 @@ import click
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
from packaging import version
|
||||
|
||||
from crewai_cli.input_prompt import (
|
||||
closest_name,
|
||||
is_interactive,
|
||||
parse_inputs_json,
|
||||
prompt_for_inputs,
|
||||
)
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
enable_prompt_line_editing,
|
||||
is_dmn_mode_enabled,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency, get_crewai_version
|
||||
@@ -36,7 +31,6 @@ _INPUT_PLACEHOLDER_RE = re.compile(r"(?<!{){([A-Za-z_][A-Za-z0-9_\-]*)}(?!})")
|
||||
_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV = "CREWAI_CLI_RUNNER_PACKAGE_DIR"
|
||||
_CREWAI_RUNNER_SOURCE_DIR_ENV = "CREWAI_RUNNER_SOURCE_DIR"
|
||||
_CREWAI_JSON_CREW_DEFINITION_ENV = "CREWAI_JSON_CREW_DEFINITION"
|
||||
_CREWAI_JSON_CREW_INPUTS_ENV = "CREWAI_JSON_CREW_INPUTS"
|
||||
_FULL_CREWAI_INSTALL_MESSAGE = f"""\
|
||||
CrewAI CLI is installed without the `crewai` package required to run crews.
|
||||
|
||||
@@ -85,8 +79,6 @@ kwargs = {
|
||||
}
|
||||
if crew_definition := os.getenv("CREWAI_JSON_CREW_DEFINITION"):
|
||||
kwargs["crew_path"] = crew_definition
|
||||
if crew_inputs := os.getenv("CREWAI_JSON_CREW_INPUTS"):
|
||||
kwargs["inputs"] = crew_inputs
|
||||
|
||||
try:
|
||||
module._run_json_crew(**kwargs)
|
||||
@@ -146,8 +138,8 @@ def _extract_input_placeholders(text: str | None) -> set[str]:
|
||||
return set(_INPUT_PLACEHOLDER_RE.findall(text))
|
||||
|
||||
|
||||
def _referenced_input_names(crew: Any) -> set[str]:
|
||||
"""All ``{placeholder}`` names referenced by a crew's agents and tasks."""
|
||||
def _missing_input_names(crew: Any, inputs: dict[str, Any]) -> list[str]:
|
||||
"""Return input placeholders used by a crew but not provided as defaults."""
|
||||
placeholders: set[str] = set()
|
||||
|
||||
for agent in getattr(crew, "agents", []) or []:
|
||||
@@ -168,70 +160,32 @@ def _referenced_input_names(crew: Any) -> set[str]:
|
||||
_extract_input_placeholders(getattr(task, "output_file", None))
|
||||
)
|
||||
|
||||
return placeholders
|
||||
return sorted(name for name in placeholders if name not in inputs)
|
||||
|
||||
|
||||
def _missing_input_names(crew: Any, inputs: dict[str, Any]) -> list[str]:
|
||||
"""Return input placeholders referenced by a crew but not provided as inputs."""
|
||||
return sorted(name for name in _referenced_input_names(crew) if name not in inputs)
|
||||
|
||||
|
||||
def _resolve_crew_inputs(
|
||||
crew: Any,
|
||||
default_inputs: dict[str, Any],
|
||||
provided: dict[str, Any] | None,
|
||||
*,
|
||||
interactive: bool,
|
||||
def _prompt_for_missing_inputs(
|
||||
crew: Any, default_inputs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve kickoff inputs for a declarative crew.
|
||||
|
||||
Mirrors the declarative-flow experience (``_resolve_flow_inputs``): layers
|
||||
``--inputs`` over the crew's declared ``inputs`` defaults, warns on provided
|
||||
keys that aren't referenced as ``{placeholder}``s, prompts for any
|
||||
still-missing placeholders when interactive, and exits with a pointed
|
||||
message when one is still missing.
|
||||
|
||||
Unlike flows — whose state schema is an authoritative contract, so unknown
|
||||
keys are dropped — the crew placeholder scan is heuristic (it only covers
|
||||
agent/task text fields). An unrecognized key is therefore warned about but
|
||||
*kept*, never dropped: dropping could silently discard a value that a field
|
||||
the scan doesn't cover actually relies on.
|
||||
"""
|
||||
referenced = _referenced_input_names(crew)
|
||||
"""Ask for runtime values for placeholders that lack default inputs."""
|
||||
inputs = dict(default_inputs or {})
|
||||
|
||||
for key, value in (provided or {}).items():
|
||||
if key not in referenced:
|
||||
suggestion = closest_name(key, referenced)
|
||||
hint = f" Did you mean '{suggestion}'?" if suggestion else ""
|
||||
click.secho(
|
||||
f" Input '{key}' isn't referenced by any {{placeholder}} "
|
||||
f"in the crew.{hint}",
|
||||
fg="yellow",
|
||||
err=True,
|
||||
)
|
||||
inputs[key] = value
|
||||
|
||||
missing = _missing_input_names(crew, inputs)
|
||||
if missing and interactive:
|
||||
inputs.update(
|
||||
prompt_for_inputs(
|
||||
missing,
|
||||
title="Crew inputs",
|
||||
subtitle="This crew needs the following to run.",
|
||||
)
|
||||
)
|
||||
missing = _missing_input_names(crew, inputs)
|
||||
if not missing:
|
||||
return inputs
|
||||
|
||||
if missing:
|
||||
for name in missing:
|
||||
click.secho(f" Missing required input '{name}'", fg="red", err=True)
|
||||
click.secho(
|
||||
" Provide them via --inputs or the `inputs` object in crew.json(c).",
|
||||
dim=True,
|
||||
err=True,
|
||||
enable_prompt_line_editing()
|
||||
|
||||
click.echo()
|
||||
click.secho(" Runtime inputs", fg="cyan", bold=True)
|
||||
click.secho(
|
||||
" Values for {placeholder} references in your agents and tasks.",
|
||||
dim=True,
|
||||
)
|
||||
|
||||
for name in missing:
|
||||
inputs[name] = click.prompt(
|
||||
click.style(f" {name}", fg="cyan"),
|
||||
prompt_suffix=click.style(" > ", fg="bright_white"),
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
return inputs
|
||||
|
||||
@@ -289,14 +243,29 @@ def _prepare_json_crew_for_tui(crew: Any) -> None:
|
||||
agent.llm.stream = True
|
||||
|
||||
|
||||
def _run_json_crew_without_tui(crew_path: Path, provided: dict[str, Any] | None) -> Any:
|
||||
def _runtime_inputs_without_prompt(
|
||||
crew: Any, default_inputs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Return runtime inputs in non-interactive mode or exit on missing values."""
|
||||
inputs = dict(default_inputs or {})
|
||||
missing = _missing_input_names(crew, inputs)
|
||||
if missing:
|
||||
missing_list = ", ".join(missing)
|
||||
click.echo(
|
||||
"Missing runtime inputs for CREWAI_DMN mode: "
|
||||
f"{missing_list}. Add them to the `inputs` object in crew.json(c).",
|
||||
err=True,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
return inputs
|
||||
|
||||
|
||||
def _run_json_crew_without_tui(crew_path: Path) -> Any:
|
||||
"""Run a JSON-defined crew with plain terminal output."""
|
||||
with _json_loading_status("Preparing crew..."):
|
||||
crew, default_inputs = _load_json_crew(crew_path)
|
||||
|
||||
runtime_inputs = _resolve_crew_inputs(
|
||||
crew, default_inputs, provided, interactive=False
|
||||
)
|
||||
runtime_inputs = _runtime_inputs_without_prompt(crew, default_inputs)
|
||||
result = crew.kickoff(inputs=runtime_inputs)
|
||||
if result is not None:
|
||||
click.echo(str(result))
|
||||
@@ -306,7 +275,6 @@ def _run_json_crew_without_tui(crew_path: Path, provided: dict[str, Any] | None)
|
||||
def _run_json_crew(
|
||||
trained_agents_file: str | None = None,
|
||||
crew_path: str | Path | None = None,
|
||||
inputs: str | None = None,
|
||||
) -> Any:
|
||||
"""Load and run a JSON-defined crew."""
|
||||
from dotenv import load_dotenv
|
||||
@@ -328,17 +296,13 @@ def _run_json_crew(
|
||||
)
|
||||
crew_path = Path(crew_path)
|
||||
|
||||
provided = parse_inputs_json(inputs)
|
||||
|
||||
if is_dmn_mode_enabled():
|
||||
return _run_json_crew_without_tui(crew_path, provided)
|
||||
return _run_json_crew_without_tui(crew_path)
|
||||
|
||||
crew_run_app_cls, crew, default_inputs, task_names, agent_names = (
|
||||
_load_json_crew_for_tui(crew_path)
|
||||
)
|
||||
runtime_inputs = _resolve_crew_inputs(
|
||||
crew, default_inputs, provided, interactive=is_interactive()
|
||||
)
|
||||
runtime_inputs = _prompt_for_missing_inputs(crew, default_inputs)
|
||||
|
||||
app = crew_run_app_cls(
|
||||
crew_name=crew.name or "Crew",
|
||||
@@ -447,18 +411,12 @@ def _json_crew_run_command(project_root: Path | None = None) -> list[str]:
|
||||
def _run_json_crew_in_project_env(
|
||||
trained_agents_file: str | None = None,
|
||||
crew_path: str | Path | None = None,
|
||||
inputs: str | None = None,
|
||||
) -> Any:
|
||||
"""Run JSON crews from the project's uv-managed environment."""
|
||||
# Validate --inputs up front so bad JSON fails before we spin up the uv env.
|
||||
if inputs is not None:
|
||||
parse_inputs_json(inputs)
|
||||
|
||||
if not (Path.cwd() / "pyproject.toml").is_file():
|
||||
return _run_json_crew(
|
||||
trained_agents_file=trained_agents_file,
|
||||
crew_path=crew_path,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
_install_json_crew_dependencies_if_needed()
|
||||
@@ -472,8 +430,6 @@ def _run_json_crew_in_project_env(
|
||||
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file
|
||||
if crew_path is not None:
|
||||
env[_CREWAI_JSON_CREW_DEFINITION_ENV] = str(crew_path)
|
||||
if inputs is not None:
|
||||
env[_CREWAI_JSON_CREW_INPUTS_ENV] = inputs
|
||||
|
||||
try:
|
||||
subprocess.run( # noqa: S603
|
||||
@@ -613,11 +569,11 @@ def run_crew(
|
||||
``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 of runtime inputs for a declarative flow
|
||||
or declarative (JSON) crew. Layered over the definition's own
|
||||
defaults; missing required values are prompted for interactively.
|
||||
inputs: Optional JSON object passed to a declarative Flow.
|
||||
"""
|
||||
# --definition is a pure override: run that flow directly.
|
||||
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,
|
||||
@@ -628,13 +584,9 @@ def run_crew(
|
||||
|
||||
pyproject_data = read_toml()
|
||||
if json_crew_definition := configured_project_json_crew(pyproject_data):
|
||||
# Declarative (JSON) crews resolve inputs the same way flows do: --inputs
|
||||
# layers over the crew's declared defaults, missing {placeholder}s are
|
||||
# prompted for, and unknown keys are flagged. Forward the raw JSON.
|
||||
_run_json_crew_in_project_env(
|
||||
trained_agents_file=trained_agents_file,
|
||||
crew_path=json_crew_definition,
|
||||
inputs=inputs,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -642,29 +594,18 @@ def run_crew(
|
||||
project_type = get_crewai_project_type(pyproject_data)
|
||||
|
||||
if project_type == "flow":
|
||||
# No --definition: resolve the configured [tool.crewai] flow — the same
|
||||
# resolution as a bare `crewai run` — and pass --inputs straight through.
|
||||
_run_flow_project(
|
||||
pyproject_data=pyproject_data,
|
||||
trained_agents_file=trained_agents_file,
|
||||
inputs=inputs,
|
||||
)
|
||||
return
|
||||
|
||||
_reject_inputs_for_non_flow(inputs)
|
||||
_run_classic_crew_project(
|
||||
pyproject_data=pyproject_data,
|
||||
trained_agents_file=trained_agents_file,
|
||||
)
|
||||
|
||||
|
||||
def _reject_inputs_for_non_flow(inputs: str | None) -> None:
|
||||
if inputs is not None:
|
||||
raise click.UsageError(
|
||||
"--inputs is only supported for declarative flows and crews"
|
||||
)
|
||||
|
||||
|
||||
def _run_explicit_declarative_flow(
|
||||
definition: str, inputs: str | None, trained_agents_file: str | None
|
||||
) -> None:
|
||||
@@ -677,9 +618,7 @@ def _run_explicit_declarative_flow(
|
||||
|
||||
|
||||
def _run_flow_project(
|
||||
pyproject_data: dict[str, Any],
|
||||
trained_agents_file: str | None,
|
||||
inputs: str | None = None,
|
||||
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")
|
||||
@@ -690,16 +629,9 @@ def _run_flow_project(
|
||||
)
|
||||
|
||||
if definition := configured_project_declarative_flow(pyproject_data):
|
||||
run_declarative_flow_in_project_env(definition=definition, inputs=inputs)
|
||||
run_declarative_flow_in_project_env(definition=definition)
|
||||
return
|
||||
|
||||
# No configured declarative flow definition to resolve inputs against.
|
||||
if inputs is not None:
|
||||
raise click.UsageError(
|
||||
"--inputs requires a declarative flow definition "
|
||||
"([tool.crewai].definition) or --definition"
|
||||
)
|
||||
|
||||
from crewai_cli.kickoff_flow import (
|
||||
_load_conversational_flow_from_kickoff_script,
|
||||
_run_conversational_flow_tui,
|
||||
|
||||
@@ -9,12 +9,6 @@ import click
|
||||
from crewai_core.project import ProjectDefinitionError, configured_project_definition
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai_cli.input_prompt import (
|
||||
closest_name,
|
||||
is_interactive,
|
||||
parse_inputs_json,
|
||||
prompt_for_inputs,
|
||||
)
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials
|
||||
|
||||
|
||||
@@ -26,13 +20,10 @@ def run_declarative_flow_in_project_env(
|
||||
run_declarative_flow(definition=definition, inputs=inputs)
|
||||
return
|
||||
|
||||
# Re-run inside the project env (so the flow loads with the project's deps).
|
||||
# The configured definition is re-resolved there; forward --inputs so the
|
||||
# in-env run kicks off with the same values instead of losing them.
|
||||
command = ["uv", "run", "crewai", "run"]
|
||||
if inputs is not None:
|
||||
command += ["--inputs", inputs]
|
||||
_execute_declarative_flow_command(command)
|
||||
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 | Path) -> None:
|
||||
@@ -45,29 +36,12 @@ def plot_declarative_flow_in_project_env(definition: str | Path) -> None:
|
||||
|
||||
|
||||
def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> None:
|
||||
"""Run a declarative flow from a definition path.
|
||||
|
||||
Inputs come from one place: the flow's own state schema. Any ``--inputs``
|
||||
JSON is layered on top as an override, missing required fields are prompted
|
||||
for interactively, and everything is validated against the schema before
|
||||
kickoff — so a bare ``crewai run`` on a configured flow just works.
|
||||
"""
|
||||
# Load the project's .env before kickoff, mirroring the JSON-crew path
|
||||
# (run_crew._run_json_crew) so flow projects pick up API keys/config the
|
||||
# same way regardless of where crewai is installed.
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_file = Path.cwd() / ".env"
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
provided = parse_inputs_json(inputs) or {}
|
||||
|
||||
flow = load_declarative_flow(definition)
|
||||
resolved_inputs = _resolve_flow_inputs(flow, provided)
|
||||
"""Run a declarative flow from a definition path."""
|
||||
parsed_inputs = _parse_inputs(inputs)
|
||||
|
||||
try:
|
||||
result = flow.kickoff(inputs=resolved_inputs or None)
|
||||
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
|
||||
@@ -77,152 +51,6 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N
|
||||
click.echo(_format_result(result))
|
||||
|
||||
|
||||
def _resolve_flow_inputs(flow: Any, provided: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Resolve kickoff inputs from the flow's state schema.
|
||||
|
||||
Warns on unknown keys, prompts for missing required fields (unless
|
||||
non-interactive), and validates types before kickoff. Exits with a pointed
|
||||
message when a required input is still missing or an input is invalid.
|
||||
"""
|
||||
schema = _flow_state_schema(flow)
|
||||
if schema is None:
|
||||
# dict / unschematized state — nothing to derive; pass inputs through.
|
||||
return dict(provided)
|
||||
|
||||
properties = {
|
||||
name: spec
|
||||
for name, spec in (schema.get("properties") or {}).items()
|
||||
if name != "id"
|
||||
}
|
||||
state_model = type(flow.state)
|
||||
defaults = _flow_state_defaults(flow)
|
||||
|
||||
# ``id`` signals a persistence restore: kickoff hydrates the full state from
|
||||
# storage, so required fields may come from the restored state rather than
|
||||
# --inputs. We still filter the rest of the payload below, but skip the
|
||||
# required-field prompt and pre-kickoff validation, which would otherwise
|
||||
# fail on fields the resume will supply.
|
||||
restoring = "id" in provided
|
||||
|
||||
# Unknown keys are almost always typos — warn and drop them (they'd fail
|
||||
# structured-state validation at kickoff anyway). ``id`` is a reserved
|
||||
# kickoff key rather than a state field, so forward it untouched.
|
||||
collected: dict[str, Any] = {}
|
||||
for key, value in provided.items():
|
||||
if key == "id":
|
||||
collected["id"] = value
|
||||
continue
|
||||
if key in properties:
|
||||
collected[key] = value
|
||||
continue
|
||||
suggestion = closest_name(key, properties)
|
||||
hint = f" Did you mean '{suggestion}'?" if suggestion else ""
|
||||
click.secho(
|
||||
f" Ignoring unknown input '{key}' — not in the flow's state schema.{hint}",
|
||||
fg="yellow",
|
||||
err=True,
|
||||
)
|
||||
|
||||
if restoring:
|
||||
return collected
|
||||
|
||||
missing = _missing_required(state_model, {**defaults, **collected})
|
||||
if missing and _is_interactive():
|
||||
collected.update(
|
||||
prompt_for_inputs(
|
||||
missing,
|
||||
title="Flow inputs",
|
||||
subtitle="This flow needs the following to run.",
|
||||
describe=lambda name: (properties.get(name) or {}).get("description"),
|
||||
coerce=lambda name, raw: _coerce_input(raw, properties.get(name) or {}),
|
||||
)
|
||||
)
|
||||
missing = _missing_required(state_model, {**defaults, **collected})
|
||||
|
||||
if missing:
|
||||
for name in missing:
|
||||
description = (properties.get(name) or {}).get("description")
|
||||
suffix = f" — {description}" if description else ""
|
||||
click.secho(
|
||||
f" Missing required input '{name}'{suffix}", fg="red", err=True
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
_validate_flow_inputs(state_model, {**defaults, **collected})
|
||||
return collected
|
||||
|
||||
|
||||
def _is_interactive() -> bool:
|
||||
"""Prompt only in an interactive terminal, never in non-interactive mode."""
|
||||
return is_interactive()
|
||||
|
||||
|
||||
def _flow_state_schema(flow: Any) -> dict[str, Any] | None:
|
||||
"""Return the flow's state JSON schema, or ``None`` for dict/plain state."""
|
||||
state = getattr(flow, "state", None)
|
||||
if state is None or isinstance(state, dict):
|
||||
return None
|
||||
model_json_schema = getattr(type(state), "model_json_schema", None)
|
||||
if not callable(model_json_schema):
|
||||
return None
|
||||
try:
|
||||
schema = model_json_schema()
|
||||
except Exception:
|
||||
return None
|
||||
return schema if isinstance(schema, dict) else None
|
||||
|
||||
|
||||
def _flow_state_defaults(flow: Any) -> dict[str, Any]:
|
||||
"""Declared state defaults (``state.default``) from the flow definition."""
|
||||
state_definition = getattr(getattr(flow, "_definition", None), "state", None)
|
||||
default = getattr(state_definition, "default", None)
|
||||
return dict(default) if isinstance(default, dict) else {}
|
||||
|
||||
|
||||
def _missing_required(state_model: Any, values: dict[str, Any]) -> list[str]:
|
||||
"""Required state fields not satisfied by ``values`` (defaults + inputs)."""
|
||||
try:
|
||||
state_model.model_validate(values)
|
||||
except ValidationError as exc:
|
||||
return [
|
||||
str(error["loc"][0])
|
||||
for error in exc.errors()
|
||||
if error.get("type") == "missing" and error.get("loc")
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _validate_flow_inputs(state_model: Any, values: dict[str, Any]) -> None:
|
||||
"""Validate inputs against the state schema; exit with pointed type errors."""
|
||||
try:
|
||||
state_model.model_validate(values)
|
||||
except ValidationError as exc:
|
||||
for error in exc.errors():
|
||||
location = ".".join(str(part) for part in error.get("loc", ()))
|
||||
click.secho(
|
||||
f" Invalid input '{location}': {error.get('msg')}", fg="red", err=True
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
|
||||
def _coerce_input(raw: str, spec: dict[str, Any]) -> Any:
|
||||
"""Best-effort coerce a prompted string to the field's JSON-schema type."""
|
||||
field_type = spec.get("type")
|
||||
if field_type == "integer":
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return raw
|
||||
if field_type == "number":
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
return raw
|
||||
if field_type == "boolean":
|
||||
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
||||
return raw
|
||||
|
||||
|
||||
def plot_declarative_flow(definition: str | Path) -> None:
|
||||
"""Plot a declarative flow from a definition path."""
|
||||
try:
|
||||
@@ -324,6 +152,23 @@ def _has_project_file(project_root: Path | None = None) -> bool:
|
||||
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 _format_result(result: Any) -> str:
|
||||
raw_result = getattr(result, "raw", result)
|
||||
if isinstance(raw_result, str):
|
||||
|
||||
@@ -258,9 +258,10 @@ Fields:
|
||||
#### Crew Agent Definition (`methods.<name>.do[call=crew].with.agents.<name>`)
|
||||
|
||||
Fields:
|
||||
- `role` (required): string. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst`
|
||||
- `goal` (required): string. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}`
|
||||
- `backstory` (required): string. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.`
|
||||
- `role` (optional): string | null; default `null`. Crew agent role. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research analyst`
|
||||
- `goal` (optional): string | null; default `null`. Crew agent goal. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Research {topic}`
|
||||
- `backstory` (optional): string | null; default `null`. Crew agent backstory. Crew inputs are interpolated with `{name}` placeholders such as `{topic}`; this is not CEL. Example: `Expert at concise technical research.`
|
||||
- `from_repository` (optional): string | null; default `null`. Agent repository name to load. Repository values supply missing agent configuration; explicitly provided local fields override the repository values. Example: `researcher`
|
||||
- `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}`
|
||||
- `llm` (optional): string or inline LLM config; default `null`. Language model that runs this crew agent. Use an object when setting LLM options such as `max_tokens`. Example: `{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}`
|
||||
- `planning_config` (optional): object | null; default `null`. Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution. Example: `{"max_attempts": 3}`
|
||||
@@ -292,15 +293,16 @@ Shape:
|
||||
- `call: agent`
|
||||
|
||||
Fields:
|
||||
- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline Agent definition outside of a crew. Example: `agent`
|
||||
- `call` (required): must be `agent`. Action discriminator. Use agent to run an individual inline or repository-backed Agent definition outside of a crew. Example: `agent`
|
||||
- `with` (required): any. Individual Agent definition to load and execute outside of a crew for this action. Put the agent input in `with.input`; agent actions do not support action-level `inputs`. Example: `{"backstory": "Precise and concise.", "goal": "Answer user questions", "input": "${state.question}", "role": "Analyst", "settings": {"llm": "openai/gpt-4o-mini"}}`
|
||||
|
||||
#### Agent Definition (`methods.<name>.do[call=agent].with`)
|
||||
|
||||
Fields:
|
||||
- `role` (required): string. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist`
|
||||
- `goal` (required): string. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply`
|
||||
- `backstory` (required): string. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.`
|
||||
- `role` (optional): string | null; default `null`. Individual agent role used by a Flow agent action outside of a crew. Example: `Support specialist`
|
||||
- `goal` (optional): string | null; default `null`. Individual agent goal for the Flow agent action outside of a crew. Example: `Draft a concise customer reply`
|
||||
- `backstory` (optional): string | null; default `null`. Individual agent backstory used to shape behavior outside of a crew. Example: `Expert at resolving SaaS support questions.`
|
||||
- `from_repository` (optional): string | null; default `null`. Agent repository name to load. Repository values supply missing agent configuration; explicitly provided local fields override the repository values. Example: `support_specialist`
|
||||
- `settings` (optional): map of string to any. Additional agent settings passed to the loader. Example: `{"llm": "openai/gpt-4o-mini"}`
|
||||
- `llm` (optional): string or inline LLM config; default `null`. Language model that runs this agent. Use an object when setting LLM options such as `max_tokens`. Example: `{"max_tokens": 4096, "model": "openai/gpt-4o-mini"}`
|
||||
- `planning_config` (optional): object | null; default `null`. Agent planning configuration. Set `max_attempts` to limit planning refinement attempts before task execution. Example: `{"max_attempts": 3}`
|
||||
|
||||
@@ -151,14 +151,12 @@ def test_run_with_definition_uses_project_runner(run_crew, runner):
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
def test_run_inputs_without_definition_calls_run_crew(run_crew, runner):
|
||||
# --inputs no longer requires --definition; the resolution happens in run_crew.
|
||||
def test_run_rejects_inputs_without_definition(run_crew, runner):
|
||||
result = runner.invoke(run, ["--inputs", '{"topic":"AI"}'])
|
||||
|
||||
assert result.exit_code == 0
|
||||
run_crew.assert_called_once_with(
|
||||
trained_agents_file=None, definition=None, inputs='{"topic":"AI"}'
|
||||
)
|
||||
assert result.exit_code == 2
|
||||
assert "Error: --inputs requires --definition" in result.output
|
||||
run_crew.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Tests for the shared runtime-input prompting used by flows and crews."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_cli.input_prompt import (
|
||||
closest_name,
|
||||
parse_inputs_json,
|
||||
prompt_for_inputs,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_inputs_json_returns_none_for_none():
|
||||
assert parse_inputs_json(None) is None
|
||||
|
||||
|
||||
def test_parse_inputs_json_parses_object():
|
||||
assert parse_inputs_json('{"topic": "AI"}') == {"topic": "AI"}
|
||||
|
||||
|
||||
def test_parse_inputs_json_rejects_invalid_json(capsys):
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parse_inputs_json("not json")
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
assert "Invalid --inputs JSON" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_parse_inputs_json_rejects_non_object(capsys):
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parse_inputs_json("[1, 2, 3]")
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
assert "expected an object" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_closest_name_suggests_near_miss():
|
||||
assert closest_name("prospect_emai", ["prospect_email", "topic"]) == "prospect_email"
|
||||
|
||||
|
||||
def test_closest_name_returns_none_when_nothing_close():
|
||||
assert closest_name("zzzzz", ["prospect_email", "topic"]) is None
|
||||
|
||||
|
||||
def test_prompt_for_inputs_uses_describe_and_coerce(monkeypatch, capsys):
|
||||
seen: list[str] = []
|
||||
|
||||
def fake_prompt(text: str, **kwargs: object) -> str:
|
||||
seen.append(text)
|
||||
return "42"
|
||||
|
||||
monkeypatch.setattr("crewai_cli.input_prompt.click.prompt", fake_prompt)
|
||||
|
||||
result = prompt_for_inputs(
|
||||
["count"],
|
||||
title="Flow inputs",
|
||||
subtitle="This flow needs the following to run.",
|
||||
describe=lambda name: f"How many {name}?",
|
||||
coerce=lambda name, raw: int(raw),
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert result == {"count": 42}
|
||||
assert any("count" in text for text in seen)
|
||||
# Header, subtitle, and description hint all render on stderr.
|
||||
assert "Flow inputs" in captured.err
|
||||
assert "How many count?" in captured.err
|
||||
|
||||
|
||||
def test_prompt_for_inputs_keeps_raw_string_without_coerce(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.input_prompt.click.prompt", lambda text, **kwargs: "AI"
|
||||
)
|
||||
|
||||
result = prompt_for_inputs(
|
||||
["topic"],
|
||||
title="Crew inputs",
|
||||
subtitle="This crew needs the following to run.",
|
||||
)
|
||||
|
||||
assert result == {"topic": "AI"}
|
||||
@@ -1,551 +0,0 @@
|
||||
"""Tests for the dynamic model catalog used by the crew-creation wizard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
import crewai_cli.model_catalog as mc
|
||||
|
||||
_ALL_KEY_ENVS = [
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
"CEREBRAS_API_KEY",
|
||||
"OLLAMA_API_BASE",
|
||||
"API_BASE",
|
||||
"OLLAMA_HOST",
|
||||
]
|
||||
|
||||
FALLBACK_ANTHROPIC = [
|
||||
("claude-opus-4-6", "Claude Opus 4.6"),
|
||||
("claude-sonnet-4-6", "Claude Sonnet 4.6"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_env(monkeypatch, tmp_path):
|
||||
"""Point the cache at a temp dir and clear provider keys for every test."""
|
||||
monkeypatch.setattr(mc, "_cache_dir", lambda: tmp_path)
|
||||
mc._reset_litellm_memo() # clear the process-level LiteLLM memo per test
|
||||
for key in _ALL_KEY_ENVS:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
# ── version / label helpers ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_version_key_parses_embedded_date():
|
||||
date_int, version = mc._version_key("claude-3-5-sonnet-20241022")
|
||||
assert date_int == 20241022
|
||||
assert version == (3, 5)
|
||||
|
||||
|
||||
def test_version_key_parses_dashed_date():
|
||||
date_int, _ = mc._version_key("gpt-4o-2024-08-06")
|
||||
assert date_int == 20240806
|
||||
|
||||
|
||||
def test_version_key_version_only():
|
||||
date_int, version = mc._version_key("claude-opus-4-6")
|
||||
assert date_int == 0
|
||||
assert version == (4, 6)
|
||||
|
||||
|
||||
def test_version_key_ranks_newer_higher():
|
||||
older = mc._version_key("claude-sonnet-4-5")
|
||||
newer = mc._version_key("claude-sonnet-4-6")
|
||||
assert newer > older
|
||||
|
||||
|
||||
def test_is_chat_model_rejects_non_chat():
|
||||
assert mc._is_chat_model("gpt-4.1-mini")
|
||||
assert not mc._is_chat_model("text-embedding-3-large")
|
||||
assert not mc._is_chat_model("whisper-1")
|
||||
assert not mc._is_chat_model("dall-e-3")
|
||||
|
||||
|
||||
def test_search_substring_not_treated_as_non_chat():
|
||||
# 'search' must not drop legitimate completion models: a token like
|
||||
# *-search-preview, or 'research' (which contains 'search' as a substring).
|
||||
assert mc._is_chat_model("gpt-4o-search-preview")
|
||||
assert mc._is_chat_model("o3-deep-research")
|
||||
# genuine non-chat markers still filter
|
||||
assert not mc._is_chat_model("text-embedding-3-large")
|
||||
|
||||
|
||||
def test_humanize():
|
||||
assert mc._humanize("gpt-4.1-mini") == "GPT 4.1 Mini"
|
||||
assert mc._humanize("anthropic/claude-opus-4-6") == "Claude Opus 4 6"
|
||||
# size suffixes uppercased, acronyms/brands cased, o-series preserved, ':' split
|
||||
assert mc._humanize("openai/gpt-oss-120b") == "GPT OSS 120B"
|
||||
assert mc._humanize("qwen/qwen3-32b") == "Qwen3 32B"
|
||||
assert mc._humanize("deepseek-r1-distill-llama-70b") == "DeepSeek R1 Distill Llama 70B"
|
||||
assert mc._humanize("o3-mini") == "o3 Mini"
|
||||
assert mc._humanize("chatgpt-4o-latest") == "ChatGPT 4o Latest"
|
||||
assert mc._humanize("llama3.3:70b") == "Llama3.3 70B"
|
||||
assert mc._humanize("gemma2-9b-it") == "Gemma2 9B IT"
|
||||
|
||||
|
||||
# ── vendor tier ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_vendor_anthropic_ranks_by_date_and_uses_display_name(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test")
|
||||
payload = {
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-3-5-sonnet-20240620",
|
||||
"display_name": "Claude 3.5 Sonnet (old)",
|
||||
"created_at": "2024-06-20T00:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-6",
|
||||
"display_name": "Claude Opus 4.6",
|
||||
"created_at": "2026-02-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "claude-haiku-4-5-20251001",
|
||||
"display_name": "Claude Haiku 4.5",
|
||||
"created_at": "2025-10-01T00:00:00Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: payload)
|
||||
|
||||
models = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
|
||||
# Newest first by created_at, display names preserved.
|
||||
assert models[0] == ("claude-opus-4-6", "Claude Opus 4.6")
|
||||
assert models[1] == ("claude-haiku-4-5-20251001", "Claude Haiku 4.5")
|
||||
assert models[2] == ("claude-3-5-sonnet-20240620", "Claude 3.5 Sonnet (old)")
|
||||
|
||||
|
||||
def test_vendor_openai_filters_non_chat_models(monkeypatch):
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
|
||||
payload = {
|
||||
"data": [
|
||||
{"id": "gpt-4.1", "created": 1_700_000_000},
|
||||
{"id": "text-embedding-3-large", "created": 1_800_000_000},
|
||||
{"id": "whisper-1", "created": 1_800_000_000},
|
||||
{"id": "gpt-5.5", "created": 1_750_000_000},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: payload)
|
||||
|
||||
models = mc.get_provider_models("openai", [])
|
||||
ids = [m for m, _ in models]
|
||||
|
||||
assert ids == ["gpt-5.5", "gpt-4.1"] # embeddings/whisper dropped, newest first
|
||||
|
||||
|
||||
def test_vendor_gemini_requires_generate_content(monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "key")
|
||||
payload = {
|
||||
"models": [
|
||||
{
|
||||
"name": "models/gemini-2.5-pro",
|
||||
"displayName": "Gemini 2.5 Pro",
|
||||
"supportedGenerationMethods": ["generateContent"],
|
||||
},
|
||||
{
|
||||
"name": "models/text-embedding-004",
|
||||
"displayName": "Embedding",
|
||||
"supportedGenerationMethods": ["embedContent"],
|
||||
},
|
||||
{
|
||||
"name": "models/gemini-1.5-pro",
|
||||
"displayName": "Gemini 1.5 Pro",
|
||||
"supportedGenerationMethods": ["generateContent"],
|
||||
},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: payload)
|
||||
|
||||
models = mc.get_provider_models("gemini", [])
|
||||
ids = [m for m, _ in models]
|
||||
|
||||
# "models/" prefix stripped, embedding excluded, newer version first.
|
||||
assert ids == ["gemini-2.5-pro", "gemini-1.5-pro"]
|
||||
|
||||
|
||||
def test_openai_excludes_fine_tunes_and_checkpoints(monkeypatch):
|
||||
# Fine-tunes/checkpoints have recent `created` timestamps and would otherwise
|
||||
# crowd out (and rank above) the base models — they must be excluded so the
|
||||
# picker shows clean foundation models.
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
|
||||
payload = {
|
||||
"data": [
|
||||
{"id": "ft:gpt-4o-mini-2024-07-18:crewai::DyJG86uF", "created": 1_900_000_000},
|
||||
{
|
||||
"id": "ft:gpt-4o-mini-2024-07-18:crewai::DyJG7Q9N:ckpt-step-84",
|
||||
"created": 1_900_000_001,
|
||||
},
|
||||
{"id": "gpt-5.5", "created": 1_800_000_000},
|
||||
{"id": "gpt-4.1", "created": 1_700_000_000},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: payload)
|
||||
|
||||
ids = [m for m, _ in mc.get_provider_models("openai", [])]
|
||||
assert ids == ["gpt-5.5", "gpt-4.1"] # fine-tunes + checkpoints dropped
|
||||
|
||||
|
||||
def test_vendor_gemini_paginates(monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "key")
|
||||
pages = {
|
||||
None: {
|
||||
"models": [
|
||||
{
|
||||
"name": "models/gemini-3.5-flash",
|
||||
"displayName": "Gemini 3.5 Flash",
|
||||
"supportedGenerationMethods": ["generateContent"],
|
||||
}
|
||||
],
|
||||
"nextPageToken": "p2",
|
||||
},
|
||||
"p2": {
|
||||
"models": [
|
||||
{
|
||||
"name": "models/gemini-2.5-pro",
|
||||
"displayName": "Gemini 2.5 Pro",
|
||||
"supportedGenerationMethods": ["generateContent"],
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
def fetch(url, headers=None, params=None):
|
||||
return pages[(params or {}).get("pageToken")]
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", fetch)
|
||||
|
||||
ids = sorted(m for m, _ in mc.get_provider_models("gemini", []))
|
||||
# Both pages contributed (newest-first ranking is _finalize's job).
|
||||
assert ids == ["gemini-2.5-pro", "gemini-3.5-flash"]
|
||||
|
||||
|
||||
def test_vendor_gemini_first_page_error_uses_fallback(monkeypatch):
|
||||
# A total (first-page) Gemini failure with a key set must fall back to the
|
||||
# curated list, not be mistaken for a successful empty result.
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "key")
|
||||
|
||||
def boom(*a, **k):
|
||||
raise RuntimeError("gemini down")
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", boom)
|
||||
models = mc.get_provider_models("gemini", [("gemini-x", "Gemini X")])
|
||||
assert models == [("gemini-x", "Gemini X")]
|
||||
|
||||
|
||||
def test_vendor_gemini_keeps_partial_on_later_page_error(monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "key")
|
||||
|
||||
def fetch(url, headers=None, params=None):
|
||||
if (params or {}).get("pageToken"):
|
||||
raise RuntimeError("page 2 down")
|
||||
return {
|
||||
"models": [
|
||||
{
|
||||
"name": "models/gemini-3.5-flash",
|
||||
"displayName": "Gemini 3.5 Flash",
|
||||
"supportedGenerationMethods": ["generateContent"],
|
||||
}
|
||||
],
|
||||
"nextPageToken": "p2",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", fetch)
|
||||
|
||||
# Page-1 models are kept; the later-page error doesn't force the fallback.
|
||||
models = mc.get_provider_models("gemini", [("fallback-x", "Fallback X")])
|
||||
assert [m for m, _ in models] == ["gemini-3.5-flash"]
|
||||
|
||||
|
||||
def test_ollama_empty_response_not_filled_with_fallback(monkeypatch):
|
||||
# A reachable Ollama with nothing installed -> empty (manual entry), not the
|
||||
# curated suggestions the crew can't actually run.
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: {"models": []})
|
||||
assert mc.get_provider_models("ollama", [("llama3.3", "Llama 3.3")]) == []
|
||||
|
||||
|
||||
def test_ollama_unreachable_uses_fallback(monkeypatch):
|
||||
# Server down (fetch raises) is different from empty -> fall back to suggestions.
|
||||
def boom(*a, **k):
|
||||
raise RuntimeError("connection refused")
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", boom)
|
||||
models = mc.get_provider_models("ollama", [("llama3.3", "Llama 3.3")])
|
||||
assert models == [("llama3.3", "Llama 3.3")]
|
||||
|
||||
|
||||
def test_ollama_excludes_embedding_models(monkeypatch):
|
||||
# /api/tags lists everything installed, including embeddings — filter them.
|
||||
monkeypatch.setattr(
|
||||
mc,
|
||||
"_http_get_json",
|
||||
lambda *a, **k: {
|
||||
"models": [
|
||||
{"model": "llama3.3:70b"},
|
||||
{"model": "nomic-embed-text"},
|
||||
{"model": "mxbai-embed-large"},
|
||||
]
|
||||
},
|
||||
)
|
||||
ids = [m for m, _ in mc.get_provider_models("ollama", [])]
|
||||
assert ids == ["llama3.3:70b"]
|
||||
|
||||
|
||||
def test_ollama_base_honors_ollama_host(monkeypatch):
|
||||
# OLLAMA_HOST (scheme-less runtime convention) is resolved with a scheme.
|
||||
monkeypatch.setenv("OLLAMA_HOST", "10.0.0.5:11434")
|
||||
assert mc._ollama_base() == "http://10.0.0.5:11434"
|
||||
|
||||
|
||||
def test_ollama_recovery_not_blocked_by_negative_cache(monkeypatch):
|
||||
# Ollama down -> fallback, but not negatively cached; once the server is up
|
||||
# the next call fetches live models rather than serving suggestions.
|
||||
calls = {"n": 0}
|
||||
|
||||
def flaky(*a, **k):
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
raise RuntimeError("connection refused")
|
||||
return {"models": [{"model": "llama-installed"}]}
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", flaky)
|
||||
first = mc.get_provider_models("ollama", [("llama3.3", "Llama 3.3")])
|
||||
assert first == [("llama3.3", "Llama 3.3")] # down -> fallback (not cached)
|
||||
second = mc.get_provider_models("ollama", [("llama3.3", "Llama 3.3")])
|
||||
assert [m for m, _ in second] == ["llama-installed"] # recovered live
|
||||
|
||||
|
||||
def test_gemini_honors_google_api_key(monkeypatch):
|
||||
# GOOGLE_API_KEY (equivalent to GEMINI_API_KEY in crewai) enables the live tier.
|
||||
monkeypatch.setenv("GOOGLE_API_KEY", "key")
|
||||
monkeypatch.setattr(
|
||||
mc,
|
||||
"_http_get_json",
|
||||
lambda *a, **k: {
|
||||
"models": [
|
||||
{
|
||||
"name": "models/gemini-3.5-flash",
|
||||
"displayName": "Gemini 3.5 Flash",
|
||||
"supportedGenerationMethods": ["generateContent"],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
models = mc.get_provider_models("gemini", [("gemini-x", "Gemini X")])
|
||||
assert [m for m, _ in models] == ["gemini-3.5-flash"] # live, not fallback
|
||||
|
||||
|
||||
def test_curated_label_overrides_raw_vendor_label(monkeypatch):
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
|
||||
payload = {"data": [{"id": "gpt-5.5", "created": 1}]}
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: payload)
|
||||
|
||||
models = mc.get_provider_models("openai", [("gpt-5.5", "GPT-5.5 (curated)")])
|
||||
assert models == [("gpt-5.5", "GPT-5.5 (curated)")]
|
||||
|
||||
|
||||
def test_truncates_to_max_models(monkeypatch):
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
|
||||
payload = {
|
||||
"data": [{"id": f"gpt-test-{i}", "created": i} for i in range(20)]
|
||||
}
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: payload)
|
||||
|
||||
models = mc.get_provider_models("openai", [])
|
||||
assert len(models) == mc.MAX_MODELS
|
||||
|
||||
|
||||
# ── litellm tier ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_litellm_tier_for_uncurated_provider(monkeypatch):
|
||||
# A provider with no curated fallback ([]) -> the LiteLLM feed is consulted.
|
||||
litellm_data = {
|
||||
"claude-opus-4-6": {"litellm_provider": "anthropic", "mode": "chat"},
|
||||
"claude-sonnet-4-5": {"litellm_provider": "anthropic", "mode": "chat"},
|
||||
"voyage-embed": {"litellm_provider": "anthropic", "mode": "embedding"},
|
||||
"gpt-4.1": {"litellm_provider": "openai", "mode": "chat"},
|
||||
}
|
||||
mc._litellm_cache_file().write_text(json.dumps(litellm_data), encoding="utf-8")
|
||||
|
||||
models = mc.get_provider_models("anthropic", []) # empty == uncurated
|
||||
ids = [m for m, _ in models]
|
||||
|
||||
# Only anthropic chat models, embedding + other providers excluded.
|
||||
assert ids == ["claude-opus-4-6", "claude-sonnet-4-5"]
|
||||
|
||||
|
||||
def test_null_litellm_provider_does_not_crash(monkeypatch):
|
||||
# A present-but-null litellm_provider must be skipped, not raise.
|
||||
litellm_data = {
|
||||
"weird-model": {"litellm_provider": None, "mode": "chat"},
|
||||
"anthropic.claude-v2": {"litellm_provider": "bedrock", "mode": "chat"},
|
||||
}
|
||||
mc._litellm_cache_file().write_text(json.dumps(litellm_data), encoding="utf-8")
|
||||
|
||||
models = mc.get_provider_models("bedrock", [])
|
||||
assert [m for m, _ in models] == ["anthropic.claude-v2"]
|
||||
|
||||
|
||||
def test_litellm_strips_provider_prefix(monkeypatch):
|
||||
litellm_data = {
|
||||
"gemini/gemini-1.5-pro": {"litellm_provider": "gemini", "mode": "chat"},
|
||||
}
|
||||
mc._litellm_cache_file().write_text(json.dumps(litellm_data), encoding="utf-8")
|
||||
|
||||
models = mc.get_provider_models("gemini", [])
|
||||
assert models == [("gemini-1.5-pro", "Gemini 1.5 Pro")]
|
||||
|
||||
|
||||
# ── fallback + caching ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_falls_back_when_everything_fails(monkeypatch):
|
||||
# No key, no litellm cache, network raises -> curated fallback verbatim.
|
||||
def boom(*a, **k):
|
||||
raise RuntimeError("network down")
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", boom)
|
||||
models = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
assert models == FALLBACK_ANTHROPIC
|
||||
|
||||
|
||||
def test_result_is_cached(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test")
|
||||
calls = {"n": 0}
|
||||
|
||||
def fetch(*a, **k):
|
||||
calls["n"] += 1
|
||||
return {"data": [{"id": "claude-opus-4-6", "created_at": "2026-01-01T00:00:00Z"}]}
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", fetch)
|
||||
|
||||
first = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
# Second call must hit the cache and not touch the network again.
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: pytest.fail("refetched"))
|
||||
second = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
|
||||
assert first == second
|
||||
assert calls["n"] == 1
|
||||
|
||||
|
||||
def test_curated_fallback_preferred_over_litellm(monkeypatch):
|
||||
# The feed lags real releases, so a non-empty curated fallback must win even
|
||||
# when a fresh LiteLLM cache is present (regression: Anthropic's feed lacked
|
||||
# Fable 5 / Opus 4.8 / Sonnet 5).
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: pytest.fail("no net"))
|
||||
litellm_data = {
|
||||
"claude-opus-4-6": {"litellm_provider": "anthropic", "mode": "chat"},
|
||||
}
|
||||
mc._litellm_cache_file().write_text(json.dumps(litellm_data), encoding="utf-8")
|
||||
|
||||
models = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
assert models == FALLBACK_ANTHROPIC
|
||||
|
||||
|
||||
def test_added_key_bypasses_negative_cache(monkeypatch):
|
||||
# A no-key call negatively-caches the fallback; adding a key afterwards must
|
||||
# fetch live models rather than serve the cached fallback (distinct cache key).
|
||||
first = mc.get_provider_models("openai", [("gpt-x", "GPT X")])
|
||||
assert first == [("gpt-x", "GPT X")] # no key -> fallback
|
||||
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
|
||||
monkeypatch.setattr(
|
||||
mc, "_http_get_json", lambda *a, **k: {"data": [{"id": "gpt-5.5", "created": 1}]}
|
||||
)
|
||||
second = mc.get_provider_models("openai", [("gpt-x", "GPT X")])
|
||||
assert [m for m, _ in second] == ["gpt-5.5"] # live fetch, not cached fallback
|
||||
|
||||
|
||||
def test_invalid_litellm_cache_falls_through_to_download(monkeypatch):
|
||||
# A corrupt-but-fresh cache must neither crash the picker nor block a
|
||||
# recoverable download — it falls through and refetches.
|
||||
mc._litellm_cache_file().write_text("[1, 2, 3]", encoding="utf-8")
|
||||
monkeypatch.setattr(
|
||||
mc,
|
||||
"_http_get_json",
|
||||
lambda *a, **k: {
|
||||
"anthropic.claude-v2": {"litellm_provider": "bedrock", "mode": "chat"}
|
||||
},
|
||||
)
|
||||
models = mc.get_provider_models("bedrock", [])
|
||||
assert [m for m, _ in models] == ["anthropic.claude-v2"] # recovered via download
|
||||
|
||||
|
||||
def test_litellm_fetch_attempted_once_per_process(monkeypatch):
|
||||
# With no cache and a failing download, the feed is fetched at most once per
|
||||
# process — repeated lookups (across providers) must not re-hit the network.
|
||||
calls = {"n": 0}
|
||||
|
||||
def boom(*a, **k):
|
||||
calls["n"] += 1
|
||||
raise RuntimeError("offline")
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", boom)
|
||||
mc.get_provider_models("bedrock", [])
|
||||
mc.get_provider_models("azure", [])
|
||||
assert calls["n"] == 1 # memoized after the first failed attempt
|
||||
|
||||
|
||||
def test_litellm_fills_uncurated_bedrock(monkeypatch):
|
||||
# No vendor fetcher and no curated fallback -> LiteLLM feed fills the gap.
|
||||
monkeypatch.setattr(mc, "_http_get_json", lambda *a, **k: pytest.fail("no net"))
|
||||
litellm_data = {
|
||||
"anthropic.claude-v2": {"litellm_provider": "bedrock", "mode": "chat"},
|
||||
}
|
||||
mc._litellm_cache_file().write_text(json.dumps(litellm_data), encoding="utf-8")
|
||||
|
||||
models = mc.get_provider_models("bedrock", [])
|
||||
assert models == [("anthropic.claude-v2", "Anthropic.claude V2")]
|
||||
|
||||
|
||||
def test_failed_fetch_is_negatively_cached(monkeypatch):
|
||||
# A failed vendor fetch must not be retried on every call — the fallback is
|
||||
# cached briefly so the picker doesn't re-hit the timeout-prone endpoint.
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test")
|
||||
calls = {"n": 0}
|
||||
|
||||
def boom(*a, **k):
|
||||
calls["n"] += 1
|
||||
raise RuntimeError("down")
|
||||
|
||||
monkeypatch.setattr(mc, "_http_get_json", boom)
|
||||
first = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
second = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
|
||||
assert first == second == FALLBACK_ANTHROPIC
|
||||
assert calls["n"] == 1 # second call served from the negative cache
|
||||
|
||||
|
||||
def test_bad_cache_json_does_not_crash(monkeypatch):
|
||||
# A corrupt cache whose root is not a mapping must not raise (get_provider_models
|
||||
# is documented to never raise).
|
||||
mc._catalog_cache_file().write_text("[1, 2, 3]", encoding="utf-8")
|
||||
|
||||
models = mc.get_provider_models("anthropic", FALLBACK_ANTHROPIC)
|
||||
assert models == FALLBACK_ANTHROPIC
|
||||
|
||||
|
||||
def test_ollama_cache_keyed_by_base(monkeypatch):
|
||||
# Changing OLLAMA_API_BASE must not serve the previous host's cached models.
|
||||
monkeypatch.setenv("OLLAMA_API_BASE", "http://host-a:11434")
|
||||
monkeypatch.setattr(
|
||||
mc, "_http_get_json", lambda *a, **k: {"models": [{"model": "llama-a"}]}
|
||||
)
|
||||
first = mc.get_provider_models("ollama", [])
|
||||
assert [m for m, _ in first] == ["llama-a"]
|
||||
|
||||
monkeypatch.setenv("OLLAMA_API_BASE", "http://host-b:11434")
|
||||
monkeypatch.setattr(
|
||||
mc, "_http_get_json", lambda *a, **k: {"models": [{"model": "llama-b"}]}
|
||||
)
|
||||
second = mc.get_provider_models("ollama", [])
|
||||
assert [m for m, _ in second] == ["llama-b"] # not the host-a cache
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_cli.plus_api import PlusAPI
|
||||
|
||||
@@ -341,23 +343,28 @@ class TestPlusAPI(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
||||
def test_get_agent(mock_make_request):
|
||||
@pytest.mark.asyncio
|
||||
@patch("httpx.AsyncClient")
|
||||
async def test_get_agent(mock_async_client_class):
|
||||
api = PlusAPI("test_api_key")
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.return_value = mock_response
|
||||
mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance
|
||||
|
||||
response = api.get_agent("test_agent_handle")
|
||||
response = await api.get_agent("test_agent_handle")
|
||||
|
||||
mock_make_request.assert_called_once_with(
|
||||
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
mock_client_instance.get.assert_called_once_with(
|
||||
f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle",
|
||||
headers=api.headers,
|
||||
)
|
||||
assert response == mock_response
|
||||
|
||||
|
||||
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
||||
@pytest.mark.asyncio
|
||||
@patch("httpx.AsyncClient")
|
||||
@patch("crewai_core.plus_api.Settings")
|
||||
def test_get_agent_with_org_uuid(mock_settings_class, mock_make_request):
|
||||
async def test_get_agent_with_org_uuid(mock_settings_class, mock_async_client_class):
|
||||
org_uuid = "test-org-uuid"
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = org_uuid
|
||||
@@ -367,12 +374,15 @@ def test_get_agent_with_org_uuid(mock_settings_class, mock_make_request):
|
||||
api = PlusAPI("test_api_key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.return_value = mock_response
|
||||
mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance
|
||||
|
||||
response = api.get_agent("test_agent_handle")
|
||||
response = await api.get_agent("test_agent_handle")
|
||||
|
||||
mock_make_request.assert_called_once_with(
|
||||
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
mock_client_instance.get.assert_called_once_with(
|
||||
f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle",
|
||||
headers=api.headers,
|
||||
)
|
||||
assert "X-Crewai-Organization-Id" in api.headers
|
||||
assert api.headers["X-Crewai-Organization-Id"] == org_uuid
|
||||
|
||||
@@ -9,7 +9,6 @@ import click
|
||||
import pytest
|
||||
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
|
||||
|
||||
import crewai_cli.input_prompt as input_prompt_module
|
||||
import crewai_cli.run_crew as run_crew_module
|
||||
|
||||
|
||||
@@ -45,12 +44,9 @@ def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch):
|
||||
)
|
||||
called: dict = {}
|
||||
|
||||
def fake_run_json_crew_in_project_env(
|
||||
trained_agents_file=None, crew_path=None, inputs=None
|
||||
):
|
||||
def fake_run_json_crew_in_project_env(trained_agents_file=None, crew_path=None):
|
||||
called["trained_agents_file"] = trained_agents_file
|
||||
called["crew_path"] = crew_path
|
||||
called["inputs"] = inputs
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
@@ -63,7 +59,6 @@ def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch):
|
||||
assert called == {
|
||||
"trained_agents_file": "some.pkl",
|
||||
"crew_path": Path("crew.jsonc"),
|
||||
"inputs": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -310,10 +305,9 @@ def test_json_run_without_pyproject_runs_in_process(monkeypatch, tmp_path: Path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
called: dict = {}
|
||||
|
||||
def fake_run_json_crew(trained_agents_file=None, crew_path=None, inputs=None):
|
||||
def fake_run_json_crew(trained_agents_file=None, crew_path=None):
|
||||
called["trained_agents_file"] = trained_agents_file
|
||||
called["crew_path"] = crew_path
|
||||
called["inputs"] = inputs
|
||||
return "result"
|
||||
|
||||
monkeypatch.setattr(run_crew_module, "_run_json_crew", fake_run_json_crew)
|
||||
@@ -324,11 +318,7 @@ def test_json_run_without_pyproject_runs_in_process(monkeypatch, tmp_path: Path)
|
||||
)
|
||||
== "result"
|
||||
)
|
||||
assert called == {
|
||||
"trained_agents_file": "trained.pkl",
|
||||
"crew_path": None,
|
||||
"inputs": None,
|
||||
}
|
||||
assert called == {"trained_agents_file": "trained.pkl", "crew_path": None}
|
||||
|
||||
|
||||
def test_json_project_env_run_failure_exits_nonzero(monkeypatch, tmp_path: Path):
|
||||
@@ -545,9 +535,7 @@ def _patch_tui_run(monkeypatch, status: str):
|
||||
lambda _path: (FakeApp, crew, {}, [], []),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_resolve_crew_inputs",
|
||||
lambda _crew, default_inputs, _provided, *, interactive: default_inputs,
|
||||
run_crew_module, "_prompt_for_missing_inputs", lambda _crew, inputs: inputs
|
||||
)
|
||||
monkeypatch.setattr(run_crew_module, "_print_post_tui_summary", lambda _app: None)
|
||||
|
||||
@@ -648,168 +636,7 @@ def test_run_json_crew_dmn_mode_exits_on_missing_inputs(
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert exc_info.value.code == 1
|
||||
assert "Missing required input 'topic'" in captured.err
|
||||
|
||||
|
||||
# ── Declarative-crew inputs: merge --inputs, warn, prompt (flow parity) ──
|
||||
|
||||
|
||||
def _crew_with_placeholders(*names: str) -> object:
|
||||
"""A minimal crew whose agent goal references each ``{name}`` placeholder."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
goal = " ".join(f"{{{name}}}" for name in names)
|
||||
return SimpleNamespace(
|
||||
agents=[SimpleNamespace(role="Researcher", goal=goal, backstory="")],
|
||||
tasks=[],
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_crew_inputs_merges_inputs_over_defaults():
|
||||
crew = _crew_with_placeholders("topic")
|
||||
|
||||
resolved = run_crew_module._resolve_crew_inputs(
|
||||
crew, {"topic": "AI"}, {"topic": "ML"}, interactive=False
|
||||
)
|
||||
|
||||
assert resolved == {"topic": "ML"}
|
||||
|
||||
|
||||
def test_resolve_crew_inputs_warns_and_keeps_unknown_input(capsys):
|
||||
# The placeholder scan is heuristic, so an unreferenced key is flagged but
|
||||
# kept (never silently dropped like a flow's schema would).
|
||||
crew = _crew_with_placeholders("topic")
|
||||
|
||||
resolved = run_crew_module._resolve_crew_inputs(
|
||||
crew, {}, {"topic": "AI", "topi": "typo"}, interactive=False
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert resolved == {"topic": "AI", "topi": "typo"}
|
||||
assert "isn't referenced by any {placeholder}" in captured.err
|
||||
assert "Did you mean 'topic'?" in captured.err
|
||||
|
||||
|
||||
def test_resolve_crew_inputs_prompts_when_interactive(monkeypatch):
|
||||
crew = _crew_with_placeholders("topic")
|
||||
prompted: list[str] = []
|
||||
|
||||
def fake_prompt(text: str, **kwargs: object) -> str:
|
||||
prompted.append(text)
|
||||
return "AI"
|
||||
|
||||
monkeypatch.setattr(input_prompt_module.click, "prompt", fake_prompt)
|
||||
|
||||
resolved = run_crew_module._resolve_crew_inputs(
|
||||
crew, {}, None, interactive=True
|
||||
)
|
||||
|
||||
assert resolved == {"topic": "AI"}
|
||||
assert any("topic" in text for text in prompted)
|
||||
|
||||
|
||||
def test_resolve_crew_inputs_errors_when_missing_non_interactive(capsys):
|
||||
crew = _crew_with_placeholders("topic")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_crew_module._resolve_crew_inputs(crew, {}, None, interactive=False)
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
assert "Missing required input 'topic'" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_run_json_crew_accepts_inputs_argument(monkeypatch, tmp_path: Path, capsys):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("CREWAI_DMN", "True")
|
||||
crew_path = tmp_path / "crew.jsonc"
|
||||
crew_path.write_text("{}")
|
||||
kickoff_calls: list[dict] = []
|
||||
|
||||
class FakeCrew:
|
||||
name = "Demo"
|
||||
agents = [_crew_with_placeholders("topic").agents[0]]
|
||||
tasks: list = []
|
||||
|
||||
def kickoff(self, inputs):
|
||||
kickoff_calls.append(inputs)
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "configured_project_json_crew", lambda: crew_path
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_load_json_crew", lambda _path: (FakeCrew(), {})
|
||||
)
|
||||
|
||||
assert run_crew_module._run_json_crew(inputs='{"topic":"AI"}') == "ok"
|
||||
assert kickoff_calls == [{"topic": "AI"}]
|
||||
|
||||
|
||||
def test_run_json_crew_in_project_env_forwards_inputs(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_install_json_crew_dependencies_if_needed", lambda: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "build_env_with_all_tool_credentials", lambda: {}
|
||||
)
|
||||
captured_kwargs: list[dict] = []
|
||||
monkeypatch.setattr(
|
||||
run_crew_module.subprocess,
|
||||
"run",
|
||||
lambda command, **kwargs: captured_kwargs.append(kwargs),
|
||||
)
|
||||
|
||||
run_crew_module._run_json_crew_in_project_env(
|
||||
crew_path=tmp_path / "crew.jsonc", inputs='{"topic":"AI"}'
|
||||
)
|
||||
|
||||
env = captured_kwargs[0]["env"]
|
||||
assert env[run_crew_module._CREWAI_JSON_CREW_INPUTS_ENV] == '{"topic":"AI"}'
|
||||
|
||||
|
||||
def test_run_json_crew_in_project_env_rejects_invalid_inputs_json(
|
||||
monkeypatch, tmp_path: Path, capsys
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
monkeypatch.setattr(
|
||||
run_crew_module.subprocess,
|
||||
"run",
|
||||
lambda *a, **k: pytest.fail("subprocess must not run on invalid --inputs"),
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
run_crew_module._run_json_crew_in_project_env(
|
||||
crew_path=tmp_path / "crew.jsonc", inputs="not json"
|
||||
)
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
assert "Invalid --inputs JSON" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_run_crew_forwards_inputs_to_json_crew(monkeypatch):
|
||||
monkeypatch.setattr(run_crew_module, "read_toml", lambda *a, **k: {})
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"configured_project_json_crew",
|
||||
lambda *a, **k: Path("crew.jsonc"),
|
||||
)
|
||||
called: dict = {}
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"_run_json_crew_in_project_env",
|
||||
lambda **kw: called.update(kw),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew(inputs='{"topic":"AI"}')
|
||||
|
||||
assert called == {
|
||||
"trained_agents_file": None,
|
||||
"crew_path": Path("crew.jsonc"),
|
||||
"inputs": '{"topic":"AI"}',
|
||||
}
|
||||
assert "Missing runtime inputs for CREWAI_DMN mode: topic" in captured.err
|
||||
|
||||
|
||||
def test_configured_project_json_crew_defers_to_declared_flow_type(
|
||||
@@ -855,51 +682,11 @@ def test_configured_project_json_crew_ignores_missing_pyproject(
|
||||
assert run_crew_module.configured_project_json_crew() is None
|
||||
|
||||
|
||||
def test_run_crew_inputs_rejected_for_classic_crew(monkeypatch):
|
||||
# --inputs works for declarative flows and declarative (JSON) crews, but a
|
||||
# classic crew takes its inputs from main.py, so it errors clearly.
|
||||
monkeypatch.setattr(run_crew_module, "read_toml", lambda *a, **k: {})
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "configured_project_json_crew", lambda *a, **k: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_warn_if_old_poetry_project", lambda *a, **k: None
|
||||
)
|
||||
monkeypatch.setattr(run_crew_module, "get_crewai_project_type", lambda *a, **k: "crew")
|
||||
|
||||
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 is only supported for declarative flows and crews"
|
||||
in exc_info.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_run_crew_inputs_without_definition_resolves_configured_flow(monkeypatch):
|
||||
# --inputs with no --definition resolves the configured [tool.crewai] flow,
|
||||
# exactly like a bare `crewai run`, and forwards the inputs.
|
||||
import crewai_cli.run_declarative_flow as rdf
|
||||
|
||||
calls: dict[str, object] = {}
|
||||
monkeypatch.setattr(run_crew_module, "read_toml", lambda *a, **k: {})
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "configured_project_json_crew", lambda *a, **k: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_crew_module, "_warn_if_old_poetry_project", lambda *a, **k: None
|
||||
)
|
||||
monkeypatch.setattr(run_crew_module, "get_crewai_project_type", lambda *a, **k: "flow")
|
||||
monkeypatch.setattr(
|
||||
rdf, "configured_project_declarative_flow", lambda *a, **k: Path("flow.yaml")
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
rdf, "run_declarative_flow_in_project_env", lambda **kw: calls.update(kw)
|
||||
)
|
||||
|
||||
run_crew_module.run_crew(inputs='{"topic":"AI"}')
|
||||
|
||||
assert calls == {"definition": Path("flow.yaml"), "inputs": '{"topic":"AI"}'}
|
||||
assert "--inputs requires --definition" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_crew_rejects_filename_with_explicit_definition():
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import crewai_cli.input_prompt as input_prompt_module
|
||||
import crewai_cli.run_declarative_flow as run_declarative_flow_module
|
||||
|
||||
|
||||
@@ -148,255 +146,3 @@ def test_run_declarative_flow_in_process_inside_uv(
|
||||
)
|
||||
|
||||
assert capsys.readouterr().out == "AI\n"
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_project_env_forwards_inputs(
|
||||
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: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module.subprocess,
|
||||
"run",
|
||||
lambda command, **kwargs: subprocess_calls.append(command),
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow_in_project_env(
|
||||
"flow.yaml", '{"topic":"AI"}'
|
||||
)
|
||||
|
||||
# --inputs is forwarded to the in-env run instead of being rejected.
|
||||
assert subprocess_calls == [
|
||||
["uv", "run", "crewai", "run", "--inputs", '{"topic":"AI"}']
|
||||
]
|
||||
|
||||
|
||||
# ── Schema-driven inputs: prompt, validate, override ────────────────
|
||||
|
||||
REQUIRED_FLOW_YAML = """\
|
||||
schema: crewai.flow/v1
|
||||
name: RequiredInputFlow
|
||||
config:
|
||||
suppress_flow_events: true
|
||||
state:
|
||||
type: json_schema
|
||||
json_schema:
|
||||
type: object
|
||||
properties:
|
||||
prospect_email:
|
||||
type: string
|
||||
description: Email address of the prospect to research
|
||||
required: [prospect_email]
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: state.prospect_email
|
||||
"""
|
||||
|
||||
DEFAULTS_FLOW_YAML = """\
|
||||
schema: crewai.flow/v1
|
||||
name: DefaultsFlow
|
||||
config:
|
||||
suppress_flow_events: true
|
||||
state:
|
||||
type: json_schema
|
||||
json_schema:
|
||||
type: object
|
||||
properties:
|
||||
topic: {type: string}
|
||||
audience: {type: string}
|
||||
required: [topic, audience]
|
||||
default:
|
||||
topic: AI
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: state.audience
|
||||
"""
|
||||
|
||||
TYPED_FLOW_YAML = """\
|
||||
schema: crewai.flow/v1
|
||||
name: TypedFlow
|
||||
config:
|
||||
suppress_flow_events: true
|
||||
state:
|
||||
type: json_schema
|
||||
json_schema:
|
||||
type: object
|
||||
properties:
|
||||
count: {type: integer}
|
||||
required: [count]
|
||||
methods:
|
||||
begin:
|
||||
start: true
|
||||
do:
|
||||
call: expression
|
||||
expr: state.count
|
||||
"""
|
||||
|
||||
|
||||
def _write(tmp_path: Path, contents: str) -> Path:
|
||||
path = tmp_path / "flow.yaml"
|
||||
path.write_text(contents, encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_inputs_flag_satisfies_required_field(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(path), '{"prospect_email":"a@b.com"}'
|
||||
)
|
||||
|
||||
assert capsys.readouterr().out == "a@b.com\n"
|
||||
|
||||
|
||||
def test_missing_required_reports_pointed_error(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(run_declarative_flow_module, "_is_interactive", lambda: False)
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow_module.run_declarative_flow(str(path))
|
||||
|
||||
assert (
|
||||
"Missing required input 'prospect_email' — "
|
||||
"Email address of the prospect to research" in capsys.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
def test_prompts_for_missing_required_when_interactive(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
monkeypatch.setattr(run_declarative_flow_module, "_is_interactive", lambda: True)
|
||||
prompted: list[str] = []
|
||||
|
||||
def fake_prompt(text: str, **kwargs: object) -> str:
|
||||
prompted.append(text)
|
||||
return "typed@example.com"
|
||||
|
||||
monkeypatch.setattr(input_prompt_module.click, "prompt", fake_prompt)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(str(path))
|
||||
|
||||
assert capsys.readouterr().out == "typed@example.com\n"
|
||||
assert any("prospect_email" in text for text in prompted)
|
||||
|
||||
|
||||
def test_defaults_satisfy_required_and_are_not_prompted(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(run_declarative_flow_module, "_is_interactive", lambda: False)
|
||||
path = _write(tmp_path, DEFAULTS_FLOW_YAML)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow_module.run_declarative_flow(str(path))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
# topic has a state default -> satisfied; only audience is missing.
|
||||
assert "Missing required input 'audience'" in err
|
||||
assert "'topic'" not in err
|
||||
|
||||
|
||||
def test_warns_on_unknown_input_with_suggestion(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(path), '{"prospect_email":"a@b.com","prospect_emai":"typo"}'
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "a@b.com\n"
|
||||
assert "Ignoring unknown input 'prospect_emai'" in captured.err
|
||||
assert "Did you mean 'prospect_email'?" in captured.err
|
||||
|
||||
|
||||
def test_validates_input_types_before_kickoff(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
path = _write(tmp_path, TYPED_FLOW_YAML)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow_module.run_declarative_flow(str(path), '{"count":"nope"}')
|
||||
|
||||
assert "Invalid input 'count'" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_reserved_id_input_is_forwarded_not_dropped(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
# `id` is a reserved kickoff key (persistence restore); it must pass through
|
||||
# instead of being flagged as an unknown key and dropped.
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(path), '{"id":"run-123","prospect_email":"a@b.com"}'
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "a@b.com\n"
|
||||
assert "Ignoring unknown input 'id'" not in captured.err
|
||||
|
||||
|
||||
def test_run_declarative_flow_loads_project_env(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# Flow projects must pick up the project's .env, like crew projects do,
|
||||
# overriding any pre-existing value.
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("DECL_FLOW_ENV_PROBE", "old")
|
||||
(tmp_path / ".env").write_text("DECL_FLOW_ENV_PROBE=from_dotenv\n", encoding="utf-8")
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow(
|
||||
str(path), '{"prospect_email":"a@b.com"}'
|
||||
)
|
||||
|
||||
assert os.environ["DECL_FLOW_ENV_PROBE"] == "from_dotenv"
|
||||
|
||||
|
||||
def test_id_only_input_skips_required_validation(tmp_path: Path) -> None:
|
||||
# Resume via `crewai run --inputs '{"id":"..."}'` must not be blocked by the
|
||||
# required-field check: kickoff hydrates required state from persistence.
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
flow = run_declarative_flow_module.load_declarative_flow(str(path))
|
||||
|
||||
resolved = run_declarative_flow_module._resolve_flow_inputs(flow, {"id": "run-123"})
|
||||
|
||||
assert resolved == {"id": "run-123"}
|
||||
|
||||
|
||||
def test_id_restore_still_drops_unknown_keys(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
# A persistence restore (`id` present) still filters typo keys so they don't
|
||||
# reach kickoff and trip strict (extra="forbid") state models — it only
|
||||
# skips the required-field prompt/validation, not the unknown-key warning.
|
||||
path = _write(tmp_path, REQUIRED_FLOW_YAML)
|
||||
flow = run_declarative_flow_module.load_declarative_flow(str(path))
|
||||
|
||||
resolved = run_declarative_flow_module._resolve_flow_inputs(
|
||||
flow, {"id": "run-123", "prospect_emai": "typo"}
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert resolved == {"id": "run-123"} # id kept, typo dropped
|
||||
assert "Ignoring unknown input 'prospect_emai'" in captured.err
|
||||
assert "Ignoring unknown input 'id'" not in captured.err
|
||||
|
||||
@@ -232,8 +232,10 @@ class PlusAPI:
|
||||
def get_tool(self, handle: str) -> httpx.Response:
|
||||
return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}")
|
||||
|
||||
def get_agent(self, handle: str) -> httpx.Response:
|
||||
return self._make_request("GET", f"{self.AGENTS_RESOURCE}/{handle}")
|
||||
async def get_agent(self, handle: str) -> httpx.Response:
|
||||
url = urljoin(self.base_url, f"{self.AGENTS_RESOURCE}/{handle}")
|
||||
async with httpx.AsyncClient() as client:
|
||||
return await client.get(url, headers=cast(dict[str, str], self.headers))
|
||||
|
||||
def publish_tool(
|
||||
self,
|
||||
|
||||
@@ -264,12 +264,10 @@ class Telemetry:
|
||||
|
||||
def flow_creation_span(self, flow_name: str) -> None:
|
||||
"""Records the creation of a new flow."""
|
||||
from crewai_core.version import get_crewai_version
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Flow Creation")
|
||||
self._add_attribute(span, "crewai_version", get_crewai_version())
|
||||
self._add_attribute(span, "flow_name", flow_name)
|
||||
close_span(span)
|
||||
|
||||
|
||||
@@ -233,31 +233,3 @@ def test_core_telemetry_records_feature_usage(
|
||||
tracer.start_span.assert_called_once_with("Feature Usage")
|
||||
span.set_attribute.assert_any_call("feature", "cli_usage:view_traces")
|
||||
span.end.assert_called_once()
|
||||
|
||||
|
||||
def test_core_telemetry_records_flow_creation_version(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from crewai_core.telemetry import Telemetry
|
||||
|
||||
Telemetry._instance = None
|
||||
monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False)
|
||||
monkeypatch.delenv("CREWAI_DISABLE_TELEMETRY", raising=False)
|
||||
monkeypatch.delenv("CREWAI_DISABLE_TRACKING", raising=False)
|
||||
monkeypatch.setattr("crewai_core.version.get_crewai_version", lambda: "1.0.0")
|
||||
|
||||
tracer = Mock()
|
||||
span = Mock()
|
||||
tracer.start_span.return_value = span
|
||||
monkeypatch.setattr(
|
||||
"crewai_core.telemetry.trace.get_tracer",
|
||||
lambda _name: tracer,
|
||||
)
|
||||
|
||||
telemetry = Telemetry()
|
||||
telemetry.flow_creation_span("ResearchFlow")
|
||||
|
||||
tracer.start_span.assert_called_once_with("Flow Creation")
|
||||
span.set_attribute.assert_any_call("crewai_version", "1.0.0")
|
||||
span.set_attribute.assert_any_call("flow_name", "ResearchFlow")
|
||||
span.end.assert_called_once()
|
||||
|
||||
@@ -87,11 +87,9 @@ class TavilyExtractorTool(BaseTool):
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
if TAVILY_AVAILABLE:
|
||||
self.client = TavilyClient(
|
||||
api_key=self.api_key, proxies=self.proxies, client_name="crewai"
|
||||
)
|
||||
self.client = TavilyClient(api_key=self.api_key, proxies=self.proxies)
|
||||
self.async_client = AsyncTavilyClient(
|
||||
api_key=self.api_key, proxies=self.proxies, client_name="crewai"
|
||||
api_key=self.api_key, proxies=self.proxies
|
||||
)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -54,10 +54,8 @@ class TavilyGetResearchTool(BaseTool):
|
||||
super().__init__(**kwargs)
|
||||
if TAVILY_AVAILABLE:
|
||||
api_key = os.getenv("TAVILY_API_KEY")
|
||||
self._client = TavilyClient(api_key=api_key, client_name="crewai")
|
||||
self._async_client = AsyncTavilyClient(
|
||||
api_key=api_key, client_name="crewai"
|
||||
)
|
||||
self._client = TavilyClient(api_key=api_key)
|
||||
self._async_client = AsyncTavilyClient(api_key=api_key)
|
||||
else:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
@@ -90,10 +90,8 @@ class TavilyResearchTool(BaseTool):
|
||||
super().__init__(**kwargs)
|
||||
if TAVILY_AVAILABLE:
|
||||
api_key = os.getenv("TAVILY_API_KEY")
|
||||
self._client = TavilyClient(api_key=api_key, client_name="crewai")
|
||||
self._async_client = AsyncTavilyClient(
|
||||
api_key=api_key, client_name="crewai"
|
||||
)
|
||||
self._client = TavilyClient(api_key=api_key)
|
||||
self._async_client = AsyncTavilyClient(api_key=api_key)
|
||||
else:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
@@ -115,11 +115,9 @@ class TavilySearchTool(BaseTool):
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
if TAVILY_AVAILABLE:
|
||||
self.client = TavilyClient(
|
||||
api_key=self.api_key, proxies=self.proxies, client_name="crewai"
|
||||
)
|
||||
self.client = TavilyClient(api_key=self.api_key, proxies=self.proxies)
|
||||
self.async_client = AsyncTavilyClient(
|
||||
api_key=self.api_key, proxies=self.proxies, client_name="crewai"
|
||||
api_key=self.api_key, proxies=self.proxies
|
||||
)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -106,7 +106,6 @@ from crewai.utilities.planning_types import (
|
||||
TodoItem,
|
||||
TodoList,
|
||||
)
|
||||
from crewai.utilities.prompts import StandardPromptResult, SystemPromptResult
|
||||
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
@@ -119,6 +118,7 @@ if TYPE_CHECKING:
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.tools.tool_types import ToolResult
|
||||
from crewai.utilities.prompts import StandardPromptResult, SystemPromptResult
|
||||
|
||||
_RouteT = TypeVar("_RouteT", bound=str)
|
||||
|
||||
@@ -218,7 +218,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
_instance_id: str = PrivateAttr(default_factory=lambda: str(uuid4())[:8])
|
||||
_step_executor: Any = PrivateAttr(default=None)
|
||||
_planner_observer: PlannerObserver | None = PrivateAttr(default=None)
|
||||
_is_feedback_iteration: bool = PrivateAttr(default=False)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _setup_executor(self) -> Self:
|
||||
@@ -297,33 +296,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
"""Set state messages."""
|
||||
self._state.messages = value
|
||||
|
||||
def _setup_messages(self, inputs: dict[str, Any]) -> None:
|
||||
"""Set up messages for the agent execution."""
|
||||
provider = get_provider()
|
||||
if provider.setup_messages(cast("ExecutorContext", self)):
|
||||
return
|
||||
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
if isinstance(self.prompt, SystemPromptResult):
|
||||
system_prompt = self._format_prompt(self.prompt["system"], inputs)
|
||||
user_prompt = self._format_prompt(self.prompt["user"], inputs)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
elif isinstance(self.prompt, StandardPromptResult):
|
||||
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
|
||||
provider.post_setup_messages(cast("ExecutorContext", self))
|
||||
|
||||
@property
|
||||
def ask_for_human_input(self) -> bool:
|
||||
"""Compatibility property - returns state ask_for_human_input."""
|
||||
@@ -342,8 +314,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
enabled on the agent, it generates a plan before execution begins.
|
||||
The plan is stored in state and todos are created from the steps.
|
||||
"""
|
||||
if self._is_feedback_iteration:
|
||||
return
|
||||
if not getattr(self.agent, "planning_enabled", False):
|
||||
return
|
||||
|
||||
@@ -2791,7 +2761,27 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
"AgentExecutor.llm or .prompt is unset; the executor was "
|
||||
"not fully restored or initialized before execution."
|
||||
)
|
||||
self._setup_messages(inputs)
|
||||
if "system" in self.prompt:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
prompt = cast("SystemPromptResult", self.prompt)
|
||||
system_prompt = self._format_prompt(prompt["system"], inputs)
|
||||
user_prompt = self._format_prompt(prompt["user"], inputs)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
else:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
|
||||
self._inject_files_from_inputs(inputs)
|
||||
|
||||
@@ -2877,7 +2867,27 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
"AgentExecutor.llm or .prompt is unset; the executor was "
|
||||
"not fully restored or initialized before execution."
|
||||
)
|
||||
self._setup_messages(inputs)
|
||||
if "system" in self.prompt:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
prompt = cast("SystemPromptResult", self.prompt)
|
||||
system_prompt = self._format_prompt(prompt["system"], inputs)
|
||||
user_prompt = self._format_prompt(prompt["user"], inputs)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
else:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
|
||||
await self._ainject_files_from_inputs(inputs)
|
||||
|
||||
@@ -3159,13 +3169,8 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
Returns:
|
||||
Final answer after feedback.
|
||||
"""
|
||||
self.messages = self.state.messages
|
||||
provider = get_provider()
|
||||
final_answer = provider.handle_feedback(
|
||||
formatted_answer, cast("ExecutorContext", self)
|
||||
)
|
||||
self._complete_feedback(final_answer)
|
||||
return final_answer
|
||||
return provider.handle_feedback(formatted_answer, cast("ExecutorContext", self))
|
||||
|
||||
async def _ahandle_human_feedback(
|
||||
self, formatted_answer: AgentFinish
|
||||
@@ -3178,63 +3183,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
Returns:
|
||||
Final answer after feedback.
|
||||
"""
|
||||
self.messages = self.state.messages
|
||||
provider = get_provider()
|
||||
final_answer = await provider.handle_feedback_async(
|
||||
return await provider.handle_feedback_async(
|
||||
formatted_answer, cast("AsyncExecutorContext", self)
|
||||
)
|
||||
self._complete_feedback(final_answer)
|
||||
return final_answer
|
||||
|
||||
def _complete_feedback(self, final_answer: AgentFinish) -> None:
|
||||
"""Mark the final reviewed answer as the completed executor state."""
|
||||
self.state.current_answer = final_answer
|
||||
self.state.is_finished = True
|
||||
self.state.ask_for_human_input = False
|
||||
self._finalize_called = True
|
||||
|
||||
def _prepare_feedback_iteration(self) -> None:
|
||||
"""Reset flow completion state before rerunning with feedback."""
|
||||
self._finalize_called = False
|
||||
self._is_feedback_iteration = True
|
||||
self.state.current_answer = None
|
||||
self.state.is_finished = False
|
||||
self.state.iterations = 0
|
||||
self.state.use_native_tools = False
|
||||
self.state.pending_tool_calls = []
|
||||
self.state.plan = None
|
||||
self.state.plan_ready = False
|
||||
self.state.todos = TodoList()
|
||||
self.state.replan_count = 0
|
||||
self.state.last_replan_reason = None
|
||||
self.state.observations = {}
|
||||
self.state.execution_log = []
|
||||
|
||||
def _invoke_loop(self) -> AgentFinish:
|
||||
"""Re-run the executor flow using the existing feedback messages."""
|
||||
self._prepare_feedback_iteration()
|
||||
try:
|
||||
self.kickoff()
|
||||
finally:
|
||||
self._is_feedback_iteration = False
|
||||
|
||||
if not isinstance(self.state.current_answer, AgentFinish):
|
||||
raise RuntimeError("Agent execution ended without reaching a final answer.")
|
||||
|
||||
return self.state.current_answer
|
||||
|
||||
async def _ainvoke_loop(self) -> AgentFinish:
|
||||
"""Re-run the executor flow asynchronously using feedback messages."""
|
||||
self._prepare_feedback_iteration()
|
||||
try:
|
||||
await self.kickoff_async()
|
||||
finally:
|
||||
self._is_feedback_iteration = False
|
||||
|
||||
if not isinstance(self.state.current_answer, AgentFinish):
|
||||
raise RuntimeError("Agent execution ended without reaching a final answer.")
|
||||
|
||||
return self.state.current_answer
|
||||
|
||||
def _is_training_mode(self) -> bool:
|
||||
"""Check if training mode is active.
|
||||
@@ -3244,12 +3196,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
"""
|
||||
return bool(self.crew and self.crew._train)
|
||||
|
||||
def _format_feedback_message(self, feedback: str) -> LLMMessage:
|
||||
"""Format human feedback as an LLM message."""
|
||||
return format_message_for_llm(
|
||||
I18N_DEFAULT.slice("feedback_instructions").format(feedback=feedback)
|
||||
)
|
||||
|
||||
|
||||
# Backward compatibility alias (deprecated)
|
||||
CrewAgentExecutorFlow = AgentExecutor
|
||||
|
||||
@@ -472,8 +472,8 @@ class FlowAgentActionDefinition(BaseModel):
|
||||
|
||||
call: Literal["agent"] = Field(
|
||||
description=(
|
||||
"Action discriminator. Use agent to run an individual inline Agent "
|
||||
"definition outside of a crew."
|
||||
"Action discriminator. Use agent to run an individual inline or "
|
||||
"repository-backed Agent definition outside of a crew."
|
||||
),
|
||||
examples=["agent"],
|
||||
)
|
||||
@@ -481,7 +481,8 @@ class FlowAgentActionDefinition(BaseModel):
|
||||
alias="with",
|
||||
description=(
|
||||
"Individual Agent definition to load and execute outside of a crew "
|
||||
"for this action."
|
||||
"for this action. Set from_repository to load agent configuration "
|
||||
"from the agent repository."
|
||||
),
|
||||
examples=[
|
||||
{
|
||||
|
||||
@@ -138,12 +138,11 @@ class CrewAction:
|
||||
|
||||
local_context = _pop_local_context(kwargs)
|
||||
if self.definition.from_declaration is not None:
|
||||
crew, default_inputs = await asyncio.to_thread(
|
||||
load_crew,
|
||||
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:
|
||||
@@ -156,9 +155,7 @@ class CrewAction:
|
||||
**crew_definition.inputs,
|
||||
**(self.definition.inputs or {}),
|
||||
}
|
||||
crew, _ = await asyncio.to_thread(
|
||||
load_crew_from_definition, crew_definition, source="crew action"
|
||||
)
|
||||
crew, _ = load_crew_from_definition(crew_definition, source="crew action")
|
||||
|
||||
inputs = Expression.from_flow(
|
||||
cast(ExpressionData, input_template),
|
||||
@@ -187,8 +184,7 @@ class AgentAction:
|
||||
if not isinstance(rendered_input, str):
|
||||
raise ValueError("agent input must render to a string")
|
||||
|
||||
agent, response_format = await asyncio.to_thread(
|
||||
load_agent_from_definition,
|
||||
agent, response_format = load_agent_from_definition(
|
||||
self.definition.with_,
|
||||
source="agent action",
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ class LLMDefinition(BaseModel):
|
||||
|
||||
|
||||
class CrewAgentDefinition(BaseModel):
|
||||
"""Inline agent definition used by a crew definition."""
|
||||
"""Agent definition used by a crew definition."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
@@ -191,9 +191,27 @@ class CrewAgentDefinition(BaseModel):
|
||||
raise ValueError("agent.settings must be a mapping")
|
||||
return value or {}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_agent_source(self) -> CrewAgentDefinition:
|
||||
if self.from_repository:
|
||||
return self
|
||||
|
||||
missing = [
|
||||
field
|
||||
for field in ("role", "goal", "backstory")
|
||||
if getattr(self, field) is None
|
||||
]
|
||||
if missing:
|
||||
missing_fields = ", ".join(f"'{field}'" for field in missing)
|
||||
raise ValueError(
|
||||
f"agent definition requires {missing_fields} unless "
|
||||
"from_repository is set"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class AgentDefinition(CrewAgentDefinition):
|
||||
"""Inline individual agent definition used outside of a crew."""
|
||||
"""Individual agent definition used outside of a crew."""
|
||||
|
||||
role: str | None = Field(
|
||||
default=None,
|
||||
@@ -217,6 +235,15 @@ class AgentDefinition(CrewAgentDefinition):
|
||||
description="Optional built-in type or Python reference used to load the agent.",
|
||||
examples=["agent", {"python": "my_project.agents.SupportAgent"}],
|
||||
)
|
||||
from_repository: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Agent repository name to load. Repository values supply missing "
|
||||
"agent configuration; explicitly provided local fields override the "
|
||||
"repository values."
|
||||
),
|
||||
examples=["support_specialist"],
|
||||
)
|
||||
settings: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Additional agent settings passed to the loader.",
|
||||
|
||||
@@ -949,7 +949,6 @@ class Telemetry:
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Flow Creation")
|
||||
self._add_attribute(span, "crewai_version", version("crewai"))
|
||||
self._add_attribute(span, "flow_name", flow_name)
|
||||
close_span(span)
|
||||
|
||||
|
||||
@@ -1125,7 +1125,7 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
|
||||
client = PlusAPI(api_key=get_auth_token())
|
||||
_print_current_organization()
|
||||
response = client.get_agent(from_repository)
|
||||
response = asyncio.run(client.get_agent(from_repository))
|
||||
if response.status_code == 404:
|
||||
raise AgentRepositoryError(
|
||||
f"Agent {from_repository} does not exist, make sure the name is correct or the agent is available on your organization."
|
||||
@@ -1158,8 +1158,6 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
raise AgentRepositoryError(
|
||||
f"Tool {tool['name']} could not be loaded: {e}"
|
||||
) from e
|
||||
elif key == "skills" and value == []:
|
||||
continue
|
||||
else:
|
||||
attributes[key] = value
|
||||
return attributes
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
import os
|
||||
import threading
|
||||
from unittest import mock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
import warnings
|
||||
|
||||
from crewai.agents.crew_agent_executor import AgentFinish, CrewAgentExecutor
|
||||
from crewai.constants import DEFAULT_LLM_MODEL
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.tool_usage_events import ToolUsageFinishedEvent
|
||||
from crewai.experimental.agent_executor import AgentExecutor
|
||||
from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.knowledge.knowledge_config import KnowledgeConfig
|
||||
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||
@@ -803,97 +802,6 @@ def test_agent_human_input():
|
||||
assert output.strip().lower() == "hello"
|
||||
|
||||
|
||||
def test_agent_default_executor_human_input():
|
||||
from crewai.core.providers.human_input import SyncHumanInputProvider
|
||||
|
||||
agent = Agent(
|
||||
role="test role",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
)
|
||||
task = Task(
|
||||
agent=agent,
|
||||
description="Say the word: Hi",
|
||||
expected_output="The word: Hi",
|
||||
human_input=True,
|
||||
)
|
||||
answers = iter(
|
||||
[
|
||||
AgentFinish(output="Hi", thought="", text="Hi"),
|
||||
AgentFinish(output="Hello", thought="", text="Hello"),
|
||||
]
|
||||
)
|
||||
feedback_responses = iter(["Don't say hi, say Hello instead!", ""])
|
||||
|
||||
def kickoff_side_effect(executor, *_args, **_kwargs):
|
||||
executor.state.current_answer = next(answers)
|
||||
executor.state.is_finished = True
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
SyncHumanInputProvider,
|
||||
"_prompt_input",
|
||||
side_effect=lambda *_args, **_kwargs: next(feedback_responses),
|
||||
) as mock_prompt_input,
|
||||
patch.object(
|
||||
AgentExecutor, "kickoff", autospec=True, side_effect=kickoff_side_effect
|
||||
) as mock_kickoff,
|
||||
):
|
||||
output = agent.execute_task(task)
|
||||
|
||||
assert output == "Hello"
|
||||
assert mock_prompt_input.call_count == 2
|
||||
assert mock_kickoff.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_default_executor_async_human_input():
|
||||
from crewai.core.providers.human_input import SyncHumanInputProvider
|
||||
|
||||
agent = Agent(
|
||||
role="test role",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
)
|
||||
task = Task(
|
||||
agent=agent,
|
||||
description="Say the word: Hi",
|
||||
expected_output="The word: Hi",
|
||||
human_input=True,
|
||||
)
|
||||
answers = iter(
|
||||
[
|
||||
AgentFinish(output="Hi", thought="", text="Hi"),
|
||||
AgentFinish(output="Hello", thought="", text="Hello"),
|
||||
]
|
||||
)
|
||||
feedback_responses = iter(["Don't say hi, say Hello instead!", ""])
|
||||
|
||||
async def kickoff_side_effect(executor, *_args, **_kwargs):
|
||||
executor.state.current_answer = next(answers)
|
||||
executor.state.is_finished = True
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
SyncHumanInputProvider,
|
||||
"_prompt_input_async",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=lambda *_args, **_kwargs: next(feedback_responses),
|
||||
) as mock_prompt_input,
|
||||
patch.object(
|
||||
AgentExecutor,
|
||||
"kickoff_async",
|
||||
autospec=True,
|
||||
side_effect=kickoff_side_effect,
|
||||
) as mock_kickoff,
|
||||
):
|
||||
output = await agent.aexecute_task(task)
|
||||
|
||||
assert output == "Hello"
|
||||
assert mock_prompt_input.await_count == 2
|
||||
assert mock_kickoff.await_count == 2
|
||||
|
||||
|
||||
def test_interpolate_inputs():
|
||||
agent = Agent(
|
||||
role="{topic} specialist",
|
||||
@@ -2335,27 +2243,6 @@ def test_agent_from_repository_override_attributes(mock_get_agent, mock_get_auth
|
||||
assert isinstance(agent.tools[0], SerperDevTool)
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
def test_agent_from_repository_ignores_empty_skills(
|
||||
mock_get_agent, mock_get_auth_token
|
||||
):
|
||||
mock_get_response = MagicMock()
|
||||
mock_get_response.status_code = 200
|
||||
mock_get_response.json.return_value = {
|
||||
"role": "test role",
|
||||
"goal": "test goal",
|
||||
"backstory": "test backstory",
|
||||
"tools": [],
|
||||
"skills": [],
|
||||
}
|
||||
mock_get_agent.return_value = mock_get_response
|
||||
|
||||
agent = Agent(from_repository="test_agent")
|
||||
|
||||
assert agent.role == "test role"
|
||||
assert agent.skills is None
|
||||
|
||||
|
||||
@patch("crewai.plus_api.PlusAPI.get_agent")
|
||||
def test_agent_from_repository_with_invalid_tools(mock_get_agent, mock_get_auth_token):
|
||||
mock_get_response = MagicMock()
|
||||
|
||||
@@ -18,7 +18,6 @@ import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
|
||||
from crewai.core.providers.human_input import SyncHumanInputProvider
|
||||
from crewai.agents.step_executor import StepExecutor
|
||||
|
||||
|
||||
@@ -28,13 +27,6 @@ def _build_executor(**kwargs: Any) -> AgentExecutor:
|
||||
Uses model_construct to skip Pydantic validators so plain Mock()
|
||||
objects are accepted for typed fields like llm, agent, crew, task.
|
||||
"""
|
||||
prompt = kwargs.get("prompt")
|
||||
if isinstance(prompt, dict):
|
||||
if "system" in prompt:
|
||||
kwargs["prompt"] = SystemPromptResult(**prompt)
|
||||
else:
|
||||
kwargs["prompt"] = StandardPromptResult(**prompt)
|
||||
|
||||
executor = AgentExecutor.model_construct(**kwargs)
|
||||
executor._state = AgentExecutorState()
|
||||
executor._methods = {}
|
||||
@@ -58,7 +50,6 @@ def _build_executor(**kwargs: Any) -> AgentExecutor:
|
||||
executor._last_context_error = None
|
||||
executor._step_executor = None
|
||||
executor._planner_observer = None
|
||||
executor._is_feedback_iteration = False
|
||||
return executor
|
||||
from crewai.agents.planner_observer import PlannerObserver
|
||||
from crewai.experimental.agent_executor import (
|
||||
@@ -77,8 +68,7 @@ from crewai.events.types.tool_usage_events import (
|
||||
)
|
||||
from crewai.tools.tool_types import ToolResult
|
||||
from crewai.utilities.step_execution_context import StepExecutionContext
|
||||
from crewai.utilities.planning_types import TodoItem, TodoList
|
||||
from crewai.utilities.prompts import StandardPromptResult, SystemPromptResult
|
||||
from crewai.utilities.planning_types import TodoItem
|
||||
from crewai.utilities.file_store import clear_files, clear_task_files, store_files
|
||||
from crewai_files import TextFile
|
||||
|
||||
@@ -129,189 +119,6 @@ class TestAgentExecutor:
|
||||
class StructuredResult(BaseModel):
|
||||
value: str
|
||||
|
||||
def test_setup_messages_calls_human_input_provider_hooks(self):
|
||||
"""Message setup should preserve the HumanInputProvider hook contract."""
|
||||
executor = _build_executor(
|
||||
prompt=StandardPromptResult(prompt="Original task: {input}"),
|
||||
)
|
||||
provider = Mock()
|
||||
provider.setup_messages.return_value = False
|
||||
|
||||
def post_setup(context: AgentExecutor) -> None:
|
||||
context.messages.append(
|
||||
{"role": "system", "content": "provider post setup"}
|
||||
)
|
||||
|
||||
provider.post_setup_messages.side_effect = post_setup
|
||||
|
||||
with patch(
|
||||
"crewai.experimental.agent_executor.get_provider", return_value=provider
|
||||
):
|
||||
executor._setup_messages(
|
||||
{"input": "draft this", "tool_names": "", "tools": ""}
|
||||
)
|
||||
|
||||
provider.setup_messages.assert_called_once_with(executor)
|
||||
provider.post_setup_messages.assert_called_once_with(executor)
|
||||
assert executor.state.messages[0]["role"] == "user"
|
||||
assert executor.state.messages[0]["content"] == "Original task: draft this"
|
||||
assert executor.state.messages[1] == {
|
||||
"role": "system",
|
||||
"content": "provider post setup",
|
||||
}
|
||||
|
||||
def test_setup_messages_can_be_owned_by_human_input_provider(self):
|
||||
"""Providers can skip standard prompt setup by returning True."""
|
||||
executor = _build_executor(
|
||||
prompt=StandardPromptResult(prompt="Original task: {input}"),
|
||||
)
|
||||
provider = Mock()
|
||||
|
||||
def setup(context: AgentExecutor) -> bool:
|
||||
context.messages.append({"role": "user", "content": "provider message"})
|
||||
return True
|
||||
|
||||
provider.setup_messages.side_effect = setup
|
||||
|
||||
with patch(
|
||||
"crewai.experimental.agent_executor.get_provider", return_value=provider
|
||||
):
|
||||
executor._setup_messages(
|
||||
{"input": "draft this", "tool_names": "", "tools": ""}
|
||||
)
|
||||
|
||||
provider.setup_messages.assert_called_once_with(executor)
|
||||
provider.post_setup_messages.assert_not_called()
|
||||
assert executor.state.messages == [
|
||||
{"role": "user", "content": "provider message"}
|
||||
]
|
||||
|
||||
def test_human_feedback_reruns_flow_with_state_messages(self):
|
||||
"""Human feedback should use AgentExecutor state messages."""
|
||||
executor = _build_executor(agent=SimpleNamespace(verbose=False), crew=None)
|
||||
executor.state.messages = [{"role": "user", "content": "original task"}]
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="", output="draft", text="draft"
|
||||
)
|
||||
executor.state.is_finished = True
|
||||
executor._finalize_called = True
|
||||
executor.ask_for_human_input = True
|
||||
executor.state.iterations = executor.max_iter
|
||||
executor.state.plan = "completed plan"
|
||||
executor.state.plan_ready = True
|
||||
executor.state.todos = TodoList(
|
||||
items=[TodoItem(step_number=1, description="Done", status="completed")]
|
||||
)
|
||||
|
||||
improved_answer = AgentFinish(thought="", output="improved", text="improved")
|
||||
feedback_responses = iter(["make it friendlier", ""])
|
||||
|
||||
def finish_feedback_iteration(*_args: Any, **_kwargs: Any) -> None:
|
||||
assert executor._is_feedback_iteration is True
|
||||
assert executor.state.iterations == 0
|
||||
assert executor.state.plan is None
|
||||
assert executor.state.todos.items == []
|
||||
executor.state.current_answer = improved_answer
|
||||
executor.state.is_finished = True
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
SyncHumanInputProvider,
|
||||
"_prompt_input",
|
||||
side_effect=lambda *_args, **_kwargs: next(feedback_responses),
|
||||
) as mock_prompt_input,
|
||||
patch.object(
|
||||
AgentExecutor, "kickoff", side_effect=finish_feedback_iteration
|
||||
) as mock_kickoff,
|
||||
):
|
||||
result = executor._handle_human_feedback(
|
||||
AgentFinish(thought="", output="draft", text="draft")
|
||||
)
|
||||
|
||||
assert result is improved_answer
|
||||
assert mock_prompt_input.call_count == 2
|
||||
mock_kickoff.assert_called_once()
|
||||
assert executor.messages is executor.state.messages
|
||||
assert "make it friendlier" in executor.state.messages[-1]["content"]
|
||||
assert executor.ask_for_human_input is False
|
||||
assert executor.state.current_answer is improved_answer
|
||||
assert executor.state.is_finished is True
|
||||
assert executor._finalize_called is True
|
||||
assert executor._is_feedback_iteration is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_human_feedback_reruns_flow_with_state_messages(self):
|
||||
"""Async human feedback should use AgentExecutor state messages."""
|
||||
executor = _build_executor(agent=SimpleNamespace(verbose=False), crew=None)
|
||||
executor.state.messages = [{"role": "user", "content": "original task"}]
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="", output="draft", text="draft"
|
||||
)
|
||||
executor.state.is_finished = True
|
||||
executor._finalize_called = True
|
||||
executor.ask_for_human_input = True
|
||||
executor.state.iterations = executor.max_iter
|
||||
executor.state.plan = "completed plan"
|
||||
executor.state.plan_ready = True
|
||||
executor.state.todos = TodoList(
|
||||
items=[TodoItem(step_number=1, description="Done", status="completed")]
|
||||
)
|
||||
|
||||
improved_answer = AgentFinish(thought="", output="improved", text="improved")
|
||||
feedback_responses = iter(["make it friendlier", ""])
|
||||
|
||||
async def finish_feedback_iteration(*_args: Any, **_kwargs: Any) -> None:
|
||||
assert executor._is_feedback_iteration is True
|
||||
assert executor.state.iterations == 0
|
||||
assert executor.state.plan is None
|
||||
assert executor.state.todos.items == []
|
||||
executor.state.current_answer = improved_answer
|
||||
executor.state.is_finished = True
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
SyncHumanInputProvider,
|
||||
"_prompt_input_async",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=lambda *_args, **_kwargs: next(feedback_responses),
|
||||
) as mock_prompt_input,
|
||||
patch.object(
|
||||
AgentExecutor,
|
||||
"kickoff_async",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=finish_feedback_iteration,
|
||||
) as mock_kickoff,
|
||||
):
|
||||
result = await executor._ahandle_human_feedback(
|
||||
AgentFinish(thought="", output="draft", text="draft")
|
||||
)
|
||||
|
||||
assert result is improved_answer
|
||||
assert mock_prompt_input.await_count == 2
|
||||
mock_kickoff.assert_awaited_once()
|
||||
assert executor.messages is executor.state.messages
|
||||
assert "make it friendlier" in executor.state.messages[-1]["content"]
|
||||
assert executor.ask_for_human_input is False
|
||||
assert executor.state.current_answer is improved_answer
|
||||
assert executor.state.is_finished is True
|
||||
assert executor._finalize_called is True
|
||||
assert executor._is_feedback_iteration is False
|
||||
|
||||
def test_feedback_iteration_skips_plan_generation(self):
|
||||
"""Feedback reruns should reason over feedback without regenerating a plan."""
|
||||
executor = _build_executor(
|
||||
agent=SimpleNamespace(planning_enabled=True, verbose=False),
|
||||
task=SimpleNamespace(),
|
||||
)
|
||||
executor._is_feedback_iteration = True
|
||||
|
||||
with patch("crewai.utilities.reasoning_handler.AgentReasoning") as reasoning:
|
||||
executor.generate_plan()
|
||||
|
||||
reasoning.assert_not_called()
|
||||
assert executor.state.plan is None
|
||||
assert executor.state.todos.items == []
|
||||
|
||||
def test_inject_files_from_crew_task_store(self):
|
||||
"""Crew-level input_files should attach to the LLM user message."""
|
||||
crew_id = uuid4()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.plus_api import PlusAPI
|
||||
|
||||
@@ -394,23 +396,28 @@ class TestPlusAPI(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
||||
def test_get_agent(mock_make_request):
|
||||
@pytest.mark.asyncio
|
||||
@patch("httpx.AsyncClient")
|
||||
async def test_get_agent(mock_async_client_class):
|
||||
api = PlusAPI("test_api_key")
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.return_value = mock_response
|
||||
mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance
|
||||
|
||||
response = api.get_agent("test_agent_handle")
|
||||
response = await api.get_agent("test_agent_handle")
|
||||
|
||||
mock_make_request.assert_called_once_with(
|
||||
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
mock_client_instance.get.assert_called_once_with(
|
||||
f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle",
|
||||
headers=api.headers,
|
||||
)
|
||||
assert response == mock_response
|
||||
|
||||
|
||||
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
||||
@pytest.mark.asyncio
|
||||
@patch("httpx.AsyncClient")
|
||||
@patch("crewai_core.plus_api.Settings")
|
||||
def test_get_agent_with_org_uuid(mock_settings_class, mock_make_request):
|
||||
async def test_get_agent_with_org_uuid(mock_settings_class, mock_async_client_class):
|
||||
org_uuid = "test-org-uuid"
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = org_uuid
|
||||
@@ -420,12 +427,15 @@ def test_get_agent_with_org_uuid(mock_settings_class, mock_make_request):
|
||||
api = PlusAPI("test_api_key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.return_value = mock_response
|
||||
mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance
|
||||
|
||||
response = api.get_agent("test_agent_handle")
|
||||
response = await api.get_agent("test_agent_handle")
|
||||
|
||||
mock_make_request.assert_called_once_with(
|
||||
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
mock_client_instance.get.assert_called_once_with(
|
||||
f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle",
|
||||
headers=api.headers,
|
||||
)
|
||||
assert "X-Crewai-Organization-Id" in api.headers
|
||||
assert api.headers["X-Crewai-Organization-Id"] == org_uuid
|
||||
|
||||
@@ -14,7 +14,7 @@ from crewai_cli.run_crew import (
|
||||
_execute_uv_script,
|
||||
_load_json_crew_for_tui,
|
||||
_missing_input_names,
|
||||
_resolve_crew_inputs,
|
||||
_prompt_for_missing_inputs,
|
||||
)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ def test_missing_input_names_scans_agent_and_task_placeholders() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_resolve_crew_inputs_merges_runtime_values(monkeypatch) -> None:
|
||||
def test_prompt_for_missing_inputs_merges_runtime_values(monkeypatch) -> None:
|
||||
crew = SimpleNamespace(
|
||||
agents=[SimpleNamespace(role="Researcher", goal="Cover {topic}", backstory="")],
|
||||
tasks=[
|
||||
@@ -123,10 +123,9 @@ def test_resolve_crew_inputs_merges_runtime_values(monkeypatch) -> None:
|
||||
return values["audience"]
|
||||
raise AssertionError(f"Unexpected prompt: {label}")
|
||||
|
||||
# Prompting now lives in the shared crewai_cli.input_prompt helper.
|
||||
monkeypatch.setattr("crewai_cli.input_prompt.click.prompt", prompt)
|
||||
monkeypatch.setattr("crewai_cli.run_crew.click.prompt", prompt)
|
||||
|
||||
assert _resolve_crew_inputs(crew, {"topic": "AI"}, None, interactive=True) == {
|
||||
assert _prompt_for_missing_inputs(crew, {"topic": "AI"}) == {
|
||||
"topic": "AI",
|
||||
"audience": "developers",
|
||||
}
|
||||
|
||||
@@ -355,22 +355,16 @@ class TestLoadAgent:
|
||||
with pytest.raises(Exception):
|
||||
load_agent(agent_file)
|
||||
|
||||
@pytest.mark.parametrize("field", ["role", "goal", "backstory"])
|
||||
def test_load_agent_rejects_null_required_fields(
|
||||
self, tmp_path: Path, field: str
|
||||
):
|
||||
def test_load_agent_rejects_null_required_fields(self, tmp_path: Path):
|
||||
agent_def = {
|
||||
"role": "Researcher",
|
||||
"role": None,
|
||||
"goal": "Find information",
|
||||
"backstory": "Expert researcher.",
|
||||
}
|
||||
agent_def[field] = None
|
||||
agent_file = tmp_path / "agent.json"
|
||||
agent_file.write_text(json.dumps(agent_def))
|
||||
|
||||
with pytest.raises(
|
||||
JSONProjectValidationError, match=f"missing required field '{field}'"
|
||||
):
|
||||
with pytest.raises(JSONProjectValidationError, match="missing required field 'role'"):
|
||||
load_agent(agent_file)
|
||||
|
||||
def test_load_agent_file_not_found(self):
|
||||
|
||||
@@ -96,32 +96,6 @@ def test_flow_execution_span_records_crewai_version():
|
||||
span.set_attribute.assert_any_call("flow_name", "ResearchFlow")
|
||||
|
||||
|
||||
def test_flow_creation_span_records_crewai_version():
|
||||
tracer = Mock()
|
||||
span = Mock()
|
||||
tracer.start_span.return_value = span
|
||||
|
||||
with (
|
||||
patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"CREWAI_DISABLE_TELEMETRY": "false",
|
||||
"CREWAI_DISABLE_TRACKING": "false",
|
||||
"OTEL_SDK_DISABLED": "false",
|
||||
},
|
||||
),
|
||||
patch("crewai.telemetry.telemetry.TracerProvider"),
|
||||
patch("crewai.telemetry.telemetry.trace.get_tracer", return_value=tracer),
|
||||
patch("crewai.telemetry.telemetry.version", return_value="9.9.9"),
|
||||
):
|
||||
telemetry = Telemetry()
|
||||
telemetry.flow_creation_span("ResearchFlow")
|
||||
|
||||
tracer.start_span.assert_called_once_with("Flow Creation")
|
||||
span.set_attribute.assert_any_call("crewai_version", "9.9.9")
|
||||
span.set_attribute.assert_any_call("flow_name", "ResearchFlow")
|
||||
|
||||
|
||||
@patch("crewai.telemetry.telemetry.logger.error")
|
||||
@patch(
|
||||
"opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter.export",
|
||||
|
||||
@@ -2908,6 +2908,12 @@ def test_manager_agent_with_tools_raises_exception(researcher, writer):
|
||||
crew.kickoff()
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason="crew.train() relies on CrewAgentExecutor._format_feedback_message; "
|
||||
"AgentExecutor (the new default) does not implement training feedback yet. "
|
||||
"Remove this xfail once training is migrated to AgentExecutor.",
|
||||
)
|
||||
@pytest.mark.vcr()
|
||||
def test_crew_train_success(researcher, writer, monkeypatch):
|
||||
task = Task(
|
||||
|
||||
@@ -88,7 +88,7 @@ def test_flow_definition_json_schema_carries_reference_descriptions():
|
||||
agent_properties = defs["FlowAgentActionDefinition"]["properties"]
|
||||
assert "Individual Agent definition" in agent_properties["with"]["description"]
|
||||
assert "outside of a crew" in agent_properties["with"]["description"]
|
||||
assert "individual inline Agent" in agent_properties["call"]["description"]
|
||||
assert "repository-backed Agent" in agent_properties["call"]["description"]
|
||||
|
||||
expression_rule = FLOW_TEMPLATE_EXPRESSION_RULES[0]
|
||||
code_properties = defs["FlowCodeActionDefinition"]["properties"]
|
||||
|
||||
@@ -1166,35 +1166,28 @@ methods:
|
||||
def test_agent_action_runs_repository_yaml_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
import crewai.agent.core as agent_core
|
||||
from crewai import Agent
|
||||
from crewai.plus_api import PlusAPI
|
||||
|
||||
fetched_agents: list[str] = []
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {
|
||||
"role": "Repository specialist",
|
||||
"goal": "Answer support questions",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"max_iter": 3,
|
||||
"tools": [],
|
||||
}
|
||||
|
||||
def fake_get_agent(self: PlusAPI, handle: str) -> FakeResponse:
|
||||
fetched_agents.append(handle)
|
||||
return FakeResponse()
|
||||
def fake_load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
assert from_repository == "support_specialist"
|
||||
return {
|
||||
"role": "Repository specialist",
|
||||
"goal": "Answer support questions",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"max_iter": 3,
|
||||
}
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Agent, messages: str, **_kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
return {"agent": self.role, "input": messages, "max_iter": self.max_iter}
|
||||
|
||||
monkeypatch.setattr("crewai.auth.token.get_auth_token", lambda: "test-token")
|
||||
monkeypatch.setattr(PlusAPI, "get_agent", fake_get_agent)
|
||||
monkeypatch.setattr(
|
||||
agent_core,
|
||||
"load_agent_from_repository",
|
||||
fake_load_agent_from_repository,
|
||||
)
|
||||
monkeypatch.setattr(Agent, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
@@ -1217,83 +1210,6 @@ methods:
|
||||
"input": "What is CrewAI?",
|
||||
"max_iter": 3,
|
||||
}
|
||||
assert fetched_agents == ["support_specialist"]
|
||||
|
||||
|
||||
def test_agent_action_repository_fetch_does_not_block_event_loop(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from crewai import Agent
|
||||
from crewai.plus_api import PlusAPI
|
||||
|
||||
loop_marker_ran = threading.Event()
|
||||
fetch_started = threading.Event()
|
||||
release_fetch = threading.Event()
|
||||
fetch_saw_loop_marker = False
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {
|
||||
"role": "Repository specialist",
|
||||
"goal": "Answer support questions",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"tools": [],
|
||||
}
|
||||
|
||||
def fake_get_agent(self: PlusAPI, handle: str) -> FakeResponse:
|
||||
nonlocal fetch_saw_loop_marker
|
||||
fetch_started.set()
|
||||
release_fetch.wait(timeout=1)
|
||||
fetch_saw_loop_marker = loop_marker_ran.is_set()
|
||||
return FakeResponse()
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Agent, messages: str, **_kwargs: Any
|
||||
) -> str:
|
||||
return f"{self.role}:{messages}"
|
||||
|
||||
monkeypatch.setattr("crewai.auth.token.get_auth_token", lambda: "test-token")
|
||||
monkeypatch.setattr(PlusAPI, "get_agent", fake_get_agent)
|
||||
monkeypatch.setattr(Agent, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
schema: crewai.flow/v1
|
||||
name: AgentFlow
|
||||
methods:
|
||||
answer:
|
||||
do:
|
||||
call: agent
|
||||
with:
|
||||
from_repository: support_specialist
|
||||
input: "${state.question}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
async def run_flow() -> str:
|
||||
async def mark_loop_progress() -> None:
|
||||
while not fetch_started.is_set():
|
||||
await asyncio.sleep(0)
|
||||
loop_marker_ran.set()
|
||||
release_fetch.set()
|
||||
|
||||
marker_task = asyncio.create_task(mark_loop_progress())
|
||||
kickoff_task = asyncio.create_task(
|
||||
flow.kickoff_async(inputs={"question": "What is CrewAI?"})
|
||||
)
|
||||
try:
|
||||
result = await asyncio.wait_for(kickoff_task, timeout=2)
|
||||
await asyncio.wait_for(marker_task, timeout=2)
|
||||
return result
|
||||
finally:
|
||||
release_fetch.set()
|
||||
|
||||
assert asyncio.run(run_flow()) == "Repository specialist:What is CrewAI?"
|
||||
assert fetch_saw_loop_marker
|
||||
|
||||
|
||||
def test_agent_action_renders_text_custom_expression_input(
|
||||
@@ -1519,167 +1435,6 @@ methods:
|
||||
}
|
||||
|
||||
|
||||
def test_crew_action_runs_repository_agent_yaml_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from crewai import Crew
|
||||
from crewai.plus_api import PlusAPI
|
||||
|
||||
fetched_agents: list[str] = []
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {
|
||||
"role": "Repository researcher",
|
||||
"goal": "Research {topic}",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"max_iter": 5,
|
||||
"tools": [],
|
||||
}
|
||||
|
||||
def fake_get_agent(self: PlusAPI, handle: str) -> FakeResponse:
|
||||
fetched_agents.append(handle)
|
||||
return FakeResponse()
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"crew": self.name,
|
||||
"agents": [
|
||||
{"role": agent.role, "max_iter": agent.max_iter}
|
||||
for agent in self.agents
|
||||
],
|
||||
"tasks": [task.description for task in self.tasks],
|
||||
"inputs": inputs,
|
||||
}
|
||||
|
||||
monkeypatch.setattr("crewai.auth.token.get_auth_token", lambda: "test-token")
|
||||
monkeypatch.setattr(PlusAPI, "get_agent", fake_get_agent)
|
||||
monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
schema: crewai.flow/v1
|
||||
name: CrewFlow
|
||||
methods:
|
||||
research:
|
||||
do:
|
||||
call: crew
|
||||
with:
|
||||
name: inline_research
|
||||
agents:
|
||||
researcher:
|
||||
from_repository: researcher
|
||||
tasks:
|
||||
- name: research_task
|
||||
description: Research {topic}
|
||||
expected_output: Findings about {topic}
|
||||
agent: researcher
|
||||
inputs:
|
||||
topic: "${state.topic}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "inline_research",
|
||||
"agents": [{"role": "Repository researcher", "max_iter": 5}],
|
||||
"tasks": ["Research {topic}"],
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
assert fetched_agents == ["researcher"]
|
||||
|
||||
|
||||
def test_crew_action_repository_fetch_does_not_block_event_loop(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from crewai import Crew
|
||||
from crewai.plus_api import PlusAPI
|
||||
|
||||
loop_marker_ran = threading.Event()
|
||||
fetch_started = threading.Event()
|
||||
release_fetch = threading.Event()
|
||||
fetch_saw_loop_marker = False
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
return {
|
||||
"role": "Repository researcher",
|
||||
"goal": "Research {topic}",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"tools": [],
|
||||
}
|
||||
|
||||
def fake_get_agent(self: PlusAPI, handle: str) -> FakeResponse:
|
||||
nonlocal fetch_saw_loop_marker
|
||||
fetch_started.set()
|
||||
release_fetch.wait(timeout=1)
|
||||
fetch_saw_loop_marker = loop_marker_ran.is_set()
|
||||
return FakeResponse()
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
return {"agents": [agent.role for agent in self.agents], "inputs": inputs}
|
||||
|
||||
monkeypatch.setattr("crewai.auth.token.get_auth_token", lambda: "test-token")
|
||||
monkeypatch.setattr(PlusAPI, "get_agent", fake_get_agent)
|
||||
monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
schema: crewai.flow/v1
|
||||
name: CrewFlow
|
||||
methods:
|
||||
research:
|
||||
do:
|
||||
call: crew
|
||||
with:
|
||||
agents:
|
||||
researcher:
|
||||
from_repository: researcher
|
||||
tasks:
|
||||
- description: Research {topic}
|
||||
expected_output: Findings about {topic}
|
||||
agent: researcher
|
||||
inputs:
|
||||
topic: "${state.topic}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
async def run_flow() -> dict[str, Any]:
|
||||
async def mark_loop_progress() -> None:
|
||||
while not fetch_started.is_set():
|
||||
await asyncio.sleep(0)
|
||||
loop_marker_ran.set()
|
||||
release_fetch.set()
|
||||
|
||||
marker_task = asyncio.create_task(mark_loop_progress())
|
||||
kickoff_task = asyncio.create_task(
|
||||
flow.kickoff_async(inputs={"topic": "AI"})
|
||||
)
|
||||
try:
|
||||
result = await asyncio.wait_for(kickoff_task, timeout=2)
|
||||
await asyncio.wait_for(marker_task, timeout=2)
|
||||
return result
|
||||
finally:
|
||||
release_fetch.set()
|
||||
|
||||
assert asyncio.run(run_flow()) == {
|
||||
"agents": ["Repository researcher"],
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
assert fetch_saw_loop_marker
|
||||
|
||||
|
||||
def test_crew_action_interpolates_runtime_strings_and_lists(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
@@ -1732,6 +1487,73 @@ methods:
|
||||
}
|
||||
|
||||
|
||||
def test_crew_action_runs_repository_agent_yaml_definition(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
import crewai.agent.core as agent_core
|
||||
from crewai import Crew
|
||||
|
||||
def fake_load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
assert from_repository == "researcher"
|
||||
return {
|
||||
"role": "Repository researcher",
|
||||
"goal": "Research {topic}",
|
||||
"backstory": "Loaded from the agent repository.",
|
||||
"max_iter": 5,
|
||||
}
|
||||
|
||||
async def fake_kickoff_async(
|
||||
self: Crew, inputs: dict[str, Any] | None = None, **_kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"crew": self.name,
|
||||
"agents": [
|
||||
{"role": agent.role, "max_iter": agent.max_iter}
|
||||
for agent in self.agents
|
||||
],
|
||||
"tasks": [task.description for task in self.tasks],
|
||||
"inputs": inputs,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
agent_core,
|
||||
"load_agent_from_repository",
|
||||
fake_load_agent_from_repository,
|
||||
)
|
||||
monkeypatch.setattr(Crew, "kickoff_async", fake_kickoff_async)
|
||||
|
||||
yaml_str = """
|
||||
schema: crewai.flow/v1
|
||||
name: CrewFlow
|
||||
methods:
|
||||
research:
|
||||
do:
|
||||
call: crew
|
||||
with:
|
||||
name: inline_research
|
||||
agents:
|
||||
researcher:
|
||||
from_repository: researcher
|
||||
tasks:
|
||||
- name: research_task
|
||||
description: Research {topic}
|
||||
expected_output: Findings about {topic}
|
||||
agent: researcher
|
||||
inputs:
|
||||
topic: "${state.topic}"
|
||||
start: true
|
||||
"""
|
||||
|
||||
flow = Flow.from_declaration(contents=yaml_str)
|
||||
|
||||
assert flow.kickoff(inputs={"topic": "AI"}) == {
|
||||
"crew": "inline_research",
|
||||
"agents": [{"role": "Repository researcher", "max_iter": 5}],
|
||||
"tasks": ["Research {topic}"],
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
|
||||
|
||||
def test_crew_action_runs_crew_from_declaration(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
):
|
||||
@@ -2024,45 +1846,36 @@ def test_crew_action_json_schema_describes_inline_crew_definitions():
|
||||
|
||||
|
||||
def test_crew_action_rejects_incomplete_inline_agent_definition():
|
||||
from crewai.project.crew_loader import load_crew_from_definition
|
||||
from crewai.project.json_loader import JSONProjectValidationError
|
||||
|
||||
definition = FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
"methods": {
|
||||
"research": {
|
||||
"start": True,
|
||||
"do": {
|
||||
"call": "crew",
|
||||
"with": {
|
||||
"agents": {
|
||||
"researcher": {
|
||||
"role": "Researcher",
|
||||
"backstory": "Knows things.",
|
||||
}
|
||||
with pytest.raises(ValidationError, match="goal"):
|
||||
FlowDefinition.from_declaration(contents=
|
||||
{
|
||||
"schema": "crewai.flow/v1",
|
||||
"name": "CrewFlow",
|
||||
"methods": {
|
||||
"research": {
|
||||
"start": True,
|
||||
"do": {
|
||||
"call": "crew",
|
||||
"with": {
|
||||
"agents": {
|
||||
"researcher": {
|
||||
"role": "Researcher",
|
||||
"backstory": "Knows things.",
|
||||
}
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"description": "Research",
|
||||
"expected_output": "Findings",
|
||||
"agent": "researcher",
|
||||
}
|
||||
],
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"description": "Research",
|
||||
"expected_output": "Findings",
|
||||
"agent": "researcher",
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
crew_definition = definition.methods["research"].do.with_
|
||||
assert crew_definition.agents["researcher"].goal is None
|
||||
|
||||
with pytest.raises(
|
||||
JSONProjectValidationError, match="missing required field 'goal'"
|
||||
):
|
||||
load_crew_from_definition(crew_definition, source="crew action")
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_crew_action_rejects_python_ref_field():
|
||||
|
||||
@@ -138,27 +138,4 @@ class TestFlowHumanInputIntegration:
|
||||
for call in call_args
|
||||
if call[0]
|
||||
)
|
||||
assert training_panel_found
|
||||
|
||||
@patch("builtins.input", return_value="please make it warmer")
|
||||
def test_non_empty_input_prints_processing_feedback(self, mock_input):
|
||||
"""Non-empty input should be displayed as feedback to process."""
|
||||
provider = SyncHumanInputProvider()
|
||||
crew = MagicMock()
|
||||
crew._train = False
|
||||
|
||||
formatter = event_listener.formatter
|
||||
|
||||
with (
|
||||
patch.object(formatter, "pause_live_updates"),
|
||||
patch.object(formatter, "resume_live_updates"),
|
||||
patch.object(formatter.console, "print") as mock_console_print,
|
||||
):
|
||||
result = provider._prompt_input(crew)
|
||||
|
||||
assert result == "please make it warmer"
|
||||
mock_input.assert_called_once()
|
||||
printed_text = "\n".join(
|
||||
str(call.args[0]) for call in mock_console_print.call_args_list
|
||||
)
|
||||
assert "Processing your feedback" in printed_text
|
||||
assert training_panel_found
|
||||
46
uv.lock
generated
@@ -13,7 +13,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-07-04T15:35:51.457693Z"
|
||||
exclude-newer = "2026-06-28T20:06:34.114646Z"
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
@@ -5463,7 +5463,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "onnx"
|
||||
version = "1.22.0"
|
||||
version = "1.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ml-dtypes" },
|
||||
@@ -5472,26 +5472,30 @@ dependencies = [
|
||||
{ name = "protobuf" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/19/8ea73a64b368b75fe339771a20a02bc61ea1f551484c9e3d9d0bfbd0450f/onnx-1.22.0.tar.gz", hash = "sha256:ef40c0aaf0b643857ea9306fc7eddce17eaf9fb0407e4801f1fc5758443a38e0", size = 12024721, upload-time = "2026-06-15T12:50:05.354Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/93/942d2a0f6a70538eea042ce0445c8aefd46559ad153469986f29a743c01c/onnx-1.21.0.tar.gz", hash = "sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764", size = 12074608, upload-time = "2026-03-27T21:33:36.118Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/04/471f234e2716c83f17a26e1b50cd64c39428373e91dd018aafb3d499c108/onnx-1.22.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:6d0ffffd63a4ecc21ddaeddd5bf02099cb701aa4243f2de00122726869065ca4", size = 20167110, upload-time = "2026-06-15T12:48:59.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/40/540a2fe3c49ce1709ff2015de20d9a351264fb442f8998f92cf0ba7e279e/onnx-1.22.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33ce94119bbb7f05d9caea4ea7549f5185a54369f6bbc9f70171bd5ee6935bbc", size = 18892738, upload-time = "2026-06-15T12:49:02.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/0c/f41d5b89c38fb2ec410ab23c24fa110af786093b140644f7f953e436743b/onnx-1.22.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87a3077958f66f9a26dec10077ac28326d9cec2cbe1f0b040947243449754573", size = 19110354, upload-time = "2026-06-15T12:49:05.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8e/9f41d132855e93c2808cdd4afab1b5af67bd5e82e4a4fa9248006e4df87e/onnx-1.22.0-cp310-cp310-win32.whl", hash = "sha256:8a5eccce2d5fc6c5046928a9aa7cdd9750ea4a586f8de341d3d40d820c35fdec", size = 17083595, upload-time = "2026-06-15T12:49:08.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/52/86caff81786a5428485795c79175ae2b12a630795bcb267b84e5f9e98450/onnx-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:5c1c0408a9d4b4df33851672e5fc7590b96301ee123396d608f9ab6f045ab06b", size = 17215270, upload-time = "2026-06-15T12:49:11.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/55/30825c02c92a0380ce84c3feeeec95d329fa77548ba58cb10ad4bbfd83c6/onnx-1.22.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2d8f229a553fa440fe623ed7b36fca5e7762da3af871c3f8f8ce451df73e2914", size = 20167891, upload-time = "2026-06-15T12:49:14.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/24/cd4ab52ecaf41c3fbed674772ccbfe39041cb257b8471a47a37e48bff3f8/onnx-1.22.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a89a7cb9ba13d78f009bdec448ec82a98972589734f157022a2bff7a5973a6", size = 18892720, upload-time = "2026-06-15T12:49:16.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/a0/c9d9d56ceadb1c0a90a7cbec5a0510520ab6538938944fa84548e4b5b054/onnx-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d0a2bdb15eb2b3cb65c438f3423d9620d14fdce32f92380e6bb1b2e09568ef5", size = 19110720, upload-time = "2026-06-15T12:49:19.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/6e/e43e5a68d9cadde55df75310027f87127333a77e5ddcea14c73e96a10cac/onnx-1.22.0-cp311-cp311-win32.whl", hash = "sha256:239958534464612fbcb6ed23d5228aaa925b39b8773f58726809ffdccb4edd1c", size = 17083746, upload-time = "2026-06-15T12:49:22.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/57/cc0a9f2cf4522e42829d089927b4b75924d32f50dca237482e7b741df003/onnx-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:8561a2c00041c07e08db0c228593b5b4694100398685f348532af7dbb84189da", size = 17215684, upload-time = "2026-06-15T12:49:26.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/99/0f049f9eaa06c8383060c5f0a338e3a6caac8822e6e326c9162f05abf95a/onnx-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:8907b9b9389893bc0dc6314cc00ee1e3a69844e48d689eacc6a0340411a7da58", size = 17210398, upload-time = "2026-06-15T12:49:29.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:596fbf0490947533c1c1045ba860851dc9fb77471023dac9a71ba5b42ceab103", size = 20167081, upload-time = "2026-06-15T12:49:32.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/55/b34fc2aa30aa54b4a775402d24c4082242c720283a274fe976ac8eb94480/onnx-1.22.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae5a563f281cd9d2845622cecf6c092a57e4ee1b138f66fdbbdd4200567a5e16", size = 18889249, upload-time = "2026-06-15T12:49:34.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:955e02e1f6d385b53d52f9cd7b9cdf5caf417c300bcfe3c64c6d542be763845b", size = 19106514, upload-time = "2026-06-15T12:49:37.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/9d/3af461ac6c714b8b369cb71499659932f4f12cfb066250b62f7567c3d530/onnx-1.22.0-cp312-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:82e9f27fc1223cb06d68a56bed6f9d3caf3d0dad1b61bce45006d529b15bd94c", size = 16966387, upload-time = "2026-06-15T12:49:40.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/f0/68195b5e5a53e333faf2660f5352ee43738d0e42fc5216cc6b1871a9fbfb/onnx-1.22.0-cp312-abi3-win32.whl", hash = "sha256:cc8b66b312f8f03a53e268afb67180a2d97dd12cc79e2b61361c6c0073448016", size = 17081568, upload-time = "2026-06-15T12:49:43.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl", hash = "sha256:72ccebab3bac07215c204ce8848d42e78eaaa666badbf72d25cd359b9f269e3a", size = 17213290, upload-time = "2026-06-15T12:49:45.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2a/8ce48d8ae26a8761ad4e5dc771961b155c5c3c7c8540ec7f2f2d71b69af0/onnx-1.22.0-cp312-abi3-win_arm64.whl", hash = "sha256:f3c120dcdb70ad738f3c061b32798f408ea299eb69f84dd69ab4a6bf3c2ec01f", size = 17207030, upload-time = "2026-06-15T12:49:48.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/28/a14b1845bf9302c3a787221e8f37cde4e7f930e10d95a8e22dd910aeb41d/onnx-1.21.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e0c21cc5c7a41d1a509828e2b14fe9c30e807c6df611ec0fd64a47b8d4b16abd", size = 17966899, upload-time = "2026-03-27T21:32:15.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/7b/788881bf022a4cfb7b0843782f88415ea51c805cee4a909dcf2e49bb8129/onnx-1.21.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1931bfcc222a4c9da6475f2ffffb84b97ab3876041ec639171c11ce802bee6a", size = 17534297, upload-time = "2026-03-27T21:32:18.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/51/eb64d4f2ec6caa98909aab5fbcfa24be9c059081e804bbb0012cc549ef89/onnx-1.21.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b56ad04039fac6b028c07e54afa1ec7f75dd340f65311f2c292e41ed7aa4d9", size = 17616697, upload-time = "2026-03-27T21:32:21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/4e/6b1f7800dae3407dc850e7e59d591ed8c83e9b3401e4cd57a1f612e400c6/onnx-1.21.0-cp310-cp310-win32.whl", hash = "sha256:3abd09872523c7e0362d767e4e63bd7c6bac52a5e2c3edbf061061fe540e2027", size = 16288893, upload-time = "2026-03-27T21:32:23.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/a8/89273e581d3943e20314af19b1596ab4d763f9c2eb07d4eaf4fb0593219b/onnx-1.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:f2c7c234c568402e10db74e33d787e4144e394ae2bcbbf11000fbfe2e017ad68", size = 16443416, upload-time = "2026-03-27T21:32:26.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/48/32e383aa6bc40b72a9fd419937aaa647078190c9bfccdc97b316d2dee687/onnx-1.21.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2aca19949260875c14866fc77ea0bc37e4e809b24976108762843d328c92d3ce", size = 17968053, upload-time = "2026-03-27T21:32:29.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/26/5726e8df7d36e96bb3c679912d1a86af42f393d77aa17d6b98a97d4289ce/onnx-1.21.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82aa6ab51144df07c58c4850cb78d4f1ae969d8c0bf657b28041796d49ba6974", size = 17534821, upload-time = "2026-03-27T21:32:32.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2b/021dcd2dd50c3c71b7959d7368526da384a295c162fb4863f36057973f78/onnx-1.21.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c3185a232089335581fabb98fba4e86d3e8246b8140f2e406082438100ebda", size = 17616664, upload-time = "2026-03-27T21:32:34.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/00/afa32a46fa122a7ed42df1cfe8796922156a3725ba8fc581c4779c96e2fc/onnx-1.21.0-cp311-cp311-win32.whl", hash = "sha256:f53b3c15a3b539c16b99655c43c365622046d68c49b680c48eba4da2a4fb6f27", size = 16289035, upload-time = "2026-03-27T21:32:37.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/8d/483cc980a24d4c0131d0af06d0ff6a37fb08ae90a7848ece8cef645194f1/onnx-1.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f78c411743db317a76e5d009f84f7e3d5380411a1567a868e82461a1e5c775d", size = 16443748, upload-time = "2026-03-27T21:32:40.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/78/9d06fd5aaaed1ec9cb8a3b70fbbf00c1bdc18db610771e96379f0ed58112/onnx-1.21.0-cp311-cp311-win_arm64.whl", hash = "sha256:ab6a488dabbb172eebc9f3b3e7ac68763f32b0c571626d4a5004608f866cc83d", size = 16406123, upload-time = "2026-03-27T21:32:45.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ae/cb644ec84c25e63575d9d8790fdcc5d1a11d67d3f62f872edb35fa38d158/onnx-1.21.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095", size = 17965930, upload-time = "2026-03-27T21:32:48.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/b6/eeb5903586645ef8a49b4b7892580438741acc3df91d7a5bd0f3a59ea9cb/onnx-1.21.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b", size = 17531344, upload-time = "2026-03-27T21:32:50.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/00/4823f06357892d1e60d6f34e7299d2ba4ed2108c487cc394f7ce85a3ff14/onnx-1.21.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285", size = 17613697, upload-time = "2026-03-27T21:32:54.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/1d/391f3c567ae068c8ac4f1d1316bae97c9eb45e702f05975fe0e17ad441f0/onnx-1.21.0-cp312-abi3-win32.whl", hash = "sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e", size = 16287200, upload-time = "2026-03-27T21:32:57.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/a6/5eefbe5b40ea96de95a766bd2e0e751f35bdea2d4b951991ec9afaa69531/onnx-1.21.0-cp312-abi3-win_amd64.whl", hash = "sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f", size = 16441045, upload-time = "2026-03-27T21:33:00.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/c4/0ed8dc037a39113d2a4d66e0005e07751c299c46b993f1ad5c2c35664c20/onnx-1.21.0-cp312-abi3-win_arm64.whl", hash = "sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb", size = 16403134, upload-time = "2026-03-27T21:33:03.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/89/0e1a9beb536401e2f45ac88735e123f2735e12fc7b56ff6c11727e097526/onnx-1.21.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f", size = 17975430, upload-time = "2026-03-27T21:33:07.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/46/e6dc71a7b3b317265591b20a5f71d0ff5c0d26c24e52283139dc90c66038/onnx-1.21.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8", size = 17537435, upload-time = "2026-03-27T21:33:09.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/2e/27affcac63eaf2ef183a44fd1a1354b11da64a6c72fe6f3fdcf5571bcee5/onnx-1.21.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840", size = 17617687, upload-time = "2026-03-27T21:33:12.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5c/ac8ed15e941593a3672ce424280b764979026317811f2e8508432bfc3429/onnx-1.21.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd", size = 16449402, upload-time = "2026-03-27T21:33:15.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/aa/d2231e0dcaad838217afc64c306c8152a080134d2034e247cc973d577674/onnx-1.21.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad", size = 16408273, upload-time = "2026-03-27T21:33:18.599Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||