Merge branch 'main' into docs/checkpointing-restructure

This commit is contained in:
Greyson LaLonde
2026-05-27 12:25:50 -07:00
committed by GitHub
295 changed files with 1909 additions and 3775 deletions

View File

@@ -0,0 +1,104 @@
---
title: "راقب أتمتاتك"
description: "راقب صحة الأسطول واستهلاك LLM وسلوك كل أتمتة من تبويب Automations."
sidebarTitle: "المراقبة"
icon: "gauge"
mode: "wide"
---
## نظرة عامة
تبويب **Automations** هو عرض العمليات للقراءة فقط في [Agent Control Plane](/ar/enterprise/features/agent-control-plane/overview). يجمع بين بطاقتَي مقاييس و sankey تفاعلي وجدولين فرعيين — **Automations** و **Consumption** — يمكنك البحث والتصفية والفرز فيهما.
<Frame>
![نظرة عامة على Agent Control Plane](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
تحترم جميع المخططات والجداول مُحدّد **آخر 24 ساعة / الأسبوع الماضي / آخر 30 يوماً** في أعلى اليمين. تقارن قيم الفرق النافذة المختارة بالنافذة السابقة بنفس الطول.
<Note>
تعرض الصفوف بيانات فقط لعمليات النشر على **crewAI v1.13 أو أحدث** — تظهر عمليات النشر الأقدم في لافتة *"We've detected N other automations that we can't display"* أسفل sankey ولا تساهم بأي مقاييس حتى يتم تحديثها وإعادة نشرها. راجع [نظرة عامة — المتطلبات](/ar/enterprise/features/agent-control-plane/overview#المتطلبات).
</Note>
## لوحة المعلومات
يحتوي رأس الصفحة على بطاقتَي مقاييس و sankey تفاعلي. النقر على أي من البطاقتين يبدّل sankey بين وضعَين:
- **وضع الصحة** — `إجمالي الأتمتات → حِزم الحالة (Critical / Warning / Healthy)`. انقر على حِزمة لتصفية جدول Automations إلى عمليات النشر تلك فقط.
- **وضع الاستهلاك** — `مزودو النماذج → الأتمتات → التكلفة الإجمالية`. انقر على مزود لتصفية جدول Consumption إلى ذلك المزود.
| البطاقة | ما تعرضه |
|------|---------------|
| **Automations** | الأتمتات `active` (والعدد الإجمالي)، إجمالي `errors` في النافذة، `active executions` الحالية (والإجمالي في النافذة)، مع الفرق مقابل الفترة السابقة. |
| **Consumption** | إجمالي `cost` و `tokens used`، مع فرق التكلفة مقابل الفترة السابقة. |
<Frame>
![نظرة عامة مع sankey الاستهلاك](/images/enterprise/acp-overview-consumption-sankey.png)
</Frame>
## جدول Automations
التبويب الفرعي **Automations** هو تفصيل صحة الأسطول لكل deployment. كل صف هو crew أو flow منشور.
<Frame>
![جدول الأتمتات مع تفصيل حالة الصحة](/images/enterprise/acp-automations-table.png)
</Frame>
| العمود | ما يعرضه |
|--------|---------------|
| **Automation** | اسم الـ deployment وأي وسوم مُسنَدة إليه (مثل `production`، `financial`). |
| **Last execution** | الوقت المنقضي منذ آخر تنفيذ. |
| **Health Status Breakdown** | شريط مكدّس بنسب `Critical` / `Warning` / `Healthy` لعمليات التنفيذ في النافذة. |
| **Executions with Errors** | إجمالي عمليات التنفيذ الفاشلة في النافذة. |
| **PII detection applied** | `Yes` إذا كان هناك تكوين PII لكل deployment أو [قاعدة PII](/ar/enterprise/features/agent-control-plane/rules) مطابِقة نشطة. |
| **Executions** | إجمالي عمليات التنفيذ في النافذة. |
| **Last updated** | متى أُعيد نشر الـ deployment آخر مرة. |
| **Crew Version** | إصدار `crewai` الذي يُبلِّغ عنه الـ deployment. يشير أيقونة المعلومات بجانب الإصدارات الأقل من `1.13` إلى صفوف لا يمكنها المساهمة بالمقاييس. |
ابحث بالاسم، صفِّ حسب `Status` (`Healthy` / `Warning` / `Critical`)، وافرز بأي رأس عمود. انقر على اسم الـ deployment لفتح **لوحة الأتمتة**.
## جدول Consumption
التبويب الفرعي **Consumption** هو تفصيل إنفاق LLM واستخدام الرموز لكل deployment.
<Frame>
![جدول الاستهلاك مُفصَّل حسب مزود LLM](/images/enterprise/acp-consumption-table.png)
</Frame>
| العمود | ما يعرضه |
|--------|---------------|
| **Automation** | اسم الـ deployment. |
| **Last execution** | الوقت المنقضي منذ آخر تنفيذ. |
| **Tokens used** | صف واحد لكل مزود LLM تستخدمه هذه الأتمتة، مع الفرق مقابل الفترة السابقة. |
| **Cost** | التكلفة لكل مزود LLM، مع الفرق مقابل الفترة السابقة. |
| **Total cost** | المجموع عبر جميع المزودين، مع الفرق. |
| **Executions** | إجمالي عمليات التنفيذ في النافذة. |
| **Last updated** | متى أُعيد نشر الـ deployment آخر مرة. |
| **Crew Version** | إصدار `crewai` الذي يُبلِّغ عنه الـ deployment. |
صفِّ حسب **LLM provider** وافرز حسب `Cost` أو `Executions` أو `Last run`.
<Info>
**عادة ما تعني الخلايا الفارغة (`—` أو `$0.00`) أن الـ deployment أدنى من crewAI v1.13.** في اللقطة أعلاه، تظهر *Automation F* (`1.7.0`) و *Automation I* (`1.12.2`) فارغة في الرموز والتكلفة — لا تزال عمليات التنفيذ تعمل، لكنها لا تُصدِر التليمتري على مستوى المزود الذي يُغذِّي هذا الجدول. حدّث هذه الـ crews وأعد نشرها لبدء جمع بيانات الاستهلاك.
</Info>
## ذو صلة
<CardGroup cols={2}>
<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="/ar/enterprise/features/agent-control-plane/rules">
طبّق قواعد PII Redaction على مستوى المؤسسة عبر العديد من الأتمتات.
</Card>
<Card title="Traces" icon="timeline" href="/ar/enterprise/features/traces">
تعمّق في تنفيذ واحد لرؤية تفكير الوكيل واستدعاءات الأدوات واستخدام الرموز.
</Card>
<Card title="النشر إلى AMP" icon="rocket" href="/ar/enterprise/guides/deploy-to-amp">
انشر crew على إصدار crewAI يدعم Agent Control Plane.
</Card>
</CardGroup>
<Card title="تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
تواصل مع فريق الدعم للمساعدة في تفسير المقاييس داخل Agent Control Plane.
</Card>

View File

@@ -0,0 +1,74 @@
---
title: نظرة عامة على Agent Control Plane
description: "مركز عمليات موحّد للأتمتات الجارية — صحة الأسطول واستهلاك LLM والسياسات على مستوى المؤسسة في مكان واحد."
sidebarTitle: نظرة عامة
icon: "book-open"
---
## نظرة عامة
**Agent Control Plane** (ACP) هو مركز العمليات لكل ما يعمل لديك على CrewAI AMP. إنها شاشة واحدة — مقسّمة إلى تبويبَي **Automations** و **Rules** — تمنح فريقك القدرة على:
- مراقبة **حالة (الصحة)** كل أتمتة حيّة (crew أو flow) بتفصيل `Critical` / `Warning` / `Healthy` وعدد عمليات التنفيذ.
- تتبع **استهلاك LLM** — الرموز (tokens) والتكلفة — لكل أتمتة ولكل مزود ولكل نموذج، مع الفرق مقابل الفترة السابقة.
- التعمّق في أي أتمتة منفردة أو مزود نماذج لرؤية المخططات الزمنية وتفصيل البيانات لكل مزود.
- تطبيق **قواعد (Rules)** على مستوى المؤسسة (اليوم: PII Redaction) عبر العديد من الأتمتات دفعة واحدة بدلاً من تعديل كل deployment على حدة.
<Frame>
![نظرة عامة على Agent Control Plane](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
<Note>
Agent Control Plane مُوسوم حالياً بـ **Beta** في CrewAI Platform.
</Note>
يجيب التبويبان عن سؤالَين مختلفَين:
- **Automations** — *"كيف يتصرف أسطولي الآن، وكم يكلّفني؟"* راجع [المراقبة](/ar/enterprise/features/agent-control-plane/monitoring).
- **Rules** — *"كيف أفرض سياسة (مثل PII redaction) عبر العديد من عمليات النشر دون إعادة نشر كل واحدة؟"* راجع [القواعد](/ar/enterprise/features/agent-control-plane/rules).
## المتطلبات
<Warning>
يُشترط **crewAI v1.13 أو أحدث** ليتمكن أي أتمتة من تعبئة أي بيانات على هذه الصفحة — تمر بيانات الصحة وعمليات التنفيذ والأخطاء والرموز والتكلفة عبر التليمتري الذي تم تفعيله في `crewai==1.13`. تظهر عمليات النشر الأقدم في لافتة *"We've detected N other automations that we can't display"* ولا تساهم بأي صفوف حتى يتم تحديثها وإعادة نشرها.
</Warning>
<Warning>
يُشترط **خطة 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` لإنشاء وتعديل وتشغيل/إيقاف وحذف القواعد.
- يمكن ضبط نطاق جميع المخططات والجداول إلى **آخر 24 ساعة** أو **الأسبوع الماضي** أو **آخر 30 يوماً** عبر مُحدّد الوقت في أعلى اليمين. تقارن قيم الفرق (`↑ 8 vs yesterday`, `↓ $20.57 vs yesterday` وغيرها) النافذة المختارة بالنافذة السابقة بنفس الطول.
## ما يمكنك فعله هنا
<CardGroup cols={2}>
<Card title="المراقبة" icon="gauge" href="/ar/enterprise/features/agent-control-plane/monitoring">
راقب صحة الأسطول وإنفاق LLM عبر بطاقات المقاييس و sankey التفاعلي وجداول لكل أتمتة ولوحات جانبية للتعمق في أي أتمتة أو مزود.
</Card>
<Card title="القواعد" icon="shield-check" href="/ar/enterprise/features/agent-control-plane/rules">
طبّق سياسات PII Redaction على مستوى المؤسسة بنطاق محدد بالأدوات والوسوم. تسري التغييرات في التنفيذ التالي — دون الحاجة لإعادة نشر.
</Card>
</CardGroup>
## ذو صلة
<CardGroup cols={2}>
<Card title="Traces" icon="timeline" href="/ar/enterprise/features/traces">
تعمّق في تنفيذ واحد لرؤية تفكير الوكيل واستدعاءات الأدوات واستخدام الرموز.
</Card>
<Card title="RBAC" icon="users" href="/ar/enterprise/features/rbac">
أدِر من يمكنه قراءة Agent Control Plane ومن يمكنه تعديل القواعد.
</Card>
<Card title="PII Redaction للـ Traces" icon="lock" href="/ar/enterprise/features/pii-trace-redactions">
كتالوج الكيانات وضبط PII لكل deployment التي تستند إليها القواعد.
</Card>
<Card title="النشر إلى AMP" icon="rocket" href="/ar/enterprise/guides/deploy-to-amp">
انشر crew على إصدار crewAI يدعم Agent Control Plane.
</Card>
</CardGroup>
<Card title="تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
تواصل مع فريق الدعم للمساعدة في تفسير المقاييس أو تصميم القواعد.
</Card>

View File

@@ -0,0 +1,114 @@
---
title: "إعداد القواعد"
description: "طبّق سياسات على مستوى المؤسسة عبر العديد من الأتمتات من مكان واحد."
sidebarTitle: "القواعد"
icon: "shield-check"
mode: "wide"
---
## نظرة عامة
تتيح لك القواعد تطبيق سياسات — اليوم: **PII Redaction** — عبر العديد من الأتمتات دفعة واحدة، بدلاً من ضبط كل deployment على حدة. افتح تبويب **Rules** في [Agent Control Plane](/ar/enterprise/features/agent-control-plane/overview) لإدارتها.
<Frame>
![قائمة القواعد](/images/enterprise/acp-rules-list.png)
</Frame>
تعرض كل بطاقة قاعدة الاسم والوصف و**النطاق (scope)** الذي تنطبق عليه القاعدة (الأدوات والوسوم المختارة) وعدد **الأتمتات المُفعَّلة** — عمليات النشر التي تطابق النطاق حالياً. يقوم المُفتاح على اليمين بتشغيل القاعدة أو إيقافها دون حذفها.
## المتطلبات
<Warning>
يُشترط **خطة 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` كافية لعرضها.
- تُسجَّل جميع تغييرات القواعد بإصدارات للتدقيق.
## أنواع القواعد المتاحة
| النوع | ما تفعله |
|------|---------------|
| **PII Redaction** | تطبّق PII redaction على عمليات التنفيذ لكل أتمتة مطابِقة، باستخدام نفس كتالوج الكيانات و recognizers المخصصة الموثَّقة في [PII Redaction للـ Traces](/ar/enterprise/features/pii-trace-redactions). |
سيتم إضافة أنواع قواعد أخرى مع الوقت.
## إنشاء قاعدة
<Frame>
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="لوحة تعديل قاعدة جانبية بالشروط ونوع قناع PII" width="450" />
</Frame>
<Steps>
<Step title="افتح المحرر">
انقر على **+ Create new** في أعلى يمين تبويب Rules، أو على **View Details** في بطاقة قاعدة موجودة.
</Step>
<Step title="سَمِّ القاعدة وصِفها">
أعطِ القاعدة اسماً واضحاً (مثل *Mask PII (CC)*) ووصفاً يشرح متى تنطبق. يظهر كلاهما على بطاقة القاعدة وفي مودال Engaged Automations.
</Step>
<Step title="اختر النوع">
اليوم **PII Redaction** فقط متاحة.
</Step>
<Step title="حدّد الشروط">
تحدد الشروط الأتمتات التي تنخرط معها القاعدة. كلاهما اختياري ويستخدم دلالات **مساواة المجموعات (set-equality)**:
- **Tools** — تنخرط فقط الأتمتات التي تتطابق مجموعة أدواتها **تطابقاً تامّاً** مع الأدوات المختارة. اختر من تطبيقات Studio و MCPs والأدوات مفتوحة المصدر وأدوات سجل Tool Repository.
- **Automations** — تنخرط فقط الأتمتات التي تتطابق مجموعة وسومها **تطابقاً تامّاً** مع الوسوم المختارة.
ترك مُحدِّد فارغ يعني "بدون تصفية على هذا البعد". ترك كليهما فارغَين يعني أن القاعدة تنطبق على **كل** أتمتة في المؤسسة.
</Step>
<Step title="اضبط جدول PII Mask Type">
حدّد كل نوع كيان تريد تغطيته واختر **Mask** (يستبدل بتسمية الكيان مثل `<CREDIT_CARD>`) أو **Redact** (يحذف النص المطابِق بالكامل). راجع [PII Redaction للـ Traces](/ar/enterprise/features/pii-trace-redactions) للاطلاع على كتالوج الكيانات الكامل وكيفية إضافة recognizers مخصصة على مستوى المؤسسة.
</Step>
<Step title="احفظ">
تنطبق القاعدة على عمليات التنفيذ **المستقبلية** لكل أتمتة مُفعَّلة بمجرد الحفظ. لا حاجة لإعادة النشر.
</Step>
</Steps>
## الأتمتات المُفعَّلة
انقر على **Engaged N automations** في أي بطاقة قاعدة لرؤية أي عمليات النشر تطابقها القاعدة حالياً بالضبط، إلى جانب آخر تنفيذ لكل منها.
<Frame>
![مودال الأتمتات المُفعَّلة](/images/enterprise/acp-rules-engaged-modal.png)
</Frame>
هذه هي أسرع طريقة للتحقق من نطاق قاعدة قبل تمكينها — على سبيل المثال، للتأكد من أن قاعدة محدَّدة بنطاق وسم `production` لا تطابق عن طريق الخطأ deployment تجريبي.
## قواعد على مستوى المؤسسة مقابل إعدادات لكل deployment
يمكن ضبط PII Redaction في مكانين:
- **لكل deployment** — ضمن **Settings → PII Protection** على كل deployment على حدة ([الدليل](/ar/enterprise/features/pii-trace-redactions))
- **على مستوى المؤسسة** — كقاعدة في هذه الصفحة
عندما يتطابق نطاق قاعدة مُفعَّلة على مستوى المؤسسة مع deployment، يُجاوز تكوين الكيانات الخاص بالقاعدة **إعدادات PII المملوكة من قبل الـ deployment** لعمليات تنفيذ ذلك الـ deployment — تصبح القاعدة المصدر الوحيد للحقيقة طالما هي مرتبطة. عطّل القاعدة أو فُكَّ ارتباطها (أو غيِّر نطاقها بحيث لا تتطابق بعد الآن) ويعود الـ deployment إلى إعدادات PII Protection الخاصة به.
فضّل القواعد على مستوى المؤسسة عندما تريد فرض سياسة متسقة عبر العديد من عمليات النشر؛ احتفظ بالضبط لكل deployment للاستثناءات الفردية.
## ذو صلة
<CardGroup cols={2}>
<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="gauge" href="/ar/enterprise/features/agent-control-plane/monitoring">
راقب الأتمتات واستهلاك LLM عبر أسطولك.
</Card>
<Card title="PII Redaction للـ Traces" icon="lock" href="/ar/enterprise/features/pii-trace-redactions">
كتالوج الكيانات، 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

@@ -17,15 +17,62 @@ mode: "wide"
- حساب Salesforce بالصلاحيات المناسبة
- ربط حساب Salesforce الخاص بك عبر [صفحة التكاملات](https://app.crewai.com/integrations)
<Note>
يتطلب Salesforce **تثبيتًا واحدًا يقوم به مسؤول النظام (admin)** لحزمة
CrewAI في مؤسستك قبل أن يتمكن أي مستخدم من الاتصال. هذا متطلب من منصة
Salesforce لجميع التكاملات المعتمدة على ExternalClientApp اعتبارًا من
إصدار Spring '26 — وليس خطوة خاصة بـ CrewAI. تدليلك خطوة Connect
Salesforce في CrewAI AMP خلال هذه العملية عند المحاولة الأولى.
</Note>
## إعداد تكامل Salesforce
### 1. ربط حساب Salesforce الخاص بك
1. انتقل إلى [تكاملات CrewAI AMP](https://app.crewai.com/crewai_plus/connectors)
2. ابحث عن **Salesforce** في قسم تكاملات المصادقة
3. انقر على **Connect** وأكمل عملية OAuth
4. امنح الصلاحيات اللازمة لإدارة CRM والمبيعات
5. انسخ رمز المؤسسة من [إعدادات التكامل](https://app.crewai.com/crewai_plus/settings/integrations)
1. انتقل إلى [تكاملات CrewAI AMP](https://app.crewai.com/crewai_plus/unified_tools).
2. ابحث عن **Salesforce** في قسم تكاملات المصادقة.
3. انقر على **Connect**.
ما يحدث بعد ذلك يعتمد على ما إذا كان مسؤول Salesforce في مؤسستك قد ثبّت
حزمة CrewAI بالفعل:
- **الحزمة مثبتة بالفعل:** سيتم نقلك مباشرة إلى شاشة موافقة OAuth في
Salesforce — اعتمدها وسيكتمل الاتصال.
- **الحزمة غير مثبتة بعد:** سترى صفحة **Install CrewAI in Salesforce**.
اتبع خطوات التثبيت لمرة واحدة أدناه، ثم عُد إلى CrewAI AMP وانقر على
**Connect** مرة أخرى.
4. امنح الصلاحيات اللازمة لإدارة CRM والمبيعات.
5. انسخ رمز المؤسسة من [إعدادات التكامل](https://app.crewai.com/crewai_plus/settings/integrations).
#### تثبيت لمرة واحدة بواسطة المسؤول (مسؤول Salesforce فقط)
عند أول نقرة على **Connect Salesforce** من أي مستخدم في مؤسستك، تقوم CrewAI
بإعادة توجيهك إلى صفحة تثبيت تُشير إلى حزمة CrewAI المُدارة. يحتاج مسؤول
Salesforce إلى تثبيتها مرة واحدة فقط لكامل المؤسسة.
1. في صفحة التثبيت داخل CrewAI، انقر على **Install in Salesforce**. (يمكنك
أيضًا مشاركة عنوان URL لتلك الصفحة مع المسؤول — رابط التثبيت يعمل لأي
شخص يفتحه.)
2. سجّل الدخول إلى Salesforce بصلاحيات مسؤول. لبيئات Sandbox، استبدل
`login.salesforce.com` بـ `test.salesforce.com` في الرابط قبل فتحه.
3. اختر **Install for All Users**، ووافق على إشعار تطبيقات الجهات
الخارجية، ثم انقر **Install**.
4. من Setup في Salesforce، ابحث عن **External Client App Manager** ←
**CrewAI App** ← افتح علامة التبويب **Policies** ← **Edit**، واضبط
القيم التالية:
- **Permitted Users:** All users may self-authorize
- **IP Relaxation:** Relax IP restrictions
- **Refresh Token Policy:** Refresh token is valid until revoked
5. احفظ التغييرات.
6. عُد إلى CrewAI AMP وانقر على **Connect Salesforce** مرة أخرى. سيكتمل
OAuth هذه المرة.
<Note>
**لست مسؤول Salesforce؟** أعِد توجيه عنوان URL لصفحة التثبيت (أو رابط
التثبيت نفسه) إلى مسؤول Salesforce لديكم واطلب منه إكمال الخطوات أعلاه.
بمجرد انتهائه، عُد إلى CrewAI AMP وانقر على **Connect** مرة أخرى.
</Note>
### 2. تثبيت الحزمة المطلوبة

View File

@@ -2384,6 +2384,15 @@
{
"group": "Manage",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"en/enterprise/features/agent-control-plane/overview",
"en/enterprise/features/agent-control-plane/monitoring",
"en/enterprise/features/agent-control-plane/rules"
]
},
"en/enterprise/features/sso",
"en/enterprise/features/rbac"
]
@@ -2868,6 +2877,15 @@
{
"group": "Manage",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"en/enterprise/features/agent-control-plane/overview",
"en/enterprise/features/agent-control-plane/monitoring",
"en/enterprise/features/agent-control-plane/rules"
]
},
"en/enterprise/features/sso",
"en/enterprise/features/rbac"
]
@@ -3352,6 +3370,15 @@
{
"group": "Manage",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"en/enterprise/features/agent-control-plane/overview",
"en/enterprise/features/agent-control-plane/monitoring",
"en/enterprise/features/agent-control-plane/rules"
]
},
"en/enterprise/features/sso",
"en/enterprise/features/rbac"
]
@@ -3836,6 +3863,15 @@
{
"group": "Manage",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"en/enterprise/features/agent-control-plane/overview",
"en/enterprise/features/agent-control-plane/monitoring",
"en/enterprise/features/agent-control-plane/rules"
]
},
"en/enterprise/features/sso",
"en/enterprise/features/rbac"
]
@@ -9101,6 +9137,15 @@
{
"group": "Gerenciar",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"pt-BR/enterprise/features/agent-control-plane/overview",
"pt-BR/enterprise/features/agent-control-plane/monitoring",
"pt-BR/enterprise/features/agent-control-plane/rules"
]
},
"pt-BR/enterprise/features/rbac"
]
},
@@ -9562,6 +9607,15 @@
{
"group": "Gerenciar",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"pt-BR/enterprise/features/agent-control-plane/overview",
"pt-BR/enterprise/features/agent-control-plane/monitoring",
"pt-BR/enterprise/features/agent-control-plane/rules"
]
},
"pt-BR/enterprise/features/rbac"
]
},
@@ -10023,6 +10077,15 @@
{
"group": "Gerenciar",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"pt-BR/enterprise/features/agent-control-plane/overview",
"pt-BR/enterprise/features/agent-control-plane/monitoring",
"pt-BR/enterprise/features/agent-control-plane/rules"
]
},
"pt-BR/enterprise/features/rbac"
]
},
@@ -10484,6 +10547,15 @@
{
"group": "Gerenciar",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"pt-BR/enterprise/features/agent-control-plane/overview",
"pt-BR/enterprise/features/agent-control-plane/monitoring",
"pt-BR/enterprise/features/agent-control-plane/rules"
]
},
"pt-BR/enterprise/features/rbac"
]
},
@@ -15671,6 +15743,15 @@
{
"group": "관리",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ko/enterprise/features/agent-control-plane/overview",
"ko/enterprise/features/agent-control-plane/monitoring",
"ko/enterprise/features/agent-control-plane/rules"
]
},
"ko/enterprise/features/rbac"
]
},
@@ -16145,6 +16226,15 @@
{
"group": "관리",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ko/enterprise/features/agent-control-plane/overview",
"ko/enterprise/features/agent-control-plane/monitoring",
"ko/enterprise/features/agent-control-plane/rules"
]
},
"ko/enterprise/features/rbac"
]
},
@@ -16619,6 +16709,15 @@
{
"group": "관리",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ko/enterprise/features/agent-control-plane/overview",
"ko/enterprise/features/agent-control-plane/monitoring",
"ko/enterprise/features/agent-control-plane/rules"
]
},
"ko/enterprise/features/rbac"
]
},
@@ -17093,6 +17192,15 @@
{
"group": "관리",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ko/enterprise/features/agent-control-plane/overview",
"ko/enterprise/features/agent-control-plane/monitoring",
"ko/enterprise/features/agent-control-plane/rules"
]
},
"ko/enterprise/features/rbac"
]
},
@@ -22358,6 +22466,15 @@
{
"group": "الإدارة",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ar/enterprise/features/agent-control-plane/overview",
"ar/enterprise/features/agent-control-plane/monitoring",
"ar/enterprise/features/agent-control-plane/rules"
]
},
"ar/enterprise/features/rbac"
]
},
@@ -22832,6 +22949,15 @@
{
"group": "الإدارة",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ar/enterprise/features/agent-control-plane/overview",
"ar/enterprise/features/agent-control-plane/monitoring",
"ar/enterprise/features/agent-control-plane/rules"
]
},
"ar/enterprise/features/rbac"
]
},
@@ -23306,6 +23432,15 @@
{
"group": "الإدارة",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ar/enterprise/features/agent-control-plane/overview",
"ar/enterprise/features/agent-control-plane/monitoring",
"ar/enterprise/features/agent-control-plane/rules"
]
},
"ar/enterprise/features/rbac"
]
},
@@ -23780,6 +23915,15 @@
{
"group": "الإدارة",
"pages": [
{
"group": "Agent Control Plane",
"icon": "gauge",
"pages": [
"ar/enterprise/features/agent-control-plane/overview",
"ar/enterprise/features/agent-control-plane/monitoring",
"ar/enterprise/features/agent-control-plane/rules"
]
},
"ar/enterprise/features/rbac"
]
},

