Compare commits

..

1 Commits

Author SHA1 Message Date
Vinicius Brasil
570b100b33 Allow repository-backed flow agents
Flow declarations can now reference full repository-backed Agent config
without duplicating it inline. The repository response is merged the same
way as `Agent(from_repository=...)`, so values like `role`, `goal`,
`backstory`, `tools`, `planning_config`, deprecated `reasoning`, `memory`,
and other Agent fields can come from the repository. Local YAML fields
still override repository values.

Standalone agent action:

```yaml
do:
  call: agent
  with:
    from_repository: support_specialist
    input: "${state.question}"
```

Inline crew agent:

```yaml
do:
  call: crew
  with:
    name: inline_research
    agents:
      researcher:
        from_repository: researcher
    tasks:
      - description: Research {topic}
        expected_output: Findings about {topic}
        agent: researcher
```
2026-07-02 09:30:37 -07:00
59 changed files with 678 additions and 3381 deletions

View File

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

View File

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

View File

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

View File

@@ -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">
تعمّق في تنفيذ واحد لرؤية تفكير الوكيل واستدعاءات الأدوات واستخدام الرموز.

View File

@@ -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>
![نظرة عامة على Agent Control Plane](/images/enterprise/acp-overview-automations-sankey.png)
@@ -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>

View File

@@ -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>
![قائمة السياسات](/images/enterprise/acp-policies-list.png)
![قائمة القواعد](/images/enterprise/acp-rules-list.png)
</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>
![مودال الأتمتات المُفعَّلة](/images/enterprise/acp-policies-engaged-modal.png)
![مودال الأتمتات المُفعَّلة](/images/enterprise/acp-rules-engaged-modal.png)
</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>

View File

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

View File

@@ -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>
![Agent Control Plane overview](/images/enterprise/acp-overview-automations-sankey.png)
@@ -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>

View File

@@ -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>
![Policies list](/images/enterprise/acp-policies-list.png)
![Rules list](/images/enterprise/acp-rules-list.png)
</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>
![Engaged automations modal](/images/enterprise/acp-policies-engaged-modal.png)
![Engaged automations modal](/images/enterprise/acp-rules-engaged-modal.png)
</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>

View File

@@ -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">
개별 실행을 드릴다운하여 에이전트의 추론, 도구 호출, 토큰 사용량을 확인합니다.

View File

@@ -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>
![Agent Control Plane 개요](/images/enterprise/acp-overview-automations-sankey.png)
@@ -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>

View File

@@ -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>
![정책 목록](/images/enterprise/acp-policies-list.png)
![규칙 목록](/images/enterprise/acp-rules-list.png)
</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>
![Engaged automations 모달](/images/enterprise/acp-policies-engaged-modal.png)
![Engaged automations 모달](/images/enterprise/acp-rules-engaged-modal.png)
</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>

View File

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

View File

@@ -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>
![Visão geral do Agent Control Plane](/images/enterprise/acp-overview-automations-sankey.png)
@@ -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>

View File

@@ -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>
![Lista de políticas](/images/enterprise/acp-policies-list.png)
</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>
![Modal de automações engajadas](/images/enterprise/acp-policies-engaged-modal.png)
</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>

View File

@@ -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>
![Lista de regras](/images/enterprise/acp-rules-list.png)
</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>
![Modal de automações engajadas](/images/enterprise/acp-rules-engaged-modal.png)
</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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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=[
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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