View File

@@ -0,0 +1,104 @@
---
title: "Watch your Automations"
description: "Watch fleet health, LLM consumption, and per-automation behavior from the Automations tab."
sidebarTitle: "Monitoring"
icon: "gauge"
mode: "wide"
---
## Overview
The **Automations** tab is the read-only operations view of the [Agent Control Plane](/en/enterprise/features/agent-control-plane/overview). It combines two metric cards, an interactive sankey, and two sub-tables — **Automations** and **Consumption** — that you can search, filter, and sort.
<Frame>
![Agent Control Plane overview](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
All charts and tables respect the **Last 24 hours / Last Week / Last 30 days** selector at the top right. Deltas compare the selected window against the previous one of the same length.
<Note>
Rows only show data for deployments on **crewAI v1.13 or higher** — older deployments appear in the *"We've detected N other automations that we can't display"* banner under the sankey and contribute zero metrics until they're updated and re-deployed. See [Overview — Requirements](/en/enterprise/features/agent-control-plane/overview#requirements).
</Note>
## Dashboard
The header of the page has two metric cards and an interactive sankey. Clicking either card switches the sankey between two modes:
- **Health mode** — `Total Automations → status buckets (Critical / Warning / Healthy)`. Click a bucket to filter the Automations table to just those deployments.
- **Consumption mode** — `Model Providers → Automations → Total Cost`. Click a provider to filter the Consumption table to that provider.
| Card | What it shows |
|------|---------------|
| **Automations** | `active` automations (and total count), total `errors` in the window, currently `active executions` (and total in the window), with a delta vs the previous period. |
| **Consumption** | Total `cost` and `tokens used`, with a cost delta vs the previous period. |
<Frame>
![Overview with consumption sankey](/images/enterprise/acp-overview-consumption-sankey.png)
</Frame>
## Automations table
The **Automations** sub-tab is the per-deployment breakdown of fleet health. Each row is one deployed crew or flow.
<Frame>
![Automations table with health status breakdown](/images/enterprise/acp-automations-table.png)
</Frame>
| Column | What it shows |
|--------|---------------|
| **Automation** | Deployment name and any tags assigned to it (e.g. `production`, `financial`). |
| **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 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. |
Search by name, filter by `Status` (`Healthy` / `Warning` / `Critical`), and sort by any column header. Click a deployment name to open the **Automation panel** (see below).
## Consumption table
The **Consumption** sub-tab is the per-deployment breakdown of LLM spend and token usage.
<Frame>
![Consumption table broken down by LLM provider](/images/enterprise/acp-consumption-table.png)
</Frame>
| Column | What it shows |
|--------|---------------|
| **Automation** | Deployment name. |
| **Last execution** | Time since the most recent run. |
| **Tokens used** | One row per LLM provider used by this automation, with the delta vs the previous period. |
| **Cost** | Cost per LLM provider, with the delta vs the previous period. |
| **Total cost** | Sum across all providers, with the delta. |
| **Executions** | Total executions in the window. |
| **Last updated** | When the deployment was last re-deployed. |
| **Crew Version** | The `crewai` version reported by the deployment. |
Filter by **LLM provider** and sort by `Cost`, `Executions`, or `Last run`.
<Info>
**Empty cells (`—` or `$0.00`) usually mean the deployment is below crewAI v1.13.** In the screenshot above, *Automation F* (`1.7.0`) and *Automation I* (`1.12.2`) show blanks for tokens and cost — their executions still run, but they don't emit the provider-level telemetry that powers this table. Update and re-deploy these crews to start collecting consumption data.
</Info>
## Related
<CardGroup cols={2}>
<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 — 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.
</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.
</Card>
</CardGroup>
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team for help interpreting metrics in the Agent Control Plane.
</Card>

View File

@@ -0,0 +1,74 @@
---
title: Agent Control Plane Overview
description: "Single operations hub for live automations — fleet health, LLM consumption, and organization-wide policies in one place."
sidebarTitle: Overview
icon: "book-open"
---
## 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 **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 **Rules** (today: PII Redaction) across many automations at once instead of editing each deployment individually.
<Frame>
![Agent Control Plane overview](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
<Note>
The Agent Control Plane is currently labeled **Beta** in CrewAI Platform.
</Note>
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).
- **Rules** — *"How do I enforce a policy (e.g. PII redaction) across many deployments without re-deploying each one?"* See [Rules](/en/enterprise/features/agent-control-plane/rules).
## Requirements
<Warning>
**crewAI v1.13 or higher** is required for an automation to populate any data on this page — health, executions, errors, tokens, and cost all flow through telemetry that lit up in `crewai==1.13`. Older deployments appear in the *"We've detected N other automations that we can't display"* banner and contribute zero rows until they are updated and re-deployed.
</Warning>
<Warning>
**Enterprise Plan or Ultra Plan** is required to create or edit [Rules](/en/enterprise/features/agent-control-plane/rules). Lower-tier organizations can open the Rules tab and view existing rules, but the editor renders read-only with an "Enterprise" lock pill and the alert *"PII Redaction rules require an Enterprise plan."* Monitoring (the Automations tab) is 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 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
<CardGroup cols={2}>
<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="Rules" icon="shield-check" href="/en/enterprise/features/agent-control-plane/rules">
Apply organization-wide PII Redaction policies scoped by tools and tags. Changes take effect on the next execution — no re-deploy required.
</Card>
</CardGroup>
## Related
<CardGroup cols={2}>
<Card title="Traces" icon="timeline" href="/en/enterprise/features/traces">
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 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 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.
</Card>
</CardGroup>
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team for help interpreting metrics or designing rules.
</Card>

View File

@@ -0,0 +1,114 @@
---
title: "Set up the Rules"
description: "Apply organization-wide policies across many automations from a single place."
sidebarTitle: "Rules"
icon: "shield-check"
mode: "wide"
---
## Overview
Rules let you apply policies — today: **PII Redaction** — 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>
![Rules list](/images/enterprise/acp-rules-list.png)
</Frame>
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 rules. Lower-tier organizations can still open the Rules tab and view existing rules, but the 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.
</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 rules. The `read` permission is enough to view them.
- All rule changes are versioned for auditing.
## Available rule types
| Type | What it does |
|------|---------------|
| **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). |
More rule types will be added over time.
## Creating a rule
<Frame>
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="Rule edit side panel with conditions and PII mask type" width="450" />
</Frame>
<Steps>
<Step title="Open the editor">
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 rule">
Give the rule a clear name (e.g. *Mask PII (CC)*) 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">
Today only **PII Redaction** is available.
</Step>
<Step title="Set the conditions">
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 rule applies to **every** automation in the organization.
</Step>
<Step title="Configure the PII Mask Type table">
Check each entity type you want covered and choose **Mask** (replaces with the entity label, e.g. `<CREDIT_CARD>`) or **Redact** (removes the matched text entirely). See [PII Redaction for Traces](/en/enterprise/features/pii-trace-redactions) for the full entity catalog and how to add organization-level custom recognizers.
</Step>
<Step title="Save">
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 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-rules-engaged-modal.png)
</Frame>
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 rules vs per-deployment settings
PII Redaction can be configured in two places:
- **Per-deployment** — under **Settings → PII Protection** on each individual deployment ([guide](/en/enterprise/features/pii-trace-redactions))
- **Org-wide** — as a Rule on this page
When an enabled org-wide rule's scope matches a deployment, the rule's entity configuration **overrides** the deployment-owned PII settings for that deployment's executions — the rule becomes the single source of truth while it's attached. Disable or detach the rule (or change its scope so it no longer matches) and the deployment falls back to its own PII Protection settings.
Prefer org-wide rules when you want to enforce a consistent policy across many deployments; reserve per-deployment configuration for one-off exceptions.
## Related
<CardGroup cols={2}>
<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 — Monitoring" icon="gauge" href="/en/enterprise/features/agent-control-plane/monitoring">
Monitor automations and LLM consumption across your fleet.
</Card>
<Card title="PII Redaction for Traces" icon="lock" href="/en/enterprise/features/pii-trace-redactions">
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 rules.
</Card>
</CardGroup>
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team for help designing rules for your organization.
</Card>

View File

@@ -17,15 +17,61 @@ Before using the Salesforce integration, ensure you have:
- A Salesforce account with appropriate permissions
- Connected your Salesforce account through the [Integrations page](https://app.crewai.com/integrations)
<Note>
Salesforce requires a **one-time admin install** of the CrewAI package in
your org before any user can connect. This is a Salesforce platform
requirement for all ExternalClientApp-based integrations as of the Spring
'26 release — not a CrewAI-specific step. The Connect Salesforce flow in
CrewAI AMP walks you through it the first time.
</Note>
## Setting Up Salesforce Integration
### 1. Connect Your Salesforce Account
1. Navigate to [CrewAI AMP Integrations](https://app.crewai.com/crewai_plus/connectors)
2. Find **Salesforce** in the Authentication Integrations section
3. Click **Connect** and complete the OAuth flow
4. Grant the necessary permissions for CRM and sales management
5. Copy your Enterprise Token from [Integration Settings](https://app.crewai.com/crewai_plus/settings/integrations)
1. Navigate to [CrewAI AMP Integrations](https://app.crewai.com/crewai_plus/unified_tools).
2. Find **Salesforce** in the Authentication Integrations section.
3. Click **Connect**.
What happens next depends on whether a Salesforce admin in your org has
already installed the CrewAI package:
- **Package already installed:** you're taken straight to the Salesforce
OAuth consent screen — approve it and you're connected.
- **Package not installed yet:** you'll see an **Install CrewAI in
Salesforce** page. Follow the one-time install steps below, then come
back to CrewAI AMP and click **Connect** again.
4. Grant the necessary permissions for CRM and sales management.
5. Copy your Enterprise Token from [Integration Settings](https://app.crewai.com/crewai_plus/settings/integrations).
#### One-time admin install (Salesforce admin only)
The first time anyone in your org clicks **Connect Salesforce**, CrewAI
redirects them to an install page that points at the CrewAI managed package.
A Salesforce admin needs to install it once for the whole org.
1. On the install page in CrewAI, click **Install in Salesforce**. (You can
also share the page URL with your admin — the install link works for
anyone who opens it.)
2. Sign in to Salesforce as an admin. For sandboxes, swap `login.salesforce.com`
for `test.salesforce.com` in the URL before opening it.
3. Choose **Install for All Users**, acknowledge the third-party app prompt,
and click **Install**.
4. In Salesforce Setup, search **External Client App Manager** → **CrewAI
App** → open the **Policies** tab → **Edit** and set:
- **Permitted Users:** All users may self-authorize
- **IP Relaxation:** Relax IP restrictions
- **Refresh Token Policy:** Refresh token is valid until revoked
5. Save.
6. Return to CrewAI AMP and click **Connect Salesforce** again. OAuth will
complete this time.
<Note>
**Not a Salesforce admin?** Forward the install page URL (or the install
link itself) to your Salesforce admin and ask them to complete the steps
above. Once they're done, return to CrewAI AMP and click **Connect** again.
</Note>
### 2. Install Required Package

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -0,0 +1,104 @@
---
title: "자동화 살펴보기"
description: "Automations 탭에서 플릿 상태, LLM 소비, 자동화별 동작을 확인합니다."
sidebarTitle: "모니터링"
icon: "gauge"
mode: "wide"
---
## 개요
**Automations** 탭은 [Agent Control Plane](/ko/enterprise/features/agent-control-plane/overview)의 읽기 전용 운영 뷰입니다. 두 개의 메트릭 카드, 인터랙티브 sankey, 그리고 **Automations**와 **Consumption** 두 개의 서브 테이블을 결합해 검색·필터·정렬을 지원합니다.
<Frame>
![Agent Control Plane 개요](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
모든 차트와 테이블은 오른쪽 상단의 **지난 24시간 / 지난 1주 / 지난 30일** 선택기를 따릅니다. 변화량은 선택한 윈도우를 같은 길이의 이전 윈도우와 비교합니다.
<Note>
행은 **crewAI v1.13 이상**의 deployment에 대해서만 데이터를 표시합니다. 그 이전 deployment는 sankey 아래 *"We've detected N other automations that we can't display"* 배너에 나타나며, 업데이트하고 재배포할 때까지 어떤 메트릭에도 기여하지 않습니다. [개요 — 요구사항](/ko/enterprise/features/agent-control-plane/overview#요구사항)을 참고하세요.
</Note>
## 대시보드
페이지 상단에는 두 개의 메트릭 카드와 인터랙티브 sankey가 있습니다. 어느 카드를 클릭해도 sankey가 다음 두 모드 사이를 전환합니다:
- **상태 모드** — `전체 자동화 → 상태 버킷(Critical / Warning / Healthy)`. 버킷을 클릭하면 Automations 테이블이 해당 deployment로 필터링됩니다.
- **소비 모드** — `모델 공급자 → 자동화 → 총 비용`. 공급자를 클릭하면 Consumption 테이블이 해당 공급자로 필터링됩니다.
| 카드 | 표시 내용 |
|------|---------------|
| **Automations** | `active` 자동화(및 전체 개수), 윈도우 내 총 `errors`, 현재 `active executions`(및 윈도우 내 총합), 이전 기간 대비 변화량. |
| **Consumption** | 총 `cost`와 `tokens used`, 이전 기간 대비 비용 변화량. |
<Frame>
![소비 sankey가 적용된 개요](/images/enterprise/acp-overview-consumption-sankey.png)
</Frame>
## Automations 테이블
**Automations** 서브 탭은 deployment 단위의 플릿 상태 분해입니다. 각 행은 배포된 하나의 crew 또는 flow입니다.
<Frame>
![상태 분해가 있는 자동화 테이블](/images/enterprise/acp-automations-table.png)
</Frame>
| 열 | 표시 내용 |
|--------|---------------|
| **Automation** | deployment 이름과 할당된 태그(예: `production`, `financial`). |
| **Last execution** | 가장 최근 실행 이후 경과 시간. |
| **Health Status Breakdown** | 윈도우 내 실행에 대한 `Critical` / `Warning` / `Healthy` 비율의 누적 막대. |
| **Executions with Errors** | 윈도우 내 총 실패 실행 수. |
| **PII detection applied** | deployment별 PII 설정 또는 일치하는 [PII 규칙](/ko/enterprise/features/agent-control-plane/rules)이 활성화된 경우 `Yes`. |
| **Executions** | 윈도우 내 총 실행 수. |
| **Last updated** | deployment의 마지막 재배포 시점. |
| **Crew Version** | deployment가 보고한 `crewai` 버전. `1.13` 미만 버전 옆의 정보 아이콘은 메트릭에 기여할 수 없는 행을 표시합니다. |
이름으로 검색하고 `Status`(`Healthy` / `Warning` / `Critical`)로 필터링하며, 컬럼 헤더로 정렬할 수 있습니다. deployment 이름을 클릭하면 **Automation 패널**이 열립니다.
## Consumption 테이블
**Consumption** 서브 탭은 deployment 단위의 LLM 지출 및 토큰 사용량 분해입니다.
<Frame>
![LLM 공급자별로 분해된 소비 테이블](/images/enterprise/acp-consumption-table.png)
</Frame>
| 열 | 표시 내용 |
|--------|---------------|
| **Automation** | deployment 이름. |
| **Last execution** | 가장 최근 실행 이후 경과 시간. |
| **Tokens used** | 이 자동화가 사용한 LLM 공급자별로 한 행, 이전 기간 대비 변화량 포함. |
| **Cost** | LLM 공급자별 비용, 이전 기간 대비 변화량 포함. |
| **Total cost** | 모든 공급자의 합계, 변화량 포함. |
| **Executions** | 윈도우 내 총 실행 수. |
| **Last updated** | deployment의 마지막 재배포 시점. |
| **Crew Version** | deployment가 보고한 `crewai` 버전. |
**LLM provider**로 필터링하고 `Cost`, `Executions` 또는 `Last run`으로 정렬할 수 있습니다.
<Info>
**빈 셀(`—` 또는 `$0.00`)은 보통 deployment가 crewAI v1.13 미만임을 의미합니다.** 위 스크린샷에서 *Automation F*(`1.7.0`)와 *Automation I*(`1.12.2`)는 토큰과 비용이 비어 있습니다. 실행은 여전히 동작하지만, 이 테이블을 채우는 공급자 수준 텔레메트리를 emit하지 않습니다. 이 crew들을 업데이트하고 재배포하면 소비 데이터가 수집되기 시작합니다.
</Info>
## 관련 문서
<CardGroup cols={2}>
<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="/ko/enterprise/features/agent-control-plane/rules">
조직 단위 PII Redaction 규칙을 여러 자동화에 적용합니다.
</Card>
<Card title="Traces" icon="timeline" href="/ko/enterprise/features/traces">
개별 실행을 드릴다운하여 에이전트의 추론, 도구 호출, 토큰 사용량을 확인합니다.
</Card>
<Card title="AMP에 배포" icon="rocket" href="/ko/enterprise/guides/deploy-to-amp">
Agent Control Plane을 지원하는 crewAI 버전으로 crew를 배포합니다.
</Card>
</CardGroup>
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
Agent Control Plane에서 메트릭을 해석하는 데 도움이 필요하시면 지원 팀에 문의하세요.
</Card>

View File

@@ -0,0 +1,74 @@
---
title: Agent Control Plane 개요
description: "라이브 자동화의 통합 운영 허브 — 플릿 상태, LLM 소비, 조직 단위 정책을 한 화면에서."
sidebarTitle: 개요
icon: "book-open"
---
## 개요
**Agent Control Plane**(ACP)은 CrewAI AMP에서 실행 중인 모든 워크로드를 위한 운영 허브입니다. **Automations**와 **Rules** 두 개의 탭으로 구성된 단일 화면에서 다음 작업을 할 수 있습니다:
- 모든 라이브 자동화(crew 또는 flow)의 **상태(health)**를 `Critical` / `Warning` / `Healthy` 분포와 실행 횟수로 모니터링합니다.
- 자동화별·공급자별·모델별 **LLM 소비**(토큰 및 비용)를 추적하고, 이전 기간 대비 변화량을 확인합니다.
- 임의의 자동화 또는 모델 공급자를 드릴다운하여 시계열 차트와 공급자별 분해를 살펴봅니다.
- 조직 전체에 **규칙(Rules)**(현재: PII Redaction)을 적용하여 각 deployment를 개별 편집하지 않고 한 번에 여러 자동화에 정책을 강제합니다.
<Frame>
![Agent Control Plane 개요](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
<Note>
Agent Control Plane은 현재 CrewAI Platform에서 **Beta**로 표시되어 있습니다.
</Note>
두 탭은 서로 다른 두 가지 질문에 답합니다:
- **Automations** — *"지금 내 플릿은 어떻게 동작하고 있고, 얼마나 비용이 들고 있는가?"* [모니터링](/ko/enterprise/features/agent-control-plane/monitoring)을 참고하세요.
- **Rules** — *"정책(예: PII redaction)을 매번 재배포하지 않고 여러 deployment에 어떻게 강제할 수 있는가?"* [규칙](/ko/enterprise/features/agent-control-plane/rules)을 참고하세요.
## 요구사항
<Warning>
이 페이지에서 자동화에 대한 데이터(상태, 실행, 오류, 토큰, 비용)를 채우려면 **crewAI v1.13 이상**이 필요합니다. 모든 데이터는 `crewai==1.13`에서 활성화된 텔레메트리를 통해 흘러갑니다. 그 이전 deployment는 *"We've detected N other automations that we can't display"* 배너에 나타나며, 업데이트하고 재배포할 때까지 어떤 행에도 데이터를 기여하지 않습니다.
</Warning>
<Warning>
[규칙](/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` 권한이 필요합니다.
- 모든 차트와 테이블은 오른쪽 상단의 시간 선택기를 통해 **지난 24시간**, **지난 1주**, **지난 30일**로 범위를 조정할 수 있습니다. 변화량(`↑ 8 vs yesterday`, `↓ $20.57 vs yesterday` 등)은 선택한 윈도우를 같은 길이의 이전 윈도우와 비교합니다.
## 여기에서 할 수 있는 일
<CardGroup cols={2}>
<Card title="모니터링" icon="gauge" href="/ko/enterprise/features/agent-control-plane/monitoring">
메트릭 카드, 인터랙티브 sankey, 자동화별 테이블, 자동화 또는 공급자별 드릴다운 사이드 패널로 플릿 상태와 LLM 지출을 살펴봅니다.
</Card>
<Card title="규칙" icon="shield-check" href="/ko/enterprise/features/agent-control-plane/rules">
도구와 태그로 범위를 지정한 PII Redaction 정책을 조직 단위로 적용합니다. 변경 사항은 다음 실행부터 적용되며 재배포가 필요 없습니다.
</Card>
</CardGroup>
## 관련 문서
<CardGroup cols={2}>
<Card title="Traces" icon="timeline" href="/ko/enterprise/features/traces">
개별 실행을 드릴다운하여 에이전트의 추론, 도구 호출, 토큰 사용량을 확인합니다.
</Card>
<Card title="RBAC" icon="users" href="/ko/enterprise/features/rbac">
누가 Agent Control Plane을 읽을 수 있고 누가 규칙을 편집할 수 있는지 관리합니다.
</Card>
<Card title="Traces용 PII Redaction" icon="lock" href="/ko/enterprise/features/pii-trace-redactions">
규칙이 참조하는 엔티티 카탈로그 및 deployment 단위 PII 설정.
</Card>
<Card title="AMP에 배포" icon="rocket" href="/ko/enterprise/guides/deploy-to-amp">
Agent Control Plane을 지원하는 crewAI 버전으로 crew를 배포합니다.
</Card>
</CardGroup>
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
메트릭 해석 또는 규칙 설계에 도움이 필요하시면 지원 팀에 문의하세요.
</Card>

View File

@@ -0,0 +1,114 @@
---
title: "규칙 설정하기"
description: "한 곳에서 조직 단위 정책을 여러 자동화에 적용합니다."
sidebarTitle: "규칙"
icon: "shield-check"
mode: "wide"
---
## 개요
규칙(Rules)은 각 deployment를 개별 설정하는 대신, 정책 — 현재: **PII Redaction** — 을 한 번에 여러 자동화에 적용할 수 있게 해줍니다. 관리하려면 [Agent Control Plane](/ko/enterprise/features/agent-control-plane/overview)에서 **Rules** 탭을 엽니다.
<Frame>
![규칙 목록](/images/enterprise/acp-rules-list.png)
</Frame>
각 규칙 카드에는 이름, 설명, 규칙이 적용되는 **범위(scope)**(선택된 도구와 태그), 그리고 현재 범위와 일치하는 deployment의 수인 **engaged automations**가 표시됩니다. 오른쪽 토글로 규칙을 삭제하지 않고 활성/비활성할 수 있습니다.
## 요구사항
<Warning>
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` 권한만으로 충분합니다.
- 모든 규칙 변경은 감사를 위해 버전 관리됩니다.
## 사용 가능한 규칙 유형
| 유형 | 동작 |
|------|---------------|
| **PII Redaction** | 일치하는 모든 자동화의 실행에 PII redaction을 적용합니다. [Traces용 PII Redaction](/ko/enterprise/features/pii-trace-redactions)에 문서화된 동일한 엔티티 카탈로그와 커스텀 recognizer를 사용합니다. |
향후 더 많은 규칙 유형이 추가될 예정입니다.
## 규칙 만들기
<Frame>
<img src="/images/enterprise/acp-rules-edit-side-panel.png" alt="조건 및 PII 마스크 유형이 있는 규칙 편집 사이드 패널" width="450" />
</Frame>
<Steps>
<Step title="편집기 열기">
Rules 탭 오른쪽 상단의 **+ Create new**를 클릭하거나, 기존 규칙 카드의 **View Details**를 클릭합니다.
</Step>
<Step title="규칙 이름과 설명 작성">
규칙에 명확한 이름(예: *Mask PII (CC)*)과 적용 시점을 설명하는 description을 부여합니다. 둘 다 규칙 카드와 Engaged Automations 모달에 표시됩니다.
</Step>
<Step title="유형 선택">
현재 **PII Redaction**만 사용할 수 있습니다.
</Step>
<Step title="조건 설정">
조건은 규칙이 어떤 자동화에 engage 할지 결정합니다. 둘 다 선택 사항이며 **집합 동일성(set-equality)** 의미론을 사용합니다:
- **Tools** — 도구 집합이 선택된 도구와 **정확히 일치**하는 자동화만 engage 됩니다. Studio 앱, MCP, OSS 도구, Tool Repository registry 도구 중에서 선택합니다.
- **Automations** — 태그 집합이 선택된 태그와 **정확히 일치**하는 자동화만 engage 됩니다.
피커를 비워두면 "이 차원에서 필터링하지 않음"을 의미합니다. 두 피커를 모두 비워두면 규칙이 조직의 **모든** 자동화에 적용됩니다.
</Step>
<Step title="PII Mask Type 테이블 구성">
적용할 각 엔티티 유형을 체크하고 **Mask**(엔티티 레이블로 치환, 예: `<CREDIT_CARD>`) 또는 **Redact**(일치하는 텍스트를 완전히 제거) 중에서 선택합니다. 전체 엔티티 카탈로그와 조직 단위 커스텀 recognizer 추가 방법은 [Traces용 PII Redaction](/ko/enterprise/features/pii-trace-redactions)을 참고하세요.
</Step>
<Step title="저장">
저장하는 즉시 engage 된 모든 자동화의 **향후** 실행에 규칙이 적용됩니다. 재배포는 필요 없습니다.
</Step>
</Steps>
## Engaged automations
규칙 카드의 **Engaged N automations**를 클릭하면 현재 규칙이 일치시키고 있는 deployment와 각 deployment의 마지막 실행을 정확히 확인할 수 있습니다.
<Frame>
![Engaged automations 모달](/images/enterprise/acp-rules-engaged-modal.png)
</Frame>
규칙을 활성화하기 전에 범위를 빠르게 점검하는 가장 좋은 방법입니다. 예를 들어, `production` 태그로 범위를 지정한 규칙이 의도치 않게 staging 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 단위 설정을 사용하세요.
## 관련 문서
<CardGroup cols={2}>
<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="gauge" href="/ko/enterprise/features/agent-control-plane/monitoring">
플릿 전반의 자동화와 LLM 소비를 모니터링합니다.
</Card>
<Card title="Traces용 PII Redaction" icon="lock" href="/ko/enterprise/features/pii-trace-redactions">
엔티티 카탈로그, 커스텀 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

@@ -17,16 +17,60 @@ Salesforce 통합을 사용하기 전에 다음을 확인하세요:
- 적절한 권한이 있는 Salesforce 계정
- [통합 페이지](https://app.crewai.com/integrations)를 통해 Salesforce 계정 연결
<Note>
Salesforce는 사용자가 연결하기 전에 **관리자가 CrewAI 패키지를 한 번 설치**
해야 합니다. 이는 Spring '26 릴리스부터 모든 ExternalClientApp 기반 통합에
적용되는 Salesforce 플랫폼의 요구 사항이며, CrewAI 고유의 단계가 아닙니다.
CrewAI AMP의 Connect Salesforce 플로우가 첫 연결 시 이 과정을 안내합니다.
</Note>
## Salesforce 통합 설정
### 1. Salesforce 계정 연결
1. [CrewAI AMP 통합](https://app.crewai.com/crewai_plus/connectors)으로 이동합니다.
1. [CrewAI AMP 통합](https://app.crewai.com/crewai_plus/unified_tools)으로 이동합니다.
2. 인증 통합 섹션에서 **Salesforce**를 찾습니다.
3. **연결**을 클릭하고 OAuth 과정을 완료합니다.
3. **연결**을 클릭합니다.
이후 동작은 관리자가 조직에 CrewAI 패키지를 이미 설치했는지에 따라 달라집니다:
- **패키지가 이미 설치된 경우:** 곧바로 Salesforce OAuth 동의 화면으로
이동합니다. 승인하면 연결이 완료됩니다.
- **패키지가 아직 설치되지 않은 경우:** **Install CrewAI in Salesforce**
페이지가 표시됩니다. 아래의 일회성 설치 단계를 따른 뒤, CrewAI AMP로
돌아와 **연결**을 다시 클릭하세요.
4. CRM 및 영업 관리에 필요한 권한을 부여합니다.
5. [통합 설정](https://app.crewai.com/crewai_plus/settings/integrations)에서 Enterprise Token을 복사합니다.
#### 일회성 관리자 설치 (Salesforce 관리자 전용)
조직 내 누군가 **Connect Salesforce**를 처음 클릭하면, CrewAI는 CrewAI
관리형 패키지의 설치 페이지로 리디렉션합니다. Salesforce 관리자가 조직
전체를 위해 한 번만 설치하면 됩니다.
1. CrewAI 내 설치 페이지에서 **Install in Salesforce**를 클릭합니다.
(해당 페이지 URL을 관리자에게 공유해도 됩니다. 설치 링크는 누구든 열 수
있도록 동작합니다.)
2. 관리자 권한으로 Salesforce에 로그인합니다. 샌드박스 환경에서는 URL의
`login.salesforce.com`을 `test.salesforce.com`으로 바꾼 뒤 엽니다.
3. **Install for All Users**를 선택하고, 서드파티 앱 동의 항목을 확인한 뒤
**Install**을 클릭합니다.
4. Salesforce Setup에서 **External Client App Manager** → **CrewAI App** →
**Policies** 탭 → **Edit**로 이동하여 다음과 같이 설정합니다:
- **Permitted Users:** All users may self-authorize
- **IP Relaxation:** Relax IP restrictions
- **Refresh Token Policy:** Refresh token is valid until revoked
5. 저장합니다.
6. CrewAI AMP로 돌아가 **Connect Salesforce**를 다시 클릭합니다. 이번에는
OAuth가 정상적으로 완료됩니다.
<Note>
**Salesforce 관리자가 아니신가요?** 설치 페이지의 URL(또는 설치 링크 자체)
을 Salesforce 관리자에게 전달하고 위 단계를 진행해 달라고 요청하세요.
관리자가 완료하면 CrewAI AMP로 돌아와 **연결**을 다시 클릭하면 됩니다.
</Note>
### 2. 필수 패키지 설치
```bash

View File

@@ -0,0 +1,104 @@
---
title: "Observe suas Automações"
description: "Acompanhe a saúde da frota, o consumo de LLM e o comportamento por automação a partir da aba Automações."
sidebarTitle: "Monitoramento"
icon: "gauge"
mode: "wide"
---
## Visão Geral
A aba **Automações** é a visão de operações somente leitura do [Agent Control Plane](/pt-BR/enterprise/features/agent-control-plane/overview). Ela combina dois cards de métricas, um sankey interativo e duas sub-tabelas — **Automações** e **Consumo** — nas quais você pode buscar, filtrar e ordenar.
<Frame>
![Visão geral do Agent Control Plane](/images/enterprise/acp-overview-automations-sankey.png)
</Frame>
Todos os gráficos e tabelas respeitam o seletor **Últimas 24 horas / Última Semana / Últimos 30 dias** no canto superior direito. As variações comparam a janela selecionada com a janela anterior de mesma duração.
<Note>
As linhas só mostram dados de deployments em **crewAI v1.13 ou superior** — deployments mais antigos aparecem no banner *"We've detected N other automations that we can't display"* abaixo do sankey e não contribuem com nenhuma métrica até que sejam atualizados e re-implantados. Veja [Visão Geral — Requisitos](/pt-BR/enterprise/features/agent-control-plane/overview#requisitos).
</Note>
## Dashboard
O topo da página tem dois cards de métricas e um sankey interativo. Clicar em qualquer um dos cards alterna o sankey entre dois modos:
- **Modo Saúde** — `Total de Automações → buckets de status (Critical / Warning / Healthy)`. Clique em um bucket para filtrar a tabela Automações para esses deployments.
- **Modo Consumo** — `Provedores de Modelo → Automações → Custo Total`. Clique em um provedor para filtrar a tabela Consumo para esse provedor.
| Card | O que mostra |
|------|---------------|
| **Automações** | Automações `active` (e total), total de `errors` na janela, `active executions` no momento (e total na janela), com variação em relação ao período anterior. |
| **Consumo** | `cost` total e `tokens used`, com variação de custo em relação ao período anterior. |
<Frame>
![Visão geral com o sankey de consumo](/images/enterprise/acp-overview-consumption-sankey.png)
</Frame>
## Tabela de Automações
A sub-aba **Automações** é o detalhamento por deployment da saúde da frota. Cada linha é uma crew ou flow implantada.
<Frame>
![Tabela de automações com o detalhamento de status de saúde](/images/enterprise/acp-automations-table.png)
</Frame>
| Coluna | O que mostra |
|--------|---------------|
| **Automação** | Nome do deployment e quaisquer tags atribuídas a ele (ex.: `production`, `financial`). |
| **Ú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 [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. |
Busque por nome, filtre por `Status` (`Healthy` / `Warning` / `Critical`) e ordene por qualquer cabeçalho de coluna. Clique no nome de um deployment para abrir o **painel da Automação**.
## Tabela de Consumo
A sub-aba **Consumo** é o detalhamento por deployment de gasto com LLM e uso de tokens.
<Frame>
![Tabela de consumo detalhada por provedor de LLM](/images/enterprise/acp-consumption-table.png)
</Frame>
| Coluna | O que mostra |
|--------|---------------|
| **Automação** | Nome do deployment. |
| **Última execução** | Tempo decorrido desde a execução mais recente. |
| **Tokens used** | Uma linha por provedor de LLM usado por esta automação, com variação em relação ao período anterior. |
| **Cost** | Custo por provedor de LLM, com variação em relação ao período anterior. |
| **Total cost** | Soma entre todos os provedores, com a variação. |
| **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. |
Filtre por **LLM provider** e ordene por `Cost`, `Executions` ou `Last run`.
<Info>
**Células vazias (`—` ou `$0.00`) normalmente significam que o deployment está abaixo do crewAI v1.13.** Na captura acima, *Automation F* (`1.7.0`) e *Automation I* (`1.12.2`) mostram valores em branco para tokens e custo — suas execuções ainda rodam, mas não emitem a telemetria a nível de provedor que alimenta esta tabela. Atualize e re-implante essas crews para começar a coletar dados de consumo.
</Info>
## 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 — 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.
</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.
</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 interpretar métricas no Agent Control Plane.
</Card>

View File

@@ -0,0 +1,74 @@
---
title: Visão Geral do Agent Control Plane
description: "Hub único de operações para automações ao vivo — saúde da frota, consumo de LLM e políticas em nível de organização em um só lugar."
sidebarTitle: Visão Geral
icon: "book-open"
---
## 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 **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 **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)
</Frame>
<Note>
O Agent Control Plane está atualmente marcado como **Beta** na CrewAI Platform.
</Note>
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).
- **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
<Warning>
**crewAI v1.13 ou superior** é necessário para que uma automação preencha qualquer dado nesta página — saúde, execuções, erros, tokens e custo passam por telemetria que foi habilitada em `crewai==1.13`. Deployments mais antigos aparecem no banner *"We've detected N other automations that we can't display"* e não contribuem com nenhum dado até que sejam atualizados e re-implantados.
</Warning>
<Warning>
**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 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
<CardGroup cols={2}>
<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="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>
## Relacionados
<CardGroup cols={2}>
<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.
</Card>
<Card title="RBAC" icon="users" href="/pt-BR/enterprise/features/rbac">
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 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.
</Card>
</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 regras.
</Card>

View File

@@ -0,0 +1,114 @@
---
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"
---
## 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>

View File

@@ -17,15 +17,65 @@ Antes de usar a integração Salesforce, certifique-se de que você possui:
- Uma conta Salesforce com permissões apropriadas
- Sua conta Salesforce conectada via a [página de Integrações](https://app.crewai.com/integrations)
<Note>
O Salesforce exige uma **instalação única feita por um administrador** do
pacote CrewAI na sua organização antes que qualquer usuário possa se
conectar. Isso é uma exigência da plataforma Salesforce para todas as
integrações baseadas em ExternalClientApp a partir da release Spring '26 —
não é uma etapa específica da CrewAI. O fluxo Connect Salesforce na CrewAI
AMP guia você por esta etapa na primeira vez.
</Note>
## Configurando a Integração Salesforce
### 1. Conecte sua Conta Salesforce
1. Acesse [CrewAI AMP Integrações](https://app.crewai.com/crewai_plus/connectors)
2. Encontre **Salesforce** na seção Integrações de Autenticação
3. Clique em **Conectar** e complete o fluxo OAuth
4. Conceda as permissões necessárias para gerenciamento de CRM e vendas
5. Copie seu Token Enterprise em [Configurações de Integração](https://app.crewai.com/crewai_plus/settings/integrations)
1. Acesse [CrewAI AMP Integrações](https://app.crewai.com/crewai_plus/unified_tools).
2. Encontre **Salesforce** na seção Integrações de Autenticação.
3. Clique em **Conectar**.
O que acontece em seguida depende de o administrador Salesforce já ter
instalado o pacote CrewAI na sua organização:
- **Pacote já instalado:** você será levado diretamente à tela de consentimento
OAuth do Salesforce — aprove e a conexão estará feita.
- **Pacote ainda não instalado:** você verá uma página **Install CrewAI in
Salesforce**. Siga as etapas de instalação única abaixo e, depois, volte à
CrewAI AMP e clique em **Conectar** novamente.
4. Conceda as permissões necessárias para gerenciamento de CRM e vendas.
5. Copie seu Token Enterprise em [Configurações de Integração](https://app.crewai.com/crewai_plus/settings/integrations).
#### Instalação única pelo administrador (apenas admin Salesforce)
Na primeira vez que alguém na sua organização clica em **Connect Salesforce**,
a CrewAI redireciona para uma página de instalação que aponta para o pacote
gerenciado CrewAI. Um administrador Salesforce precisa instalá-lo uma única
vez para toda a organização.
1. Na página de instalação dentro da CrewAI, clique em **Install in
Salesforce**. (Você também pode compartilhar a URL dessa página com seu
administrador — o link de instalação funciona para qualquer pessoa que o
abra.)
2. Entre no Salesforce como administrador. Para sandboxes, troque
`login.salesforce.com` por `test.salesforce.com` na URL antes de abrir.
3. Escolha **Install for All Users**, confirme o aviso sobre aplicativos de
terceiros e clique em **Install**.
4. No Setup do Salesforce, busque **External Client App Manager** → **CrewAI
App** → abra a aba **Policies** → **Edit** e configure:
- **Permitted Users:** All users may self-authorize
- **IP Relaxation:** Relax IP restrictions
- **Refresh Token Policy:** Refresh token is valid until revoked
5. Salve.
6. Volte à CrewAI AMP e clique em **Connect Salesforce** novamente. Desta vez
o OAuth será concluído.
<Note>
**Não é administrador Salesforce?** Encaminhe a URL da página de instalação
(ou o link de instalação em si) para o seu administrador e peça que ele
conclua as etapas acima. Quando ele terminar, volte à CrewAI AMP e clique
em **Conectar** novamente.
</Note>
### 2. Instale o Pacote Necessário

View File

@@ -186,8 +186,6 @@ class Telemetry:
self._safe_telemetry_procedure(_operation)
# --- CLI-facing spans ---------------------------------------------------
def deploy_signup_error_span(self) -> None:
"""Records when an error occurs during the deployment signup process."""

View File

@@ -114,14 +114,12 @@ def format_multimodal_content(
content_blocks: list[dict[str, Any]] = []
provider_type = _normalize_provider(provider)
# Add text block first if provided
if text:
content_blocks.append(_format_text_block(text, provider_type, api))
if not files:
return content_blocks
# Use API-specific constraints for OpenAI
constraints_key: str = provider_type
if api == "responses" and "openai" in provider_type.lower():
constraints_key = "openai_responses"
@@ -186,7 +184,6 @@ async def aformat_multimodal_content(
if not files:
return content_blocks
# Use API-specific constraints for OpenAI
constraints_key: str = provider_type
if api == "responses" and "openai" in provider_type.lower():
constraints_key = "openai_responses"

View File

@@ -245,7 +245,6 @@ class FileResolver:
type_constraint = self._get_type_constraint(content_type, constraints)
if type_constraint is not None:
# Check if file exceeds type-specific inline limit
if file_size > type_constraint.max_size_bytes:
logger.debug(
f"File {file.filename} ({file_size}B) exceeds {content_type} "

View File

@@ -162,7 +162,6 @@ class TestFileProcessorValidate:
image=ImageConstraints(max_size_bytes=10),
)
processor = FileProcessor(constraints=constraints)
# Set mode to strict on the file
file = ImageFile(
source=FileBytes(data=MINIMAL_PNG, filename="test.png"), mode="strict"
)
@@ -199,7 +198,6 @@ class TestFileProcessorProcess:
image=ImageConstraints(max_size_bytes=10),
)
processor = FileProcessor(constraints=constraints)
# Set mode to strict on the file
file = ImageFile(
source=FileBytes(data=MINIMAL_PNG, filename="test.png"), mode="strict"
)
@@ -214,7 +212,6 @@ class TestFileProcessorProcess:
image=ImageConstraints(max_size_bytes=10),
)
processor = FileProcessor(constraints=constraints)
# Set mode to warn on the file
file = ImageFile(
source=FileBytes(data=MINIMAL_PNG, filename="test.png"), mode="warn"
)

View File

@@ -93,14 +93,11 @@ class TestFileResolver:
resolver = FileResolver(upload_cache=cache)
file = ImageFile(source=FileBytes(data=MINIMAL_PNG, filename="test.png"))
# First resolution
resolved1 = resolver.resolve(file, "openai")
# Second resolution (should use same base64 encoding)
resolved2 = resolver.resolve(file, "openai")
assert isinstance(resolved1, InlineBase64)
assert isinstance(resolved2, InlineBase64)
# Data should be identical
assert resolved1.data == resolved2.data
def test_clear_cache(self):
@@ -108,7 +105,6 @@ class TestFileResolver:
cache = UploadCache()
file = ImageFile(source=FileBytes(data=MINIMAL_PNG, filename="test.png"))
# Add something to cache manually
cache.set(file=file, provider="gemini", file_id="test")
resolver = FileResolver(upload_cache=cache)

View File

@@ -162,7 +162,6 @@ class TestUploadCache:
source=FileBytes(data=MINIMAL_PNG + b"x", filename="test2.png")
)
# Add one expired and one valid entry
past = datetime.now(timezone.utc) - timedelta(hours=1)
future = datetime.now(timezone.utc) + timedelta(hours=24)

View File

@@ -252,12 +252,10 @@ class CrewAIRagAdapter(Adapter):
if filename.startswith("."):
continue
# Skip binary files based on extension
file_ext = os.path.splitext(filename)[1].lower()
if file_ext in binary_extensions:
continue
# Skip __pycache__ directories
if "__pycache__" in root:
continue

View File

@@ -46,7 +46,6 @@ class EnterpriseActionTool(BaseTool):
schema_props, required = self._extract_schema_info(action_schema)
# Define field definitions for the model
field_definitions = {}
for param_name, param_details in schema_props.items():
param_desc = param_details.get("description", "")
@@ -59,12 +58,10 @@ class EnterpriseActionTool(BaseTool):
except Exception:
field_type = str
# Create field definition based on requirement
field_definitions[param_name] = self._create_field_definition(
field_type, is_required, param_desc
)
# Create the model
if field_definitions:
try:
args_schema = create_model( # type: ignore[call-overload]

View File

@@ -16,7 +16,6 @@ class RAGAdapter(Adapter):
):
super().__init__()
# Prepare embedding configuration
embedding_config = {"api_key": embedding_api_key, **embedding_kwargs}
self._adapter = RAG(

View File

@@ -14,7 +14,6 @@ from crewai_tools.aws.bedrock.exceptions import (
)
# Load environment variables from .env file
load_dotenv()
@@ -66,29 +65,24 @@ class BedrockInvokeAgentTool(BaseTool):
self.enable_trace = enable_trace
self.end_session = end_session
# Update the description if provided
if description:
self.description = description
# Validate parameters
self._validate_parameters()
def _validate_parameters(self) -> None:
"""Validate the parameters according to AWS API requirements."""
try:
# Validate agent_id
if not self.agent_id:
raise BedrockValidationError("agent_id cannot be empty")
if not isinstance(self.agent_id, str):
raise BedrockValidationError("agent_id must be a string")
# Validate agent_alias_id
if not self.agent_alias_id:
raise BedrockValidationError("agent_alias_id cannot be empty")
if not isinstance(self.agent_alias_id, str):
raise BedrockValidationError("agent_alias_id must be a string")
# Validate session_id if provided
if self.session_id and not isinstance(self.session_id, str):
raise BedrockValidationError("session_id must be a string")
@@ -113,7 +107,6 @@ class BedrockInvokeAgentTool(BaseTool):
),
)
# Format the prompt with current time
current_utc = datetime.now(timezone.utc)
prompt = f"""
The current time is: {current_utc}
@@ -132,12 +125,9 @@ Below is the users query or task. Complete it and answer it consicely and to the
endSession=self.end_session,
)
# Process the response
completion = ""
# Check if response contains a completion field
if "completion" in response:
# Process streaming response format
for event in response.get("completion", []):
if "chunk" in event and "bytes" in event["chunk"]:
chunk_bytes = event["chunk"]["bytes"]
@@ -161,7 +151,6 @@ Below is the users query or task. Complete it and answer it consicely and to the
"response_keys": list(response.keys()),
}
# Add more debug info
if "chunk" in response:
debug_info["chunk_keys"] = list(response["chunk"].keys())

View File

@@ -135,10 +135,8 @@ class NavigateTool(BrowserBaseTool):
def _run(self, url: str, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Get page for this thread
page = self.get_sync_page(thread_id)
# Validate URL scheme
parsed_url = urlparse(url)
if parsed_url.scheme not in ("http", "https"):
raise ValueError("URL scheme must be 'http' or 'https'")
@@ -153,10 +151,8 @@ class NavigateTool(BrowserBaseTool):
async def _arun(self, url: str, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the async tool."""
try:
# Get page for this thread
page = await self.get_async_page(thread_id)
# Validate URL scheme
parsed_url = urlparse(url)
if parsed_url.scheme not in ("http", "https"):
raise ValueError("URL scheme must be 'http' or 'https'")
@@ -191,7 +187,6 @@ class ClickTool(BrowserBaseTool):
def _run(self, selector: str, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Get the current page
page = self.get_sync_page(thread_id)
# Click on the element
@@ -218,7 +213,6 @@ class ClickTool(BrowserBaseTool):
) -> str:
"""Use the async tool."""
try:
# Get the current page
page = await self.get_async_page(thread_id)
# Click on the element
@@ -251,7 +245,6 @@ class NavigateBackTool(BrowserBaseTool):
def _run(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Get the current page
page = self.get_sync_page(thread_id)
# Navigate back
@@ -266,7 +259,6 @@ class NavigateBackTool(BrowserBaseTool):
async def _arun(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the async tool."""
try:
# Get the current page
page = await self.get_async_page(thread_id)
# Navigate back
@@ -289,7 +281,6 @@ class ExtractTextTool(BrowserBaseTool):
def _run(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Import BeautifulSoup
try:
from bs4 import BeautifulSoup
except ImportError:
@@ -298,10 +289,8 @@ class ExtractTextTool(BrowserBaseTool):
" Please install it with 'pip install beautifulsoup4'."
)
# Get the current page
page = self.get_sync_page(thread_id)
# Extract text
content = page.content()
soup = BeautifulSoup(content, "html.parser")
return soup.get_text(separator="\n").strip()
@@ -311,7 +300,6 @@ class ExtractTextTool(BrowserBaseTool):
async def _arun(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the async tool."""
try:
# Import BeautifulSoup
try:
from bs4 import BeautifulSoup
except ImportError:
@@ -320,10 +308,8 @@ class ExtractTextTool(BrowserBaseTool):
" Please install it with 'pip install beautifulsoup4'."
)
# Get the current page
page = await self.get_async_page(thread_id)
# Extract text
content = await page.content()
soup = BeautifulSoup(content, "html.parser")
return soup.get_text(separator="\n").strip()
@@ -341,7 +327,6 @@ class ExtractHyperlinksTool(BrowserBaseTool):
def _run(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Import BeautifulSoup
try:
from bs4 import BeautifulSoup, Tag
except ImportError:
@@ -350,10 +335,8 @@ class ExtractHyperlinksTool(BrowserBaseTool):
" Please install it with 'pip install beautifulsoup4'."
)
# Get the current page
page = self.get_sync_page(thread_id)
# Extract hyperlinks
content = page.content()
soup = BeautifulSoup(content, "html.parser")
links = []
@@ -374,7 +357,6 @@ class ExtractHyperlinksTool(BrowserBaseTool):
async def _arun(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the async tool."""
try:
# Import BeautifulSoup
try:
from bs4 import BeautifulSoup, Tag
except ImportError:
@@ -383,10 +365,8 @@ class ExtractHyperlinksTool(BrowserBaseTool):
" Please install it with 'pip install beautifulsoup4'."
)
# Get the current page
page = await self.get_async_page(thread_id)
# Extract hyperlinks
content = await page.content()
soup = BeautifulSoup(content, "html.parser")
links = []
@@ -415,10 +395,8 @@ class GetElementsTool(BrowserBaseTool):
def _run(self, selector: str, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Get the current page
page = self.get_sync_page(thread_id)
# Get elements
elements = page.query_selector_all(selector)
if not elements:
return f"No elements found with selector '{selector}'"
@@ -437,10 +415,8 @@ class GetElementsTool(BrowserBaseTool):
) -> str:
"""Use the async tool."""
try:
# Get the current page
page = await self.get_async_page(thread_id)
# Get elements
elements = await page.query_selector_all(selector)
if not elements:
return f"No elements found with selector '{selector}'"
@@ -465,10 +441,8 @@ class CurrentWebPageTool(BrowserBaseTool):
def _run(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the sync tool."""
try:
# Get the current page
page = self.get_sync_page(thread_id)
# Get information
url = page.url
title = page.title()
return f"URL: {url}\nTitle: {title}"
@@ -478,10 +452,8 @@ class CurrentWebPageTool(BrowserBaseTool):
async def _arun(self, thread_id: str = "default", **kwargs: Any) -> str:
"""Use the async tool."""
try:
# Get the current page
page = await self.get_async_page(thread_id)
# Get information
url = page.url
title = await page.title()
return f"URL: {url}\nTitle: {title}"

View File

@@ -155,12 +155,10 @@ class ExecuteCodeTool(BaseTool):
thread_id: str = "default",
) -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Execute code
response = code_interpreter.invoke(
method="executeCode",
params={
@@ -204,12 +202,10 @@ class ExecuteCommandTool(BaseTool):
def _run(self, command: str, thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Execute command
response = code_interpreter.invoke(
method="executeCommand", params={"command": command}
)
@@ -237,12 +233,10 @@ class ReadFilesTool(BaseTool):
def _run(self, paths: list[str], thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Read files
response = code_interpreter.invoke(
method="readFiles", params={"paths": paths}
)
@@ -270,7 +264,6 @@ class ListFilesTool(BaseTool):
def _run(self, directory_path: str = "", thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
@@ -303,12 +296,10 @@ class DeleteFilesTool(BaseTool):
def _run(self, paths: list[str], thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Remove files
response = code_interpreter.invoke(
method="removeFiles", params={"paths": paths}
)
@@ -336,12 +327,10 @@ class WriteFilesTool(BaseTool):
def _run(self, files: list[dict[str, str]], thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Write files
response = code_interpreter.invoke(
method="writeFiles", params={"content": files}
)
@@ -371,12 +360,10 @@ class StartCommandTool(BaseTool):
def _run(self, command: str, thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Start command execution
response = code_interpreter.invoke(
method="startCommandExecution", params={"command": command}
)
@@ -404,12 +391,10 @@ class GetTaskTool(BaseTool):
def _run(self, task_id: str, thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Get task status
response = code_interpreter.invoke(
method="getTask", params={"taskId": task_id}
)
@@ -437,12 +422,10 @@ class StopTaskTool(BaseTool):
def _run(self, task_id: str, thread_id: str = "default") -> str:
try:
# Get or create code interpreter
code_interpreter = self.toolkit._get_or_create_interpreter(
thread_id=thread_id
)
# Stop task
response = code_interpreter.invoke(
method="stopTask", params={"taskId": task_id}
)
@@ -555,7 +538,6 @@ class CodeInterpreterToolkit:
f"Started code interpreter with session_id:{code_interpreter.session_id} for thread:{thread_id}"
)
# Store the interpreter
self._code_interpreters[thread_id] = code_interpreter
return code_interpreter
@@ -582,7 +564,6 @@ class CodeInterpreterToolkit:
thread_id: Optional thread ID to clean up. If None, cleans up all sessions.
"""
if thread_id:
# Clean up a specific thread's session
if thread_id in self._code_interpreters:
try:
self._code_interpreters[thread_id].stop()
@@ -595,7 +576,6 @@ class CodeInterpreterToolkit:
f"Error stopping code interpreter for thread {thread_id}: {e}"
)
else:
# Clean up all sessions
thread_ids = list(self._code_interpreters.keys())
for tid in thread_ids:
try:

View File

@@ -12,7 +12,6 @@ from crewai_tools.aws.bedrock.exceptions import (
)
# Load environment variables from .env file
load_dotenv()
@@ -69,7 +68,6 @@ class BedrockKBRetrieverTool(BaseTool):
else:
self.retrieval_configuration = retrieval_configuration
# Validate parameters
self._validate_parameters()
# Update the description to include the knowledge base details
@@ -83,7 +81,6 @@ class BedrockKBRetrieverTool(BaseTool):
"""
vector_search_config = {}
# Add number of results if provided
if self.number_of_results is not None:
vector_search_config["numberOfResults"] = self.number_of_results
@@ -92,7 +89,6 @@ class BedrockKBRetrieverTool(BaseTool):
def _validate_parameters(self) -> None:
"""Validate the parameters according to AWS API requirements."""
try:
# Validate knowledge_base_id
if not self.knowledge_base_id:
raise BedrockValidationError("knowledge_base_id cannot be empty")
if not isinstance(self.knowledge_base_id, str):
@@ -106,7 +102,6 @@ class BedrockKBRetrieverTool(BaseTool):
"knowledge_base_id must contain only alphanumeric characters"
)
# Validate next_token if provided
if self.next_token:
if not isinstance(self.next_token, str):
raise BedrockValidationError("next_token must be a string")
@@ -117,7 +112,6 @@ class BedrockKBRetrieverTool(BaseTool):
if " " in self.next_token:
raise BedrockValidationError("next_token cannot contain spaces")
# Validate number_of_results if provided
if self.number_of_results is not None:
if not isinstance(self.number_of_results, int):
raise BedrockValidationError("number_of_results must be an integer")
@@ -138,12 +132,10 @@ class BedrockKBRetrieverTool(BaseTool):
Returns:
Dict[str, Any]: Processed result with standardized format
"""
# Extract content
content_obj = result.get("content", {})
content = content_obj.get("text", "")
content_type = content_obj.get("type", "text")
# Extract location information
location = result.get("location", {})
location_type = location.get("type", "unknown")
source_uri = None
@@ -160,7 +152,6 @@ class BedrockKBRetrieverTool(BaseTool):
"sqlLocation": {"field": "query", "type": "SQL"},
}
# Extract the URI based on location type
for loc_key, config in location_mapping.items():
if loc_key in location:
source_uri = location[loc_key].get(config["field"])
@@ -168,7 +159,6 @@ class BedrockKBRetrieverTool(BaseTool):
location_type = config["type"]
break
# Create result object
result_object = {
"content": content,
"content_type": content_type,
@@ -176,18 +166,15 @@ class BedrockKBRetrieverTool(BaseTool):
"source_uri": source_uri,
}
# Add optional fields if available
if "score" in result:
result_object["score"] = result["score"]
if "metadata" in result:
result_object["metadata"] = result["metadata"]
# Handle byte content if present
if "byteContent" in content_obj:
result_object["byte_content"] = content_obj["byteContent"]
# Handle row content if present
if "row" in content_obj:
result_object["row_content"] = content_obj["row"]
@@ -212,13 +199,11 @@ class BedrockKBRetrieverTool(BaseTool):
# AWS SDK will automatically use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from environment
)
# Prepare the request parameters
retrieve_params = {
"knowledgeBaseId": self.knowledge_base_id,
"retrievalQuery": {"text": query},
}
# Add optional parameters if provided
if self.retrieval_configuration:
retrieve_params["retrievalConfiguration"] = self.retrieval_configuration
@@ -228,16 +213,13 @@ class BedrockKBRetrieverTool(BaseTool):
if self.next_token:
retrieve_params["nextToken"] = self.next_token
# Make the retrieve API call
response = bedrock_agent_runtime.retrieve(**retrieve_params)
# Process the response
results = []
for result in response.get("retrievalResults", []):
processed_result = self._process_retrieval_result(result)
results.append(processed_result)
# Build the response object
response_object = {}
if results:
response_object["results"] = results
@@ -250,7 +232,6 @@ class BedrockKBRetrieverTool(BaseTool):
if "guardrailAction" in response:
response_object["guardrailAction"] = response["guardrailAction"]
# Return the results as a JSON string
return json.dumps(response_object, indent=2)
except ClientError as e:

View File

@@ -37,7 +37,6 @@ class S3ReaderTool(BaseTool):
aws_secret_access_key=os.getenv("CREW_AWS_SEC_ACCESS_KEY"),
)
# Read file content from S3
response = s3.get_object(Bucket=bucket_name, Key=object_key)
result: str = response["Body"].read().decode("utf-8")
return result

View File

@@ -12,15 +12,15 @@ class TextChunker(BaseChunker):
if separators is None:
separators = [
"\n\n\n", # Multiple line breaks (sections)
"\n\n", # Paragraph breaks
"\n", # Line breaks
". ", # Sentence endings
"! ", # Exclamation endings
"? ", # Question endings
"; ", # Semicolon breaks
", ", # Comma breaks
" ", # Word breaks
"", # Character level
"\n\n",
"\n",
". ",
"! ",
"? ",
"; ",
", ",
" ",
"",
]
super().__init__(chunk_size, chunk_overlap, separators, keep_separator)
@@ -36,15 +36,15 @@ class DocxChunker(BaseChunker):
if separators is None:
separators = [
"\n\n\n", # Multiple line breaks (major sections)
"\n\n", # Paragraph breaks
"\n", # Line breaks
". ", # Sentence endings
"! ", # Exclamation endings
"? ", # Question endings
"; ", # Semicolon breaks
", ", # Comma breaks
" ", # Word breaks
"", # Character level
"\n\n",
"\n",
". ",
"! ",
"? ",
"; ",
", ",
" ",
"",
]
super().__init__(chunk_size, chunk_overlap, separators, keep_separator)
@@ -62,15 +62,15 @@ class MdxChunker(BaseChunker):
"\n## ", # H2 headers (major sections)
"\n### ", # H3 headers (subsections)
"\n#### ", # H4 headers (sub-subsections)
"\n\n", # Paragraph breaks
"\n```", # Code block boundaries
"\n", # Line breaks
". ", # Sentence endings
"! ", # Exclamation endings
"? ", # Question endings
"; ", # Semicolon breaks
", ", # Comma breaks
" ", # Word breaks
"", # Character level
"\n\n",
"\n```",
"\n",
". ",
"! ",
"? ",
"; ",
", ",
" ",
"",
]
super().__init__(chunk_size, chunk_overlap, separators, keep_separator)

View File

@@ -11,15 +11,15 @@ class WebsiteChunker(BaseChunker):
):
if separators is None:
separators = [
"\n\n\n", # Major section breaks
"\n\n", # Paragraph breaks
"\n", # Line breaks
". ", # Sentence endings
"! ", # Exclamation endings
"? ", # Question endings
"; ", # Semicolon breaks
", ", # Comma breaks
" ", # Word breaks
"", # Character level
"\n\n\n",
"\n\n",
"\n",
". ",
"! ",
"? ",
"; ",
", ",
" ",
"",
]
super().__init__(chunk_size, chunk_overlap, separators, keep_separator)

View File

@@ -191,7 +191,6 @@ class RAG(Adapter):
metadatas = results.get("metadatas", [None])[0] or []
distances = results.get("distances", [None])[0] or []
# Return sources with relevance scores
formatted_results = []
for i, doc in enumerate(documents):
metadata = metadatas[i] if i < len(metadatas) else {}

View File

@@ -37,7 +37,6 @@ class DataType(str, Enum):
DataType.TEXT: ("text_chunker", "TextChunker"),
DataType.DOCX: ("text_chunker", "DocxChunker"),
DataType.MDX: ("text_chunker", "MdxChunker"),
# Structured formats
DataType.CSV: ("structured_chunker", "CsvChunker"),
DataType.JSON: ("structured_chunker", "JsonChunker"),
DataType.XML: ("structured_chunker", "XmlChunker"),

View File

@@ -113,10 +113,8 @@ class EmbeddingService:
try:
from crewai.rag.embeddings.factory import build_embedder
# Build the configuration for CrewAI's factory
config = self._build_provider_config()
# Create the embedding function
self._embedding_function = build_embedder(config)
logger.info(
@@ -287,7 +285,6 @@ class EmbeddingService:
if not texts:
return []
# Filter out empty texts
valid_texts = [text for text in texts if text and text.strip()]
if not valid_texts:
logger.warning("No valid texts provided for batch embedding")

View File

@@ -39,16 +39,12 @@ class MDXLoader(BaseLoader):
def _parse_mdx(self, content: str, source_ref: str) -> LoaderResult:
cleaned_content = content
# Remove import statements
cleaned_content = _IMPORT_PATTERN.sub("", cleaned_content)
# Remove export statements
cleaned_content = _EXPORT_PATTERN.sub("", cleaned_content)
# Remove JSX tags (simple approach)
cleaned_content = _JSX_TAG_PATTERN.sub("", cleaned_content)
# Clean up extra whitespace
cleaned_content = _EXTRA_NEWLINES_PATTERN.sub("\n\n", cleaned_content)
cleaned_content = cleaned_content.strip()

View File

@@ -31,9 +31,7 @@ def sanitize_metadata_for_chromadb(metadata: dict[str, Any]) -> dict[str, Any]:
if isinstance(value, (str, int, float, bool)) or value is None:
sanitized[key] = value
elif isinstance(value, (list, tuple)):
# Convert lists/tuples to pipe-separated strings
sanitized[key] = " | ".join(str(v) for v in value)
else:
# Convert other types to string
sanitized[key] = str(value)
return sanitized

View File

@@ -27,11 +27,6 @@ def _is_escape_hatch_enabled() -> bool:
return os.environ.get(_UNSAFE_PATHS_ENV, "").lower() in ("true", "1", "yes")
# ---------------------------------------------------------------------------
# File path validation
# ---------------------------------------------------------------------------
def validate_file_path(path: str, base_dir: str | None = None) -> str:
"""Validate that a file path is safe to read.
@@ -101,10 +96,6 @@ def validate_directory_path(path: str, base_dir: str | None = None) -> str:
return validated
# ---------------------------------------------------------------------------
# URL validation
# ---------------------------------------------------------------------------
# Private and reserved IP ranges that should not be accessed
_BLOCKED_IPV4_NETWORKS = [
ipaddress.ip_network("10.0.0.0/8"),
@@ -185,7 +176,6 @@ def validate_url(url: str) -> str:
if not parsed.hostname:
raise ValueError(f"URL has no hostname: '{url}'")
# Resolve DNS and check IPs
try:
addrinfos = socket.getaddrinfo(
parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80)

View File

@@ -62,7 +62,6 @@ class AIMindTool(BaseTool):
minds_client = Client(api_key=self.api_key)
# Convert the datasources to DatabaseConfig objects.
datasources = []
for datasource in self.datasources:
config = DatabaseConfig(
@@ -74,7 +73,6 @@ class AIMindTool(BaseTool):
)
datasources.append(config)
# Generate a random name for the Mind.
name = f"{AIMindToolConstants.MIND_NAME_PREFIX}_{secrets.token_hex(5)}"
mind = minds_client.minds.create(
@@ -84,7 +82,6 @@ class AIMindTool(BaseTool):
self.mind_name = mind.name
def _run(self, query: str) -> str | None:
# Run the query on the AI-Mind.
# The Minds API is OpenAI compatible and therefore, the OpenAI client can be used.
openai_client = OpenAI(
base_url=AIMindToolConstants.MINDS_API_BASE_URL, api_key=self.api_key

View File

@@ -186,7 +186,6 @@ class BraveSearchToolBase(BaseTool, ABC):
for attempt in range(_max_retries):
self._rate_limit()
# Make the request
try:
resp = requests.get(
self.search_url,
@@ -203,7 +202,6 @@ class BraveSearchToolBase(BaseTool, ABC):
f"Brave Search API request timed out after {self._timeout}s: {exc}"
) from exc
# Log the rate limit headers and request details
logger.debug(
"Brave Search API request: %s %s -> %d",
"GET",
@@ -251,7 +249,6 @@ class BraveSearchToolBase(BaseTool, ABC):
params = self._common_payload_refinement(params)
# Validate only schema fields
schema_keys = self.args_schema.model_fields
payload_in = {k: v for k, v in params.items() if k in schema_keys}
@@ -301,7 +298,6 @@ class BraveSearchToolBase(BaseTool, ABC):
if k not in fields or fields[k].is_required() or v not in self._EMPTY_VALUES
}
# Make sure params has "q" for query instead of "query" or "search_query"
query = params.get("query") or params.get("search_query")
if query is not None and "q" not in params:
params["q"] = query

View File

@@ -27,7 +27,6 @@ class BraveImageSearchTool(BraveSearchToolBase):
return params
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
# Make the response more concise, and easier to consume
results = response.get("results", [])
return [
{

View File

@@ -27,7 +27,6 @@ class BraveNewsSearchTool(BraveSearchToolBase):
return params
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
# Make the response more concise, and easier to consume
results = response.get("results", [])
return [
{

View File

@@ -68,7 +68,6 @@ class BraveSearchTool(BaseTool):
)
BraveSearchTool._last_request_time = time.time()
# Construct and send the request
try:
# Fallback to "query" or "search_query" for backwards compatibility
query = kwargs.get("q") or kwargs.get("query") or kwargs.get("search_query")
@@ -123,11 +122,9 @@ class BraveSearchTool(BaseTool):
payload["operators"] = operators
# Limit the result types to "web" since there is presently no
# handling of other types like "discussions", "faq", "infobox",
# "news", "videos", or "locations".
payload["result_filter"] = "web"
# Setup Request Headers
headers = {
"X-Subscription-Token": os.environ["BRAVE_API_KEY"],
"Accept": "application/json",
@@ -136,7 +133,7 @@ class BraveSearchTool(BaseTool):
response = requests.get(
self.search_url, headers=headers, params=payload, timeout=30
)
response.raise_for_status() # Handle non-200 responses
response.raise_for_status()
results = response.json()
# TODO: Handle other result types like "discussions", "faq", etc.

View File

@@ -27,7 +27,6 @@ class BraveVideoSearchTool(BraveSearchToolBase):
return params
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
# Make the response more concise, and easier to consume
results = response.get("results", [])
return [
{

View File

@@ -496,7 +496,6 @@ class BrightDataDatasetTool(BaseTool):
)
async with aiohttp.ClientSession() as session:
# Step 1: Trigger job
async with session.post(
f"{BRIGHTDATA_API_URL}/datasets/v3/trigger",
params={"dataset_id": dataset_id, "include_errors": "true"},
@@ -511,7 +510,6 @@ class BrightDataDatasetTool(BaseTool):
trigger_data = await trigger_response.json()
snapshot_id = trigger_data.get("snapshot_id")
# Step 2: Poll for completion
elapsed = 0
while elapsed < timeout:
await asyncio.sleep(polling_interval)
@@ -536,7 +534,6 @@ class BrightDataDatasetTool(BaseTool):
else:
raise TimeoutError("Polling timed out before job completed.")
# Step 3: Retrieve result
async with session.get(
f"{BRIGHTDATA_API_URL}/datasets/v3/snapshot/{snapshot_id}",
params={"format": output_format},

View File

@@ -173,15 +173,12 @@ class BrightDataSearchTool(BaseTool):
)
results_count = kwargs.get("results_count", "10")
# Validate required parameters
if not query:
raise ValueError("query is required either in constructor or method call")
# Build the search URL
query = urllib.parse.quote(query)
url = self.get_search_url(search_engine, query)
# Add parameters to the URL
params = []
if country:
@@ -214,7 +211,6 @@ class BrightDataSearchTool(BaseTool):
if params:
url += "&" + "&".join(params)
# Set up the API request parameters
request_params = {"zone": self.zone, "url": url, "format": "raw"}
request_params = {k: v for k, v in request_params.items() if v is not None}

View File

@@ -53,7 +53,6 @@ class ContextualAICreateAgentTool(BaseTool):
try:
import os
# Create datastore
datastore = self.contextual_client.datastores.create(name=datastore_name)
datastore_id = datastore.id
@@ -71,7 +70,6 @@ class ContextualAICreateAgentTool(BaseTool):
)
document_ids.append(ingestion_result.id)
# Create agent
agent = self.contextual_client.agents.create(
name=agent_name,
description=agent_description,

View File

@@ -96,7 +96,6 @@ class ContextualAIParseTool(BaseTool):
sleep(5)
# Get parse results
results_url = f"{base_url}/parse/jobs/{job_id}/results"
result = requests.get(
results_url,

View File

@@ -84,22 +84,18 @@ class CouchbaseFTSVectorSearchTool(BaseTool):
"""
scope_collection_map: dict[str, Any] = {}
# Get a list of all scopes in the bucket
for scope in self._bucket.collections().get_all_scopes():
scope_collection_map[scope.name] = []
# Get a list of all the collections in the scope
for collection in scope.collections:
scope_collection_map[scope.name].append(collection.name)
# Check if the scope exists
if self.scope_name not in scope_collection_map:
raise ValueError(
f"Scope {self.scope_name} not found in Couchbase "
f"bucket {self.bucket_name}"
)
# Check if the collection exists in the scope
if self.collection_name not in scope_collection_map[self.scope_name]:
raise ValueError(
f"Collection {self.collection_name} not found in scope "
@@ -162,7 +158,6 @@ class CouchbaseFTSVectorSearchTool(BaseTool):
"Please check the connection and credentials"
) from e
# check if bucket exists
if not self._check_bucket_exists():
raise ValueError(
f"Bucket {self.bucket_name} does not exist. "

View File

@@ -172,13 +172,11 @@ class DatabricksQueryTool(BaseTool):
if not results:
return "Query returned no results."
# Get column names from the first row
if not results[0]:
return "Query returned empty rows with no columns."
columns = list(results[0].keys())
# If we have rows but they're all empty, handle that case
if not columns:
return "Query returned rows but with no column data."
@@ -186,19 +184,14 @@ class DatabricksQueryTool(BaseTool):
col_widths = {col: len(col) for col in columns}
for row in results:
for col in columns:
# Convert value to string and get its length
# Handle None values gracefully
value_str = str(row[col]) if row[col] is not None else "NULL"
col_widths[col] = max(col_widths[col], len(value_str))
# Create header row
header = " | ".join(f"{col:{col_widths[col]}}" for col in columns)
separator = "-+-".join("-" * col_widths[col] for col in columns)
# Format data rows
data_rows = []
for row in results:
# Handle None values by displaying "NULL"
row_values = {
col: str(row[col]) if row[col] is not None else "NULL"
for col in columns
@@ -208,7 +201,6 @@ class DatabricksQueryTool(BaseTool):
)
data_rows.append(data_row)
# Add row count information
result_info = f"({len(results)} row{'s' if len(results) != 1 else ''} returned)"
# Combine all parts
@@ -231,14 +223,12 @@ class DatabricksQueryTool(BaseTool):
str: Formatted query results
"""
try:
# Get parameters with fallbacks to default values
query = kwargs.get("query")
catalog = kwargs.get("catalog") or self.default_catalog
db_schema = kwargs.get("db_schema") or self.default_schema
warehouse_id = kwargs.get("warehouse_id") or self.default_warehouse_id
row_limit = kwargs.get("row_limit", 1000)
# Validate schema and query
validated_input = DatabricksQueryToolSchema(
query=query,
catalog=catalog,
@@ -247,7 +237,6 @@ class DatabricksQueryTool(BaseTool):
row_limit=row_limit,
)
# Extract validated parameters
query = validated_input.query
catalog = validated_input.catalog
db_schema = validated_input.db_schema
@@ -256,26 +245,21 @@ class DatabricksQueryTool(BaseTool):
if warehouse_id is None:
return "SQL warehouse ID must be provided either as a parameter or as a default."
# Setup SQL context with catalog/schema if provided
context: ExecutionContext = {}
if catalog:
context["catalog"] = catalog
if db_schema:
context["schema"] = db_schema
# Execute query
statement = self.workspace_client.statement_execution
try:
# Execute the statement
execution = statement.execute_statement(
warehouse_id=warehouse_id, statement=query, **context
)
statement_id = execution.statement_id
except Exception as execute_error:
# Handle immediate execution errors
return f"Error starting query execution: {execute_error!s}"
# Poll for results with better error handling
@@ -284,7 +268,7 @@ class DatabricksQueryTool(BaseTool):
timeout = 300 # 5 minutes timeout
start_time = time.time()
poll_count = 0
previous_state = None # Track previous state to detect changes
previous_state = None
if statement_id is None:
return "Failed to retrieve statement ID after execution."
@@ -292,27 +276,21 @@ class DatabricksQueryTool(BaseTool):
while time.time() - start_time < timeout:
poll_count += 1
try:
# Get statement status
result = statement.get_statement(statement_id)
# Check if finished - be very explicit about state checking
if hasattr(result, "status") and hasattr(result.status, "state"):
state_value = str(
result.status.state # type: ignore[union-attr]
) # Convert to string to handle both string and enum
# Track state changes for debugging
if previous_state != state_value:
previous_state = state_value
# Check if state indicates completion
if "SUCCEEDED" in state_value:
break
if "FAILED" in state_value:
# Extract error message with more robust handling
error_info = "No detailed error info"
try:
# First try direct access to error.message
if (
hasattr(result.status, "error")
and result.status.error # type: ignore[union-attr]
@@ -322,16 +300,13 @@ class DatabricksQueryTool(BaseTool):
# Some APIs may have a different structure
elif hasattr(result.status.error, "error_message"): # type: ignore[union-attr]
error_info = result.status.error.error_message # type: ignore[union-attr]
# Last resort, try to convert the whole error object to string
else:
error_info = str(result.status.error) # type: ignore[union-attr]
except Exception as err_extract_error:
# If all else fails, try to get any info we can
error_info = (
f"Error details unavailable: {err_extract_error!s}"
)
# Return immediately on first FAILED state detection
return f"Query execution failed: {error_info}"
if "CANCELED" in state_value:
return "Query was canceled"
@@ -341,17 +316,14 @@ class DatabricksQueryTool(BaseTool):
if poll_count > 3:
return f"Error checking query status: {poll_error!s}"
# Wait before polling again
time.sleep(2)
# Check if we timed out
if result is None:
return "Query returned no result (likely timed out or failed)"
if not hasattr(result, "status") or not hasattr(result.status, "state"):
return "Query completed but returned an invalid result structure"
# Convert state to string for comparison
state_value = str(result.status.state) # type: ignore[union-attr]
if not any(
state in state_value for state in ["SUCCEEDED", "FAILED", "CANCELED"]
@@ -372,7 +344,6 @@ class DatabricksQueryTool(BaseTool):
if has_schema and has_result:
try:
# Get schema for column names
columns = [col.name for col in result.manifest.schema.columns] # type: ignore[union-attr]
# Debug info for schema
@@ -382,16 +353,13 @@ class DatabricksQueryTool(BaseTool):
# Dump the raw structure of result data to help troubleshoot
if _has_data_array(result):
# Add defensive check for None data_array
if result.result.data_array is None:
# Return empty result handling rather than trying to process null data
return "Query executed successfully (no data returned)"
# IMPROVED DETECTION LOGIC: Check if we're possibly dealing with rows where each item
# contains a single value or character (which could indicate incorrect row structure)
is_likely_incorrect_row_structure = False
# Only try to analyze sample if data_array exists and has content
if (
_has_data_array(result)
and len(result.result.data_array) > 0
@@ -421,7 +389,6 @@ class DatabricksQueryTool(BaseTool):
single_digit_count += 1
# If a significant portion of the first values are single characters or digits,
# this likely indicates data is being incorrectly structured
if (
total_items > 0
and (single_char_count + single_digit_count)
@@ -465,14 +432,12 @@ class DatabricksQueryTool(BaseTool):
else:
needs_special_string_handling = False
# Process results differently based on detection
if (
"needs_special_string_handling" in locals()
and needs_special_string_handling
):
# We're dealing with data where the rows may be incorrectly structured
# Collect all values into a flat list
all_values: list[Any] = []
if (
hasattr(result.result, "data_array")
@@ -486,10 +451,8 @@ class DatabricksQueryTool(BaseTool):
else:
all_values.append(item)
# Get the expected column count from schema
expected_column_count = len(columns)
# Try to reconstruct rows using pattern recognition
reconstructed_rows = []
# PATTERN RECOGNITION APPROACH
@@ -509,7 +472,6 @@ class DatabricksQueryTool(BaseTool):
# This value looks like an ID, might be the start of a row
if i < len(all_values) - 1:
next_few_values = all_values[i + 1 : i + 5]
# If following values look like they could be part of a title
if any(
isinstance(v, str) and len(v) > 1
for v in next_few_values
@@ -517,7 +479,6 @@ class DatabricksQueryTool(BaseTool):
id_indices.append(i)
if id_indices:
# If we found potential row starts, use them to extract rows
for i in range(len(id_indices)):
start_idx = id_indices[i]
end_idx = (
@@ -526,7 +487,6 @@ class DatabricksQueryTool(BaseTool):
else len(all_values)
)
# Extract values for this row
row_values = all_values[start_idx:end_idx]
# Special handling for Netflix title data
@@ -535,9 +495,7 @@ class DatabricksQueryTool(BaseTool):
"Title" in columns
and len(row_values) > expected_column_count
):
# Try to reconstruct by looking for patterns
# We know ID is first, then Title (which may be split)
# Then other fields like Genre, etc.
# Take first value as ID
row_dict = {columns[0]: row_values[0]}
@@ -546,7 +504,6 @@ class DatabricksQueryTool(BaseTool):
title_end_idx = 1
for j in range(2, min(100, len(row_values))):
val = row_values[j]
# Check for common genres or non-title markers
if isinstance(val, str) and val in [
"Comedy",
"Drama",
@@ -562,7 +519,6 @@ class DatabricksQueryTool(BaseTool):
# Reconstruct title from individual characters
if title_end_idx > 1:
title_chars = row_values[1:title_end_idx]
# Check if they're individual characters
if all(
isinstance(c, str) and len(c) == 1
for c in title_chars
@@ -607,24 +563,21 @@ class DatabricksQueryTool(BaseTool):
)
if title_idx >= 0:
# Try to detect if title is split across multiple values
i = 0
while i < len(all_values):
# Check if this could be an ID (start of a row)
if isinstance(
all_values[i], str
) and id_pattern.match(all_values[i]):
row_dict = {columns[0]: all_values[i]}
i += 1
# Try to reconstruct title if it appears to be split
title_chars = []
while (
i < len(all_values)
and isinstance(all_values[i], str)
and len(all_values[i]) <= 1
and len(title_chars) < 100
): # Cap title length
):
title_chars.append(all_values[i])
i += 1
@@ -633,7 +586,6 @@ class DatabricksQueryTool(BaseTool):
title_chars
)
# Add remaining fields
for j in range(title_idx + 1, len(columns)):
if i < len(all_values):
row_dict[columns[j]] = all_values[i]
@@ -655,7 +607,6 @@ class DatabricksQueryTool(BaseTool):
]
for chunk in chunks:
# Skip chunks that seem to be partial/incomplete rows
if (
len(chunk) < expected_column_count * 0.75
): # Allow for some missing values
@@ -663,7 +614,6 @@ class DatabricksQueryTool(BaseTool):
row_dict = {}
# Map values to column names
for i, col in enumerate(columns):
if i < len(chunk):
row_dict[col] = chunk[i]
@@ -672,7 +622,6 @@ class DatabricksQueryTool(BaseTool):
reconstructed_rows.append(row_dict)
# Apply post-processing to fix known issues
if reconstructed_rows and "Title" in columns:
for row in reconstructed_rows:
# Fix titles that might still have issues
@@ -680,7 +629,6 @@ class DatabricksQueryTool(BaseTool):
isinstance(row.get("Title"), str)
and len(row.get("Title")) <= 1 # type: ignore[arg-type]
):
# This is likely still a fragmented title - mark as potentially incomplete
row["Title"] = f"[INCOMPLETE] {row.get('Title')}"
# Ensure we respect the row limit
@@ -689,18 +637,13 @@ class DatabricksQueryTool(BaseTool):
chunk_results = reconstructed_rows
else:
# Process normal result structure as before
# Check different result structures
if (
hasattr(result.result, "data_array")
and result.result.data_array # type: ignore[union-attr]
):
# Check if data appears to be malformed within chunks
for _chunk_idx, chunk in enumerate(
result.result.data_array # type: ignore[union-attr]
):
# Check if chunk might actually contain individual columns of a single row
# This is another way data might be malformed - check the first few values
if len(chunk) > 0 and len(columns) > 1:
# If there seems to be a mismatch between chunk structure and expected columns
@@ -714,10 +657,9 @@ class DatabricksQueryTool(BaseTool):
len(chunk) > len(columns) * 3
): # Heuristic: if chunk has way more items than columns
# This chunk might actually be values of multiple rows - try to reconstruct
values = chunk # All values in this chunk
values = chunk
reconstructed_rows = []
# Try to create rows based on expected column count
for i in range(
0, len(values), len(columns)
):
@@ -739,7 +681,7 @@ class DatabricksQueryTool(BaseTool):
if reconstructed_rows:
chunk_results.extend(reconstructed_rows)
continue # Skip normal processing for this chunk
continue
# Special case: when chunk contains exactly the right number of values for a single row
# This handles the case where instead of a list of rows, we just got all values in a flat list
@@ -752,12 +694,9 @@ class DatabricksQueryTool(BaseTool):
len(chunk) > 0
and len(chunk) % len(columns) == 0
):
# Process flat list of values as rows
for i in range(0, len(chunk), len(columns)):
row_values = chunk[i : i + len(columns)]
if len(row_values) == len(
columns
): # Only process complete rows
if len(row_values) == len(columns):
row_dict = {
col: val
for col, val in zip(
@@ -768,25 +707,19 @@ class DatabricksQueryTool(BaseTool):
}
chunk_results.append(row_dict)
# Skip regular row processing for this chunk
continue
# Normal processing for typical row structure
for _row_idx, row in enumerate(chunk):
# Ensure row is actually a collection of values
if not isinstance(row, (list, tuple, dict)):
# This might be a single value; skip it or handle specially
continue
# Convert each row to a dictionary with column names as keys
row_dict = {}
# Handle dict rows directly
if isinstance(row, dict):
# Use the existing column mapping
row_dict = dict(row)
elif isinstance(row, (list, tuple)):
# Map list of values to columns
for i, val in enumerate(row):
if (
i < len(columns)
@@ -798,7 +731,6 @@ class DatabricksQueryTool(BaseTool):
row_dict[dynamic_col] = val
all_columns.add(dynamic_col)
# If we have fewer values than columns, set missing values to None
for col in columns:
if col not in row_dict:
row_dict[col] = None
@@ -824,7 +756,6 @@ class DatabricksQueryTool(BaseTool):
row_dict[dynamic_col] = val
all_columns.add(dynamic_col)
# If we have fewer values than columns, set missing values to None
for i, col in enumerate(columns):
if i >= len(row):
row_dict[col] = None
@@ -840,7 +771,6 @@ class DatabricksQueryTool(BaseTool):
}
normalized_results.append(normalized_row)
# Replace the original results with normalized ones
chunk_results = normalized_results
except Exception as results_error:
@@ -856,7 +786,6 @@ class DatabricksQueryTool(BaseTool):
if "SUCCEEDED" in state_value:
return "Query executed successfully (no results to display)"
# Format and return results
return self._format_results(chunk_results) # type: ignore[arg-type]
except Exception as e:

View File

@@ -37,11 +37,9 @@ class FileWriterTool(BaseTool):
filepath = os.path.join(directory, filename)
# Prevent path traversal: the resolved path must be strictly inside
# the resolved directory. This blocks ../sequences, absolute paths in
# filename, and symlink escapes regardless of how directory is set.
# is_relative_to() does a proper path-component comparison that is
# safe on case-insensitive filesystems and avoids the "// " edge case
# that plagues startswith(real_directory + os.sep).
# We also reject the case where filepath resolves to the directory
# itself, since that is not a valid file target.
real_directory = Path(directory).resolve()

View File

@@ -93,11 +93,9 @@ class FileCompressorTool(BaseTool):
def _generate_output_path(input_path: str, format: str) -> str:
"""Generates output path based on input path and format."""
if os.path.isfile(input_path):
base_name = os.path.splitext(os.path.basename(input_path))[
0
] # Remove extension
base_name = os.path.splitext(os.path.basename(input_path))[0]
else:
base_name = os.path.basename(os.path.normpath(input_path)) # Directory name
base_name = os.path.basename(os.path.normpath(input_path))
return os.path.join(os.getcwd(), f"{base_name}.{format}")
@staticmethod

View File

@@ -57,7 +57,7 @@ class FirecrawlScrapeWebsiteTool(BaseTool):
"only_main_content": True,
"include_tags": [],
"exclude_tags": [],
"max_age": 172800000, # 2 days cache
"max_age": 172800000,
"headers": {},
"wait_for": 0,
"mobile": False,

View File

@@ -67,7 +67,7 @@ class InvokeCrewAIAutomationTool(BaseTool):
crew_api_url: str
crew_bearer_token: str
max_polling_time: int = 10 * 60 # 10 minutes
max_polling_time: int = 10 * 60
def __init__(
self,
@@ -88,12 +88,9 @@ class InvokeCrewAIAutomationTool(BaseTool):
max_polling_time: Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes)
crew_inputs: Optional dictionary defining custom input schema fields
"""
# Create dynamic args_schema if custom inputs provided
if crew_inputs:
# Start with the base prompt field
fields = {}
# Add custom fields
for field_name, field_def in crew_inputs.items():
if isinstance(field_def, tuple):
fields[field_name] = field_def
@@ -101,12 +98,10 @@ class InvokeCrewAIAutomationTool(BaseTool):
# Assume it's a Field object, extract type from annotation if available
fields[field_name] = (str, field_def)
# Create dynamic model
args_schema = create_model("DynamicInvokeCrewAIAutomationInput", **fields) # type: ignore[call-overload]
else:
args_schema = InvokeCrewAIAutomationInput
# Initialize the parent class with proper field values
super().__init__(
name=crew_name,
description=crew_description,
@@ -162,7 +157,6 @@ class InvokeCrewAIAutomationTool(BaseTool):
if kwargs is None:
kwargs = {}
# Start the crew
response = self._kickoff_crew(inputs=kwargs)
kickoff_id: str | None = response.get("kickoff_id")
@@ -178,7 +172,7 @@ class InvokeCrewAIAutomationTool(BaseTool):
if status_response.get("state", "").lower() == "failed":
return f"Error: Crew task failed. Response: {status_response}"
except Exception as e:
if i == self.max_polling_time - 1: # Last attempt
if i == self.max_polling_time - 1:
return f"Error: Failed to get crew status after {self.max_polling_time} attempts. Last error: {e}"
time.sleep(1)

View File

@@ -91,7 +91,6 @@ class MergeAgentHandlerTool(BaseTool):
if params:
payload["params"] = params
# Log the full payload for debugging
logger.debug(f"MCP Request to {url}: {json.dumps(payload, indent=2)}")
try:
@@ -99,7 +98,6 @@ class MergeAgentHandlerTool(BaseTool):
response.raise_for_status()
result = response.json()
# Handle JSON-RPC error responses
if "error" in result:
error_msg = result["error"].get("message", "Unknown error")
error_code = result["error"].get("code", -1)
@@ -119,20 +117,16 @@ class MergeAgentHandlerTool(BaseTool):
def _run(self, **kwargs: Any) -> Any:
"""Execute the Agent Handler tool with the given arguments."""
try:
# Log what we're about to send
logger.info(f"Executing {self.tool_name} with arguments: {kwargs}")
# Make the tool call via MCP
result = self._make_mcp_request(
method="tools/call",
params={"name": self.tool_name, "arguments": kwargs},
)
# Extract the actual result from the MCP response
if "result" in result and "content" in result["result"]:
content = result["result"]["content"]
if content and len(content) > 0:
# Parse the text content (it's JSON-encoded)
text_content = content[0].get("text", "")
try:
return json.loads(text_content)
@@ -176,10 +170,8 @@ class MergeAgentHandlerTool(BaseTool):
... registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa",
... )
"""
# Create an empty args schema model (proper BaseModel subclass)
empty_args_schema = create_model(f"{tool_name.replace('__', '_').title()}Args")
# Initialize session and get tool schema
instance = cls(
name=tool_name,
description=f"Execute {tool_name} via Agent Handler",
@@ -191,7 +183,6 @@ class MergeAgentHandlerTool(BaseTool):
**kwargs,
)
# Try to fetch the actual tool schema from Agent Handler
try:
result = instance._make_mcp_request(method="tools/list")
if "result" in result and "tools" in result["result"]:
@@ -222,7 +213,6 @@ class MergeAgentHandlerTool(BaseTool):
field_type: Any = Any
field_default: Any = ...
# Map JSON schema types to Python types
json_type = field_schema.get("type", "string")
if json_type == "string":
field_type = str
@@ -237,7 +227,6 @@ class MergeAgentHandlerTool(BaseTool):
elif json_type == "object":
field_type = dict[str, Any]
# Make field optional if not required
if field_name not in required:
field_type = field_type | None
field_default = None
@@ -303,7 +292,6 @@ class MergeAgentHandlerTool(BaseTool):
... tool_names=["linear__create_issue", "linear__get_issues"],
... )
"""
# Create a temporary instance to fetch the tool list
temp_instance = cls(
name="temp",
description="temp",
@@ -315,7 +303,6 @@ class MergeAgentHandlerTool(BaseTool):
)
try:
# Fetch available tools
result = temp_instance._make_mcp_request(method="tools/list")
if "result" not in result or "tools" not in result["result"]:
@@ -325,13 +312,11 @@ class MergeAgentHandlerTool(BaseTool):
available_tools = result["result"]["tools"]
# Filter tools if specific names were requested
if tool_names:
available_tools = [
t for t in available_tools if t.get("name") in tool_names
]
# Check if all requested tools were found
found_names = {t.get("name") for t in available_tools}
missing_names = set(tool_names) - found_names
if missing_names:
@@ -339,7 +324,6 @@ class MergeAgentHandlerTool(BaseTool):
f"The following tools were not found in the Tool Pack: {missing_names}"
)
# Create tool instances
tools = []
for tool_schema in available_tools:
tool_name = tool_schema.get("name")

View File

@@ -260,7 +260,6 @@ class MongoDBVectorSearchTool(BaseTool):
)
]
operations = [ReplaceOne({"_id": doc["_id"]}, doc, upsert=True) for doc in docs]
# insert the documents in MongoDB Atlas
result = self._coll.bulk_write(operations)
if result.upserted_ids is None:
raise ValueError("No documents were inserted.")
@@ -277,7 +276,6 @@ class MongoDBVectorSearchTool(BaseTool):
include_embeddings = query_config.include_embeddings
post_filter_pipeline = query_config.post_filter_pipeline
# Create the embedding for the query
query_vector = self._embed_texts([query])[0]
# Atlas Vector Search, potentially with filter
@@ -296,7 +294,6 @@ class MongoDBVectorSearchTool(BaseTool):
{"$set": {"score": {"$meta": "vectorSearchScore"}}},
]
# Remove embeddings unless requested
if not include_embeddings:
pipeline.append({"$project": {self.embedding_key: 0}})
@@ -308,7 +305,6 @@ class MongoDBVectorSearchTool(BaseTool):
cursor = self._coll.aggregate(pipeline) # type: ignore[arg-type]
docs = []
# Format
for doc in cursor:
docs.append(doc) # noqa: PERF402
return json_util.dumps(docs)

View File

@@ -8,7 +8,6 @@ os.environ["OPENAI_API_KEY"] = "Your Key"
multion_browse_tool = MultiOnTool(api_key="Your Key")
# Create a new agent
Browser = Agent(
role="Browser Agent",
goal="control web browsers using natural language ",
@@ -17,7 +16,6 @@ Browser = Agent(
verbose=True,
)
# Define tasks
browse = Task(
description="Summarize the top 3 trending AI News headlines",
expected_output="A summary of the top 3 trending AI News headlines",

View File

@@ -80,7 +80,6 @@ _AS_PAREN_RE = re.compile(r"\bAS\s*\(", re.IGNORECASE)
def _iter_as_paren_matches(stmt: str) -> Iterator[re.Match[str]]:
"""Yield regex matches for ``AS\\s*(`` outside of string literals."""
# Build a set of character positions that are inside string literals.
in_string: set[int] = set()
i = 0
while i < len(stmt):
@@ -124,7 +123,6 @@ def _skip_string_literal(stmt: str, pos: int) -> int:
i = pos + 1
while i < len(stmt):
if stmt[i] == quote_char:
# Check for escaped quote ('')
if i + 1 < len(stmt) and stmt[i + 1] == quote_char:
i += 2
continue
@@ -290,9 +288,7 @@ class NL2SQLTool(BaseTool):
self.tables = tables
self.columns = data
# ------------------------------------------------------------------
# Query validation
# ------------------------------------------------------------------
def _validate_query(self, sql_query: str) -> None:
"""Raise ValueError if *sql_query* is not permitted under the current config.
@@ -323,7 +319,6 @@ class NL2SQLTool(BaseTool):
# EXPLAIN ANALYZE / EXPLAIN ANALYSE actually *executes* the underlying
# query. Resolve the real command so write operations are caught.
# Handles both space-separated ("EXPLAIN ANALYZE DELETE …") and
# parenthesized ("EXPLAIN (ANALYZE) DELETE …", "EXPLAIN (ANALYZE, VERBOSE) DELETE …").
# EXPLAIN ANALYZE actually executes the underlying query — resolve the
# real command so write operations are caught.
@@ -332,10 +327,8 @@ class NL2SQLTool(BaseTool):
if resolved:
command = resolved
# WITH starts a CTE. Read-only CTEs are fine; writable CTEs
# (e.g. WITH d AS (DELETE …) SELECT …) must be blocked in read-only mode.
if command == "WITH":
# Check for write commands inside CTE bodies.
write_found = _detect_writable_cte(stmt)
if write_found:
found = write_found
@@ -352,7 +345,6 @@ class NL2SQLTool(BaseTool):
)
return
# Check the main query after the CTE definitions.
main_query = _extract_main_query_after_cte(stmt)
if main_query:
main_cmd = main_query.split()[0].upper().rstrip(";")
@@ -404,9 +396,7 @@ class NL2SQLTool(BaseTool):
first_token = stripped.split()[0] if stripped.split() else ""
return first_token.upper().rstrip(";")
# ------------------------------------------------------------------
# Schema introspection helpers
# ------------------------------------------------------------------
def _fetch_available_tables(self) -> list[dict[str, Any]] | str:
return self.execute_sql(
@@ -428,9 +418,7 @@ class NL2SQLTool(BaseTool):
params={"table_name": table_name},
)
# ------------------------------------------------------------------
# Core execution
# ------------------------------------------------------------------
def _run(self, sql_query: str) -> list[dict[str, Any]] | str:
try:
@@ -497,7 +485,6 @@ class NL2SQLTool(BaseTool):
try:
result = session.execute(text(sql_query), params or {})
# Only commit when the operation actually mutates state
if self.allow_dml and is_write:
session.commit()

View File

@@ -107,7 +107,6 @@ class OxylabsAmazonProductScraperTool(BaseTool):
username, password = self._get_credentials_from_env()
if OXYLABS_AVAILABLE:
# import RealtimeClient to make it accessible for the current scope
from oxylabs import RealtimeClient
kwargs["oxylabs_api"] = RealtimeClient(

View File

@@ -109,7 +109,6 @@ class OxylabsAmazonSearchScraperTool(BaseTool):
username, password = self._get_credentials_from_env()
if OXYLABS_AVAILABLE:
# import RealtimeClient to make it accessible for the current scope
from oxylabs import RealtimeClient
kwargs["oxylabs_api"] = RealtimeClient(

View File

@@ -112,7 +112,6 @@ class OxylabsGoogleSearchScraperTool(BaseTool):
username, password = self._get_credentials_from_env()
if OXYLABS_AVAILABLE:
# import RealtimeClient to make it accessible for the current scope
from oxylabs import RealtimeClient
kwargs["oxylabs_api"] = RealtimeClient(

View File

@@ -103,7 +103,6 @@ class OxylabsUniversalScraperTool(BaseTool):
username, password = self._get_credentials_from_env()
if OXYLABS_AVAILABLE:
# import RealtimeClient to make it accessible for the current scope
from oxylabs import RealtimeClient
kwargs["oxylabs_api"] = RealtimeClient(

View File

@@ -11,7 +11,6 @@ from patronus_local_evaluator_tool import ( # type: ignore[import-not-found]
)
# Test the PatronusLocalEvaluatorTool where agent uses the local evaluator
client = Client()
@@ -41,7 +40,6 @@ patronus_eval_tool = PatronusLocalEvaluatorTool(
evaluated_model_gold_answer="example label",
)
# Create a new agent
coding_agent = Agent(
role="Coding Agent",
goal="Generate high quality code and verify that the output is code by using Patronus AI's evaluation tool.",
@@ -50,7 +48,6 @@ coding_agent = Agent(
verbose=True,
)
# Define tasks
generate_code = Task(
description="Create a simple program to generate the first N numbers in the Fibonacci sequence. Select the most appropriate evaluator and criteria for evaluating your output.",
expected_output="Program that generates the first N numbers in the Fibonacci sequence.",

View File

@@ -119,7 +119,6 @@ class PatronusEvalTool(BaseTool):
evaluated_model_retrieved_context: str | None,
evaluators: list[dict[str, str]],
) -> Any:
# Assert correct format of evaluators
evals = []
for ev in evaluators:
evals.append( # noqa: PERF401

View File

@@ -103,7 +103,6 @@ class PatronusLocalEvaluatorTool(BaseTool):
try:
# Only rebuild if the class hasn't been initialized yet
if not hasattr(PatronusLocalEvaluatorTool, "_model_rebuilt"):
PatronusLocalEvaluatorTool.model_rebuild()
PatronusLocalEvaluatorTool._model_rebuilt = True # type: ignore[attr-defined]

View File

@@ -43,7 +43,6 @@ class QdrantVectorSearchTool(BaseTool):
model_config = ConfigDict(arbitrary_types_allowed=True)
# --- Metadata ---
name: str = "QdrantVectorSearchTool"
description: str = "Search Qdrant vector DB for relevant documents."
args_schema: type[BaseModel] = QdrantToolSchema
@@ -68,7 +67,6 @@ class QdrantVectorSearchTool(BaseTool):
@model_validator(mode="after")
def _setup_qdrant(self) -> QdrantVectorSearchTool:
# Import the qdrant_package if it's a string
if isinstance(self.qdrant_package, str):
self.qdrant_package = importlib.import_module(self.qdrant_package)

View File

@@ -125,7 +125,6 @@ class ScrapegraphScrapeTool(BaseTool):
if user_prompt is not None:
self.user_prompt = user_prompt
# Configure logging only if enabled
if self.enable_logging:
sgai_logger.set_logging(level="INFO")
@@ -170,11 +169,9 @@ class ScrapegraphScrapeTool(BaseTool):
if not website_url:
raise ValueError("website_url is required")
# Validate URL format
self._validate_url(website_url)
try:
# Make the SmartScraper request
if self._client is None:
raise RuntimeError("Client not initialized")
return self._client.smartscraper(

View File

@@ -192,7 +192,6 @@ class SeleniumScrapingTool(BaseTool):
if not url:
raise ValueError("URL cannot be empty")
# Validate URL format
if not re.match(r"^https?://", url):
raise ValueError("URL must start with http:// or https://")

View File

@@ -49,16 +49,13 @@ class SerperScrapeWebsiteTool(BaseTool):
# Serper API endpoint
api_url = "https://scrape.serper.dev"
# Get API key from environment variable for security
api_key = os.getenv("SERPER_API_KEY")
# Prepare the payload
payload = json.dumps({"url": url, "includeMarkdown": include_markdown})
# Set headers
headers = {"X-API-KEY": api_key or "", "Content-Type": "application/json"}
# Make the API request
response = requests.post(
api_url,
headers=headers,
@@ -66,11 +63,9 @@ class SerperScrapeWebsiteTool(BaseTool):
timeout=30,
)
# Check if request was successful
if response.status_code == 200:
result = response.json()
# Extract the scraped content
if "text" in result:
return str(result["text"])
return f"Successfully scraped {url}, but no text content found in response: {response.text}"

View File

@@ -61,7 +61,6 @@ class SerplyJobSearchTool(RagTool):
elif search_query is not None:
query_payload["q"] = search_query
# build the url
url = f"{self.request_url}{urlencode(query_payload)}"
response = requests.request("GET", url, headers=self.headers, timeout=30)

View File

@@ -53,7 +53,6 @@ class SerplyNewsSearchTool(BaseTool):
self,
**kwargs: Any,
) -> Any:
# build query parameters
query_payload = {}
if "query" in kwargs:
@@ -61,7 +60,6 @@ class SerplyNewsSearchTool(BaseTool):
elif "search_query" in kwargs:
query_payload["q"] = kwargs["search_query"]
# build the url
url = f"{self.search_url}{urlencode(query_payload)}"
response = requests.request(

View File

@@ -64,7 +64,6 @@ class SerplyScholarSearchTool(BaseTool):
elif "search_query" in kwargs:
query_payload["q"] = kwargs["search_query"]
# build the url
url = f"{self.search_url}{urlencode(query_payload)}"
response = requests.request(

View File

@@ -58,7 +58,6 @@ class SerplyWebSearchTool(BaseTool):
self.device_type = device_type
self.proxy_location = proxy_location
# build query parameters
self.query_payload = {
"num": limit,
"gl": proxy_location.upper(),
@@ -80,7 +79,6 @@ class SerplyWebSearchTool(BaseTool):
elif "search_query" in kwargs:
self.query_payload["q"] = kwargs["search_query"] # type: ignore[index]
# build the url
url = f"{self.search_url}{urlencode(self.query_payload)}" # type: ignore[arg-type]
response = requests.request(

View File

@@ -123,7 +123,6 @@ class SingleStoreSearchTool(BaseTool):
def __init__(
self,
tables: list[str] | None = None,
# Basic connection parameters
host: str | None = None,
user: str | None = None,
password: str | None = None,
@@ -147,7 +146,6 @@ class SingleStoreSearchTool(BaseTool):
conv: dict[int, Callable[..., Any]] | None = None,
credential_type: str | None = None,
autocommit: bool | None = None,
# Result formatting options
results_type: str | None = None,
buffered: bool | None = None,
results_format: str | None = None,
@@ -210,13 +208,10 @@ class SingleStoreSearchTool(BaseTool):
"`singlestore` package not found, please run `uv add crewai-tools[singlestore]`"
)
# Set the data type for the parent class
kwargs["data_type"] = "singlestore"
super().__init__(**kwargs)
# Build connection arguments dictionary with sensible defaults
self.connection_args = {
# Basic connection parameters
"host": host,
"user": user,
"password": password,
@@ -240,7 +235,6 @@ class SingleStoreSearchTool(BaseTool):
"conv": conv or {},
"credential_type": credential_type,
"autocommit": autocommit,
# Result formatting
"results_type": results_type,
"buffered": buffered,
"results_format": results_format,
@@ -266,13 +260,11 @@ class SingleStoreSearchTool(BaseTool):
):
self.connection_args["conn_attrs"] = dict()
# Add tool identification to connection attributes
self.connection_args["conn_attrs"]["_connector_name"] = (
"crewAI SingleStore Tool"
)
self.connection_args["conn_attrs"]["_connector_version"] = "1.0"
# Initialize connection pool for efficient connection management
self.connection_pool = QueuePool(
creator=self._create_connection,
pool_size=pool_size or 5,
@@ -280,7 +272,6 @@ class SingleStoreSearchTool(BaseTool):
timeout=timeout or 30.0,
)
# Validate database schema and initialize table information
self._initialize_tables(tables)
def _initialize_tables(self, tables: list[str]) -> None:
@@ -295,22 +286,18 @@ class SingleStoreSearchTool(BaseTool):
conn = self._get_connection()
try:
with conn.cursor() as cursor:
# Get all existing tables in the database
cursor.execute("SHOW TABLES")
existing_tables = {table[0] for table in cursor.fetchall()}
# Validate that the database has tables
if not existing_tables or len(existing_tables) == 0:
raise ValueError(
"No tables found in the database. "
"Please ensure the database is initialized with the required tables."
)
# Use all tables if none specified
if not tables or len(tables) == 0:
tables = list(existing_tables)
# Build table definitions for description
table_definitions = []
for table in tables:
if table not in existing_tables:
@@ -319,7 +306,6 @@ class SingleStoreSearchTool(BaseTool):
f"Please ensure the table is created."
)
# Get column information for each table
cursor.execute(f"SHOW COLUMNS FROM {table}")
columns = cursor.fetchall()
column_info = ", ".join(f"{row[0]} {row[1]}" for row in columns)
@@ -328,7 +314,6 @@ class SingleStoreSearchTool(BaseTool):
# Ensure the connection is returned to the pool
conn.close()
# Update the tool description with actual table information
self.description = (
f"A tool that can be used to semantic search a query from a SingleStore "
f"database's {', '.join(table_definitions)} table(s) content."
@@ -379,11 +364,9 @@ class SingleStoreSearchTool(BaseTool):
Returns:
tuple: (is_valid: bool, message: str)
"""
# Check if the input is a string
if not isinstance(search_query, str):
return False, "Search query must be a string."
# Remove leading/trailing whitespace and convert to lowercase for checking
query_lower = search_query.strip().lower()
# Allow only SELECT and SHOW statements
@@ -405,25 +388,20 @@ class SingleStoreSearchTool(BaseTool):
Returns:
str: Formatted search results or error message
"""
# Validate the query before execution
valid, message = self._validate_query(search_query)
if not valid:
return f"Invalid search query: {message}"
# Execute the query using a connection from the pool
conn = self._get_connection()
try:
with conn.cursor() as cursor:
try:
# Execute the validated search query
cursor.execute(search_query)
results = cursor.fetchall()
# Handle empty results
if not results:
return "No results found."
# Format the results for readable output
formatted_results = "\n".join(
[", ".join([str(item) for item in row]) for row in results]
)

View File

@@ -11,7 +11,6 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr
if TYPE_CHECKING:
# Import types for type checking only
from snowflake.connector.connection import (
SnowflakeConnection,
)
@@ -29,7 +28,6 @@ try:
except ImportError:
SNOWFLAKE_AVAILABLE = False
# Configure logging
logger = logging.getLogger(__name__)
# Cache for query results
@@ -257,7 +255,6 @@ class SnowflakeSearchTool(BaseTool):
) -> Any:
"""Execute the search query."""
try:
# Override database/schema if provided
if database:
await self._execute_query(f"USE DATABASE {database}")
if snowflake_schema:
@@ -284,7 +281,6 @@ class SnowflakeSearchTool(BaseTool):
try:
# Only rebuild if the class hasn't been initialized yet
if not hasattr(SnowflakeSearchTool, "_model_rebuilt"):
SnowflakeSearchTool.model_rebuild()
SnowflakeSearchTool._model_rebuilt = True

View File

@@ -28,23 +28,19 @@ from crewai_tools import StagehandTool
_printer = Printer()
# Load environment variables from .env file
load_dotenv()
# Get API keys from environment variables
# You can set these in your shell or in a .env file
browserbase_api_key = os.environ.get("BROWSERBASE_API_KEY")
browserbase_project_id = os.environ.get("BROWSERBASE_PROJECT_ID")
model_api_key = os.environ.get("OPENAI_API_KEY") # or OPENAI_API_KEY
model_api_key = os.environ.get("OPENAI_API_KEY")
# Initialize the StagehandTool with your credentials and use context manager
with StagehandTool(
api_key=browserbase_api_key, # New parameter naming
project_id=browserbase_project_id, # New parameter naming
api_key=browserbase_api_key,
project_id=browserbase_project_id,
model_api_key=model_api_key,
model_name=AvailableModel.GPT_4O, # Using the enum from schemas
model_name=AvailableModel.GPT_4O,
) as stagehand_tool:
# Create a web researcher agent with the StagehandTool
researcher = Agent(
role="Web Researcher",
goal="Find and extract information from websites using different Stagehand primitives",
@@ -74,7 +70,6 @@ with StagehandTool(
tools=[stagehand_tool],
)
# Define a research task that demonstrates all three primitives
research_task = Task(
description=(
"Demonstrate Stagehand capabilities by performing the following steps:\n"
@@ -104,7 +99,6 @@ with StagehandTool(
agent=researcher,
)
# Set up the crew
crew = Crew(
agents=[researcher],
tasks=[research_task], # You can switch this to web_research_task if you prefer
@@ -112,7 +106,6 @@ with StagehandTool(
process=Process.sequential,
)
# Run the crew and get the result
result = crew.kickoff()
_printer.print("\n==== RESULTS ====\n", color="cyan")

View File

@@ -11,7 +11,6 @@ from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, Field
# Define a flag to track whether stagehand is available
_HAS_STAGEHAND = False
try:
@@ -37,7 +36,6 @@ except ImportError:
ExtractOptions = Any
ObserveOptions = Any
# Mock configure_logging function
def configure_logging(
level: str | None = None,
remove_logger_name: bool | None = None,
@@ -45,7 +43,6 @@ except ImportError:
) -> None:
pass
# Define only what's needed for class defaults
class AvailableModel: # type: ignore[no-redef]
CLAUDE_3_7_SONNET_LATEST = "anthropic.claude-3-7-sonnet-20240607"
@@ -203,7 +200,6 @@ class StagehandTool(BaseTool):
self._testing = _testing
super().__init__(**kwargs)
# Set up logger
import logging
self._logger = logging.getLogger(__name__)
@@ -231,7 +227,6 @@ class StagehandTool(BaseTool):
self._session_id = session_id
# Configure logging based on verbosity level
if not self._testing:
log_level = {1: "INFO", 2: "WARNING", 3: "DEBUG"}.get(self.verbose, "ERROR")
configure_logging(
@@ -263,7 +258,6 @@ class StagehandTool(BaseTool):
def _get_model_api_key(self) -> str | None:
"""Get the appropriate API key based on the model being used."""
# Check model type and get appropriate key
model_str = str(self.model_name)
if "gpt" in model_str.lower():
return self.model_api_key or os.getenv("OPENAI_API_KEY")
@@ -280,10 +274,9 @@ class StagehandTool(BaseTool):
async def _setup_stagehand(self, session_id: str | None = None) -> tuple[Any, Any]:
"""Initialize Stagehand if not already set up."""
# If we're in testing mode, return mock objects
if self._testing:
if not self._stagehand:
# Create mock objects for testing
class MockPage:
async def act(self, options: Any) -> Any:
mock_result = type("MockResult", (), {})()
@@ -331,7 +324,6 @@ class StagehandTool(BaseTool):
# Normal initialization for non-testing mode
if not self._stagehand:
# Get the appropriate API key based on model type
model_api_key = self._get_model_api_key()
if not model_api_key:
@@ -339,7 +331,6 @@ class StagehandTool(BaseTool):
"No appropriate API key found for model. Please set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY"
)
# Build the StagehandConfig with proper parameter names
config = StagehandConfig(
env="BROWSERBASE",
apiKey=self.api_key, # Browserbase API key (camelCase)
@@ -356,10 +347,8 @@ class StagehandTool(BaseTool):
browserbaseSessionID=session_id or self._session_id,
)
# Initialize Stagehand with config
self._stagehand = Stagehand(config=config) # type: ignore[call-arg]
# Initialize the Stagehand instance
await self._stagehand.init()
self._page = self._stagehand.page
self._session_id = self._stagehand.session_id
@@ -368,7 +357,6 @@ class StagehandTool(BaseTool):
def _extract_steps(self, instruction: str) -> list[str]:
"""Extract individual steps from multi-step instructions."""
# Check for numbered steps (Step 1:, Step 2:, etc.)
if re.search(r"Step \d+:", instruction, re.IGNORECASE):
steps = re.findall(
r"Step \d+:\s*([^;]+?)(?=Step \d+:|$)",
@@ -376,14 +364,12 @@ class StagehandTool(BaseTool):
re.IGNORECASE | re.DOTALL,
)
return [step.strip() for step in steps if step.strip()]
# Check for semicolon-separated instructions
if ";" in instruction:
return [step.strip() for step in instruction.split(";") if step.strip()]
return [instruction]
def _simplify_instruction(self, instruction: str) -> str:
"""Simplify complex instructions to basic actions."""
# Extract the core action from complex instructions
instruction_lower = instruction.lower()
if "search" in instruction_lower and "click" in instruction_lower:
@@ -392,7 +378,6 @@ class StagehandTool(BaseTool):
return "click on the search input field"
return "search for content on the page"
if "click" in instruction_lower:
# Extract what to click
if "button" in instruction_lower:
return "click the button"
if "link" in instruction_lower:
@@ -402,7 +387,7 @@ class StagehandTool(BaseTool):
return "click on the element"
if "type" in instruction_lower or "enter" in instruction_lower:
return "type in the input field"
return instruction # Return as-is if can't simplify
return instruction
async def _async_run(
self,
@@ -411,7 +396,6 @@ class StagehandTool(BaseTool):
command_type: str = "act",
) -> StagehandResult:
"""Override _async_run with improved atomic action handling."""
# Handle missing instruction based on command type
if not instruction:
if command_type == "navigate" and url:
instruction = f"Navigate to {url}"
@@ -439,7 +423,6 @@ class StagehandTool(BaseTool):
f"Executing {command_type} with instruction: {instruction}"
)
# Get the API key to pass to model operations
model_api_key = self._get_model_api_key()
model_client_options: dict[str, Any] = {"apiKey": model_api_key}
@@ -451,9 +434,7 @@ class StagehandTool(BaseTool):
# Small delay to ensure page is fully loaded
await asyncio.sleep(1)
# Process according to command type
if command_type.lower() == "act":
# Extract steps from complex instructions
steps = self._extract_steps(instruction)
self._logger.info(f"Extracted {len(steps)} steps: {steps}")
@@ -462,7 +443,6 @@ class StagehandTool(BaseTool):
self._logger.info(f"Executing step {i + 1}/{len(steps)}: {step}")
try:
# Create act options with API key for each step
from stagehand.schemas import ActOptions
act_options = ActOptions(
@@ -483,7 +463,6 @@ class StagehandTool(BaseTool):
error_msg = f"Step failed: {step_error}"
self._logger.warning(f"Step {i + 1} failed: {error_msg}")
# Try with simplified instruction
try:
simplified = self._simplify_instruction(step)
if simplified != step:
@@ -501,13 +480,11 @@ class StagehandTool(BaseTool):
result = await page.act(act_options)
results.append(result.model_dump())
else:
# If we can't simplify or retry fails, record the error
results.append({"error": error_msg, "step": step})
except Exception as retry_error:
self._logger.error(f"Retry also failed: {retry_error}")
results.append({"error": str(retry_error), "step": step})
# Return combined results
if len(results) == 1:
# Single step, return as-is
if "error" in results[0]:
@@ -537,7 +514,6 @@ class StagehandTool(BaseTool):
)
if command_type.lower() == "extract":
# Create extract options with API key
from stagehand.schemas import ExtractOptions
extract_options = ExtractOptions(
@@ -545,7 +521,7 @@ class StagehandTool(BaseTool):
modelName=self.model_name,
domSettleTimeoutMs=self.dom_settle_timeout_ms,
useTextExtract=True,
modelClientOptions=model_client_options, # Add API key here
modelClientOptions=model_client_options,
)
result = await page.extract(extract_options)
@@ -553,7 +529,6 @@ class StagehandTool(BaseTool):
return self._format_result(True, result.model_dump())
if command_type.lower() == "observe":
# Create observe options with API key
from stagehand.schemas import ObserveOptions
observe_options = ObserveOptions(
@@ -561,12 +536,11 @@ class StagehandTool(BaseTool):
modelName=self.model_name,
onlyVisible=True,
domSettleTimeoutMs=self.dom_settle_timeout_ms,
modelClientOptions=model_client_options, # Add API key here
modelClientOptions=model_client_options,
)
observe_results = await page.observe(observe_options)
# Format the observation results
formatted_results: list[dict[str, Any]] = []
for i, obs_result in enumerate(observe_results):
formatted_results.append(
@@ -616,7 +590,6 @@ class StagehandTool(BaseTool):
Returns:
The result of the browser automation task
"""
# Handle missing instruction based on command type
if not instruction:
if command_type == "navigate" and url:
instruction = f"Navigate to {url}"
@@ -626,7 +599,6 @@ class StagehandTool(BaseTool):
instruction = "Extract information from the page"
else:
instruction = "Perform the requested action"
# Create an event loop if we're not already in one
try:
loop = asyncio.get_event_loop()
if loop.is_running():
@@ -647,7 +619,6 @@ class StagehandTool(BaseTool):
self._async_run(instruction, url, command_type)
)
# Format the result for output
if result.success:
if command_type.lower() == "act":
if isinstance(result.data, dict) and "steps" in result.data:
@@ -696,7 +667,6 @@ class StagehandTool(BaseTool):
async def _async_close(self) -> None:
"""Asynchronously clean up Stagehand resources."""
# Skip for test mode
if self._testing:
self._stagehand = None
self._page = None
@@ -710,7 +680,6 @@ class StagehandTool(BaseTool):
def close(self) -> None:
"""Clean up Stagehand resources."""
# Skip actual closing for testing mode
if self._testing:
self._stagehand = None
self._page = None
@@ -741,7 +710,6 @@ class StagehandTool(BaseTool):
else:
close_method()
except Exception: # noqa: S110
# Log but don't raise - we're cleaning up
pass
self._stagehand = None

View File

@@ -25,7 +25,6 @@ class ImagePromptSchema(BaseModel):
if not path.exists():
raise ValueError(f"Image file does not exist: {v}")
# Validate supported formats
valid_extensions = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
if path.suffix.lower() not in valid_extensions:
raise ValueError(

View File

@@ -137,7 +137,6 @@ def test_context_manager_with_filtered_tools(echo_server_script):
assert len(tools) == 1
assert tools[0].name == "echo_tool"
assert tools[0].run(text="hello") == "Echo: hello"
# Check that calc_tool is not present
with pytest.raises(IndexError):
_ = tools[1]
with pytest.raises(KeyError):
@@ -152,7 +151,6 @@ def test_context_manager_sse_with_filtered_tools(echo_sse_server):
assert len(tools) == 1
assert tools[0].name == "calc_tool"
assert tools[0].run(a=10, b=5) == "15"
# Check that echo_tool is not present
with pytest.raises(IndexError):
_ = tools[1]
with pytest.raises(KeyError):

View File

@@ -10,7 +10,6 @@ def test_creating_a_tool_using_annotation():
"""Clear description for what this tool is useful for, you agent will need this information to use it."""
return question
# Assert all the right attributes were defined
assert my_tool.name == "Name of my tool"
assert (
my_tool.description
@@ -48,7 +47,6 @@ def test_creating_a_tool_using_baseclass():
return question
my_tool = MyCustomTool()
# Assert all the right attributes were defined
assert my_tool.name == "Name of my tool"
assert (
my_tool.description
@@ -87,7 +85,6 @@ def test_setting_cache_function():
return question
my_tool = MyCustomTool()
# Assert all the right attributes were defined
assert not my_tool.cache_function()
@@ -100,5 +97,4 @@ def test_default_cache_function_is_true():
return question
my_tool = MyCustomTool()
# Assert all the right attributes were defined
assert my_tool.cache_function()

View File

@@ -6,18 +6,15 @@ from crewai_tools import FileReadTool
def test_file_read_tool_constructor():
"""Test FileReadTool initialization with file_path."""
# Create a temporary test file
test_file = "/tmp/test_file.txt"
test_content = "Hello, World!"
with open(test_file, "w") as f:
f.write(test_content)
# Test initialization with file_path
tool = FileReadTool(file_path=test_file)
assert tool.file_path == test_file
assert "test_file.txt" in tool.description
# Clean up
os.remove(test_file)
@@ -28,7 +25,6 @@ def test_file_read_tool_run():
# Use mock_open to mock file operations
with patch("builtins.open", mock_open(read_data=test_content)):
# Test reading file with runtime file_path
tool = FileReadTool()
result = tool._run(file_path=test_file)
assert result == test_content
@@ -36,16 +32,13 @@ def test_file_read_tool_run():
def test_file_read_tool_error_handling():
"""Test FileReadTool error handling."""
# Test missing file path
tool = FileReadTool()
result = tool._run()
assert "Error: No file path provided" in result
# Test non-existent file
result = tool._run(file_path="/nonexistent/file.txt")
assert "Error: File not found at path:" in result
# Test permission error
with patch("builtins.open", side_effect=PermissionError()):
result = tool._run(file_path="/tmp/no_permission.txt")
assert "Error: Permission denied" in result
@@ -58,7 +51,6 @@ def test_file_read_tool_constructor_and_run():
content1 = "File 1 content"
content2 = "File 2 content"
# First test with content1
with patch("builtins.open", mock_open(read_data=content1)):
tool = FileReadTool(file_path=test_file1)
result = tool._run()
@@ -90,7 +82,6 @@ def test_file_read_tool_chunk_reading():
with patch("builtins.open", mock_open(read_data=file_content)):
tool = FileReadTool()
# Test reading a specific chunk (lines 3-5)
result = tool._run(file_path=test_file, start_line=3, line_count=3)
expected = "".join(lines[2:5]) # Lines are 0-indexed in the array
assert result == expected
@@ -120,7 +111,6 @@ def test_file_read_tool_chunk_error_handling():
with patch("builtins.open", mock_open(read_data=file_content)):
tool = FileReadTool()
# Test start_line exceeding file length
result = tool._run(file_path=test_file, start_line=10)
assert "Error: Start line 10 exceeds the number of lines in the file" in result
@@ -139,12 +129,10 @@ def test_file_read_tool_zero_or_negative_start_line():
with patch("builtins.open", mock_open(read_data=file_content)):
tool = FileReadTool()
# Test with start_line = None
result = tool._run(file_path=test_file, start_line=None)
expected = "".join(lines) # Should read the entire file
assert result == expected
# Test with start_line = 0
result = tool._run(file_path=test_file, start_line=0)
expected = "".join(lines) # Should read the entire file
assert result == expected
@@ -154,7 +142,6 @@ def test_file_read_tool_zero_or_negative_start_line():
expected = "".join(lines[0:3]) # Should read first 3 lines
assert result == expected
# Test with negative start_line
result = tool._run(file_path=test_file, start_line=-5)
expected = "".join(lines) # Should read the entire file
assert result == expected

View File

@@ -14,7 +14,7 @@ class TestDOCXLoader:
mock_doc.paragraphs = [
Mock(text="First paragraph"),
Mock(text="Second paragraph"),
Mock(text=" "), # Blank paragraph
Mock(text=" "),
]
mock_doc.tables = []
mock_docx_class.return_value = mock_doc

View File

@@ -65,24 +65,20 @@ class TestEmbeddingService:
"""Test getting default API keys from environment."""
service = EmbeddingService.__new__(EmbeddingService) # Create without __init__
# Test with environment variable set
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-openai-key"}):
api_key = service._get_default_api_key("openai")
assert api_key == "test-openai-key"
# Test with no environment variable
with patch.dict(os.environ, {}, clear=True):
api_key = service._get_default_api_key("openai")
assert api_key is None
# Test unknown provider
api_key = service._get_default_api_key("unknown-provider")
assert api_key is None
@patch('crewai.rag.embeddings.factory.build_embedder')
def test_initialization_success(self, mock_build_embedder):
"""Test successful initialization."""
# Mock the embedding function
mock_embedding_function = Mock()
mock_build_embedder.return_value = mock_embedding_function
@@ -97,7 +93,6 @@ class TestEmbeddingService:
assert service.config.api_key == "test-key"
assert service._embedding_function == mock_embedding_function
# Verify build_embedder was called with correct config
mock_build_embedder.assert_called_once()
call_args = mock_build_embedder.call_args[0][0]
assert call_args["provider"] == "openai"
@@ -115,7 +110,6 @@ class TestEmbeddingService:
@patch('crewai.rag.embeddings.factory.build_embedder')
def test_embed_text_success(self, mock_build_embedder):
"""Test successful text embedding."""
# Mock the embedding function
mock_embedding_function = Mock()
mock_embedding_function.return_value = [[0.1, 0.2, 0.3]]
mock_build_embedder.return_value = mock_embedding_function
@@ -147,7 +141,6 @@ class TestEmbeddingService:
@patch('crewai.rag.embeddings.factory.build_embedder')
def test_embed_batch_success(self, mock_build_embedder):
"""Test successful batch embedding."""
# Mock the embedding function
mock_embedding_function = Mock()
mock_embedding_function.return_value = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]
mock_build_embedder.return_value = mock_embedding_function
@@ -182,7 +175,6 @@ class TestEmbeddingService:
@patch('crewai.rag.embeddings.factory.build_embedder')
def test_validate_connection(self, mock_build_embedder):
"""Test connection validation."""
# Mock successful embedding
mock_embedding_function = Mock()
mock_embedding_function.return_value = [[0.1, 0.2, 0.3]]
mock_build_embedder.return_value = mock_embedding_function
@@ -191,14 +183,12 @@ class TestEmbeddingService:
assert service.validate_connection() is True
# Mock failed embedding
mock_embedding_function.side_effect = Exception("Connection failed")
assert service.validate_connection() is False
@patch('crewai.rag.embeddings.factory.build_embedder')
def test_get_service_info(self, mock_build_embedder):
"""Test getting service information."""
# Mock the embedding function
mock_embedding_function = Mock()
mock_embedding_function.return_value = [[0.1, 0.2, 0.3]]
mock_build_embedder.return_value = mock_embedding_function
@@ -277,7 +267,6 @@ class TestProviderConfigurations:
extra_config={"dimensions": 1024}
)
# Check the configuration passed to build_embedder
call_args = mock_build_embedder.call_args[0][0]
assert call_args["provider"] == "openai"
assert call_args["config"]["api_key"] == "test-key"
@@ -298,7 +287,6 @@ class TestProviderConfigurations:
extra_config={"input_type": "document"}
)
# Check the configuration passed to build_embedder
call_args = mock_build_embedder.call_args[0][0]
assert call_args["provider"] == "voyageai"
assert call_args["config"]["api_key"] == "test-key"
@@ -318,7 +306,6 @@ class TestProviderConfigurations:
api_key="test-key"
)
# Check the configuration passed to build_embedder
call_args = mock_build_embedder.call_args[0][0]
assert call_args["provider"] == "cohere"
assert call_args["config"]["api_key"] == "test-key"
@@ -335,7 +322,6 @@ class TestProviderConfigurations:
api_key="test-key"
)
# Check the configuration passed to build_embedder
call_args = mock_build_embedder.call_args[0][0]
assert call_args["provider"] == "google-generativeai"
assert call_args["config"]["api_key"] == "test-key"

View File

@@ -126,9 +126,6 @@ class TestJSONLoader:
finally:
os.unlink(path)
# ------------------------------
# URL-based tests
# ------------------------------
@patch("requests.get")
def test_url_response_valid_json(self, mock_get):

View File

@@ -45,7 +45,6 @@ class MockTool(BaseTool):
)
# --- Intermediate base class (like RagTool, BraveSearchToolBase) ---
class MockIntermediateBase(BaseTool):
"""Simulates an intermediate tool base class (e.g. RagTool, BraveSearchToolBase)."""

View File

@@ -51,9 +51,6 @@ def _mock_response(
return resp
# Fixtures
@pytest.fixture(autouse=True)
def _brave_env_and_rate_limit():
"""Set BRAVE_API_KEY for every test. Rate limiting is per-instance (each tool starts with a fresh clock)."""
@@ -81,8 +78,6 @@ def video_tool():
return BraveVideoSearchTool()
# Initialization
ALL_TOOL_CLASSES = [
BraveWebSearchTool,
BraveImageSearchTool,
@@ -343,7 +338,6 @@ def test_refine_request_payload_passes_multiple_goggles_as_multiple_params(web_t
# Null-like / empty value stripping
#
# crewAI's ensure_all_properties_required (pydantic_schema_utils.py) marks
# every schema property as required for OpenAI strict-mode compatibility.
# Because optional Brave API parameters look required to the LLM, it fills

View File

@@ -20,7 +20,6 @@ class TestBrightDataSearchTool(unittest.TestCase):
mock_response.text = "mock response text"
mock_post.return_value = mock_response
# Define search input
input_data = {
"query": "latest AI news",
"search_engine": "google",
@@ -46,7 +45,6 @@ class TestBrightDataSearchTool(unittest.TestCase):
self.assertIn("Error", result)
def tearDown(self):
# Clean up env vars
pass

Some files were not shown because too many files have changed in this diff Show More