mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-01 08:38:14 +00:00
Compare commits
1 Commits
main
...
iris/fix/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bd055fcf3 |
@@ -4,70 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="31 مارس 2026">
|
||||
## v1.13.0a5
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a5)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0a4
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="1 أبريل 2026">
|
||||
## v1.13.0a4
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a4)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0a3
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="1 أبريل 2026">
|
||||
## v1.13.0a3
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a3)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إصدار بيانات استخدام الرمز في LLMCallCompletedEvent
|
||||
- استخراج ونشر بيانات الأداة إلى AMP
|
||||
|
||||
### إصلاح الأخطاء
|
||||
- التعامل مع نماذج GPT-5.x التي لا تدعم معلمة API `stop`
|
||||
|
||||
### الوثائق
|
||||
- إصلاح عدم الدقة في قدرات الوكيل عبر جميع اللغات
|
||||
- إضافة نظرة عامة على قدرات الوكيل وتحسين وثائق المهارات
|
||||
- إضافة دليل شامل لتكوين SSO
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0rc1
|
||||
|
||||
### إعادة الهيكلة
|
||||
- تحويل Flow إلى Pydantic BaseModel
|
||||
- تحويل فئات LLM إلى Pydantic BaseModel
|
||||
- استبدال InstanceOf[T] بتعليقات نوع عادية
|
||||
- إزالة الطرق غير المستخدمة
|
||||
|
||||
## المساهمون
|
||||
|
||||
@dependabot[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @lucasgomide, @thiagomoretto
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 مارس 2026">
|
||||
## v1.13.0rc1
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
title: "قدرات الوكيل"
|
||||
description: "فهم الطرق الخمس لتوسيع وكلاء CrewAI: الأدوات، MCP، التطبيقات، المهارات، والمعرفة."
|
||||
icon: puzzle-piece
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
يمكن توسيع وكلاء CrewAI بـ **خمسة أنواع مميزة من القدرات**، كل منها يخدم غرضًا مختلفًا. فهم متى تستخدم كل نوع — وكيف يعملون معًا — هو المفتاح لبناء وكلاء فعّالين.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="الأدوات" icon="wrench" href="/ar/concepts/tools" color="#3B82F6">
|
||||
**دوال قابلة للاستدعاء** — تمنح الوكلاء القدرة على اتخاذ إجراءات. البحث على الويب، عمليات الملفات، استدعاءات API، تنفيذ الكود.
|
||||
</Card>
|
||||
<Card title="خوادم MCP" icon="plug" href="/ar/mcp/overview" color="#8B5CF6">
|
||||
**خوادم أدوات عن بُعد** — تربط الوكلاء بخوادم أدوات خارجية عبر Model Context Protocol. نفس تأثير الأدوات، لكن مستضافة خارجيًا.
|
||||
</Card>
|
||||
<Card title="التطبيقات" icon="grid-2" color="#EC4899">
|
||||
**تكاملات المنصة** — تربط الوكلاء بتطبيقات SaaS (Gmail، Slack، Jira، Salesforce) عبر منصة CrewAI. تعمل محليًا مع رمز تكامل المنصة.
|
||||
</Card>
|
||||
<Card title="المهارات" icon="bolt" href="/ar/concepts/skills" color="#F59E0B">
|
||||
**خبرة المجال** — تحقن التعليمات والإرشادات والمواد المرجعية في إرشادات الوكلاء. المهارات تخبر الوكلاء *كيف يفكرون*.
|
||||
</Card>
|
||||
<Card title="المعرفة" icon="book" href="/ar/concepts/knowledge" color="#10B981">
|
||||
**حقائق مُسترجعة** — توفر للوكلاء بيانات من المستندات والملفات وعناوين URL عبر البحث الدلالي (RAG). المعرفة تعطي الوكلاء *ما يحتاجون معرفته*.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
---
|
||||
|
||||
## التمييز الأساسي
|
||||
|
||||
أهم شيء يجب فهمه: **هذه القدرات تنقسم إلى فئتين**.
|
||||
|
||||
### قدرات الإجراء (الأدوات، MCP، التطبيقات)
|
||||
|
||||
تمنح الوكلاء القدرة على **فعل أشياء** — استدعاء APIs، قراءة الملفات، البحث على الويب، إرسال رسائل البريد الإلكتروني. عند التنفيذ، تتحول الأنواع الثلاثة إلى نفس التنسيق الداخلي (مثيلات `BaseTool`) وتظهر في قائمة أدوات موحدة يمكن للوكيل استدعاؤها.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find and compile market data",
|
||||
backstory="Expert market analyst",
|
||||
tools=[SerperDevTool(), FileReadTool()], # أدوات محلية
|
||||
mcps=["https://mcp.example.com/sse"], # أدوات خادم MCP عن بُعد
|
||||
apps=["gmail", "google_sheets"], # تكاملات المنصة
|
||||
)
|
||||
```
|
||||
|
||||
### قدرات السياق (المهارات، المعرفة)
|
||||
|
||||
تُعدّل **إرشادات** الوكيل — بحقن الخبرة أو التعليمات أو البيانات المُسترجعة قبل أن يبدأ الوكيل في التفكير. لا تمنح الوكلاء إجراءات جديدة؛ بل تُشكّل كيف يفكر الوكلاء وما هي المعلومات التي يمكنهم الوصول إليها.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Security Auditor",
|
||||
goal="Audit cloud infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security with 10 years of experience",
|
||||
skills=["./skills/security-audit"], # تعليمات المجال
|
||||
knowledge_sources=[pdf_source, url_source], # حقائق مُسترجعة
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## متى تستخدم ماذا
|
||||
|
||||
| تحتاج إلى... | استخدم | مثال |
|
||||
| :------------------------------------------------------- | :---------------- | :--------------------------------------- |
|
||||
| الوكيل يبحث على الويب | **الأدوات** | `tools=[SerperDevTool()]` |
|
||||
| الوكيل يستدعي API عن بُعد عبر MCP | **MCP** | `mcps=["https://api.example.com/sse"]` |
|
||||
| الوكيل يرسل بريد إلكتروني عبر Gmail | **التطبيقات** | `apps=["gmail"]` |
|
||||
| الوكيل يتبع إجراءات محددة | **المهارات** | `skills=["./skills/code-review"]` |
|
||||
| الوكيل يرجع لمستندات الشركة | **المعرفة** | `knowledge_sources=[pdf_source]` |
|
||||
| الوكيل يبحث على الويب ويتبع إرشادات المراجعة | **الأدوات + المهارات** | استخدم كليهما معًا |
|
||||
|
||||
---
|
||||
|
||||
## دمج القدرات
|
||||
|
||||
في الممارسة العملية، غالبًا ما يستخدم الوكلاء **أنواعًا متعددة من القدرات معًا**. إليك مثال واقعي:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
# وكيل بحث مجهز بالكامل
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Produce comprehensive market analysis reports",
|
||||
backstory="Expert analyst with deep industry knowledge",
|
||||
|
||||
# الإجراء: ما يمكن للوكيل فعله
|
||||
tools=[
|
||||
SerperDevTool(), # البحث على الويب
|
||||
FileReadTool(), # قراءة الملفات المحلية
|
||||
CodeInterpreterTool(), # تشغيل كود Python للتحليل
|
||||
],
|
||||
mcps=["https://data-api.example.com/sse"], # الوصول لـ API بيانات عن بُعد
|
||||
apps=["google_sheets"], # الكتابة في Google Sheets
|
||||
|
||||
# السياق: ما يعرفه الوكيل
|
||||
skills=["./skills/research-methodology"], # كيفية إجراء البحث
|
||||
knowledge_sources=[company_docs], # بيانات خاصة بالشركة
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## جدول المقارنة
|
||||
|
||||
| الميزة | الأدوات | MCP | التطبيقات | المهارات | المعرفة |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: |
|
||||
| **يمنح الوكيل إجراءات** | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **يُعدّل الإرشادات** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| **يتطلب كود** | نعم | إعداد فقط | إعداد فقط | Markdown فقط | إعداد فقط |
|
||||
| **يعمل محليًا** | نعم | يعتمد | نعم (مع متغير بيئة) | غير متاح | نعم |
|
||||
| **يحتاج مفاتيح API** | لكل أداة | لكل خادم | رمز التكامل | لا | المُضمّن فقط |
|
||||
| **يُعيَّن على Agent** | `tools=[]` | `mcps=[]` | `apps=[]` | `skills=[]` | `knowledge_sources=[]` |
|
||||
| **يُعيَّن على Crew** | ❌ | ❌ | ❌ | `skills=[]` | `knowledge_sources=[]` |
|
||||
|
||||
---
|
||||
|
||||
## تعمّق أكثر
|
||||
|
||||
هل أنت مستعد لمعرفة المزيد عن كل نوع من أنواع القدرات؟
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="الأدوات" icon="wrench" href="/ar/concepts/tools">
|
||||
إنشاء أدوات مخصصة، استخدام كتالوج OSS مع أكثر من 75 خيارًا، تكوين التخزين المؤقت والتنفيذ غير المتزامن.
|
||||
</Card>
|
||||
<Card title="تكامل MCP" icon="plug" href="/ar/mcp/overview">
|
||||
الاتصال بخوادم MCP عبر stdio أو SSE أو HTTP. تصفية الأدوات، تكوين المصادقة.
|
||||
</Card>
|
||||
<Card title="المهارات" icon="bolt" href="/ar/concepts/skills">
|
||||
بناء حزم المهارات مع SKILL.md، حقن خبرة المجال، استخدام الكشف التدريجي.
|
||||
</Card>
|
||||
<Card title="المعرفة" icon="book" href="/ar/concepts/knowledge">
|
||||
إضافة المعرفة من ملفات PDF وCSV وعناوين URL والمزيد. تكوين المُضمّنات والاسترجاع.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,217 +1,15 @@
|
||||
---
|
||||
title: المهارات
|
||||
description: حزم المهارات المبنية على نظام الملفات التي تحقن خبرة المجال والتعليمات في إرشادات الوكلاء.
|
||||
description: حزم المهارات المبنية على نظام الملفات التي تحقن السياق في إرشادات الوكيل.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
المهارات هي مجلدات مستقلة توفر للوكلاء **تعليمات وإرشادات ومواد مرجعية خاصة بالمجال**. تُعرّف كل مهارة بملف `SKILL.md` يحتوي على بيانات وصفية YAML ومحتوى Markdown.
|
||||
المهارات هي مجلدات مستقلة توفر للوكلاء تعليمات ومراجع وموارد خاصة بالمجال. تُعرّف كل مهارة بملف `SKILL.md` يحتوي على بيانات وصفية YAML ومحتوى Markdown.
|
||||
|
||||
عند التفعيل، يتم حقن تعليمات المهارة مباشرة في إرشادات مهمة الوكيل — مما يمنح الوكيل خبرة دون الحاجة لأي تغييرات في الكود.
|
||||
|
||||
<Note type="info" title="المهارات مقابل الأدوات — التمييز الأساسي">
|
||||
**المهارات ليست أدوات.** هذه هي نقطة الارتباك الأكثر شيوعًا.
|
||||
|
||||
- **المهارات** تحقن *تعليمات وسياق* في إرشادات الوكيل. تخبر الوكيل *كيف يفكر* في مشكلة ما.
|
||||
- **الأدوات** تمنح الوكيل *دوال قابلة للاستدعاء* لاتخاذ إجراءات (البحث، قراءة الملفات، استدعاء APIs).
|
||||
|
||||
غالبًا ما تحتاج **كليهما**: مهارات للخبرة، وأدوات للإجراء. يتم تكوينهما بشكل مستقل ويُكمّلان بعضهما.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## البداية السريعة
|
||||
|
||||
### 1. إنشاء مجلد المهارة
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # مطلوب — التعليمات
|
||||
├── references/ # اختياري — مستندات مرجعية
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # اختياري — سكربتات قابلة للتنفيذ
|
||||
```
|
||||
|
||||
### 2. كتابة SKILL.md الخاص بك
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-review
|
||||
description: Guidelines for conducting thorough code reviews with focus on security and performance.
|
||||
metadata:
|
||||
author: your-team
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## إرشادات مراجعة الكود
|
||||
|
||||
عند مراجعة الكود، اتبع قائمة التحقق هذه:
|
||||
|
||||
1. **الأمان**: تحقق من ثغرات الحقن وتجاوز المصادقة وكشف البيانات
|
||||
2. **الأداء**: ابحث عن استعلامات N+1 والتخصيصات غير الضرورية والاستدعاءات المحظورة
|
||||
3. **القابلية للقراءة**: تأكد من وضوح التسمية والتعليقات المناسبة والأسلوب المتسق
|
||||
4. **الاختبارات**: تحقق من تغطية اختبار كافية للوظائف الجديدة
|
||||
|
||||
### مستويات الخطورة
|
||||
- **حرج**: ثغرات أمنية، مخاطر فقدان البيانات → حظر الدمج
|
||||
- **رئيسي**: مشاكل أداء، أخطاء منطقية → طلب تغييرات
|
||||
- **ثانوي**: مسائل أسلوبية، اقتراحات تسمية → الموافقة مع تعليقات
|
||||
```
|
||||
|
||||
### 3. ربطها بوكيل
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import GithubSearchTool, FileReadTool
|
||||
|
||||
reviewer = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["./skills"], # يحقن إرشادات المراجعة
|
||||
tools=[GithubSearchTool(), FileReadTool()], # يسمح للوكيل بقراءة الكود
|
||||
)
|
||||
```
|
||||
|
||||
الوكيل الآن لديه **خبرة** (من المهارة) و**قدرات** (من الأدوات) معًا.
|
||||
|
||||
---
|
||||
|
||||
## المهارات + الأدوات: العمل معًا
|
||||
|
||||
إليك أنماط شائعة توضح كيف تُكمّل المهارات والأدوات بعضهما:
|
||||
|
||||
### النمط 1: مهارات فقط (خبرة المجال، بدون إجراءات مطلوبة)
|
||||
|
||||
استخدم عندما يحتاج الوكيل لتعليمات محددة لكن لا يحتاج لاستدعاء خدمات خارجية:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write clear API documentation",
|
||||
backstory="Expert technical writer",
|
||||
skills=["./skills/api-docs-style"], # إرشادات وقوالب الكتابة
|
||||
# لا حاجة لأدوات — الوكيل يكتب بناءً على السياق المقدم
|
||||
)
|
||||
```
|
||||
|
||||
### النمط 2: أدوات فقط (إجراءات، بدون خبرة خاصة)
|
||||
|
||||
استخدم عندما يحتاج الوكيل لاتخاذ إجراءات لكن لا يحتاج لتعليمات مجال محددة:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
|
||||
|
||||
agent = Agent(
|
||||
role="Web Researcher",
|
||||
goal="Find information about a topic",
|
||||
backstory="Skilled at finding information online",
|
||||
tools=[SerperDevTool(), ScrapeWebsiteTool()], # يمكنه البحث والاستخراج
|
||||
# لا حاجة لمهارات — البحث العام لا يحتاج إرشادات خاصة
|
||||
)
|
||||
```
|
||||
|
||||
### النمط 3: مهارات + أدوات (خبرة وإجراءات)
|
||||
|
||||
النمط الأكثر شيوعًا في العالم الحقيقي. المهارة توفر *كيف* تقترب من العمل؛ الأدوات توفر *ما* يمكن للوكيل فعله:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
analyst = Agent(
|
||||
role="Security Analyst",
|
||||
goal="Audit infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security and compliance",
|
||||
skills=["./skills/security-audit"], # منهجية وقوائم تحقق التدقيق
|
||||
tools=[
|
||||
SerperDevTool(), # البحث عن ثغرات معروفة
|
||||
FileReadTool(), # قراءة ملفات التكوين
|
||||
CodeInterpreterTool(), # تشغيل سكربتات التحليل
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### النمط 4: مهارات + MCP
|
||||
|
||||
المهارات تعمل مع خوادم MCP بنفس الطريقة التي تعمل بها مع الأدوات:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze customer data and generate reports",
|
||||
backstory="Expert data analyst with strong statistical background",
|
||||
skills=["./skills/data-analysis"], # منهجية التحليل
|
||||
mcps=["https://data-warehouse.example.com/sse"], # وصول بيانات عن بُعد
|
||||
)
|
||||
```
|
||||
|
||||
### النمط 5: مهارات + تطبيقات
|
||||
|
||||
المهارات يمكن أن توجّه كيف يستخدم الوكيل تكاملات المنصة:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Customer Support Agent",
|
||||
goal="Respond to customer inquiries professionally",
|
||||
backstory="Experienced support representative",
|
||||
skills=["./skills/support-playbook"], # قوالب الردود وقواعد التصعيد
|
||||
apps=["gmail", "zendesk"], # يمكنه إرسال رسائل بريد وتحديث التذاكر
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## المهارات على مستوى الطاقم
|
||||
|
||||
يمكن تعيين المهارات على الطاقم لتُطبّق على **جميع الوكلاء**:
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher, writer, reviewer],
|
||||
tasks=[research_task, write_task, review_task],
|
||||
skills=["./skills"], # جميع الوكلاء يحصلون على هذه المهارات
|
||||
)
|
||||
```
|
||||
|
||||
المهارات على مستوى الوكيل لها الأولوية — إذا تم اكتشاف نفس المهارة في كلا المستويين، يتم استخدام نسخة الوكيل.
|
||||
|
||||
---
|
||||
|
||||
## تنسيق SKILL.md
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: وصف قصير لما تفعله هذه المهارة ومتى تُستخدم.
|
||||
license: Apache-2.0 # اختياري
|
||||
compatibility: crewai>=0.1.0 # اختياري
|
||||
metadata: # اختياري
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # اختياري، تجريبي
|
||||
---
|
||||
|
||||
التعليمات للوكيل تُكتب هنا. يتم حقن محتوى Markdown هذا
|
||||
في إرشادات الوكيل عند تفعيل المهارة.
|
||||
```
|
||||
|
||||
### حقول البيانات الوصفية
|
||||
|
||||
| الحقل | مطلوب | الوصف |
|
||||
| :-------------- | :------- | :----------------------------------------------------------------------- |
|
||||
| `name` | نعم | 1-64 حرف. أحرف صغيرة أبجدية رقمية وشرطات. يجب أن يطابق اسم المجلد. |
|
||||
| `description` | نعم | 1-1024 حرف. يصف ما تفعله المهارة ومتى تُستخدم. |
|
||||
| `license` | لا | اسم الترخيص أو مرجع لملف ترخيص مضمّن. |
|
||||
| `compatibility` | لا | حد أقصى 500 حرف. متطلبات البيئة (منتجات، حزم، شبكة). |
|
||||
| `metadata` | لا | تعيين مفتاح-قيمة نصي عشوائي. |
|
||||
| `allowed-tools` | لا | قائمة أدوات معتمدة مسبقًا مفصولة بمسافات. تجريبي. |
|
||||
|
||||
---
|
||||
تستخدم المهارات **الكشف التدريجي** — يتم تحميل البيانات الوصفية أولاً، ثم التعليمات الكاملة فقط عند التفعيل، وكتالوجات الموارد فقط عند الحاجة.
|
||||
|
||||
## هيكل المجلد
|
||||
|
||||
@@ -223,25 +21,79 @@ my-skill/
|
||||
└── assets/ # اختياري — ملفات ثابتة (إعدادات، بيانات)
|
||||
```
|
||||
|
||||
يجب أن يتطابق اسم المجلد مع حقل `name` في `SKILL.md`. مجلدات `scripts/` و `references/` و `assets/` متاحة في مسار المهارة `path` للوكلاء الذين يحتاجون للإشارة إلى الملفات مباشرة.
|
||||
يجب أن يتطابق اسم المجلد مع حقل `name` في `SKILL.md`.
|
||||
|
||||
## تنسيق SKILL.md
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: Short description of what this skill does and when to use it.
|
||||
license: Apache-2.0 # optional
|
||||
compatibility: crewai>=0.1.0 # optional
|
||||
metadata: # optional
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # optional, space-delimited
|
||||
---
|
||||
|
||||
## المهارات المحمّلة مسبقًا
|
||||
Instructions for the agent go here. This markdown body is injected
|
||||
into the agent's prompt when the skill is activated.
|
||||
```
|
||||
|
||||
للمزيد من التحكم، يمكنك اكتشاف المهارات وتفعيلها برمجيًا:
|
||||
### حقول البيانات الوصفية
|
||||
|
||||
| الحقل | مطلوب | القيود |
|
||||
| :-------------- | :------- | :----------------------------------------------------------------------- |
|
||||
| `name` | نعم | 1-64 حرف. أحرف صغيرة أبجدية رقمية وشرطات. بدون شرطات بادئة/لاحقة/متتالية. يجب أن يطابق اسم المجلد. |
|
||||
| `description` | نعم | 1-1024 حرف. يصف ما تفعله المهارة ومتى تُستخدم. |
|
||||
| `license` | لا | اسم الترخيص أو مرجع لملف ترخيص مضمّن. |
|
||||
| `compatibility` | لا | حد أقصى 500 حرف. متطلبات البيئة (منتجات، حزم، شبكة). |
|
||||
| `metadata` | لا | تعيين مفتاح-قيمة نصي عشوائي. |
|
||||
| `allowed-tools` | لا | قائمة أدوات معتمدة مسبقًا مفصولة بمسافات. تجريبي. |
|
||||
|
||||
## الاستخدام
|
||||
|
||||
### المهارات على مستوى الوكيل
|
||||
|
||||
مرر مسارات مجلدات المهارات إلى وكيل:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
backstory="An expert researcher.",
|
||||
skills=["./skills"], # يكتشف جميع المهارات في هذا المجلد
|
||||
)
|
||||
```
|
||||
|
||||
### المهارات على مستوى الطاقم
|
||||
|
||||
تُدمج مسارات المهارات في الطاقم مع كل وكيل:
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
skills=["./skills"],
|
||||
)
|
||||
```
|
||||
|
||||
### المهارات المحمّلة مسبقًا
|
||||
|
||||
يمكنك أيضًا تمرير كائنات `Skill` مباشرة:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.skills import discover_skills, activate_skill
|
||||
|
||||
# اكتشاف جميع المهارات في مجلد
|
||||
skills = discover_skills(Path("./skills"))
|
||||
|
||||
# تفعيلها (تحميل محتوى SKILL.md الكامل)
|
||||
activated = [activate_skill(s) for s in skills]
|
||||
|
||||
# تمرير إلى وكيل
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
@@ -250,57 +102,13 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## كيف يتم تحميل المهارات
|
||||
|
||||
تستخدم المهارات **الكشف التدريجي** — تحمّل فقط ما هو مطلوب في كل مرحلة:
|
||||
يتم تحميل المهارات تدريجيًا — فقط البيانات المطلوبة في كل مرحلة يتم قراءتها:
|
||||
|
||||
| المرحلة | ما يتم تحميله | متى |
|
||||
| :--------- | :------------------------------------ | :------------------ |
|
||||
| الاكتشاف | الاسم، الوصف، حقول البيانات الوصفية | `discover_skills()` |
|
||||
| التفعيل | نص محتوى SKILL.md الكامل | `activate_skill()` |
|
||||
| المرحلة | ما يتم تحميله | متى |
|
||||
| :--------------- | :------------------------------------------------ | :----------------- |
|
||||
| الاكتشاف | الاسم، الوصف، حقول البيانات الوصفية | `discover_skills()` |
|
||||
| التفعيل | نص محتوى SKILL.md الكامل | `activate_skill()` |
|
||||
|
||||
أثناء التنفيذ العادي للوكيل (تمرير مسارات المجلدات عبر `skills=["./skills"]`)، يتم اكتشاف المهارات وتفعيلها تلقائيًا. التحميل التدريجي مهم فقط عند استخدام الواجهة البرمجية.
|
||||
|
||||
---
|
||||
|
||||
## المهارات مقابل المعرفة
|
||||
|
||||
كلا المهارات والمعرفة تُعدّل إرشادات الوكيل، لكنهما يخدمان أغراضًا مختلفة:
|
||||
|
||||
| الجانب | المهارات | المعرفة |
|
||||
| :--- | :--- | :--- |
|
||||
| **ما توفره** | تعليمات، إجراءات، إرشادات | حقائق، بيانات، معلومات |
|
||||
| **كيف تُخزّن** | ملفات Markdown (SKILL.md) | مُضمّنة في مخزن متجهي (ChromaDB) |
|
||||
| **كيف تُسترجع** | يتم حقن المحتوى الكامل في الإرشادات | البحث الدلالي يجد الأجزاء ذات الصلة |
|
||||
| **الأفضل لـ** | المنهجيات، قوائم التحقق، أدلة الأسلوب | مستندات الشركة، معلومات المنتج، بيانات مرجعية |
|
||||
| **يُعيّن عبر** | `skills=["./skills"]` | `knowledge_sources=[source]` |
|
||||
|
||||
**القاعدة العامة:** إذا كان الوكيل يحتاج لاتباع *عملية*، استخدم مهارة. إذا كان يحتاج للرجوع إلى *بيانات*، استخدم المعرفة.
|
||||
|
||||
---
|
||||
|
||||
## الأسئلة الشائعة
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="هل أحتاج لتعيين المهارات والأدوات معًا؟">
|
||||
يعتمد على حالة الاستخدام. المهارات والأدوات **مستقلتان** — يمكنك استخدام أيّ منهما أو كليهما أو لا شيء.
|
||||
|
||||
- **مهارات فقط**: عندما يحتاج الوكيل خبرة لكن لا يحتاج إجراءات خارجية (مثال: الكتابة بإرشادات أسلوبية)
|
||||
- **أدوات فقط**: عندما يحتاج الوكيل إجراءات لكن لا يحتاج منهجية خاصة (مثال: بحث بسيط على الويب)
|
||||
- **كليهما**: عندما يحتاج الوكيل خبرة وإجراءات (مثال: تدقيق أمني بقوائم تحقق محددة وقدرة على فحص الكود)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="هل توفر المهارات أدوات تلقائيًا؟">
|
||||
**لا.** حقل `allowed-tools` في SKILL.md هو بيانات وصفية تجريبية فقط — لا يُنشئ أو يحقن أي أدوات. يجب عليك دائمًا تعيين الأدوات بشكل منفصل عبر `tools=[]` أو `mcps=[]` أو `apps=[]`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="ماذا يحدث إذا عيّنت نفس المهارة على كل من الوكيل والطاقم؟">
|
||||
المهارة على مستوى الوكيل لها الأولوية. يتم إزالة التكرار حسب الاسم — مهارات الوكيل تُعالج أولاً، لذا إذا ظهر نفس اسم المهارة في كلا المستويين، تُستخدم نسخة الوكيل.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="ما الحجم الأقصى لمحتوى SKILL.md؟">
|
||||
هناك تحذير ناعم عند 50,000 حرف، لكن بدون حد صارم. حافظ على تركيز المهارات وإيجازها للحصول على أفضل النتائج — الحقن الكبيرة في الإرشادات قد تُشتت انتباه الوكيل.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
أثناء التنفيذ العادي للوكيل، يتم اكتشاف المهارات وتفعيلها تلقائيًا. مجلدات `scripts/` و `references/` و `assets/` متاحة في مسار المهارة `path` للوكلاء الذين يحتاجون للإشارة إلى الملفات مباشرة.
|
||||
|
||||
@@ -10,10 +10,6 @@ mode: "wide"
|
||||
تُمكّن أدوات CrewAI الوكلاء بقدرات تتراوح من البحث على الويب وتحليل البيانات إلى التعاون وتفويض المهام بين الزملاء.
|
||||
توضح هذه الوثائق كيفية إنشاء هذه الأدوات ودمجها والاستفادة منها ضمن إطار عمل CrewAI، بما في ذلك التركيز على أدوات التعاون.
|
||||
|
||||
<Note type="info" title="الأدوات هي أحد أنواع قدرات الوكيل الخمسة">
|
||||
الأدوات تمنح الوكلاء **دوال قابلة للاستدعاء** لاتخاذ إجراءات. تعمل جنبًا إلى جنب مع [MCP](/ar/mcp/overview) (خوادم أدوات عن بُعد) و[التطبيقات](/ar/concepts/agent-capabilities) (تكاملات المنصة) و[المهارات](/ar/concepts/skills) (خبرة المجال) و[المعرفة](/ar/concepts/knowledge) (حقائق مُسترجعة). راجع نظرة عامة على [قدرات الوكيل](/ar/concepts/agent-capabilities) لفهم متى تستخدم كل نوع.
|
||||
</Note>
|
||||
|
||||
## ما هي الأداة؟
|
||||
|
||||
الأداة في CrewAI هي مهارة أو وظيفة يمكن للوكلاء استخدامها لأداء إجراءات مختلفة.
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
"group": "Core Concepts",
|
||||
"pages": [
|
||||
"en/concepts/agents",
|
||||
"en/concepts/agent-capabilities",
|
||||
"en/concepts/tasks",
|
||||
"en/concepts/crews",
|
||||
"en/concepts/flows",
|
||||
@@ -3463,7 +3462,6 @@
|
||||
"group": "Conceitos-Chave",
|
||||
"pages": [
|
||||
"pt-BR/concepts/agents",
|
||||
"pt-BR/concepts/agent-capabilities",
|
||||
"pt-BR/concepts/tasks",
|
||||
"pt-BR/concepts/crews",
|
||||
"pt-BR/concepts/flows",
|
||||
@@ -6671,7 +6669,6 @@
|
||||
"pages": [
|
||||
"ko/concepts/agents",
|
||||
"ko/concepts/tasks",
|
||||
"ko/concepts/agent-capabilities",
|
||||
"ko/concepts/crews",
|
||||
"ko/concepts/flows",
|
||||
"ko/concepts/production-architecture",
|
||||
@@ -9961,7 +9958,6 @@
|
||||
"group": "\u0627\u0644\u0645\u0641\u0627\u0647\u064a\u0645 \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0629",
|
||||
"pages": [
|
||||
"ar/concepts/agents",
|
||||
"ar/concepts/agent-capabilities",
|
||||
"ar/concepts/tasks",
|
||||
"ar/concepts/crews",
|
||||
"ar/concepts/flows",
|
||||
|
||||
@@ -4,70 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 31, 2026">
|
||||
## v1.13.0a5
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a5)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a4
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Apr 01, 2026">
|
||||
## v1.13.0a4
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a4)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a3
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Apr 01, 2026">
|
||||
## v1.13.0a3
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a3)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Emit token usage data in LLMCallCompletedEvent
|
||||
- Extract and publish tool metadata to AMP
|
||||
|
||||
### Bug Fixes
|
||||
- Handle GPT-5.x models not supporting the `stop` API parameter
|
||||
|
||||
### Documentation
|
||||
- Fix inaccuracies in agent-capabilities across all languages
|
||||
- Add Agent Capabilities overview and improve Skills documentation
|
||||
- Add comprehensive SSO configuration guide
|
||||
- Update changelog and version for v1.13.0rc1
|
||||
|
||||
### Refactoring
|
||||
- Convert Flow to Pydantic BaseModel
|
||||
- Convert LLM classes to Pydantic BaseModel
|
||||
- Replace InstanceOf[T] with plain type annotations
|
||||
- Remove unused methods
|
||||
|
||||
## Contributors
|
||||
|
||||
@dependabot[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @lucasgomide, @thiagomoretto
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 27, 2026">
|
||||
## v1.13.0rc1
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
title: "Agent Capabilities"
|
||||
description: "Understand the five ways to extend CrewAI agents: Tools, MCPs, Apps, Skills, and Knowledge."
|
||||
icon: puzzle-piece
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CrewAI agents can be extended with **five distinct capability types**, each serving a different purpose. Understanding when to use each one — and how they work together — is key to building effective agents.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Tools" icon="wrench" href="/en/concepts/tools" color="#3B82F6">
|
||||
**Callable functions** — give agents the ability to take action. Web searches, file operations, API calls, code execution.
|
||||
</Card>
|
||||
<Card title="MCP Servers" icon="plug" href="/en/mcp/overview" color="#8B5CF6">
|
||||
**Remote tool servers** — connect agents to external tool servers via the Model Context Protocol. Same effect as tools, but hosted externally.
|
||||
</Card>
|
||||
<Card title="Apps" icon="grid-2" color="#EC4899">
|
||||
**Platform integrations** — connect agents to SaaS apps (Gmail, Slack, Jira, Salesforce) via CrewAI's platform. Runs locally with a platform integration token.
|
||||
</Card>
|
||||
<Card title="Skills" icon="bolt" href="/en/concepts/skills" color="#F59E0B">
|
||||
**Domain expertise** — inject instructions, guidelines, and reference material into agent prompts. Skills tell agents *how to think*.
|
||||
</Card>
|
||||
<Card title="Knowledge" icon="book" href="/en/concepts/knowledge" color="#10B981">
|
||||
**Retrieved facts** — provide agents with data from documents, files, and URLs via semantic search (RAG). Knowledge gives agents *what to know*.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
---
|
||||
|
||||
## The Key Distinction
|
||||
|
||||
The most important thing to understand: **these capabilities fall into two categories**.
|
||||
|
||||
### Action Capabilities (Tools, MCPs, Apps)
|
||||
|
||||
These give agents the ability to **do things** — call APIs, read files, search the web, send emails. At execution time, all three resolve into the same internal format (`BaseTool` instances) and appear in a unified tool list the agent can call.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find and compile market data",
|
||||
backstory="Expert market analyst",
|
||||
tools=[SerperDevTool(), FileReadTool()], # Local tools
|
||||
mcps=["https://mcp.example.com/sse"], # Remote MCP server tools
|
||||
apps=["gmail", "google_sheets"], # Platform integrations
|
||||
)
|
||||
```
|
||||
|
||||
### Context Capabilities (Skills, Knowledge)
|
||||
|
||||
These modify the agent's **prompt** — injecting expertise, instructions, or retrieved data before the agent starts reasoning. They don't give agents new actions; they shape how agents think and what information they have access to.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Security Auditor",
|
||||
goal="Audit cloud infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security with 10 years of experience",
|
||||
skills=["./skills/security-audit"], # Domain instructions
|
||||
knowledge_sources=[pdf_source, url_source], # Retrieved facts
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use What
|
||||
|
||||
| You need... | Use | Example |
|
||||
| :------------------------------------------------ | :---------------- | :--------------------------------------- |
|
||||
| Agent to search the web | **Tools** | `tools=[SerperDevTool()]` |
|
||||
| Agent to call a remote API via MCP | **MCPs** | `mcps=["https://api.example.com/sse"]` |
|
||||
| Agent to send emails via Gmail | **Apps** | `apps=["gmail"]` |
|
||||
| Agent to follow specific procedures | **Skills** | `skills=["./skills/code-review"]` |
|
||||
| Agent to reference company docs | **Knowledge** | `knowledge_sources=[pdf_source]` |
|
||||
| Agent to search the web AND follow review guidelines | **Tools + Skills** | Use both together |
|
||||
|
||||
---
|
||||
|
||||
## Combining Capabilities
|
||||
|
||||
In practice, agents often use **multiple capability types together**. Here's a realistic example:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
# A fully-equipped research agent
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Produce comprehensive market analysis reports",
|
||||
backstory="Expert analyst with deep industry knowledge",
|
||||
|
||||
# ACTION: What the agent can DO
|
||||
tools=[
|
||||
SerperDevTool(), # Search the web
|
||||
FileReadTool(), # Read local files
|
||||
CodeInterpreterTool(), # Run Python code for analysis
|
||||
],
|
||||
mcps=["https://data-api.example.com/sse"], # Access remote data API
|
||||
apps=["google_sheets"], # Write to Google Sheets
|
||||
|
||||
# CONTEXT: What the agent KNOWS
|
||||
skills=["./skills/research-methodology"], # How to conduct research
|
||||
knowledge_sources=[company_docs], # Company-specific data
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison Table
|
||||
|
||||
| Feature | Tools | MCPs | Apps | Skills | Knowledge |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: |
|
||||
| **Gives agent actions** | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **Modifies prompt** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| **Requires code** | Yes | Config only | Config only | Markdown only | Config only |
|
||||
| **Runs locally** | Yes | Depends | Yes (with env var) | N/A | Yes |
|
||||
| **Needs API keys** | Per tool | Per server | Integration token | No | Embedder only |
|
||||
| **Set on Agent** | `tools=[]` | `mcps=[]` | `apps=[]` | `skills=[]` | `knowledge_sources=[]` |
|
||||
| **Set on Crew** | ❌ | ❌ | ❌ | `skills=[]` | `knowledge_sources=[]` |
|
||||
|
||||
---
|
||||
|
||||
## Deep Dives
|
||||
|
||||
Ready to learn more about each capability type?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Tools" icon="wrench" href="/en/concepts/tools">
|
||||
Create custom tools, use the 75+ OSS catalog, configure caching and async execution.
|
||||
</Card>
|
||||
<Card title="MCP Integration" icon="plug" href="/en/mcp/overview">
|
||||
Connect to MCP servers via stdio, SSE, or HTTP. Filter tools, configure auth.
|
||||
</Card>
|
||||
<Card title="Skills" icon="bolt" href="/en/concepts/skills">
|
||||
Build skill packages with SKILL.md, inject domain expertise, use progressive disclosure.
|
||||
</Card>
|
||||
<Card title="Knowledge" icon="book" href="/en/concepts/knowledge">
|
||||
Add knowledge from PDFs, CSVs, URLs, and more. Configure embedders and retrieval.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,186 +1,27 @@
|
||||
---
|
||||
title: Skills
|
||||
description: Filesystem-based skill packages that inject domain expertise and instructions into agent prompts.
|
||||
description: Filesystem-based skill packages that inject context into agent prompts.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Skills are self-contained directories that provide agents with **domain-specific instructions, guidelines, and reference material**. Each skill is defined by a `SKILL.md` file with YAML frontmatter and a markdown body.
|
||||
Skills are self-contained directories that provide agents with domain-specific instructions, references, and assets. Each skill is defined by a `SKILL.md` file with YAML frontmatter and a markdown body.
|
||||
|
||||
When activated, a skill's instructions are injected directly into the agent's task prompt — giving the agent expertise without requiring any code changes.
|
||||
Skills use **progressive disclosure** — metadata is loaded first, full instructions only when activated, and resource catalogs only when needed.
|
||||
|
||||
<Note type="info" title="Skills vs Tools — The Key Distinction">
|
||||
**Skills are NOT tools.** This is the most common point of confusion.
|
||||
|
||||
- **Skills** inject *instructions and context* into the agent's prompt. They tell the agent *how to think* about a problem.
|
||||
- **Tools** give the agent *callable functions* to take action (search, read files, call APIs).
|
||||
|
||||
You often need **both**: skills for expertise, tools for action. They are configured independently and complement each other.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Skill Directory
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # Required — instructions
|
||||
├── references/ # Optional — reference docs
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # Optional — executable scripts
|
||||
my-skill/
|
||||
├── SKILL.md # Required — frontmatter + instructions
|
||||
├── scripts/ # Optional — executable scripts
|
||||
├── references/ # Optional — reference documents
|
||||
└── assets/ # Optional — static files (configs, data)
|
||||
```
|
||||
|
||||
### 2. Write Your SKILL.md
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-review
|
||||
description: Guidelines for conducting thorough code reviews with focus on security and performance.
|
||||
metadata:
|
||||
author: your-team
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
When reviewing code, follow this checklist:
|
||||
|
||||
1. **Security**: Check for injection vulnerabilities, auth bypasses, and data exposure
|
||||
2. **Performance**: Look for N+1 queries, unnecessary allocations, and blocking calls
|
||||
3. **Readability**: Ensure clear naming, appropriate comments, and consistent style
|
||||
4. **Testing**: Verify adequate test coverage for new functionality
|
||||
|
||||
### Severity Levels
|
||||
- **Critical**: Security vulnerabilities, data loss risks → block merge
|
||||
- **Major**: Performance issues, logic errors → request changes
|
||||
- **Minor**: Style issues, naming suggestions → approve with comments
|
||||
```
|
||||
|
||||
### 3. Attach to an Agent
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import GithubSearchTool, FileReadTool
|
||||
|
||||
reviewer = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["./skills"], # Injects review guidelines
|
||||
tools=[GithubSearchTool(), FileReadTool()], # Lets agent read code
|
||||
)
|
||||
```
|
||||
|
||||
The agent now has both **expertise** (from the skill) and **capabilities** (from the tools).
|
||||
|
||||
---
|
||||
|
||||
## Skills + Tools: Working Together
|
||||
|
||||
Here are common patterns showing how skills and tools complement each other:
|
||||
|
||||
### Pattern 1: Skills Only (Domain Expertise, No Actions Needed)
|
||||
|
||||
Use when the agent needs specific instructions but doesn't need to call external services:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write clear API documentation",
|
||||
backstory="Expert technical writer",
|
||||
skills=["./skills/api-docs-style"], # Writing guidelines and templates
|
||||
# No tools needed — agent writes based on provided context
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 2: Tools Only (Actions, No Special Expertise)
|
||||
|
||||
Use when the agent needs to take action but doesn't need domain-specific instructions:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
|
||||
|
||||
agent = Agent(
|
||||
role="Web Researcher",
|
||||
goal="Find information about a topic",
|
||||
backstory="Skilled at finding information online",
|
||||
tools=[SerperDevTool(), ScrapeWebsiteTool()], # Can search and scrape
|
||||
# No skills needed — general research doesn't need special guidelines
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 3: Skills + Tools (Expertise AND Actions)
|
||||
|
||||
The most common real-world pattern. The skill provides *how* to approach the work; tools provide *what* the agent can do:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
analyst = Agent(
|
||||
role="Security Analyst",
|
||||
goal="Audit infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security and compliance",
|
||||
skills=["./skills/security-audit"], # Audit methodology and checklists
|
||||
tools=[
|
||||
SerperDevTool(), # Research known vulnerabilities
|
||||
FileReadTool(), # Read config files
|
||||
CodeInterpreterTool(), # Run analysis scripts
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 4: Skills + MCPs
|
||||
|
||||
Skills work alongside MCP servers the same way they work with tools:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze customer data and generate reports",
|
||||
backstory="Expert data analyst with strong statistical background",
|
||||
skills=["./skills/data-analysis"], # Analysis methodology
|
||||
mcps=["https://data-warehouse.example.com/sse"], # Remote data access
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 5: Skills + Apps
|
||||
|
||||
Skills can guide how an agent uses platform integrations:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Customer Support Agent",
|
||||
goal="Respond to customer inquiries professionally",
|
||||
backstory="Experienced support representative",
|
||||
skills=["./skills/support-playbook"], # Response templates and escalation rules
|
||||
apps=["gmail", "zendesk"], # Can send emails and update tickets
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Crew-Level Skills
|
||||
|
||||
Skills can be set on a crew to apply to **all agents**:
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher, writer, reviewer],
|
||||
tasks=[research_task, write_task, review_task],
|
||||
skills=["./skills"], # All agents get these skills
|
||||
)
|
||||
```
|
||||
|
||||
Agent-level skills take priority — if the same skill is discovered at both levels, the agent's version is used.
|
||||
|
||||
---
|
||||
The directory name must match the `name` field in `SKILL.md`.
|
||||
|
||||
## SKILL.md Format
|
||||
|
||||
@@ -193,7 +34,7 @@ compatibility: crewai>=0.1.0 # optional
|
||||
metadata: # optional
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # optional, experimental
|
||||
allowed-tools: web-search file-read # optional, space-delimited
|
||||
---
|
||||
|
||||
Instructions for the agent go here. This markdown body is injected
|
||||
@@ -202,46 +43,57 @@ into the agent's prompt when the skill is activated.
|
||||
|
||||
### Frontmatter Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
| Field | Required | Constraints |
|
||||
| :-------------- | :------- | :----------------------------------------------------------------------- |
|
||||
| `name` | Yes | 1–64 chars. Lowercase alphanumeric and hyphens. Must match directory name. |
|
||||
| `name` | Yes | 1–64 chars. Lowercase alphanumeric and hyphens. No leading/trailing/consecutive hyphens. Must match directory name. |
|
||||
| `description` | Yes | 1–1024 chars. Describes what the skill does and when to use it. |
|
||||
| `license` | No | License name or reference to a bundled license file. |
|
||||
| `compatibility` | No | Max 500 chars. Environment requirements (products, packages, network). |
|
||||
| `metadata` | No | Arbitrary string key-value mapping. |
|
||||
| `allowed-tools` | No | Space-delimited list of pre-approved tools. Experimental. |
|
||||
|
||||
---
|
||||
## Usage
|
||||
|
||||
## Directory Structure
|
||||
### Agent-level Skills
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # Required — frontmatter + instructions
|
||||
├── scripts/ # Optional — executable scripts
|
||||
├── references/ # Optional — reference documents
|
||||
└── assets/ # Optional — static files (configs, data)
|
||||
Pass skill directory paths to an agent:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
backstory="An expert researcher.",
|
||||
skills=["./skills"], # discovers all skills in this directory
|
||||
)
|
||||
```
|
||||
|
||||
The directory name must match the `name` field in `SKILL.md`. The `scripts/`, `references/`, and `assets/` directories are available on the skill's `path` for agents that need to reference files directly.
|
||||
### Crew-level Skills
|
||||
|
||||
---
|
||||
Skill paths on a crew are merged into every agent:
|
||||
|
||||
## Pre-loading Skills
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
For more control, you can discover and activate skills programmatically:
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
skills=["./skills"],
|
||||
)
|
||||
```
|
||||
|
||||
### Pre-loaded Skills
|
||||
|
||||
You can also pass `Skill` objects directly:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.skills import discover_skills, activate_skill
|
||||
|
||||
# Discover all skills in a directory
|
||||
skills = discover_skills(Path("./skills"))
|
||||
|
||||
# Activate them (loads full SKILL.md body)
|
||||
activated = [activate_skill(s) for s in skills]
|
||||
|
||||
# Pass to an agent
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
@@ -250,57 +102,14 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How Skills Are Loaded
|
||||
|
||||
Skills use **progressive disclosure** — only loading what's needed at each stage:
|
||||
Skills load progressively — only the data needed at each stage is read:
|
||||
|
||||
| Stage | What's loaded | When |
|
||||
| :--------- | :------------------------------------ | :------------------ |
|
||||
| Discovery | Name, description, frontmatter fields | `discover_skills()` |
|
||||
| Activation | Full SKILL.md body text | `activate_skill()` |
|
||||
| Stage | What's loaded | When |
|
||||
| :--------------- | :------------------------------------------------ | :----------------- |
|
||||
| Discovery | Name, description, frontmatter fields | `discover_skills()` |
|
||||
| Activation | Full SKILL.md body text | `activate_skill()` |
|
||||
|
||||
During normal agent execution (passing directory paths via `skills=["./skills"]`), skills are automatically discovered and activated. The progressive loading only matters when using the programmatic API.
|
||||
During normal agent execution, skills are automatically discovered and activated. The `scripts/`, `references/`, and `assets/` directories are available on the skill's `path` for agents that need to reference files directly.
|
||||
|
||||
---
|
||||
|
||||
## Skills vs Knowledge
|
||||
|
||||
Both skills and knowledge modify the agent's prompt, but they serve different purposes:
|
||||
|
||||
| Aspect | Skills | Knowledge |
|
||||
| :--- | :--- | :--- |
|
||||
| **What it provides** | Instructions, procedures, guidelines | Facts, data, information |
|
||||
| **How it's stored** | Markdown files (SKILL.md) | Embedded in vector store (ChromaDB) |
|
||||
| **How it's retrieved** | Entire body injected into prompt | Semantic search finds relevant chunks |
|
||||
| **Best for** | Methodology, checklists, style guides | Company docs, product info, reference data |
|
||||
| **Set via** | `skills=["./skills"]` | `knowledge_sources=[source]` |
|
||||
|
||||
**Rule of thumb:** If the agent needs to follow a *process*, use a skill. If the agent needs to reference *data*, use knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Common Questions
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Do I need to set skills AND tools?">
|
||||
It depends on your use case. Skills and tools are **independent** — you can use either, both, or neither.
|
||||
|
||||
- **Skills alone**: When the agent needs expertise but no external actions (e.g., writing with style guidelines)
|
||||
- **Tools alone**: When the agent needs actions but no special methodology (e.g., simple web search)
|
||||
- **Both**: When the agent needs expertise AND actions (e.g., security audit with specific checklists AND ability to scan code)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Do skills automatically provide tools?">
|
||||
**No.** The `allowed-tools` field in SKILL.md is experimental metadata only — it does not provision or inject any tools. You must always set tools separately via `tools=[]`, `mcps=[]`, or `apps=[]`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="What happens if I set the same skill on both an agent and its crew?">
|
||||
The agent-level skill takes priority. Skills are deduplicated by name — the agent's skills are processed first, so if the same skill name appears at both levels, the agent's version is used.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How large can a SKILL.md body be?">
|
||||
There's a soft warning at 50,000 characters, but no hard limit. Keep skills focused and concise for best results — large prompt injections can dilute the agent's attention.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -10,10 +10,6 @@ mode: "wide"
|
||||
CrewAI tools empower agents with capabilities ranging from web searching and data analysis to collaboration and delegating tasks among coworkers.
|
||||
This documentation outlines how to create, integrate, and leverage these tools within the CrewAI framework, including a new focus on collaboration tools.
|
||||
|
||||
<Note type="info" title="Tools are one of five agent capability types">
|
||||
Tools give agents **callable functions** to take action. They work alongside [MCPs](/en/mcp/overview) (remote tool servers), [Apps](/en/concepts/agent-capabilities) (platform integrations), [Skills](/en/concepts/skills) (domain expertise), and [Knowledge](/en/concepts/knowledge) (retrieved facts). See the [Agent Capabilities](/en/concepts/agent-capabilities) overview to understand when to use each.
|
||||
</Note>
|
||||
|
||||
## What is a Tool?
|
||||
|
||||
A tool in CrewAI is a skill or function that agents can utilize to perform various actions.
|
||||
|
||||
@@ -4,70 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 31일">
|
||||
## v1.13.0a5
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a5)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- v1.13.0a4에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 4월 1일">
|
||||
## v1.13.0a4
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a4)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- v1.13.0a3에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 4월 1일">
|
||||
## v1.13.0a3
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a3)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- LLMCallCompletedEvent에서 토큰 사용 데이터 발행
|
||||
- 도구 메타데이터를 AMP로 추출 및 게시
|
||||
|
||||
### 버그 수정
|
||||
- `stop` API 매개변수를 지원하지 않는 GPT-5.x 모델 처리
|
||||
|
||||
### 문서
|
||||
- 모든 언어에서 에이전트 기능의 부정확성 수정
|
||||
- 에이전트 기능 개요 추가 및 기술 문서 개선
|
||||
- 포괄적인 SSO 구성 가이드 추가
|
||||
- v1.13.0rc1에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
### 리팩토링
|
||||
- Flow를 Pydantic BaseModel로 변환
|
||||
- LLM 클래스를 Pydantic BaseModel로 변환
|
||||
- InstanceOf[T]를 일반 타입 주석으로 교체
|
||||
- 사용되지 않는 메서드 제거
|
||||
|
||||
## 기여자
|
||||
|
||||
@dependabot[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @lucasgomide, @thiagomoretto
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 27일">
|
||||
## v1.13.0rc1
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
title: "에이전트 기능"
|
||||
description: "CrewAI 에이전트를 확장하는 다섯 가지 방법 이해하기: 도구, MCP, 앱, 스킬, 지식."
|
||||
icon: puzzle-piece
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
CrewAI 에이전트는 **다섯 가지 고유한 기능 유형**으로 확장할 수 있으며, 각각 다른 목적을 가지고 있습니다. 각 유형을 언제 사용해야 하는지, 그리고 어떻게 함께 작동하는지 이해하는 것이 효과적인 에이전트를 구축하는 핵심입니다.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="도구" icon="wrench" href="/ko/concepts/tools" color="#3B82F6">
|
||||
**호출 가능한 함수** — 에이전트가 행동을 취할 수 있게 합니다. 웹 검색, 파일 작업, API 호출, 코드 실행.
|
||||
</Card>
|
||||
<Card title="MCP 서버" icon="plug" href="/ko/mcp/overview" color="#8B5CF6">
|
||||
**원격 도구 서버** — Model Context Protocol을 통해 에이전트를 외부 도구 서버에 연결합니다. 도구와 같은 효과이지만 외부에서 호스팅됩니다.
|
||||
</Card>
|
||||
<Card title="앱" icon="grid-2" color="#EC4899">
|
||||
**플랫폼 통합** — CrewAI 플랫폼을 통해 에이전트를 SaaS 앱(Gmail, Slack, Jira, Salesforce)에 연결합니다. 플랫폼 통합 토큰으로 로컬에서 실행됩니다.
|
||||
</Card>
|
||||
<Card title="스킬" icon="bolt" href="/ko/concepts/skills" color="#F59E0B">
|
||||
**도메인 전문성** — 에이전트 프롬프트에 지침, 가이드라인 및 참조 자료를 주입합니다. 스킬은 에이전트에게 *어떻게 생각할지*를 알려줍니다.
|
||||
</Card>
|
||||
<Card title="지식" icon="book" href="/ko/concepts/knowledge" color="#10B981">
|
||||
**검색된 사실** — 시맨틱 검색(RAG)을 통해 문서, 파일 및 URL에서 에이전트에게 데이터를 제공합니다. 지식은 에이전트에게 *무엇을 알아야 하는지*를 제공합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
---
|
||||
|
||||
## 핵심 구분
|
||||
|
||||
가장 중요한 점: **이 기능들은 두 가지 범주로 나뉩니다**.
|
||||
|
||||
### 액션 기능 (도구, MCP, 앱)
|
||||
|
||||
에이전트에게 **무언가를 할 수 있는** 능력을 부여합니다 — API 호출, 파일 읽기, 웹 검색, 이메일 전송. 실행 시점에 세 가지 모두 동일한 내부 형식(`BaseTool` 인스턴스)으로 변환되며, 에이전트가 호출할 수 있는 통합 도구 목록에 나타납니다.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find and compile market data",
|
||||
backstory="Expert market analyst",
|
||||
tools=[SerperDevTool(), FileReadTool()], # 로컬 도구
|
||||
mcps=["https://mcp.example.com/sse"], # 원격 MCP 서버 도구
|
||||
apps=["gmail", "google_sheets"], # 플랫폼 통합
|
||||
)
|
||||
```
|
||||
|
||||
### 컨텍스트 기능 (스킬, 지식)
|
||||
|
||||
에이전트의 **프롬프트**를 수정합니다 — 에이전트가 추론을 시작하기 전에 전문성, 지침 또는 검색된 데이터를 주입합니다. 에이전트에게 새로운 액션을 제공하는 것이 아니라, 에이전트가 어떻게 생각하고 어떤 정보에 접근할 수 있는지를 형성합니다.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Security Auditor",
|
||||
goal="Audit cloud infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security with 10 years of experience",
|
||||
skills=["./skills/security-audit"], # 도메인 지침
|
||||
knowledge_sources=[pdf_source, url_source], # 검색된 사실
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 언제 무엇을 사용할까
|
||||
|
||||
| 필요한 것... | 사용할 것 | 예시 |
|
||||
| :------------------------------------------------------- | :---------------- | :--------------------------------------- |
|
||||
| 에이전트가 웹을 검색 | **도구** | `tools=[SerperDevTool()]` |
|
||||
| 에이전트가 MCP를 통해 원격 API 호출 | **MCP** | `mcps=["https://api.example.com/sse"]` |
|
||||
| 에이전트가 Gmail로 이메일 전송 | **앱** | `apps=["gmail"]` |
|
||||
| 에이전트가 특정 절차를 따름 | **스킬** | `skills=["./skills/code-review"]` |
|
||||
| 에이전트가 회사 문서 참조 | **지식** | `knowledge_sources=[pdf_source]` |
|
||||
| 에이전트가 웹 검색 AND 리뷰 가이드라인 준수 | **도구 + 스킬** | 둘 다 함께 사용 |
|
||||
|
||||
---
|
||||
|
||||
## 기능 조합하기
|
||||
|
||||
실제로 에이전트는 종종 **여러 기능 유형을 함께** 사용합니다. 현실적인 예시입니다:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
# 완전히 갖춘 리서치 에이전트
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Produce comprehensive market analysis reports",
|
||||
backstory="Expert analyst with deep industry knowledge",
|
||||
|
||||
# 액션: 에이전트가 할 수 있는 것
|
||||
tools=[
|
||||
SerperDevTool(), # 웹 검색
|
||||
FileReadTool(), # 로컬 파일 읽기
|
||||
CodeInterpreterTool(), # 분석을 위한 Python 코드 실행
|
||||
],
|
||||
mcps=["https://data-api.example.com/sse"], # 원격 데이터 API 접근
|
||||
apps=["google_sheets"], # Google Sheets에 쓰기
|
||||
|
||||
# 컨텍스트: 에이전트가 아는 것
|
||||
skills=["./skills/research-methodology"], # 연구 수행 방법
|
||||
knowledge_sources=[company_docs], # 회사 특화 데이터
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 비교 테이블
|
||||
|
||||
| 특성 | 도구 | MCP | 앱 | 스킬 | 지식 |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: |
|
||||
| **에이전트에게 액션 부여** | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **프롬프트 수정** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| **코드 필요** | 예 | 설정만 | 설정만 | 마크다운만 | 설정만 |
|
||||
| **로컬 실행** | 예 | 경우에 따라 | 예 (환경 변수 필요) | N/A | 예 |
|
||||
| **API 키 필요** | 도구별 | 서버별 | 통합 토큰 | 아니오 | 임베더만 |
|
||||
| **Agent에 설정** | `tools=[]` | `mcps=[]` | `apps=[]` | `skills=[]` | `knowledge_sources=[]` |
|
||||
| **Crew에 설정** | ❌ | ❌ | ❌ | `skills=[]` | `knowledge_sources=[]` |
|
||||
|
||||
---
|
||||
|
||||
## 상세 가이드
|
||||
|
||||
각 기능 유형에 대해 더 알아볼 준비가 되셨나요?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="도구" icon="wrench" href="/ko/concepts/tools">
|
||||
맞춤형 도구 생성, 75개 이상의 OSS 카탈로그 사용, 캐싱 및 비동기 실행 설정.
|
||||
</Card>
|
||||
<Card title="MCP 통합" icon="plug" href="/ko/mcp/overview">
|
||||
stdio, SSE 또는 HTTP를 통해 MCP 서버에 연결. 도구 필터링, 인증 설정.
|
||||
</Card>
|
||||
<Card title="스킬" icon="bolt" href="/ko/concepts/skills">
|
||||
SKILL.md로 스킬 패키지 구축, 도메인 전문성 주입, 점진적 공개 사용.
|
||||
</Card>
|
||||
<Card title="지식" icon="book" href="/ko/concepts/knowledge">
|
||||
PDF, CSV, URL 등에서 지식 추가. 임베더 및 검색 설정.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,186 +1,27 @@
|
||||
---
|
||||
title: 스킬
|
||||
description: 에이전트 프롬프트에 도메인 전문성과 지침을 주입하는 파일 시스템 기반 스킬 패키지.
|
||||
description: 에이전트 프롬프트에 컨텍스트를 주입하는 파일 시스템 기반 스킬 패키지.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
스킬은 에이전트에게 **도메인별 지침, 가이드라인 및 참조 자료**를 제공하는 자체 포함 디렉터리입니다. 각 스킬은 YAML 프론트매터와 마크다운 본문이 포함된 `SKILL.md` 파일로 정의됩니다.
|
||||
스킬은 에이전트에게 도메인별 지침, 참조 자료, 에셋을 제공하는 자체 포함 디렉터리입니다. 각 스킬은 YAML 프론트매터와 마크다운 본문이 포함된 `SKILL.md` 파일로 정의됩니다.
|
||||
|
||||
활성화되면 스킬의 지침이 에이전트의 작업 프롬프트에 직접 주입됩니다 — 코드 변경 없이 에이전트에게 전문성을 부여합니다.
|
||||
스킬은 **점진적 공개**를 사용합니다 — 메타데이터가 먼저 로드되고, 활성화 시에만 전체 지침이 로드되며, 필요할 때만 리소스 카탈로그가 로드됩니다.
|
||||
|
||||
<Note type="info" title="스킬 vs 도구 — 핵심 구분">
|
||||
**스킬은 도구가 아닙니다.** 이것이 가장 흔한 혼동 포인트입니다.
|
||||
|
||||
- **스킬**은 에이전트의 프롬프트에 *지침과 컨텍스트*를 주입합니다. 에이전트에게 문제에 대해 *어떻게 생각할지*를 알려줍니다.
|
||||
- **도구**는 에이전트에게 행동을 취할 수 있는 *호출 가능한 함수*를 제공합니다 (검색, 파일 읽기, API 호출).
|
||||
|
||||
흔히 **둘 다** 필요합니다: 전문성을 위한 스킬과 행동을 위한 도구. 이들은 독립적으로 구성되며 서로 보완합니다.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 1. 스킬 디렉터리 생성
|
||||
## 디렉터리 구조
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # 필수 — 지침
|
||||
├── references/ # 선택 — 참조 문서
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
my-skill/
|
||||
├── SKILL.md # 필수 — 프론트매터 + 지침
|
||||
├── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
├── references/ # 선택 — 참조 문서
|
||||
└── assets/ # 선택 — 정적 파일 (설정, 데이터)
|
||||
```
|
||||
|
||||
### 2. SKILL.md 작성
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-review
|
||||
description: Guidelines for conducting thorough code reviews with focus on security and performance.
|
||||
metadata:
|
||||
author: your-team
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## 코드 리뷰 가이드라인
|
||||
|
||||
코드를 리뷰할 때 이 체크리스트를 따르세요:
|
||||
|
||||
1. **보안**: 인젝션 취약점, 인증 우회, 데이터 노출 확인
|
||||
2. **성능**: N+1 쿼리, 불필요한 할당, 블로킹 호출 확인
|
||||
3. **가독성**: 명확한 네이밍, 적절한 주석, 일관된 스타일 보장
|
||||
4. **테스트**: 새로운 기능에 대한 적절한 테스트 커버리지 확인
|
||||
|
||||
### 심각도 수준
|
||||
- **크리티컬**: 보안 취약점, 데이터 손실 위험 → 머지 차단
|
||||
- **메이저**: 성능 문제, 로직 오류 → 변경 요청
|
||||
- **마이너**: 스타일 문제, 네이밍 제안 → 코멘트와 함께 승인
|
||||
```
|
||||
|
||||
### 3. 에이전트에 연결
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import GithubSearchTool, FileReadTool
|
||||
|
||||
reviewer = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["./skills"], # 리뷰 가이드라인 주입
|
||||
tools=[GithubSearchTool(), FileReadTool()], # 에이전트가 코드를 읽을 수 있게 함
|
||||
)
|
||||
```
|
||||
|
||||
이제 에이전트는 **전문성** (스킬에서)과 **기능** (도구에서) 모두를 갖추게 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 스킬 + 도구: 함께 작동하기
|
||||
|
||||
스킬과 도구가 어떻게 보완하는지 보여주는 일반적인 패턴입니다:
|
||||
|
||||
### 패턴 1: 스킬만 (도메인 전문성, 액션 불필요)
|
||||
|
||||
에이전트가 특정 지침이 필요하지만 외부 서비스를 호출할 필요가 없을 때 사용:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write clear API documentation",
|
||||
backstory="Expert technical writer",
|
||||
skills=["./skills/api-docs-style"], # 작성 가이드라인 및 템플릿
|
||||
# 도구 불필요 — 에이전트가 제공된 컨텍스트를 기반으로 작성
|
||||
)
|
||||
```
|
||||
|
||||
### 패턴 2: 도구만 (액션, 특별한 전문성 불필요)
|
||||
|
||||
에이전트가 행동을 취해야 하지만 도메인별 지침이 필요 없을 때 사용:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
|
||||
|
||||
agent = Agent(
|
||||
role="Web Researcher",
|
||||
goal="Find information about a topic",
|
||||
backstory="Skilled at finding information online",
|
||||
tools=[SerperDevTool(), ScrapeWebsiteTool()], # 검색 및 스크래핑 가능
|
||||
# 스킬 불필요 — 일반 연구에는 특별한 가이드라인이 필요 없음
|
||||
)
|
||||
```
|
||||
|
||||
### 패턴 3: 스킬 + 도구 (전문성 AND 액션)
|
||||
|
||||
가장 일반적인 실제 패턴. 스킬은 작업에 *어떻게* 접근할지를 제공하고, 도구는 에이전트가 *무엇을* 할 수 있는지를 제공합니다:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
analyst = Agent(
|
||||
role="Security Analyst",
|
||||
goal="Audit infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security and compliance",
|
||||
skills=["./skills/security-audit"], # 감사 방법론 및 체크리스트
|
||||
tools=[
|
||||
SerperDevTool(), # 알려진 취약점 조사
|
||||
FileReadTool(), # 설정 파일 읽기
|
||||
CodeInterpreterTool(), # 분석 스크립트 실행
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### 패턴 4: 스킬 + MCP
|
||||
|
||||
스킬은 도구와 마찬가지로 MCP 서버와 함께 작동합니다:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze customer data and generate reports",
|
||||
backstory="Expert data analyst with strong statistical background",
|
||||
skills=["./skills/data-analysis"], # 분석 방법론
|
||||
mcps=["https://data-warehouse.example.com/sse"], # 원격 데이터 접근
|
||||
)
|
||||
```
|
||||
|
||||
### 패턴 5: 스킬 + 앱
|
||||
|
||||
스킬은 에이전트가 플랫폼 통합을 사용하는 방법을 안내할 수 있습니다:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Customer Support Agent",
|
||||
goal="Respond to customer inquiries professionally",
|
||||
backstory="Experienced support representative",
|
||||
skills=["./skills/support-playbook"], # 응답 템플릿 및 에스컬레이션 규칙
|
||||
apps=["gmail", "zendesk"], # 이메일 전송 및 티켓 업데이트 가능
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 크루 레벨 스킬
|
||||
|
||||
스킬을 크루에 설정하여 **모든 에이전트**에 적용할 수 있습니다:
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher, writer, reviewer],
|
||||
tasks=[research_task, write_task, review_task],
|
||||
skills=["./skills"], # 모든 에이전트가 이 스킬을 받음
|
||||
)
|
||||
```
|
||||
|
||||
에이전트 레벨 스킬이 우선합니다 — 동일한 스킬이 양쪽 레벨에서 발견되면 에이전트의 버전이 사용됩니다.
|
||||
|
||||
---
|
||||
디렉터리 이름은 `SKILL.md`의 `name` 필드와 일치해야 합니다.
|
||||
|
||||
## SKILL.md 형식
|
||||
|
||||
@@ -193,7 +34,7 @@ compatibility: crewai>=0.1.0 # 선택
|
||||
metadata: # 선택
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # 선택, 실험적
|
||||
allowed-tools: web-search file-read # 선택, 공백으로 구분
|
||||
---
|
||||
|
||||
에이전트를 위한 지침이 여기에 들어갑니다. 이 마크다운 본문은
|
||||
@@ -202,46 +43,57 @@ allowed-tools: web-search file-read # 선택, 실험적
|
||||
|
||||
### 프론트매터 필드
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| 필드 | 필수 | 제약 조건 |
|
||||
| :-------------- | :----- | :----------------------------------------------------------------------- |
|
||||
| `name` | 예 | 1–64자. 소문자 영숫자와 하이픈. 디렉터리 이름과 일치 필수. |
|
||||
| `name` | 예 | 1–64자. 소문자 영숫자와 하이픈. 선행/후행/연속 하이픈 불가. 디렉터리 이름과 일치 필수. |
|
||||
| `description` | 예 | 1–1024자. 스킬이 무엇을 하고 언제 사용하는지 설명. |
|
||||
| `license` | 아니오 | 라이선스 이름 또는 번들된 라이선스 파일 참조. |
|
||||
| `compatibility` | 아니오 | 최대 500자. 환경 요구 사항 (제품, 패키지, 네트워크). |
|
||||
| `metadata` | 아니오 | 임의의 문자열 키-값 매핑. |
|
||||
| `allowed-tools` | 아니오 | 공백으로 구분된 사전 승인 도구 목록. 실험적. |
|
||||
|
||||
---
|
||||
## 사용법
|
||||
|
||||
## 디렉터리 구조
|
||||
### 에이전트 레벨 스킬
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # 필수 — 프론트매터 + 지침
|
||||
├── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
├── references/ # 선택 — 참조 문서
|
||||
└── assets/ # 선택 — 정적 파일 (설정, 데이터)
|
||||
에이전트에 스킬 디렉터리 경로를 전달합니다:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
backstory="An expert researcher.",
|
||||
skills=["./skills"], # 이 디렉터리의 모든 스킬을 검색
|
||||
)
|
||||
```
|
||||
|
||||
디렉터리 이름은 `SKILL.md`의 `name` 필드와 일치해야 합니다. `scripts/`, `references/`, `assets/` 디렉터리는 파일을 직접 참조해야 하는 에이전트를 위해 스킬의 `path`에서 사용할 수 있습니다.
|
||||
### 크루 레벨 스킬
|
||||
|
||||
---
|
||||
크루의 스킬 경로는 모든 에이전트에 병합됩니다:
|
||||
|
||||
## 사전 로드된 스킬
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
더 세밀한 제어를 위해 프로그래밍 방식으로 스킬을 검색하고 활성화할 수 있습니다:
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
skills=["./skills"],
|
||||
)
|
||||
```
|
||||
|
||||
### 사전 로드된 스킬
|
||||
|
||||
`Skill` 객체를 직접 전달할 수도 있습니다:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.skills import discover_skills, activate_skill
|
||||
|
||||
# 디렉터리의 모든 스킬 검색
|
||||
skills = discover_skills(Path("./skills"))
|
||||
|
||||
# 활성화 (전체 SKILL.md 본문 로드)
|
||||
activated = [activate_skill(s) for s in skills]
|
||||
|
||||
# 에이전트에 전달
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
@@ -250,57 +102,13 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 스킬 로드 방식
|
||||
|
||||
스킬은 **점진적 공개**를 사용합니다 — 각 단계에서 필요한 것만 로드합니다:
|
||||
스킬은 점진적으로 로드됩니다 — 각 단계에서 필요한 데이터만 읽습니다:
|
||||
|
||||
| 단계 | 로드되는 내용 | 시점 |
|
||||
| :------- | :------------------------------------ | :------------------ |
|
||||
| 검색 | 이름, 설명, 프론트매터 필드 | `discover_skills()` |
|
||||
| 활성화 | 전체 SKILL.md 본문 텍스트 | `activate_skill()` |
|
||||
| 단계 | 로드되는 내용 | 시점 |
|
||||
| :--------------- | :------------------------------------------------ | :----------------- |
|
||||
| 검색 | 이름, 설명, 프론트매터 필드 | `discover_skills()` |
|
||||
| 활성화 | 전체 SKILL.md 본문 텍스트 | `activate_skill()` |
|
||||
|
||||
일반적인 에이전트 실행 중(`skills=["./skills"]`로 디렉터리 경로 전달 시) 스킬은 자동으로 검색되고 활성화됩니다. 점진적 로딩은 프로그래밍 API를 사용할 때만 관련됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 스킬 vs 지식
|
||||
|
||||
스킬과 지식 모두 에이전트의 프롬프트를 수정하지만, 서로 다른 목적을 가지고 있습니다:
|
||||
|
||||
| 측면 | 스킬 | 지식 |
|
||||
| :--- | :--- | :--- |
|
||||
| **제공하는 것** | 지침, 절차, 가이드라인 | 사실, 데이터, 정보 |
|
||||
| **저장 방식** | 마크다운 파일 (SKILL.md) | 벡터 스토어에 임베딩 (ChromaDB) |
|
||||
| **검색 방식** | 전체 본문이 프롬프트에 주입 | 시맨틱 검색으로 관련 청크 찾기 |
|
||||
| **적합한 용도** | 방법론, 체크리스트, 스타일 가이드 | 회사 문서, 제품 정보, 참조 데이터 |
|
||||
| **설정 방법** | `skills=["./skills"]` | `knowledge_sources=[source]` |
|
||||
|
||||
**경험 법칙:** 에이전트가 *프로세스*를 따라야 하면 스킬을 사용하세요. 에이전트가 *데이터*를 참조해야 하면 지식을 사용하세요.
|
||||
|
||||
---
|
||||
|
||||
## 자주 묻는 질문
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="스킬과 도구를 모두 설정해야 하나요?">
|
||||
사용 사례에 따라 다릅니다. 스킬과 도구는 **독립적**입니다 — 둘 중 하나, 둘 다, 또는 아무것도 사용하지 않을 수 있습니다.
|
||||
|
||||
- **스킬만**: 에이전트가 전문성은 필요하지만 외부 액션이 필요 없을 때 (예: 스타일 가이드라인으로 작성)
|
||||
- **도구만**: 에이전트가 액션은 필요하지만 특별한 방법론이 필요 없을 때 (예: 간단한 웹 검색)
|
||||
- **둘 다**: 에이전트가 전문성 AND 액션이 필요할 때 (예: 특정 체크리스트로 보안 감사 AND 코드 스캔 기능)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="스킬이 자동으로 도구를 제공하나요?">
|
||||
**아니요.** SKILL.md의 `allowed-tools` 필드는 실험적 메타데이터일 뿐 — 도구를 프로비저닝하거나 주입하지 않습니다. 항상 `tools=[]`, `mcps=[]` 또는 `apps=[]`를 통해 별도로 도구를 설정해야 합니다.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="에이전트와 크루 모두에 같은 스킬을 설정하면 어떻게 되나요?">
|
||||
에이전트 레벨 스킬이 우선합니다. 스킬은 이름으로 중복 제거됩니다 — 에이전트의 스킬이 먼저 처리되므로, 같은 스킬 이름이 양쪽 레벨에 나타나면 에이전트의 버전이 사용됩니다.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="SKILL.md 본문의 최대 크기는 얼마인가요?">
|
||||
50,000자에서 소프트 경고가 있지만 하드 리밋은 없습니다. 최상의 결과를 위해 스킬을 집중적이고 간결하게 유지하세요 — 너무 큰 프롬프트 주입은 에이전트의 주의를 분산시킬 수 있습니다.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
일반적인 에이전트 실행 중에 스킬은 자동으로 검색되고 활성화됩니다. `scripts/`, `references/`, `assets/` 디렉터리는 파일을 직접 참조해야 하는 에이전트를 위해 스킬의 `path`에서 사용할 수 있습니다.
|
||||
|
||||
@@ -10,10 +10,6 @@ mode: "wide"
|
||||
CrewAI 도구는 에이전트에게 웹 검색, 데이터 분석부터 동료 간 협업 및 작업 위임에 이르기까지 다양한 기능을 제공합니다.
|
||||
이 문서에서는 CrewAI 프레임워크 내에서 이러한 도구를 생성, 통합 및 활용하는 방법과, 협업 도구에 초점을 맞춘 새로운 기능에 대해 설명합니다.
|
||||
|
||||
<Note type="info" title="도구는 다섯 가지 에이전트 기능 유형 중 하나입니다">
|
||||
도구는 에이전트에게 행동을 취할 수 있는 **호출 가능한 함수**를 제공합니다. [MCP](/ko/mcp/overview) (원격 도구 서버), [앱](/ko/concepts/agent-capabilities) (플랫폼 통합), [스킬](/ko/concepts/skills) (도메인 전문성), [지식](/ko/concepts/knowledge) (검색된 사실)과 함께 작동합니다. 각 유형을 언제 사용해야 하는지 알아보려면 [에이전트 기능](/ko/concepts/agent-capabilities) 개요를 참조하세요.
|
||||
</Note>
|
||||
|
||||
## Tool이란 무엇인가?
|
||||
|
||||
CrewAI에서 tool은 에이전트가 다양한 작업을 수행하기 위해 활용할 수 있는 기술 또는 기능입니다.
|
||||
|
||||
@@ -4,70 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="31 mar 2026">
|
||||
## v1.13.0a5
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a5)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a4
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="01 abr 2026">
|
||||
## v1.13.0a4
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a4)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a3
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="01 abr 2026">
|
||||
## v1.13.0a3
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a3)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Emitir dados de uso de token no LLMCallCompletedEvent
|
||||
- Extrair e publicar metadados de ferramentas no AMP
|
||||
|
||||
### Correções de Bugs
|
||||
- Lidar com modelos GPT-5.x que não suportam o parâmetro de API `stop`
|
||||
|
||||
### Documentação
|
||||
- Corrigir imprecisões nas capacidades do agente em todas as línguas
|
||||
- Adicionar visão geral das Capacidades do Agente e melhorar a documentação de Habilidades
|
||||
- Adicionar um guia abrangente de configuração de SSO
|
||||
- Atualizar o changelog e a versão para v1.13.0rc1
|
||||
|
||||
### Refatoração
|
||||
- Converter Flow para Pydantic BaseModel
|
||||
- Converter classes LLM para Pydantic BaseModel
|
||||
- Substituir InstanceOf[T] por anotações de tipo simples
|
||||
- Remover métodos não utilizados
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@dependabot[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @lucasgomide, @thiagomoretto
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 mar 2026">
|
||||
## v1.13.0rc1
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
title: "Capacidades do Agente"
|
||||
description: "Entenda as cinco formas de estender agentes CrewAI: Ferramentas, MCPs, Apps, Skills e Knowledge."
|
||||
icon: puzzle-piece
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Agentes CrewAI podem ser estendidos com **cinco tipos distintos de capacidades**, cada um servindo a um propósito diferente. Entender quando usar cada um — e como eles funcionam juntos — é fundamental para construir agentes eficazes.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Ferramentas" icon="wrench" href="/pt-BR/concepts/tools" color="#3B82F6">
|
||||
**Funções chamáveis** — permitem que agentes tomem ações. Buscas na web, operações com arquivos, chamadas de API, execução de código.
|
||||
</Card>
|
||||
<Card title="Servidores MCP" icon="plug" href="/pt-BR/mcp/overview" color="#8B5CF6">
|
||||
**Servidores de ferramentas remotos** — conectam agentes a servidores de ferramentas externos via Model Context Protocol. Mesmo efeito de ferramentas, mas hospedados externamente.
|
||||
</Card>
|
||||
<Card title="Apps" icon="grid-2" color="#EC4899">
|
||||
**Integrações com plataformas** — conectam agentes a aplicativos SaaS (Gmail, Slack, Jira, Salesforce) via plataforma CrewAI. Executa localmente com um token de integração.
|
||||
</Card>
|
||||
<Card title="Skills" icon="bolt" href="/pt-BR/concepts/skills" color="#F59E0B">
|
||||
**Expertise de domínio** — injetam instruções, diretrizes e material de referência nos prompts dos agentes. Skills dizem aos agentes *como pensar*.
|
||||
</Card>
|
||||
<Card title="Knowledge" icon="book" href="/pt-BR/concepts/knowledge" color="#10B981">
|
||||
**Fatos recuperados** — fornecem aos agentes dados de documentos, arquivos e URLs via busca semântica (RAG). Knowledge dá aos agentes *o que saber*.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
---
|
||||
|
||||
## A Distinção Fundamental
|
||||
|
||||
O mais importante a entender: **essas capacidades se dividem em duas categorias**.
|
||||
|
||||
### Capacidades de Ação (Ferramentas, MCPs, Apps)
|
||||
|
||||
Estas dão aos agentes a capacidade de **fazer coisas** — chamar APIs, ler arquivos, buscar na web, enviar emails. No momento da execução, os três tipos se resolvem no mesmo formato interno (instâncias de `BaseTool`) e aparecem em uma lista unificada de ferramentas que o agente pode chamar.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find and compile market data",
|
||||
backstory="Expert market analyst",
|
||||
tools=[SerperDevTool(), FileReadTool()], # Ferramentas locais
|
||||
mcps=["https://mcp.example.com/sse"], # Ferramentas de servidor MCP remoto
|
||||
apps=["gmail", "google_sheets"], # Integrações com plataformas
|
||||
)
|
||||
```
|
||||
|
||||
### Capacidades de Contexto (Skills, Knowledge)
|
||||
|
||||
Estas modificam o **prompt** do agente — injetando expertise, instruções ou dados recuperados antes do agente começar a raciocinar. Não dão aos agentes novas ações; elas moldam como os agentes pensam e a quais informações têm acesso.
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Security Auditor",
|
||||
goal="Audit cloud infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security with 10 years of experience",
|
||||
skills=["./skills/security-audit"], # Instruções de domínio
|
||||
knowledge_sources=[pdf_source, url_source], # Fatos recuperados
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quando Usar o Quê
|
||||
|
||||
| Você precisa... | Use | Exemplo |
|
||||
| :------------------------------------------------------- | :---------------- | :--------------------------------------- |
|
||||
| Agente buscar na web | **Ferramentas** | `tools=[SerperDevTool()]` |
|
||||
| Agente chamar uma API remota via MCP | **MCPs** | `mcps=["https://api.example.com/sse"]` |
|
||||
| Agente enviar emails pelo Gmail | **Apps** | `apps=["gmail"]` |
|
||||
| Agente seguir procedimentos específicos | **Skills** | `skills=["./skills/code-review"]` |
|
||||
| Agente consultar documentos da empresa | **Knowledge** | `knowledge_sources=[pdf_source]` |
|
||||
| Agente buscar na web E seguir diretrizes de revisão | **Ferramentas + Skills** | Use ambos juntos |
|
||||
|
||||
---
|
||||
|
||||
## Combinando Capacidades
|
||||
|
||||
Na prática, agentes frequentemente usam **múltiplos tipos de capacidades juntos**. Aqui está um exemplo realista:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
# Um agente de pesquisa totalmente equipado
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal="Produce comprehensive market analysis reports",
|
||||
backstory="Expert analyst with deep industry knowledge",
|
||||
|
||||
# AÇÃO: O que o agente pode FAZER
|
||||
tools=[
|
||||
SerperDevTool(), # Buscar na web
|
||||
FileReadTool(), # Ler arquivos locais
|
||||
CodeInterpreterTool(), # Executar código Python para análise
|
||||
],
|
||||
mcps=["https://data-api.example.com/sse"], # Acessar API de dados remota
|
||||
apps=["google_sheets"], # Escrever no Google Sheets
|
||||
|
||||
# CONTEXTO: O que o agente SABE
|
||||
skills=["./skills/research-methodology"], # Como conduzir pesquisas
|
||||
knowledge_sources=[company_docs], # Dados específicos da empresa
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tabela Comparativa
|
||||
|
||||
| Característica | Ferramentas | MCPs | Apps | Skills | Knowledge |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: |
|
||||
| **Dá ações ao agente** | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **Modifica o prompt** | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| **Requer código** | Sim | Apenas config | Apenas config | Apenas Markdown | Apenas config |
|
||||
| **Executa localmente** | Sim | Depende | Sim (com variável de ambiente) | N/A | Sim |
|
||||
| **Precisa de chaves API** | Por ferramenta | Por servidor | Token de integração | Não | Apenas embedder |
|
||||
| **Definido no Agent** | `tools=[]` | `mcps=[]` | `apps=[]` | `skills=[]` | `knowledge_sources=[]` |
|
||||
| **Definido no Crew** | ❌ | ❌ | ❌ | `skills=[]` | `knowledge_sources=[]` |
|
||||
|
||||
---
|
||||
|
||||
## Aprofundamentos
|
||||
|
||||
Pronto para aprender mais sobre cada tipo de capacidade?
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Ferramentas" icon="wrench" href="/pt-BR/concepts/tools">
|
||||
Crie ferramentas personalizadas, use o catálogo OSS com 75+ opções, configure cache e execução assíncrona.
|
||||
</Card>
|
||||
<Card title="Integração MCP" icon="plug" href="/pt-BR/mcp/overview">
|
||||
Conecte-se a servidores MCP via stdio, SSE ou HTTP. Filtre ferramentas, configure autenticação.
|
||||
</Card>
|
||||
<Card title="Skills" icon="bolt" href="/pt-BR/concepts/skills">
|
||||
Construa pacotes de skills com SKILL.md, injete expertise de domínio, use divulgação progressiva.
|
||||
</Card>
|
||||
<Card title="Knowledge" icon="book" href="/pt-BR/concepts/knowledge">
|
||||
Adicione conhecimento de PDFs, CSVs, URLs e mais. Configure embedders e recuperação.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,186 +1,27 @@
|
||||
---
|
||||
title: Skills
|
||||
description: Pacotes de skills baseados em sistema de arquivos que injetam expertise de domínio e instruções nos prompts dos agentes.
|
||||
description: Pacotes de skills baseados em sistema de arquivos que injetam contexto nos prompts dos agentes.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Skills são diretórios autocontidos que fornecem aos agentes **instruções, diretrizes e material de referência específicos de domínio**. Cada skill é definida por um arquivo `SKILL.md` com frontmatter YAML e um corpo em markdown.
|
||||
Skills são diretórios autocontidos que fornecem aos agentes instruções, referências e assets específicos de domínio. Cada skill é definida por um arquivo `SKILL.md` com frontmatter YAML e um corpo em markdown.
|
||||
|
||||
Quando ativada, as instruções de uma skill são injetadas diretamente no prompt da tarefa do agente — dando ao agente expertise sem exigir alterações de código.
|
||||
Skills usam **divulgação progressiva** — metadados são carregados primeiro, instruções completas apenas quando ativadas, e catálogos de recursos apenas quando necessário.
|
||||
|
||||
<Note type="info" title="Skills vs Ferramentas — A Distinção Fundamental">
|
||||
**Skills NÃO são ferramentas.** Este é o ponto de confusão mais comum.
|
||||
|
||||
- **Skills** injetam *instruções e contexto* no prompt do agente. Elas dizem ao agente *como pensar* sobre um problema.
|
||||
- **Ferramentas** dão ao agente *funções chamáveis* para tomar ações (buscar, ler arquivos, chamar APIs).
|
||||
|
||||
Frequentemente você precisa de **ambos**: skills para expertise, ferramentas para ação. Eles são configurados independentemente e se complementam.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Início Rápido
|
||||
|
||||
### 1. Crie um Diretório de Skill
|
||||
## Estrutura de Diretório
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # Obrigatório — instruções
|
||||
├── references/ # Opcional — documentos de referência
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # Opcional — scripts executáveis
|
||||
my-skill/
|
||||
├── SKILL.md # Obrigatório — frontmatter + instruções
|
||||
├── scripts/ # Opcional — scripts executáveis
|
||||
├── references/ # Opcional — documentos de referência
|
||||
└── assets/ # Opcional — arquivos estáticos (configs, dados)
|
||||
```
|
||||
|
||||
### 2. Escreva seu SKILL.md
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: code-review
|
||||
description: Guidelines for conducting thorough code reviews with focus on security and performance.
|
||||
metadata:
|
||||
author: your-team
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
## Diretrizes de Code Review
|
||||
|
||||
Ao revisar código, siga esta checklist:
|
||||
|
||||
1. **Segurança**: Verifique vulnerabilidades de injeção, bypasses de autenticação e exposição de dados
|
||||
2. **Performance**: Procure por queries N+1, alocações desnecessárias e chamadas bloqueantes
|
||||
3. **Legibilidade**: Garanta nomenclatura clara, comentários apropriados e estilo consistente
|
||||
4. **Testes**: Verifique cobertura adequada de testes para novas funcionalidades
|
||||
|
||||
### Níveis de Severidade
|
||||
- **Crítico**: Vulnerabilidades de segurança, riscos de perda de dados → bloquear merge
|
||||
- **Major**: Problemas de performance, erros de lógica → solicitar alterações
|
||||
- **Minor**: Questões de estilo, sugestões de nomenclatura → aprovar com comentários
|
||||
```
|
||||
|
||||
### 3. Anexe a um Agente
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import GithubSearchTool, FileReadTool
|
||||
|
||||
reviewer = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["./skills"], # Injeta diretrizes de revisão
|
||||
tools=[GithubSearchTool(), FileReadTool()], # Permite ao agente ler código
|
||||
)
|
||||
```
|
||||
|
||||
O agente agora tem tanto **expertise** (da skill) quanto **capacidades** (das ferramentas).
|
||||
|
||||
---
|
||||
|
||||
## Skills + Ferramentas: Trabalhando Juntos
|
||||
|
||||
Aqui estão padrões comuns mostrando como skills e ferramentas se complementam:
|
||||
|
||||
### Padrão 1: Apenas Skills (Expertise de Domínio, Sem Ações Necessárias)
|
||||
|
||||
Use quando o agente precisa de instruções específicas mas não precisa chamar serviços externos:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write clear API documentation",
|
||||
backstory="Expert technical writer",
|
||||
skills=["./skills/api-docs-style"], # Diretrizes e templates de escrita
|
||||
# Sem ferramentas necessárias — agente escreve baseado no contexto fornecido
|
||||
)
|
||||
```
|
||||
|
||||
### Padrão 2: Apenas Ferramentas (Ações, Sem Expertise Especial)
|
||||
|
||||
Use quando o agente precisa tomar ações mas não precisa de instruções específicas de domínio:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
|
||||
|
||||
agent = Agent(
|
||||
role="Web Researcher",
|
||||
goal="Find information about a topic",
|
||||
backstory="Skilled at finding information online",
|
||||
tools=[SerperDevTool(), ScrapeWebsiteTool()], # Pode buscar e extrair dados
|
||||
# Sem skills necessárias — pesquisa geral não precisa de diretrizes especiais
|
||||
)
|
||||
```
|
||||
|
||||
### Padrão 3: Skills + Ferramentas (Expertise E Ações)
|
||||
|
||||
O padrão mais comum no mundo real. A skill fornece *como* abordar o trabalho; ferramentas fornecem *o que* o agente pode fazer:
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
||||
|
||||
analyst = Agent(
|
||||
role="Security Analyst",
|
||||
goal="Audit infrastructure for vulnerabilities",
|
||||
backstory="Expert in cloud security and compliance",
|
||||
skills=["./skills/security-audit"], # Metodologia e checklists de auditoria
|
||||
tools=[
|
||||
SerperDevTool(), # Pesquisar vulnerabilidades conhecidas
|
||||
FileReadTool(), # Ler arquivos de configuração
|
||||
CodeInterpreterTool(), # Executar scripts de análise
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Padrão 4: Skills + MCPs
|
||||
|
||||
Skills funcionam junto com servidores MCP da mesma forma que com ferramentas:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze customer data and generate reports",
|
||||
backstory="Expert data analyst with strong statistical background",
|
||||
skills=["./skills/data-analysis"], # Metodologia de análise
|
||||
mcps=["https://data-warehouse.example.com/sse"], # Acesso remoto a dados
|
||||
)
|
||||
```
|
||||
|
||||
### Padrão 5: Skills + Apps
|
||||
|
||||
Skills podem guiar como um agente usa integrações de plataforma:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Customer Support Agent",
|
||||
goal="Respond to customer inquiries professionally",
|
||||
backstory="Experienced support representative",
|
||||
skills=["./skills/support-playbook"], # Templates de resposta e regras de escalação
|
||||
apps=["gmail", "zendesk"], # Pode enviar emails e atualizar tickets
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skills no Nível do Crew
|
||||
|
||||
Skills podem ser definidas no crew para aplicar a **todos os agentes**:
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
crew = Crew(
|
||||
agents=[researcher, writer, reviewer],
|
||||
tasks=[research_task, write_task, review_task],
|
||||
skills=["./skills"], # Todos os agentes recebem essas skills
|
||||
)
|
||||
```
|
||||
|
||||
Skills no nível do agente têm prioridade — se a mesma skill é descoberta em ambos os níveis, a versão do agente é usada.
|
||||
|
||||
---
|
||||
O nome do diretório deve corresponder ao campo `name` no `SKILL.md`.
|
||||
|
||||
## Formato do SKILL.md
|
||||
|
||||
@@ -193,7 +34,7 @@ compatibility: crewai>=0.1.0 # opcional
|
||||
metadata: # opcional
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # opcional, experimental
|
||||
allowed-tools: web-search file-read # opcional, delimitado por espaços
|
||||
---
|
||||
|
||||
Instruções para o agente vão aqui. Este corpo em markdown é injetado
|
||||
@@ -202,46 +43,57 @@ no prompt do agente quando a skill é ativada.
|
||||
|
||||
### Campos do Frontmatter
|
||||
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| Campo | Obrigatório | Restrições |
|
||||
| :-------------- | :---------- | :----------------------------------------------------------------------- |
|
||||
| `name` | Sim | 1–64 chars. Alfanumérico minúsculo e hifens. Deve corresponder ao nome do diretório. |
|
||||
| `name` | Sim | 1–64 chars. Alfanumérico minúsculo e hifens. Sem hifens iniciais/finais/consecutivos. Deve corresponder ao nome do diretório. |
|
||||
| `description` | Sim | 1–1024 chars. Descreve o que a skill faz e quando usá-la. |
|
||||
| `license` | Não | Nome da licença ou referência a um arquivo de licença incluído. |
|
||||
| `compatibility` | Não | Máx 500 chars. Requisitos de ambiente (produtos, pacotes, rede). |
|
||||
| `metadata` | Não | Mapeamento arbitrário de chave-valor string. |
|
||||
| `allowed-tools` | Não | Lista de ferramentas pré-aprovadas delimitada por espaços. Experimental. |
|
||||
|
||||
---
|
||||
## Uso
|
||||
|
||||
## Estrutura de Diretório
|
||||
### Skills no Nível do Agente
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # Obrigatório — frontmatter + instruções
|
||||
├── scripts/ # Opcional — scripts executáveis
|
||||
├── references/ # Opcional — documentos de referência
|
||||
└── assets/ # Opcional — arquivos estáticos (configs, dados)
|
||||
Passe caminhos de diretório de skills para um agente:
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
backstory="An expert researcher.",
|
||||
skills=["./skills"], # descobre todas as skills neste diretório
|
||||
)
|
||||
```
|
||||
|
||||
O nome do diretório deve corresponder ao campo `name` no `SKILL.md`. Os diretórios `scripts/`, `references/` e `assets/` estão disponíveis no `path` da skill para agentes que precisam referenciar arquivos diretamente.
|
||||
### Skills no Nível do Crew
|
||||
|
||||
---
|
||||
Caminhos de skills no crew são mesclados em todos os agentes:
|
||||
|
||||
## Skills Pré-carregadas
|
||||
```python
|
||||
from crewai import Crew
|
||||
|
||||
Para mais controle, você pode descobrir e ativar skills programaticamente:
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
skills=["./skills"],
|
||||
)
|
||||
```
|
||||
|
||||
### Skills Pré-carregadas
|
||||
|
||||
Você também pode passar objetos `Skill` diretamente:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from crewai.skills import discover_skills, activate_skill
|
||||
|
||||
# Descobrir todas as skills em um diretório
|
||||
skills = discover_skills(Path("./skills"))
|
||||
|
||||
# Ativá-las (carrega o corpo completo do SKILL.md)
|
||||
activated = [activate_skill(s) for s in skills]
|
||||
|
||||
# Passar para um agente
|
||||
agent = Agent(
|
||||
role="Researcher",
|
||||
goal="Find relevant information",
|
||||
@@ -250,57 +102,13 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Como as Skills São Carregadas
|
||||
|
||||
Skills usam **divulgação progressiva** — carregando apenas o necessário em cada estágio:
|
||||
Skills carregam progressivamente — apenas os dados necessários em cada etapa são lidos:
|
||||
|
||||
| Estágio | O que é carregado | Quando |
|
||||
| :--------- | :------------------------------------ | :------------------ |
|
||||
| Descoberta | Nome, descrição, campos do frontmatter | `discover_skills()` |
|
||||
| Ativação | Texto completo do corpo do SKILL.md | `activate_skill()` |
|
||||
| Etapa | O que é carregado | Quando |
|
||||
| :--------------- | :------------------------------------------------ | :------------------ |
|
||||
| Descoberta | Nome, descrição, campos do frontmatter | `discover_skills()` |
|
||||
| Ativação | Texto completo do corpo do SKILL.md | `activate_skill()` |
|
||||
|
||||
Durante a execução normal do agente (passando caminhos de diretório via `skills=["./skills"]`), skills são automaticamente descobertas e ativadas. O carregamento progressivo só importa quando usando a API programática.
|
||||
|
||||
---
|
||||
|
||||
## Skills vs Knowledge
|
||||
|
||||
Tanto skills quanto knowledge modificam o prompt do agente, mas servem propósitos diferentes:
|
||||
|
||||
| Aspecto | Skills | Knowledge |
|
||||
| :--- | :--- | :--- |
|
||||
| **O que fornece** | Instruções, procedimentos, diretrizes | Fatos, dados, informações |
|
||||
| **Como é armazenado** | Arquivos Markdown (SKILL.md) | Embarcado em banco vetorial (ChromaDB) |
|
||||
| **Como é recuperado** | Corpo inteiro injetado no prompt | Busca semântica encontra trechos relevantes |
|
||||
| **Melhor para** | Metodologia, checklists, guias de estilo | Documentos da empresa, info de produto, dados de referência |
|
||||
| **Definido via** | `skills=["./skills"]` | `knowledge_sources=[source]` |
|
||||
|
||||
**Regra prática:** Se o agente precisa seguir um *processo*, use uma skill. Se o agente precisa consultar *dados*, use knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Perguntas Frequentes
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Preciso definir skills E ferramentas?">
|
||||
Depende do seu caso de uso. Skills e ferramentas são **independentes** — você pode usar qualquer um, ambos ou nenhum.
|
||||
|
||||
- **Apenas skills**: Quando o agente precisa de expertise mas não de ações externas (ex: escrever com diretrizes de estilo)
|
||||
- **Apenas ferramentas**: Quando o agente precisa de ações mas não de metodologia especial (ex: busca simples na web)
|
||||
- **Ambos**: Quando o agente precisa de expertise E ações (ex: auditoria de segurança com checklists específicas E capacidade de escanear código)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Skills fornecem ferramentas automaticamente?">
|
||||
**Não.** O campo `allowed-tools` no SKILL.md é apenas metadado experimental — ele não provisiona nem injeta nenhuma ferramenta. Você deve sempre definir ferramentas separadamente via `tools=[]`, `mcps=[]` ou `apps=[]`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="O que acontece se eu definir a mesma skill tanto no agente quanto no crew?">
|
||||
A skill no nível do agente tem prioridade. Skills são deduplicadas por nome — as skills do agente são processadas primeiro, então se o mesmo nome de skill aparece em ambos os níveis, a versão do agente é usada.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Qual o tamanho máximo do corpo do SKILL.md?">
|
||||
Há um aviso suave em 50.000 caracteres, mas sem limite rígido. Mantenha skills focadas e concisas para melhores resultados — injeções de prompt muito grandes podem diluir a atenção do agente.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
Durante a execução normal do agente, skills são automaticamente descobertas e ativadas. Os diretórios `scripts/`, `references/` e `assets/` estão disponíveis no `path` da skill para agentes que precisam referenciar arquivos diretamente.
|
||||
|
||||
@@ -10,10 +10,6 @@ mode: "wide"
|
||||
As ferramentas do CrewAI capacitam agentes com habilidades que vão desde busca na web e análise de dados até colaboração e delegação de tarefas entre colegas de trabalho.
|
||||
Esta documentação descreve como criar, integrar e aproveitar essas ferramentas dentro do framework CrewAI, incluindo um novo foco em ferramentas de colaboração.
|
||||
|
||||
<Note type="info" title="Ferramentas são um dos cinco tipos de capacidades de agentes">
|
||||
Ferramentas dão aos agentes **funções chamáveis** para tomar ações. Elas funcionam junto com [MCPs](/pt-BR/mcp/overview) (servidores de ferramentas remotos), [Apps](/pt-BR/concepts/agent-capabilities) (integrações com plataformas), [Skills](/pt-BR/concepts/skills) (expertise de domínio) e [Knowledge](/pt-BR/concepts/knowledge) (fatos recuperados). Veja a visão geral de [Capacidades do Agente](/pt-BR/concepts/agent-capabilities) para entender quando usar cada um.
|
||||
</Note>
|
||||
|
||||
## O que é uma Ferramenta?
|
||||
|
||||
Uma ferramenta no CrewAI é uma habilidade ou função que os agentes podem utilizar para executar diversas ações.
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0rc1"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.13.0a5",
|
||||
"crewai==1.13.0rc1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0rc1"
|
||||
|
||||
@@ -14281,349 +14281,10 @@
|
||||
],
|
||||
"title": "EnvVar",
|
||||
"type": "object"
|
||||
},
|
||||
"JsonResponseFormat": {
|
||||
"description": "Response format requesting raw JSON output (e.g. ``{\"type\": \"json_object\"}``).",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "json_object",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "JsonResponseFormat",
|
||||
"type": "object"
|
||||
},
|
||||
"LLM": {
|
||||
"properties": {
|
||||
"additional_params": {
|
||||
"additionalProperties": true,
|
||||
"title": "Additional Params",
|
||||
"type": "object"
|
||||
},
|
||||
"api_base": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Api Base"
|
||||
},
|
||||
"api_key": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Api Key"
|
||||
},
|
||||
"api_version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Api Version"
|
||||
},
|
||||
"base_url": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Base Url"
|
||||
},
|
||||
"callbacks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Callbacks"
|
||||
},
|
||||
"completion_cost": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Completion Cost"
|
||||
},
|
||||
"context_window_size": {
|
||||
"default": 0,
|
||||
"title": "Context Window Size",
|
||||
"type": "integer"
|
||||
},
|
||||
"frequency_penalty": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Frequency Penalty"
|
||||
},
|
||||
"interceptor": {
|
||||
"default": null,
|
||||
"title": "Interceptor"
|
||||
},
|
||||
"is_anthropic": {
|
||||
"default": false,
|
||||
"title": "Is Anthropic",
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_litellm": {
|
||||
"default": false,
|
||||
"title": "Is Litellm",
|
||||
"type": "boolean"
|
||||
},
|
||||
"logit_bias": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Logit Bias"
|
||||
},
|
||||
"logprobs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Logprobs"
|
||||
},
|
||||
"max_completion_tokens": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Max Completion Tokens"
|
||||
},
|
||||
"max_tokens": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Max Tokens"
|
||||
},
|
||||
"model": {
|
||||
"title": "Model",
|
||||
"type": "string"
|
||||
},
|
||||
"n": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "N"
|
||||
},
|
||||
"prefer_upload": {
|
||||
"default": false,
|
||||
"title": "Prefer Upload",
|
||||
"type": "boolean"
|
||||
},
|
||||
"presence_penalty": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Presence Penalty"
|
||||
},
|
||||
"provider": {
|
||||
"default": "openai",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"reasoning_effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"none",
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Reasoning Effort"
|
||||
},
|
||||
"response_format": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/JsonResponseFormat"
|
||||
},
|
||||
{},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Response Format"
|
||||
},
|
||||
"seed": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Seed"
|
||||
},
|
||||
"stop": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Stop",
|
||||
"type": "array"
|
||||
},
|
||||
"stream": {
|
||||
"default": false,
|
||||
"title": "Stream",
|
||||
"type": "boolean"
|
||||
},
|
||||
"temperature": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Temperature"
|
||||
},
|
||||
"thinking": {
|
||||
"default": null,
|
||||
"title": "Thinking"
|
||||
},
|
||||
"timeout": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Timeout"
|
||||
},
|
||||
"top_logprobs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Top Logprobs"
|
||||
},
|
||||
"top_p": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Top P"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"model"
|
||||
],
|
||||
"title": "LLM",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "A tool for performing Optical Character Recognition on images.\n\nThis tool leverages LLMs to extract text from images. It can process\nboth local image files and images available via URLs.\n\nAttributes:\n name (str): Name of the tool.\n description (str): Description of the tool's functionality.\n args_schema (Type[BaseModel]): Pydantic schema for input validation.\n\nPrivate Attributes:\n _llm (Optional[LLM]): Language model instance for making API calls.",
|
||||
"properties": {
|
||||
"llm": {
|
||||
"$ref": "#/$defs/LLM"
|
||||
}
|
||||
},
|
||||
"properties": {},
|
||||
"title": "OCRTool",
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies = [
|
||||
"uv~=0.9.13",
|
||||
"aiosqlite~=0.21.0",
|
||||
"pyyaml~=6.0",
|
||||
"lancedb>=0.29.2,<0.30.1",
|
||||
"lancedb>=0.29.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.13.0a5",
|
||||
"crewai-tools==1.13.0rc1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -4,8 +4,6 @@ from typing import Any
|
||||
import urllib.request
|
||||
import warnings
|
||||
|
||||
from pydantic import PydanticUserError
|
||||
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
from crewai.crew import Crew
|
||||
@@ -44,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0rc1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
@@ -95,38 +93,6 @@ def __getattr__(name: str) -> Any:
|
||||
raise AttributeError(f"module 'crewai' has no attribute {name!r}")
|
||||
|
||||
|
||||
try:
|
||||
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
|
||||
from crewai.experimental.agent_executor import AgentExecutor as _AgentExecutor
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext as _LLMCallHookContext
|
||||
from crewai.tools.tool_types import ToolResult as _ToolResult
|
||||
from crewai.utilities.prompts import (
|
||||
StandardPromptResult as _StandardPromptResult,
|
||||
SystemPromptResult as _SystemPromptResult,
|
||||
)
|
||||
|
||||
_AgentExecutor.model_rebuild(
|
||||
force=True,
|
||||
_types_namespace={
|
||||
"Agent": Agent,
|
||||
"ToolsHandler": _ToolsHandler,
|
||||
"Crew": Crew,
|
||||
"BaseLLM": BaseLLM,
|
||||
"Task": Task,
|
||||
"StandardPromptResult": _StandardPromptResult,
|
||||
"SystemPromptResult": _SystemPromptResult,
|
||||
"LLMCallHookContext": _LLMCallHookContext,
|
||||
"ToolResult": _ToolResult,
|
||||
},
|
||||
)
|
||||
except (ImportError, PydanticUserError):
|
||||
import logging as _logging
|
||||
|
||||
_logging.getLogger(__name__).warning(
|
||||
"AgentExecutor.model_rebuild() failed; forward refs may be unresolved.",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LLM",
|
||||
"Agent",
|
||||
|
||||
@@ -25,6 +25,7 @@ from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
@@ -166,10 +167,10 @@ class Agent(BaseAgent):
|
||||
default=True,
|
||||
description="Use system prompt for the agent.",
|
||||
)
|
||||
llm: str | BaseLLM | None = Field(
|
||||
llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
function_calling_llm: str | BaseLLM | None = Field(
|
||||
function_calling_llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
system_template: str | None = Field(
|
||||
@@ -1011,7 +1012,7 @@ class Agent(BaseAgent):
|
||||
self.agent_executor.tools = tools
|
||||
self.agent_executor.original_tools = raw_tools
|
||||
self.agent_executor.prompt = prompt
|
||||
self.agent_executor.stop_words = stop_words
|
||||
self.agent_executor.stop = stop_words
|
||||
self.agent_executor.tools_names = get_tool_names(tools)
|
||||
self.agent_executor.tools_description = render_text_description_and_args(tools)
|
||||
self.agent_executor.response_model = (
|
||||
|
||||
@@ -12,6 +12,7 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
@@ -184,7 +185,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default=None,
|
||||
description="Knowledge sources for the agent.",
|
||||
)
|
||||
knowledge_storage: BaseKnowledgeStorage | None = Field(
|
||||
knowledge_storage: InstanceOf[BaseKnowledgeStorage] | None = Field(
|
||||
default=None,
|
||||
description="Custom knowledge storage for the agent.",
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.13.0a5"
|
||||
"crewai[tools]==1.13.0rc1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.13.0a5"
|
||||
"crewai[tools]==1.13.0rc1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.13.0a5"
|
||||
"crewai[tools]==1.13.0rc1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -22,6 +22,7 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
Json,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
@@ -175,7 +176,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
_rpm_controller: RPMController = PrivateAttr()
|
||||
_logger: Logger = PrivateAttr()
|
||||
_file_handler: FileHandler = PrivateAttr()
|
||||
_cache_handler: CacheHandler = PrivateAttr(default_factory=CacheHandler)
|
||||
_cache_handler: InstanceOf[CacheHandler] = PrivateAttr(default_factory=CacheHandler)
|
||||
_memory: Memory | MemoryScope | MemorySlice | None = PrivateAttr(default=None)
|
||||
_train: bool | None = PrivateAttr(default=False)
|
||||
_train_iteration: int | None = PrivateAttr()
|
||||
@@ -209,13 +210,13 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=None,
|
||||
description="Metrics for the LLM usage during all tasks execution.",
|
||||
)
|
||||
manager_llm: str | BaseLLM | None = Field(
|
||||
manager_llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
manager_agent: BaseAgent | None = Field(
|
||||
description="Custom agent that will be used as manager.", default=None
|
||||
)
|
||||
function_calling_llm: str | LLM | None = Field(
|
||||
function_calling_llm: str | InstanceOf[LLM] | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
config: Json[dict[str, Any]] | dict[str, Any] | None = Field(default=None)
|
||||
@@ -266,7 +267,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=False,
|
||||
description="Plan the crew execution and add the plan to the crew.",
|
||||
)
|
||||
planning_llm: str | BaseLLM | Any | None = Field(
|
||||
planning_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Language model that will run the AgentPlanner if planning is True."
|
||||
@@ -287,7 +288,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
"knowledge object."
|
||||
),
|
||||
)
|
||||
chat_llm: str | BaseLLM | Any | None = Field(
|
||||
chat_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
default=None,
|
||||
description="LLM used to handle chatting with the crew.",
|
||||
)
|
||||
@@ -1799,7 +1800,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
def test(
|
||||
self,
|
||||
n_iterations: int,
|
||||
eval_llm: str | BaseLLM,
|
||||
eval_llm: str | InstanceOf[BaseLLM],
|
||||
inputs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Test and evaluate the Crew with the given inputs for n iterations.
|
||||
|
||||
@@ -57,7 +57,6 @@ class LLMCallCompletedEvent(LLMEventBase):
|
||||
messages: str | list[dict[str, Any]] | None = None
|
||||
response: Any
|
||||
call_type: LLMCallType
|
||||
usage: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class LLMCallFailedEvent(LLMEventBase):
|
||||
|
||||
@@ -11,15 +11,10 @@ import threading
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic import BaseModel, Field, GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||
from crewai.agents.parser import (
|
||||
@@ -124,7 +119,6 @@ class AgentExecutorState(BaseModel):
|
||||
(todos, observations, replan tracking) in a single validated model.
|
||||
"""
|
||||
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
messages: list[LLMMessage] = Field(default_factory=list)
|
||||
iterations: int = Field(default=0)
|
||||
current_answer: AgentAction | AgentFinish | None = Field(default=None)
|
||||
@@ -158,9 +152,6 @@ class AgentExecutorState(BaseModel):
|
||||
class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
"""Agent Executor for both standalone agents and crew-bound agents.
|
||||
|
||||
_skip_auto_memory prevents Flow from eagerly allocating a Memory
|
||||
instance — the executor uses agent/crew memory, not its own.
|
||||
|
||||
Inherits from:
|
||||
- Flow[AgentExecutorState]: Provides flow orchestration capabilities
|
||||
- CrewAgentExecutorMixin: Provides memory methods (short/long/external term)
|
||||
@@ -168,74 +159,136 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
This executor can operate in two modes:
|
||||
- Standalone mode: When crew and task are None (used by Agent.kickoff())
|
||||
- Crew mode: When crew and task are provided (used by Agent.execute_task())
|
||||
|
||||
Note: Multiple instances may be created during agent initialization
|
||||
(cache setup, RPM controller setup, etc.) but only the final instance
|
||||
should execute tasks via invoke().
|
||||
"""
|
||||
|
||||
_skip_auto_memory: bool = True
|
||||
def __init__(
|
||||
self,
|
||||
llm: BaseLLM,
|
||||
agent: Agent,
|
||||
prompt: SystemPromptResult | StandardPromptResult,
|
||||
max_iter: int,
|
||||
tools: list[CrewStructuredTool],
|
||||
tools_names: str,
|
||||
stop_words: list[str],
|
||||
tools_description: str,
|
||||
tools_handler: ToolsHandler,
|
||||
task: Task | None = None,
|
||||
crew: Crew | None = None,
|
||||
step_callback: Any = None,
|
||||
original_tools: list[BaseTool] | None = None,
|
||||
function_calling_llm: BaseLLM | Any | None = None,
|
||||
respect_context_window: bool = False,
|
||||
request_within_rpm_limit: Callable[[], bool] | None = None,
|
||||
callbacks: list[Any] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
i18n: I18N | None = None,
|
||||
) -> None:
|
||||
"""Initialize the flow-based agent executor.
|
||||
|
||||
suppress_flow_events: bool = True # always suppress for executor
|
||||
llm: BaseLLM = Field(exclude=True)
|
||||
agent: Agent = Field(exclude=True)
|
||||
prompt: SystemPromptResult | StandardPromptResult = Field(exclude=True)
|
||||
max_iter: int = Field(default=25, exclude=True)
|
||||
tools: list[CrewStructuredTool] = Field(default_factory=list, exclude=True)
|
||||
tools_names: str = Field(default="", exclude=True)
|
||||
stop_words: list[str] = Field(default_factory=list, exclude=True)
|
||||
tools_description: str = Field(default="", exclude=True)
|
||||
tools_handler: ToolsHandler | None = Field(default=None, exclude=True)
|
||||
task: Task | None = Field(default=None, exclude=True)
|
||||
crew: Crew | None = Field(default=None, exclude=True)
|
||||
step_callback: Any = Field(default=None, exclude=True)
|
||||
original_tools: list[BaseTool] = Field(default_factory=list, exclude=True)
|
||||
function_calling_llm: BaseLLM | None = Field(default=None, exclude=True)
|
||||
respect_context_window: bool = Field(default=False, exclude=True)
|
||||
request_within_rpm_limit: Callable[[], bool] | None = Field(
|
||||
default=None, exclude=True
|
||||
)
|
||||
callbacks: list[Any] = Field(default_factory=list, exclude=True)
|
||||
response_model: type[BaseModel] | None = Field(default=None, exclude=True)
|
||||
i18n: I18N | None = Field(default=None, exclude=True)
|
||||
log_error_after: int = Field(default=3, exclude=True)
|
||||
before_llm_call_hooks: list[BeforeLLMCallHookType | BeforeLLMCallHookCallable] = (
|
||||
Field(default_factory=list, exclude=True)
|
||||
)
|
||||
after_llm_call_hooks: list[AfterLLMCallHookType | AfterLLMCallHookCallable] = Field(
|
||||
default_factory=list, exclude=True
|
||||
)
|
||||
Args:
|
||||
llm: Language model instance.
|
||||
agent: Agent to execute.
|
||||
prompt: Prompt templates.
|
||||
max_iter: Maximum iterations.
|
||||
tools: Available tools.
|
||||
tools_names: Tool names string.
|
||||
stop_words: Stop word list.
|
||||
tools_description: Tool descriptions.
|
||||
tools_handler: Tool handler instance.
|
||||
task: Optional task to execute (None for standalone agent execution).
|
||||
crew: Optional crew instance (None for standalone agent execution).
|
||||
step_callback: Optional step callback.
|
||||
original_tools: Original tool list.
|
||||
function_calling_llm: Optional function calling LLM.
|
||||
respect_context_window: Respect context limits.
|
||||
request_within_rpm_limit: RPM limit check function.
|
||||
callbacks: Optional callbacks list.
|
||||
response_model: Optional Pydantic model for structured outputs.
|
||||
"""
|
||||
self._i18n: I18N = i18n or get_i18n()
|
||||
self.llm = llm
|
||||
self.task: Task | None = task
|
||||
self.agent = agent
|
||||
self.crew: Crew | None = crew
|
||||
self.prompt = prompt
|
||||
self.tools = tools
|
||||
self.tools_names = tools_names
|
||||
self.stop = stop_words
|
||||
self.max_iter = max_iter
|
||||
self.callbacks = callbacks or []
|
||||
self._printer: Printer = Printer()
|
||||
self.tools_handler = tools_handler
|
||||
self.original_tools = original_tools or []
|
||||
self.step_callback = step_callback
|
||||
self.tools_description = tools_description
|
||||
self.function_calling_llm = function_calling_llm
|
||||
self.respect_context_window = respect_context_window
|
||||
self.request_within_rpm_limit = request_within_rpm_limit
|
||||
self.response_model = response_model
|
||||
self.log_error_after = 3
|
||||
self._console: Console = Console()
|
||||
|
||||
_i18n: I18N = PrivateAttr(default_factory=get_i18n)
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
_console: Console = PrivateAttr(default_factory=Console)
|
||||
_last_parser_error: OutputParserError | None = PrivateAttr(default=None)
|
||||
_last_context_error: Exception | None = PrivateAttr(default=None)
|
||||
_execution_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
_finalize_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
_finalize_called: bool = PrivateAttr(default=False)
|
||||
_is_executing: bool = PrivateAttr(default=False)
|
||||
_has_been_invoked: bool = PrivateAttr(default=False)
|
||||
_instance_id: str = PrivateAttr(default_factory=lambda: str(uuid4())[:8])
|
||||
_step_executor: Any = PrivateAttr(default=None)
|
||||
_planner_observer: Any = PrivateAttr(default=None)
|
||||
# Error context storage for recovery
|
||||
self._last_parser_error: OutputParserError | None = None
|
||||
self._last_context_error: Exception | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _setup_executor(self) -> Self:
|
||||
"""Configure executor after Pydantic field initialization."""
|
||||
self._i18n = self.i18n or get_i18n()
|
||||
# Execution guard to prevent concurrent/duplicate executions
|
||||
self._execution_lock = threading.Lock()
|
||||
self._finalize_lock = threading.Lock()
|
||||
self._finalize_called: bool = False
|
||||
self._is_executing: bool = False
|
||||
self._has_been_invoked: bool = False
|
||||
self._flow_initialized: bool = False
|
||||
|
||||
self._instance_id = str(uuid4())[:8]
|
||||
|
||||
self.before_llm_call_hooks: list[
|
||||
BeforeLLMCallHookType | BeforeLLMCallHookCallable
|
||||
] = []
|
||||
self.after_llm_call_hooks: list[
|
||||
AfterLLMCallHookType | AfterLLMCallHookCallable
|
||||
] = []
|
||||
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
|
||||
self.after_llm_call_hooks.extend(get_after_llm_call_hooks())
|
||||
|
||||
if self.llm:
|
||||
existing_stop = getattr(self.llm, "stop", [])
|
||||
if not isinstance(existing_stop, list):
|
||||
existing_stop = []
|
||||
self.llm.stop = list(set(existing_stop + self.stop_words))
|
||||
|
||||
self.llm.stop = list(
|
||||
set(
|
||||
existing_stop + self.stop
|
||||
if isinstance(existing_stop, list)
|
||||
else self.stop
|
||||
)
|
||||
)
|
||||
self._state = AgentExecutorState()
|
||||
self.max_method_calls = self.max_iter * 10
|
||||
|
||||
current_tracing = is_tracing_enabled_in_context()
|
||||
self.tracing = current_tracing if current_tracing else None
|
||||
self._flow_post_init()
|
||||
return self
|
||||
# Plan-and-Execute components (Phase 2)
|
||||
# Lazy-imported to avoid circular imports during module load
|
||||
self._step_executor: Any = None
|
||||
self._planner_observer: Any = None
|
||||
|
||||
def _ensure_flow_initialized(self) -> None:
|
||||
"""Ensure Flow.__init__() has been called.
|
||||
|
||||
This is deferred from __init__ to prevent FlowCreatedEvent emission
|
||||
during agent setup when multiple executor instances are created.
|
||||
Only the instance that actually executes via invoke() will emit events.
|
||||
"""
|
||||
if not self._flow_initialized:
|
||||
current_tracing = is_tracing_enabled_in_context()
|
||||
# Now call Flow's __init__ which will replace self._state
|
||||
# with Flow's managed state. Suppress flow events since this is
|
||||
# an agent executor, not a user-facing flow.
|
||||
super().__init__(
|
||||
suppress_flow_events=True,
|
||||
tracing=current_tracing if current_tracing else None,
|
||||
max_method_calls=self.max_iter * 10,
|
||||
)
|
||||
self._flow_initialized = True
|
||||
|
||||
def _check_native_tool_support(self) -> bool:
|
||||
"""Check if LLM supports native function calling."""
|
||||
@@ -265,13 +318,19 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
|
||||
@property
|
||||
def state(self) -> AgentExecutorState:
|
||||
"""Get thread-safe state proxy."""
|
||||
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
|
||||
"""Get state - returns temporary state if Flow not yet initialized.
|
||||
|
||||
Flow initialization is deferred to prevent event emission during agent setup.
|
||||
Returns the temporary state until invoke() is called.
|
||||
"""
|
||||
if self._flow_initialized and hasattr(self, "_state_lock"):
|
||||
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def iterations(self) -> int:
|
||||
"""Compatibility property for mixin - returns state iterations."""
|
||||
return self._state.iterations # type: ignore[no-any-return]
|
||||
return self._state.iterations
|
||||
|
||||
@iterations.setter
|
||||
def iterations(self, value: int) -> None:
|
||||
@@ -281,7 +340,7 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
@property
|
||||
def messages(self) -> list[LLMMessage]:
|
||||
"""Compatibility property - returns state messages."""
|
||||
return self._state.messages # type: ignore[no-any-return]
|
||||
return self._state.messages
|
||||
|
||||
@messages.setter
|
||||
def messages(self, value: list[LLMMessage]) -> None:
|
||||
@@ -1910,7 +1969,8 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
@listen("initialized")
|
||||
def continue_iteration(self) -> Literal["check_iteration"]:
|
||||
"""Bridge listener that connects iteration loop back to iteration check."""
|
||||
self._discard_or_listener(FlowMethodName("continue_iteration"))
|
||||
if self._flow_initialized:
|
||||
self._discard_or_listener(FlowMethodName("continue_iteration"))
|
||||
return "check_iteration"
|
||||
|
||||
@router(or_(initialize_reasoning, continue_iteration))
|
||||
@@ -2538,6 +2598,8 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
if is_inside_event_loop():
|
||||
return self.invoke_async(inputs)
|
||||
|
||||
self._ensure_flow_initialized()
|
||||
|
||||
with self._execution_lock:
|
||||
if self._is_executing:
|
||||
raise RuntimeError(
|
||||
@@ -2628,6 +2690,8 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
Returns:
|
||||
Dictionary with agent output.
|
||||
"""
|
||||
self._ensure_flow_initialized()
|
||||
|
||||
with self._execution_lock:
|
||||
if self._is_executing:
|
||||
raise RuntimeError(
|
||||
@@ -2943,6 +3007,17 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
"""
|
||||
return bool(self.crew and self.crew._train)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, _source_type: Any, _handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
"""Generate Pydantic core schema for Protocol compatibility.
|
||||
|
||||
Allows the executor to be used in Pydantic models without
|
||||
requiring arbitrary_types_allowed=True.
|
||||
"""
|
||||
return core_schema.any_schema()
|
||||
|
||||
|
||||
# Backward compatibility alias (deprecated)
|
||||
CrewAgentExecutorFlow = AgentExecutor
|
||||
|
||||
@@ -39,14 +39,7 @@ from uuid import uuid4
|
||||
|
||||
from opentelemetry import baggage
|
||||
from opentelemetry.context import attach, detach
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
ValidationError,
|
||||
)
|
||||
from pydantic._internal._model_construction import ModelMetaclass
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
@@ -88,7 +81,6 @@ from crewai.flow.flow_wrappers import (
|
||||
SimpleFlowCondition,
|
||||
StartMethod,
|
||||
)
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult
|
||||
from crewai.flow.input_provider import InputProvider
|
||||
from crewai.flow.persistence.base import FlowPersistence
|
||||
from crewai.flow.types import (
|
||||
@@ -116,6 +108,7 @@ if TYPE_CHECKING:
|
||||
from crewai_files import FileInput
|
||||
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
|
||||
from crewai.flow.visualization import build_flow_structure, render_interactive
|
||||
@@ -735,7 +728,7 @@ class StateProxy(Generic[T]):
|
||||
return result
|
||||
|
||||
|
||||
class FlowMeta(ModelMetaclass):
|
||||
class FlowMeta(type):
|
||||
def __new__(
|
||||
mcs,
|
||||
name: str,
|
||||
@@ -743,45 +736,6 @@ class FlowMeta(ModelMetaclass):
|
||||
namespace: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> type:
|
||||
parent_fields: set[str] = set()
|
||||
for base in bases:
|
||||
if hasattr(base, "model_fields"):
|
||||
parent_fields.update(base.model_fields)
|
||||
|
||||
annotations = namespace.get("__annotations__", {})
|
||||
_skip_types = (classmethod, staticmethod, property)
|
||||
|
||||
for base in bases:
|
||||
if isinstance(base, ModelMetaclass):
|
||||
continue
|
||||
for attr_name in getattr(base, "__annotations__", {}):
|
||||
if attr_name not in annotations and attr_name not in namespace:
|
||||
annotations[attr_name] = ClassVar
|
||||
|
||||
for attr_name, attr_value in namespace.items():
|
||||
if isinstance(attr_value, property) and attr_name not in annotations:
|
||||
for base in bases:
|
||||
base_ann = getattr(base, "__annotations__", {})
|
||||
if attr_name in base_ann:
|
||||
annotations[attr_name] = ClassVar
|
||||
|
||||
for attr_name, attr_value in list(namespace.items()):
|
||||
if attr_name in annotations or attr_name.startswith("_"):
|
||||
continue
|
||||
if attr_name in parent_fields:
|
||||
annotations[attr_name] = Any
|
||||
if isinstance(attr_value, BaseModel):
|
||||
namespace[attr_name] = Field(
|
||||
default_factory=lambda v=attr_value: v, exclude=True
|
||||
)
|
||||
continue
|
||||
if callable(attr_value) or isinstance(
|
||||
attr_value, (*_skip_types, FlowMethod)
|
||||
):
|
||||
continue
|
||||
annotations[attr_name] = ClassVar[type(attr_value)]
|
||||
namespace["__annotations__"] = annotations
|
||||
|
||||
cls = super().__new__(mcs, name, bases, namespace)
|
||||
|
||||
start_methods = []
|
||||
@@ -866,90 +820,88 @@ class FlowMeta(ModelMetaclass):
|
||||
return cls
|
||||
|
||||
|
||||
class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
class Flow(Generic[T], metaclass=FlowMeta):
|
||||
"""Base class for all flows.
|
||||
|
||||
type parameter T must be either dict[str, Any] or a subclass of BaseModel."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
ignored_types=(StartMethod, ListenMethod, RouterMethod),
|
||||
revalidate_instances="never",
|
||||
)
|
||||
__hash__ = object.__hash__
|
||||
|
||||
_start_methods: ClassVar[list[FlowMethodName]] = []
|
||||
_listeners: ClassVar[dict[FlowMethodName, SimpleFlowCondition | FlowCondition]] = {}
|
||||
_routers: ClassVar[set[FlowMethodName]] = set()
|
||||
_router_paths: ClassVar[dict[FlowMethodName, list[FlowMethodName]]] = {}
|
||||
initial_state: type[T] | T | None = None
|
||||
name: str | None = None
|
||||
tracing: bool | None = None
|
||||
stream: bool = False
|
||||
memory: Memory | MemoryScope | MemorySlice | None = None
|
||||
input_provider: InputProvider | None = None
|
||||
|
||||
initial_state: Any = Field(default=None)
|
||||
name: str | None = Field(default=None)
|
||||
tracing: bool | None = Field(default=None)
|
||||
stream: bool = Field(default=False)
|
||||
memory: Memory | MemoryScope | MemorySlice | None = Field(default=None)
|
||||
input_provider: InputProvider | None = Field(default=None)
|
||||
suppress_flow_events: bool = Field(default=False)
|
||||
human_feedback_history: list[HumanFeedbackResult] = Field(default_factory=list)
|
||||
last_human_feedback: HumanFeedbackResult | None = Field(default=None)
|
||||
|
||||
persistence: Any = Field(default=None, exclude=True)
|
||||
max_method_calls: int = Field(default=100, exclude=True)
|
||||
|
||||
_methods: dict[FlowMethodName, FlowMethod[Any, Any]] = PrivateAttr(
|
||||
default_factory=dict
|
||||
)
|
||||
_method_execution_counts: dict[FlowMethodName, int] = PrivateAttr(
|
||||
default_factory=dict
|
||||
)
|
||||
_pending_and_listeners: dict[PendingListenerKey, set[FlowMethodName]] = PrivateAttr(
|
||||
default_factory=dict
|
||||
)
|
||||
_fired_or_listeners: set[FlowMethodName] = PrivateAttr(default_factory=set)
|
||||
_method_outputs: list[Any] = PrivateAttr(default_factory=list)
|
||||
_state_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
_or_listeners_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
_completed_methods: set[FlowMethodName] = PrivateAttr(default_factory=set)
|
||||
_method_call_counts: dict[FlowMethodName, int] = PrivateAttr(default_factory=dict)
|
||||
_is_execution_resuming: bool = PrivateAttr(default=False)
|
||||
_event_futures: list[Future[None]] = PrivateAttr(default_factory=list)
|
||||
_pending_feedback_context: PendingFeedbackContext | None = PrivateAttr(default=None)
|
||||
_human_feedback_method_outputs: dict[str, Any] = PrivateAttr(default_factory=dict)
|
||||
_input_history: list[InputHistoryEntry] = PrivateAttr(default_factory=list)
|
||||
_state: Any = PrivateAttr(default=None)
|
||||
|
||||
def __class_getitem__(cls: type[Flow[T]], item: type[T]) -> type[Flow[T]]: # type: ignore[override]
|
||||
class _FlowGeneric(cls): # type: ignore[valid-type,misc]
|
||||
pass
|
||||
def __class_getitem__(cls: type[Flow[T]], item: type[T]) -> type[Flow[T]]:
|
||||
class _FlowGeneric(cls): # type: ignore
|
||||
_initial_state_t = item
|
||||
|
||||
_FlowGeneric.__name__ = f"{cls.__name__}[{item.__name__}]"
|
||||
_FlowGeneric._initial_state_t = item
|
||||
return _FlowGeneric
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
"""Allow arbitrary attribute assignment for backward compat with plain class."""
|
||||
if name in self.model_fields or name in self.__private_attributes__:
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
object.__setattr__(self, name, value)
|
||||
def __init__(
|
||||
self,
|
||||
persistence: FlowPersistence | None = None,
|
||||
tracing: bool | None = None,
|
||||
suppress_flow_events: bool = False,
|
||||
max_method_calls: int = 100,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a new Flow instance.
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
self._flow_post_init()
|
||||
Args:
|
||||
persistence: Optional persistence backend for storing flow states
|
||||
tracing: Whether to enable tracing. True=always enable, False=always disable, None=check environment/user settings
|
||||
suppress_flow_events: Whether to suppress flow event emissions (internal use)
|
||||
max_method_calls: Maximum times a single method can be called per execution before raising RecursionError
|
||||
**kwargs: Additional state values to initialize or override
|
||||
"""
|
||||
# Initialize basic instance attributes
|
||||
self._methods: dict[FlowMethodName, FlowMethod[Any, Any]] = {}
|
||||
self._method_execution_counts: dict[FlowMethodName, int] = {}
|
||||
self._pending_and_listeners: dict[PendingListenerKey, set[FlowMethodName]] = {}
|
||||
self._fired_or_listeners: set[FlowMethodName] = (
|
||||
set()
|
||||
) # Track OR listeners that already fired
|
||||
self._method_outputs: list[Any] = [] # list to store all method outputs
|
||||
self._state_lock = threading.Lock()
|
||||
self._or_listeners_lock = threading.Lock()
|
||||
self._completed_methods: set[FlowMethodName] = (
|
||||
set()
|
||||
) # Track completed methods for reload
|
||||
self._method_call_counts: dict[FlowMethodName, int] = {}
|
||||
self._max_method_calls = max_method_calls
|
||||
self._persistence: FlowPersistence | None = persistence
|
||||
self._is_execution_resuming: bool = False
|
||||
self._event_futures: list[Future[None]] = []
|
||||
|
||||
def _flow_post_init(self) -> None:
|
||||
"""Heavy initialization: state creation, events, memory, method registration."""
|
||||
if getattr(self, "_flow_post_init_done", False):
|
||||
return
|
||||
object.__setattr__(self, "_flow_post_init_done", True)
|
||||
# Human feedback storage
|
||||
self.human_feedback_history: list[HumanFeedbackResult] = []
|
||||
self.last_human_feedback: HumanFeedbackResult | None = None
|
||||
self._pending_feedback_context: PendingFeedbackContext | None = None
|
||||
# Per-method stash for real @human_feedback output (keyed by method name)
|
||||
# Used to decouple routing outcome from method return value when emit is set
|
||||
self._human_feedback_method_outputs: dict[str, Any] = {}
|
||||
self.suppress_flow_events: bool = suppress_flow_events
|
||||
|
||||
if self._state is None:
|
||||
self._state = self._create_initial_state()
|
||||
# User input history (for self.ask())
|
||||
self._input_history: list[InputHistoryEntry] = []
|
||||
|
||||
# Initialize state with initial values
|
||||
self._state = self._create_initial_state()
|
||||
self.tracing = tracing
|
||||
tracing_enabled = should_enable_tracing(override=self.tracing)
|
||||
set_tracing_enabled(tracing_enabled)
|
||||
|
||||
trace_listener = TraceCollectionListener()
|
||||
trace_listener.setup_listeners(crewai_event_bus)
|
||||
# Apply any additional kwargs
|
||||
if kwargs:
|
||||
self._initialize_state(kwargs)
|
||||
|
||||
if not self.suppress_flow_events:
|
||||
crewai_event_bus.emit(
|
||||
@@ -1433,8 +1385,8 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
self._pending_feedback_context = None
|
||||
|
||||
# Clear pending feedback from persistence
|
||||
if self.persistence:
|
||||
self.persistence.clear_pending_feedback(context.flow_id)
|
||||
if self._persistence:
|
||||
self._persistence.clear_pending_feedback(context.flow_id)
|
||||
|
||||
# Emit feedback received event
|
||||
crewai_event_bus.emit(
|
||||
@@ -1475,17 +1427,17 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
self._pending_feedback_context = e.context
|
||||
|
||||
if self.persistence is None:
|
||||
if self._persistence is None:
|
||||
from crewai.flow.persistence import SQLiteFlowPersistence
|
||||
|
||||
self.persistence = SQLiteFlowPersistence()
|
||||
self._persistence = SQLiteFlowPersistence()
|
||||
|
||||
state_data = (
|
||||
self._state
|
||||
if isinstance(self._state, dict)
|
||||
else self._state.model_dump()
|
||||
)
|
||||
self.persistence.save_pending_feedback(
|
||||
self._persistence.save_pending_feedback(
|
||||
flow_uuid=e.context.flow_id,
|
||||
context=e.context,
|
||||
state_data=state_data,
|
||||
@@ -1535,33 +1487,39 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
"""
|
||||
init_state = self.initial_state
|
||||
|
||||
# Handle case where initial_state is None but we have a type parameter
|
||||
if init_state is None and hasattr(self, "_initial_state_t"):
|
||||
state_type = self._initial_state_t
|
||||
if isinstance(state_type, type):
|
||||
if issubclass(state_type, FlowState):
|
||||
# Create instance - FlowState auto-generates id via default_factory
|
||||
instance = state_type()
|
||||
# Ensure id is set - generate UUID if empty
|
||||
if not getattr(instance, "id", None):
|
||||
object.__setattr__(instance, "id", str(uuid4()))
|
||||
return cast(T, instance)
|
||||
if issubclass(state_type, BaseModel):
|
||||
|
||||
# Create a new type with FlowState first for proper id default
|
||||
class StateWithId(FlowState, state_type): # type: ignore
|
||||
pass
|
||||
|
||||
instance = StateWithId()
|
||||
# Ensure id is set - generate UUID if empty
|
||||
if not getattr(instance, "id", None):
|
||||
object.__setattr__(instance, "id", str(uuid4()))
|
||||
return cast(T, instance)
|
||||
if state_type is dict:
|
||||
return cast(T, {"id": str(uuid4())})
|
||||
|
||||
# Handle case where no initial state is provided
|
||||
if init_state is None:
|
||||
return cast(T, {"id": str(uuid4())})
|
||||
|
||||
# Handle case where initial_state is a type (class)
|
||||
if isinstance(init_state, type):
|
||||
state_class = init_state
|
||||
if issubclass(state_class, FlowState):
|
||||
return cast(T, state_class())
|
||||
return state_class()
|
||||
if issubclass(state_class, BaseModel):
|
||||
model_fields = getattr(state_class, "model_fields", None)
|
||||
if not model_fields or "id" not in model_fields:
|
||||
@@ -1569,7 +1527,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
model_instance = state_class()
|
||||
if not getattr(model_instance, "id", None):
|
||||
object.__setattr__(model_instance, "id", str(uuid4()))
|
||||
return cast(T, model_instance)
|
||||
return model_instance
|
||||
if init_state is dict:
|
||||
return cast(T, {"id": str(uuid4())})
|
||||
|
||||
@@ -1580,21 +1538,32 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
new_state["id"] = str(uuid4())
|
||||
return cast(T, new_state)
|
||||
|
||||
# Handle BaseModel instance case
|
||||
if isinstance(init_state, BaseModel):
|
||||
model = init_state
|
||||
if hasattr(model, "id"):
|
||||
model = cast(BaseModel, init_state)
|
||||
if not hasattr(model, "id"):
|
||||
raise ValueError("Flow state model must have an 'id' field")
|
||||
|
||||
# Create new instance with same values to avoid mutations
|
||||
if hasattr(model, "model_dump"):
|
||||
# Pydantic v2
|
||||
state_dict = model.model_dump()
|
||||
if not state_dict.get("id"):
|
||||
state_dict["id"] = str(uuid4())
|
||||
model_class = type(model)
|
||||
return cast(T, model_class(**state_dict))
|
||||
elif hasattr(model, "dict"):
|
||||
# Pydantic v1
|
||||
state_dict = model.dict()
|
||||
else:
|
||||
# Fallback for other BaseModel implementations
|
||||
state_dict = {
|
||||
k: v for k, v in model.__dict__.items() if not k.startswith("_")
|
||||
}
|
||||
|
||||
class StateWithId(FlowState, type(model)): # type: ignore
|
||||
pass
|
||||
# Ensure id is set - generate UUID if empty
|
||||
if not state_dict.get("id"):
|
||||
state_dict["id"] = str(uuid4())
|
||||
|
||||
state_dict = model.model_dump()
|
||||
state_dict["id"] = str(uuid4())
|
||||
return cast(T, StateWithId(**state_dict))
|
||||
# Create new instance of the same class
|
||||
model_class = type(model)
|
||||
return cast(T, model_class(**state_dict))
|
||||
raise TypeError(
|
||||
f"Initial state must be dict or BaseModel, got {type(self.initial_state)}"
|
||||
)
|
||||
@@ -1607,17 +1576,17 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
"""
|
||||
if isinstance(self._state, BaseModel):
|
||||
try:
|
||||
return cast(T, self._state.model_copy(deep=True))
|
||||
return self._state.model_copy(deep=True)
|
||||
except (TypeError, AttributeError):
|
||||
try:
|
||||
state_dict = self._state.model_dump()
|
||||
model_class = type(self._state)
|
||||
return cast(T, model_class(**state_dict))
|
||||
return model_class(**state_dict)
|
||||
except Exception:
|
||||
return cast(T, self._state.model_copy(deep=False))
|
||||
return self._state.model_copy(deep=False)
|
||||
else:
|
||||
try:
|
||||
return cast(T, copy.deepcopy(self._state))
|
||||
return copy.deepcopy(self._state)
|
||||
except (TypeError, AttributeError):
|
||||
return cast(T, self._state.copy())
|
||||
|
||||
@@ -1693,7 +1662,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
elif isinstance(self._state, BaseModel):
|
||||
# For BaseModel states, preserve existing fields unless overridden
|
||||
try:
|
||||
model = self._state
|
||||
model = cast(BaseModel, self._state)
|
||||
# Get current state as dict
|
||||
if hasattr(model, "model_dump"):
|
||||
current_state = model.model_dump()
|
||||
@@ -1744,7 +1713,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
self._state.update(stored_state)
|
||||
elif isinstance(self._state, BaseModel):
|
||||
# For BaseModel states, create new instance with stored values
|
||||
model = self._state
|
||||
model = cast(BaseModel, self._state)
|
||||
if hasattr(model, "model_validate"):
|
||||
# Pydantic v2
|
||||
self._state = cast(T, type(model).model_validate(stored_state))
|
||||
@@ -1969,7 +1938,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
|
||||
try:
|
||||
# Reset flow state for fresh execution unless restoring from persistence
|
||||
is_restoring = inputs and "id" in inputs and self.persistence is not None
|
||||
is_restoring = inputs and "id" in inputs and self._persistence is not None
|
||||
if not is_restoring:
|
||||
# Clear completed methods and outputs for a fresh start
|
||||
self._completed_methods.clear()
|
||||
@@ -1995,9 +1964,9 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
setattr(self._state, "id", inputs["id"]) # noqa: B010
|
||||
|
||||
# If persistence is enabled, attempt to restore the stored state using the provided id.
|
||||
if "id" in inputs and self.persistence is not None:
|
||||
if "id" in inputs and self._persistence is not None:
|
||||
restore_uuid = inputs["id"]
|
||||
stored_state = self.persistence.load_state(restore_uuid)
|
||||
stored_state = self._persistence.load_state(restore_uuid)
|
||||
if stored_state:
|
||||
self._log_flow_event(
|
||||
f"Loading flow state from memory for UUID: {restore_uuid}"
|
||||
@@ -2067,17 +2036,17 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
# Auto-save pending feedback (create default persistence if needed)
|
||||
if self.persistence is None:
|
||||
if self._persistence is None:
|
||||
from crewai.flow.persistence import SQLiteFlowPersistence
|
||||
|
||||
self.persistence = SQLiteFlowPersistence()
|
||||
self._persistence = SQLiteFlowPersistence()
|
||||
|
||||
state_data = (
|
||||
self._state
|
||||
if isinstance(self._state, dict)
|
||||
else self._state.model_dump()
|
||||
)
|
||||
self.persistence.save_pending_feedback(
|
||||
self._persistence.save_pending_feedback(
|
||||
flow_uuid=e.context.flow_id,
|
||||
context=e.context,
|
||||
state_data=state_data,
|
||||
@@ -2363,10 +2332,10 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
e.context.method_name = method_name
|
||||
|
||||
if self.persistence is None:
|
||||
if self._persistence is None:
|
||||
from crewai.flow.persistence import SQLiteFlowPersistence
|
||||
|
||||
self.persistence = SQLiteFlowPersistence()
|
||||
self._persistence = SQLiteFlowPersistence()
|
||||
|
||||
# Emit paused event (not failed)
|
||||
if not self.suppress_flow_events:
|
||||
@@ -2727,9 +2696,9 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
- Catches and logs any exceptions during execution, preventing individual listener failures from breaking the entire flow
|
||||
"""
|
||||
count = self._method_call_counts.get(listener_name, 0) + 1
|
||||
if count > self.max_method_calls:
|
||||
if count > self._max_method_calls:
|
||||
raise RecursionError(
|
||||
f"Method '{listener_name}' has been called {self.max_method_calls} times in "
|
||||
f"Method '{listener_name}' has been called {self._max_method_calls} times in "
|
||||
f"this flow execution, which indicates an infinite loop. "
|
||||
f"This commonly happens when a @listen label matches the "
|
||||
f"method's own name."
|
||||
@@ -2836,7 +2805,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
|
||||
This is best-effort: if persistence is not configured, this is a no-op.
|
||||
"""
|
||||
if self.persistence is None:
|
||||
if self._persistence is None:
|
||||
return
|
||||
try:
|
||||
state_data = (
|
||||
@@ -2844,7 +2813,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
if isinstance(self._state, dict)
|
||||
else self._state.model_dump()
|
||||
)
|
||||
self.persistence.save_state(
|
||||
self._persistence.save_state(
|
||||
flow_uuid=self.flow_id,
|
||||
method_name="_ask_checkpoint",
|
||||
state_data=state_data,
|
||||
|
||||
@@ -3,15 +3,12 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.rag.types import SearchResult
|
||||
|
||||
|
||||
class BaseKnowledgeStorage(BaseModel, ABC):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
class BaseKnowledgeStorage(ABC):
|
||||
"""Abstract base class for knowledge storage implementations."""
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -3,9 +3,6 @@ import traceback
|
||||
from typing import Any, cast
|
||||
import warnings
|
||||
|
||||
from pydantic import Field, PrivateAttr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
|
||||
from crewai.rag.chromadb.config import ChromaDBConfig
|
||||
from crewai.rag.chromadb.types import ChromaEmbeddingFunctionWrapper
|
||||
@@ -25,32 +22,31 @@ class KnowledgeStorage(BaseKnowledgeStorage):
|
||||
search efficiency.
|
||||
"""
|
||||
|
||||
collection_name: str | None = None
|
||||
embedder: (
|
||||
ProviderSpec
|
||||
def __init__(
|
||||
self,
|
||||
embedder: ProviderSpec
|
||||
| BaseEmbeddingsProvider[Any]
|
||||
| type[BaseEmbeddingsProvider[Any]]
|
||||
| None
|
||||
) = Field(default=None, exclude=True)
|
||||
_client: BaseClient | None = PrivateAttr(default=None)
|
||||
| None = None,
|
||||
collection_name: str | None = None,
|
||||
) -> None:
|
||||
self.collection_name = collection_name
|
||||
self._client: BaseClient | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_client(self) -> Self:
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=r".*'model_fields'.*is deprecated.*",
|
||||
module=r"^chromadb(\.|$)",
|
||||
)
|
||||
|
||||
if self.embedder:
|
||||
embedding_function = build_embedder(self.embedder) # type: ignore[arg-type]
|
||||
if embedder:
|
||||
embedding_function = build_embedder(embedder) # type: ignore[arg-type]
|
||||
config = ChromaDBConfig(
|
||||
embedding_function=cast(
|
||||
ChromaEmbeddingFunctionWrapper, embedding_function
|
||||
)
|
||||
)
|
||||
self._client = create_client(config)
|
||||
return self
|
||||
|
||||
def _get_client(self) -> BaseClient:
|
||||
"""Get the appropriate client - instance-specific or global."""
|
||||
|
||||
@@ -22,6 +22,7 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
@@ -203,7 +204,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
role: str = Field(description="Role of the agent")
|
||||
goal: str = Field(description="Goal of the agent")
|
||||
backstory: str = Field(description="Backstory of the agent")
|
||||
llm: str | BaseLLM | Any | None = Field(
|
||||
llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
default=None, description="Language model that will run the agent"
|
||||
)
|
||||
tools: list[BaseTool] = Field(
|
||||
|
||||
@@ -20,7 +20,8 @@ from typing import (
|
||||
)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
@@ -36,12 +37,7 @@ from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.llms.base_llm import (
|
||||
BaseLLM,
|
||||
JsonResponseFormat,
|
||||
get_current_call_id,
|
||||
llm_call_context,
|
||||
)
|
||||
from crewai.llms.base_llm import BaseLLM, get_current_call_id, llm_call_context
|
||||
from crewai.llms.constants import (
|
||||
ANTHROPIC_MODELS,
|
||||
AZURE_MODELS,
|
||||
@@ -67,6 +63,8 @@ except ImportError:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicThinkingConfig
|
||||
from crewai.task import Task
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.utilities.types import LLMMessage
|
||||
@@ -344,27 +342,6 @@ class AccumulatedToolArgs(BaseModel):
|
||||
|
||||
class LLM(BaseLLM):
|
||||
completion_cost: float | None = None
|
||||
timeout: float | int | None = None
|
||||
top_p: float | None = None
|
||||
n: int | None = None
|
||||
max_completion_tokens: int | None = None
|
||||
max_tokens: int | float | None = None
|
||||
presence_penalty: float | None = None
|
||||
frequency_penalty: float | None = None
|
||||
logit_bias: dict[int, float] | None = None
|
||||
response_format: JsonResponseFormat | type[BaseModel] | None = None
|
||||
seed: int | None = None
|
||||
logprobs: int | None = None
|
||||
top_logprobs: int | None = None
|
||||
api_base: str | None = None
|
||||
api_version: str | None = None
|
||||
callbacks: list[Any] | None = None
|
||||
reasoning_effort: Literal["none", "low", "medium", "high"] | None = None
|
||||
stream: bool = False
|
||||
interceptor: Any = None
|
||||
thinking: Any = None
|
||||
context_window_size: int = 0
|
||||
is_anthropic: bool = False
|
||||
|
||||
def __new__(cls, model: str, is_litellm: bool = False, **kwargs: Any) -> LLM:
|
||||
"""Factory method that routes to native SDK or falls back to LiteLLM.
|
||||
@@ -459,7 +436,10 @@ class LLM(BaseLLM):
|
||||
logger.error(error_msg)
|
||||
raise ImportError(error_msg) from None
|
||||
|
||||
return object.__new__(cls)
|
||||
instance = object.__new__(cls)
|
||||
super(LLM, instance).__init__(model=model, is_litellm=True, **kwargs)
|
||||
instance.is_litellm = True
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def _matches_provider_pattern(cls, model: str, provider: str) -> bool:
|
||||
@@ -644,23 +624,89 @@ class LLM(BaseLLM):
|
||||
|
||||
return None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _validate_llm_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
model = data.get("model", "")
|
||||
data["is_anthropic"] = cls._is_anthropic_model(model)
|
||||
return data
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
timeout: float | int | None = None,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
n: int | None = None,
|
||||
stop: str | list[str] | None = None,
|
||||
max_completion_tokens: int | None = None,
|
||||
max_tokens: int | float | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
logit_bias: dict[int, float] | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
logprobs: int | None = None,
|
||||
top_logprobs: int | None = None,
|
||||
base_url: str | None = None,
|
||||
api_base: str | None = None,
|
||||
api_version: str | None = None,
|
||||
api_key: str | None = None,
|
||||
callbacks: list[Any] | None = None,
|
||||
reasoning_effort: Literal["none", "low", "medium", "high"] | None = None,
|
||||
stream: bool = False,
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
|
||||
thinking: AnthropicThinkingConfig | dict[str, Any] | None = None,
|
||||
prefer_upload: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize LLM instance.
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_litellm(self) -> LLM:
|
||||
self.is_litellm = True
|
||||
if LITELLM_AVAILABLE:
|
||||
litellm.drop_params = True
|
||||
self.set_callbacks(self.callbacks or [])
|
||||
self.set_env_callbacks()
|
||||
return self
|
||||
Note: This __init__ method is only called for fallback instances.
|
||||
Native provider instances handle their own initialization in their respective classes.
|
||||
"""
|
||||
super().__init__(
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
timeout=timeout,
|
||||
**kwargs,
|
||||
)
|
||||
self.model = model
|
||||
self.timeout = timeout
|
||||
self.temperature = temperature
|
||||
self.top_p = top_p
|
||||
self.n = n
|
||||
self.max_completion_tokens = max_completion_tokens
|
||||
self.max_tokens = max_tokens
|
||||
self.presence_penalty = presence_penalty
|
||||
self.frequency_penalty = frequency_penalty
|
||||
self.logit_bias = logit_bias
|
||||
self.response_format = response_format
|
||||
self.seed = seed
|
||||
self.logprobs = logprobs
|
||||
self.top_logprobs = top_logprobs
|
||||
self.base_url = base_url
|
||||
self.api_base = api_base
|
||||
self.api_version = api_version
|
||||
self.api_key = api_key
|
||||
self.callbacks = callbacks
|
||||
self.context_window_size = 0
|
||||
self.reasoning_effort = reasoning_effort
|
||||
self.prefer_upload = prefer_upload
|
||||
self.additional_params = {
|
||||
k: v for k, v in kwargs.items() if k not in ("is_litellm", "provider")
|
||||
}
|
||||
self.is_anthropic = self._is_anthropic_model(model)
|
||||
self.stream = stream
|
||||
self.interceptor = interceptor
|
||||
|
||||
litellm.drop_params = True
|
||||
|
||||
# Normalize self.stop to always be a list[str]
|
||||
if stop is None:
|
||||
self.stop: list[str] = []
|
||||
elif isinstance(stop, str):
|
||||
self.stop = [stop]
|
||||
else:
|
||||
self.stop = stop
|
||||
|
||||
self.set_callbacks(callbacks or [])
|
||||
self.set_env_callbacks()
|
||||
|
||||
@staticmethod
|
||||
def _is_anthropic_model(model: str) -> bool:
|
||||
@@ -970,25 +1016,21 @@ class LLM(BaseLLM):
|
||||
)
|
||||
result = instructor_instance.to_pydantic()
|
||||
structured_response = result.model_dump_json()
|
||||
usage_dict = self._usage_to_dict(usage_info)
|
||||
self._handle_emit_call_events(
|
||||
response=structured_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_dict,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
usage_dict = self._usage_to_dict(usage_info)
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_dict,
|
||||
)
|
||||
return full_response
|
||||
|
||||
@@ -998,14 +1040,12 @@ class LLM(BaseLLM):
|
||||
return tool_result
|
||||
|
||||
# --- 10) Emit completion event and return response
|
||||
usage_dict = self._usage_to_dict(usage_info)
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_dict,
|
||||
)
|
||||
return full_response
|
||||
|
||||
@@ -1027,7 +1067,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=self._usage_to_dict(usage_info),
|
||||
)
|
||||
return full_response
|
||||
|
||||
@@ -1179,7 +1218,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=None,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1210,8 +1248,6 @@ class LLM(BaseLLM):
|
||||
raise LLMContextLengthExceededError(error_msg) from e
|
||||
raise
|
||||
|
||||
response_usage = self._usage_to_dict(getattr(response, "usage", None))
|
||||
|
||||
# --- 2) Handle structured output response (when response_model is provided)
|
||||
if response_model is not None:
|
||||
# When using instructor/response_model, litellm returns a Pydantic model instance
|
||||
@@ -1223,7 +1259,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1255,7 +1290,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1279,7 +1313,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1329,7 +1362,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=None,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1356,8 +1388,6 @@ class LLM(BaseLLM):
|
||||
raise LLMContextLengthExceededError(error_msg) from e
|
||||
raise
|
||||
|
||||
response_usage = self._usage_to_dict(getattr(response, "usage", None))
|
||||
|
||||
if response_model is not None:
|
||||
if isinstance(response, BaseModel):
|
||||
structured_response = response.model_dump_json()
|
||||
@@ -1367,7 +1397,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1397,7 +1426,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1420,7 +1448,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1567,14 +1594,12 @@ class LLM(BaseLLM):
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
usage_dict = self._usage_to_dict(usage_info)
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("messages"),
|
||||
usage=usage_dict,
|
||||
)
|
||||
return full_response
|
||||
|
||||
@@ -1596,7 +1621,6 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("messages"),
|
||||
usage=self._usage_to_dict(usage_info),
|
||||
)
|
||||
return full_response
|
||||
raise
|
||||
@@ -1983,19 +2007,6 @@ class LLM(BaseLLM):
|
||||
)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _usage_to_dict(usage: Any) -> dict[str, Any] | None:
|
||||
if usage is None:
|
||||
return None
|
||||
if isinstance(usage, dict):
|
||||
return usage
|
||||
if hasattr(usage, "model_dump"):
|
||||
result: dict[str, Any] = usage.model_dump()
|
||||
return result
|
||||
if hasattr(usage, "__dict__"):
|
||||
return {k: v for k, v in vars(usage).items() if not k.startswith("_")}
|
||||
return None
|
||||
|
||||
def _handle_emit_call_events(
|
||||
self,
|
||||
response: Any,
|
||||
@@ -2003,7 +2014,6 @@ class LLM(BaseLLM):
|
||||
from_task: Task | None = None,
|
||||
from_agent: Agent | None = None,
|
||||
messages: str | list[LLMMessage] | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Handle the events for the LLM call.
|
||||
|
||||
@@ -2013,7 +2023,6 @@ class LLM(BaseLLM):
|
||||
from_task: Optional task object
|
||||
from_agent: Optional agent object
|
||||
messages: Optional messages object
|
||||
usage: Optional token usage data
|
||||
"""
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
@@ -2025,7 +2034,6 @@ class LLM(BaseLLM):
|
||||
from_agent=from_agent,
|
||||
model=self.model,
|
||||
call_id=get_current_call_id(),
|
||||
usage=usage,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2434,7 +2442,7 @@ class LLM(BaseLLM):
|
||||
**filtered_params,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo: dict[int, Any] | None = None) -> LLM:
|
||||
def __deepcopy__(self, memo: dict[int, Any] | None) -> LLM:
|
||||
"""Create a deep copy of the LLM instance."""
|
||||
import copy
|
||||
|
||||
|
||||
@@ -14,18 +14,10 @@ from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
import uuid
|
||||
|
||||
from pydantic import (
|
||||
AliasChoices,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
from typing_extensions import TypedDict
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.llm_events import (
|
||||
@@ -59,12 +51,6 @@ if TYPE_CHECKING:
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
class JsonResponseFormat(TypedDict):
|
||||
"""Response format requesting raw JSON output (e.g. ``{"type": "json_object"}``)."""
|
||||
|
||||
type: Literal["json_object"]
|
||||
|
||||
|
||||
DEFAULT_CONTEXT_WINDOW_SIZE: Final[int] = 4096
|
||||
DEFAULT_SUPPORTS_STOP_WORDS: Final[bool] = True
|
||||
_JSON_EXTRACTION_PATTERN: Final[re.Pattern[str]] = re.compile(r"\{.*}", re.DOTALL)
|
||||
@@ -96,7 +82,7 @@ def get_current_call_id() -> str:
|
||||
return call_id
|
||||
|
||||
|
||||
class BaseLLM(BaseModel, ABC):
|
||||
class BaseLLM(ABC):
|
||||
"""Abstract base class for LLM implementations.
|
||||
|
||||
This class defines the interface that all LLM implementations must follow.
|
||||
@@ -115,100 +101,56 @@ class BaseLLM(BaseModel, ABC):
|
||||
additional_params: Additional provider-specific parameters.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
|
||||
|
||||
model: str
|
||||
temperature: float | None = None
|
||||
api_key: str | None = None
|
||||
base_url: str | None = None
|
||||
provider: str = Field(default="openai")
|
||||
prefer_upload: bool = False
|
||||
is_litellm: bool = False
|
||||
stop: list[str] = Field(
|
||||
default_factory=list,
|
||||
validation_alias=AliasChoices("stop", "stop_sequences"),
|
||||
)
|
||||
additional_params: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name in ("stop", "stop_sequences"):
|
||||
if value is None:
|
||||
value = []
|
||||
elif isinstance(value, str):
|
||||
value = [value]
|
||||
elif not isinstance(value, list):
|
||||
value = list(value)
|
||||
name = "stop"
|
||||
try:
|
||||
super().__setattr__(name, value)
|
||||
except ValueError:
|
||||
if name in self.model_fields:
|
||||
raise # Re-raise validation errors on declared fields
|
||||
# Fallback for attributes not declared as fields (e.g. mock patching)
|
||||
object.__setattr__(self, name, value)
|
||||
except AttributeError:
|
||||
object.__setattr__(self, name, value)
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
temperature: float | None = None,
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
provider: str | None = None,
|
||||
prefer_upload: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the BaseLLM with default attributes.
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
try:
|
||||
super().__delattr__(name)
|
||||
except AttributeError:
|
||||
object.__delattr__(self, name)
|
||||
|
||||
@property
|
||||
def stop_sequences(self) -> list[str]:
|
||||
"""Alias for ``stop`` — kept for backward compatibility with provider APIs.
|
||||
|
||||
Writes are handled by ``__setattr__``, which normalizes and redirects
|
||||
``stop_sequences`` assignments to the ``stop`` field.
|
||||
Args:
|
||||
model: The model identifier/name.
|
||||
temperature: Optional temperature setting for response generation.
|
||||
stop: Optional list of stop sequences for generation.
|
||||
prefer_upload: Whether to prefer file upload over inline base64.
|
||||
**kwargs: Additional provider-specific parameters.
|
||||
"""
|
||||
return self.stop
|
||||
if not model:
|
||||
raise ValueError("Model name is required and cannot be empty")
|
||||
|
||||
_token_usage: dict[str, int] = PrivateAttr(
|
||||
default_factory=lambda: {
|
||||
self.model = model
|
||||
self.temperature = temperature
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
self.prefer_upload = prefer_upload
|
||||
# Store additional parameters for provider-specific use
|
||||
self.additional_params = kwargs
|
||||
self._provider = provider or "openai"
|
||||
|
||||
stop = kwargs.pop("stop", None)
|
||||
if stop is None:
|
||||
self.stop: list[str] = []
|
||||
elif isinstance(stop, str):
|
||||
self.stop = [stop]
|
||||
elif isinstance(stop, list):
|
||||
self.stop = stop
|
||||
else:
|
||||
self.stop = []
|
||||
|
||||
self._token_usage = {
|
||||
"total_tokens": 0,
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"successful_requests": 0,
|
||||
"cached_prompt_tokens": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _validate_init_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
if not data.get("model"):
|
||||
raise ValueError("Model name is required and cannot be empty")
|
||||
|
||||
# Normalize stop: accept str, list, or None; also accept stop_sequences alias
|
||||
stop_seqs = data.pop("stop_sequences", None)
|
||||
stop = stop_seqs if stop_seqs is not None else data.get("stop")
|
||||
if stop is None:
|
||||
data["stop"] = []
|
||||
elif isinstance(stop, str):
|
||||
data["stop"] = [stop]
|
||||
elif isinstance(stop, list):
|
||||
data["stop"] = stop
|
||||
else:
|
||||
data["stop"] = list(stop)
|
||||
|
||||
# Default provider
|
||||
if not data.get("provider"):
|
||||
data["provider"] = "openai"
|
||||
|
||||
# Collect unknown kwargs into additional_params
|
||||
known_fields = set(cls.model_fields.keys())
|
||||
extras = {k: v for k, v in data.items() if k not in known_fields}
|
||||
for k in extras:
|
||||
data.pop(k)
|
||||
existing = data.get("additional_params") or {}
|
||||
existing.update(extras)
|
||||
data["additional_params"] = existing
|
||||
|
||||
return data
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Serialize this LLM to a dict that can reconstruct it via ``LLM(**config)``.
|
||||
@@ -232,6 +174,16 @@ class BaseLLM(BaseModel, ABC):
|
||||
|
||||
return config
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
"""Get the provider of the LLM."""
|
||||
return self._provider
|
||||
|
||||
@provider.setter
|
||||
def provider(self, value: str) -> None:
|
||||
"""Set the provider of the LLM."""
|
||||
self._provider = value
|
||||
|
||||
@abstractmethod
|
||||
def call(
|
||||
self,
|
||||
@@ -460,7 +412,6 @@ class BaseLLM(BaseModel, ABC):
|
||||
from_task: Task | None = None,
|
||||
from_agent: Agent | None = None,
|
||||
messages: str | list[LLMMessage] | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Emit LLM call completed event."""
|
||||
from crewai.utilities.serialization import to_serializable
|
||||
@@ -475,7 +426,6 @@ class BaseLLM(BaseModel, ABC):
|
||||
from_agent=from_agent,
|
||||
model=self.model,
|
||||
call_id=get_current_call_id(),
|
||||
usage=usage,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,13 +3,12 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Final, Literal, TypeGuard, cast
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeGuard, cast
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType
|
||||
from crewai.llms.base_llm import BaseLLM, JsonResponseFormat, llm_call_context
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.llms.base_llm import BaseLLM, llm_call_context
|
||||
from crewai.llms.hooks.transport import AsyncHTTPTransport, HTTPTransport
|
||||
from crewai.utilities.agent_utils import is_context_length_exceeded
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
@@ -18,6 +17,9 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
|
||||
try:
|
||||
from anthropic import Anthropic, AsyncAnthropic, transform_schema
|
||||
from anthropic.types import (
|
||||
@@ -148,47 +150,60 @@ class AnthropicCompletion(BaseLLM):
|
||||
offering native tool use, streaming support, and proper message formatting.
|
||||
"""
|
||||
|
||||
model: str = "claude-3-5-sonnet-20241022"
|
||||
timeout: float | None = None
|
||||
max_retries: int = 2
|
||||
max_tokens: int = 4096
|
||||
top_p: float | None = None
|
||||
stream: bool = False
|
||||
client_params: dict[str, Any] | None = None
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None
|
||||
thinking: AnthropicThinkingConfig | None = None
|
||||
response_format: JsonResponseFormat | type[BaseModel] | None = None
|
||||
tool_search: AnthropicToolSearchConfig | None = None
|
||||
is_claude_3: bool = False
|
||||
supports_tools: bool = True
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "claude-3-5-sonnet-20241022",
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
timeout: float | None = None,
|
||||
max_retries: int = 2,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int = 4096, # Required for Anthropic
|
||||
top_p: float | None = None,
|
||||
stop_sequences: list[str] | None = None,
|
||||
stream: bool = False,
|
||||
client_params: dict[str, Any] | None = None,
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
|
||||
thinking: AnthropicThinkingConfig | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
tool_search: AnthropicToolSearchConfig | bool | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Anthropic chat completion client.
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_async_client: Any = PrivateAttr(default=None)
|
||||
_previous_thinking_blocks: list[Any] = PrivateAttr(default_factory=list)
|
||||
Args:
|
||||
model: Anthropic model name (e.g., 'claude-3-5-sonnet-20241022')
|
||||
api_key: Anthropic API key (defaults to ANTHROPIC_API_KEY env var)
|
||||
base_url: Custom base URL for Anthropic API
|
||||
timeout: Request timeout in seconds
|
||||
max_retries: Maximum number of retries
|
||||
temperature: Sampling temperature (0-1)
|
||||
max_tokens: Maximum tokens in response (required for Anthropic)
|
||||
top_p: Nucleus sampling parameter
|
||||
stop_sequences: Stop sequences (Anthropic uses stop_sequences, not stop)
|
||||
stream: Enable streaming responses
|
||||
client_params: Additional parameters for the Anthropic client
|
||||
interceptor: HTTP interceptor for modifying requests/responses at transport level.
|
||||
response_format: Pydantic model for structured output. When provided, responses
|
||||
will be validated against this model schema.
|
||||
tool_search: Enable Anthropic's server-side tool search. When True, uses "bm25"
|
||||
variant by default. Pass an AnthropicToolSearchConfig to choose "regex" or
|
||||
"bm25". When enabled, tools are automatically marked with defer_loading=True
|
||||
and a tool search tool is injected into the tools list.
|
||||
**kwargs: Additional parameters
|
||||
"""
|
||||
super().__init__(
|
||||
model=model, temperature=temperature, stop=stop_sequences or [], **kwargs
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_anthropic_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
# Anthropic uses stop_sequences; normalize from stop kwarg
|
||||
popped = data.pop("stop_sequences", None)
|
||||
seqs = popped if popped is not None else (data.get("stop") or [])
|
||||
if isinstance(seqs, str):
|
||||
seqs = [seqs]
|
||||
data["stop"] = seqs
|
||||
data["is_claude_3"] = "claude-3" in data.get("model", "").lower()
|
||||
# Normalize tool_search
|
||||
ts = data.get("tool_search")
|
||||
if ts is True:
|
||||
data["tool_search"] = AnthropicToolSearchConfig()
|
||||
elif ts is not None and not isinstance(ts, AnthropicToolSearchConfig):
|
||||
data["tool_search"] = None
|
||||
return data
|
||||
# Client params
|
||||
self.interceptor = interceptor
|
||||
self.client_params = client_params
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self.max_retries = max_retries
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> AnthropicCompletion:
|
||||
self._client = Anthropic(**self._get_client_params())
|
||||
self.client = Anthropic(**self._get_client_params())
|
||||
|
||||
async_client_params = self._get_client_params()
|
||||
if self.interceptor:
|
||||
@@ -196,8 +211,51 @@ class AnthropicCompletion(BaseLLM):
|
||||
async_http_client = httpx.AsyncClient(transport=async_transport)
|
||||
async_client_params["http_client"] = async_http_client
|
||||
|
||||
self._async_client = AsyncAnthropic(**async_client_params)
|
||||
return self
|
||||
self.async_client = AsyncAnthropic(**async_client_params)
|
||||
|
||||
# Store completion parameters
|
||||
self.max_tokens = max_tokens
|
||||
self.top_p = top_p
|
||||
self.stream = stream
|
||||
self.stop_sequences = stop_sequences or []
|
||||
self.thinking = thinking
|
||||
self.previous_thinking_blocks: list[ThinkingBlock] = []
|
||||
self.response_format = response_format
|
||||
# Tool search config
|
||||
self.tool_search: AnthropicToolSearchConfig | None
|
||||
if tool_search is True:
|
||||
self.tool_search = AnthropicToolSearchConfig()
|
||||
elif isinstance(tool_search, AnthropicToolSearchConfig):
|
||||
self.tool_search = tool_search
|
||||
else:
|
||||
self.tool_search = None
|
||||
# Model-specific settings
|
||||
self.is_claude_3 = "claude-3" in model.lower()
|
||||
self.supports_tools = True
|
||||
|
||||
@property
|
||||
def stop(self) -> list[str]:
|
||||
"""Get stop sequences sent to the API."""
|
||||
return self.stop_sequences
|
||||
|
||||
@stop.setter
|
||||
def stop(self, value: list[str] | str | None) -> None:
|
||||
"""Set stop sequences.
|
||||
|
||||
Synchronizes stop_sequences to ensure values set by CrewAgentExecutor
|
||||
are properly sent to the Anthropic API.
|
||||
|
||||
Args:
|
||||
value: Stop sequences as a list, single string, or None
|
||||
"""
|
||||
if value is None:
|
||||
self.stop_sequences = []
|
||||
elif isinstance(value, str):
|
||||
self.stop_sequences = [value]
|
||||
elif isinstance(value, list):
|
||||
self.stop_sequences = value
|
||||
else:
|
||||
self.stop_sequences = []
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Anthropic-specific fields."""
|
||||
@@ -693,11 +751,11 @@ class AnthropicCompletion(BaseLLM):
|
||||
)
|
||||
elif isinstance(content, list):
|
||||
formatted_messages.append({"role": "assistant", "content": content})
|
||||
elif self.thinking and self._previous_thinking_blocks:
|
||||
elif self.thinking and self.previous_thinking_blocks:
|
||||
structured_content = cast(
|
||||
list[dict[str, Any]],
|
||||
[
|
||||
*self._previous_thinking_blocks,
|
||||
*self.previous_thinking_blocks,
|
||||
{"type": "text", "text": content if content else ""},
|
||||
],
|
||||
)
|
||||
@@ -751,7 +809,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle non-streaming message completion."""
|
||||
uses_file_api = _contains_file_id_reference(params.get("messages", []))
|
||||
@@ -785,11 +843,11 @@ class AnthropicCompletion(BaseLLM):
|
||||
try:
|
||||
if betas:
|
||||
params["betas"] = betas
|
||||
response = self._client.beta.messages.create(
|
||||
response = self.client.beta.messages.create(
|
||||
**params, extra_body=extra_body
|
||||
)
|
||||
else:
|
||||
response = self._client.messages.create(**params)
|
||||
response = self.client.messages.create(**params)
|
||||
|
||||
except Exception as e:
|
||||
if is_context_length_exceeded(e):
|
||||
@@ -811,7 +869,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
else:
|
||||
@@ -827,7 +884,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -850,7 +906,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(tool_uses)
|
||||
|
||||
@@ -873,7 +928,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
thinking_blocks.append(cast(ThinkingBlock, thinking_block))
|
||||
|
||||
if thinking_blocks:
|
||||
self._previous_thinking_blocks = thinking_blocks
|
||||
self.previous_thinking_blocks = thinking_blocks
|
||||
|
||||
content = self._apply_stop_words(content)
|
||||
self._emit_call_completed_event(
|
||||
@@ -882,7 +937,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -898,7 +952,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle streaming message completion."""
|
||||
betas: list[str] = []
|
||||
@@ -937,9 +991,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
current_tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
stream_context = (
|
||||
self._client.beta.messages.stream(**stream_params, extra_body=extra_body)
|
||||
self.client.beta.messages.stream(**stream_params, extra_body=extra_body)
|
||||
if betas
|
||||
else self._client.messages.stream(**stream_params)
|
||||
else self.client.messages.stream(**stream_params)
|
||||
)
|
||||
with stream_context as stream:
|
||||
response_id = None
|
||||
@@ -1018,7 +1072,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
thinking_blocks.append(cast(ThinkingBlock, thinking_block))
|
||||
|
||||
if thinking_blocks:
|
||||
self._previous_thinking_blocks = thinking_blocks
|
||||
self.previous_thinking_blocks = thinking_blocks
|
||||
|
||||
usage = self._extract_anthropic_token_usage(final_message)
|
||||
self._track_token_usage_internal(usage)
|
||||
@@ -1032,7 +1086,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
for block in final_message.content:
|
||||
@@ -1047,7 +1100,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -1077,7 +1129,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -1218,7 +1269,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
try:
|
||||
# Send tool results back to Claude for final response
|
||||
final_response: Message = self._client.messages.create(**follow_up_params)
|
||||
final_response: Message = self.client.messages.create(**follow_up_params)
|
||||
|
||||
# Track token usage for follow-up call
|
||||
follow_up_usage = self._extract_anthropic_token_usage(final_response)
|
||||
@@ -1237,7 +1288,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
thinking_blocks.append(cast(ThinkingBlock, thinking_block))
|
||||
|
||||
if thinking_blocks:
|
||||
self._previous_thinking_blocks = thinking_blocks
|
||||
self.previous_thinking_blocks = thinking_blocks
|
||||
|
||||
final_content = self._apply_stop_words(final_content)
|
||||
|
||||
@@ -1248,7 +1299,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=follow_up_params["messages"],
|
||||
usage=follow_up_usage,
|
||||
)
|
||||
|
||||
# Log combined token usage
|
||||
@@ -1280,7 +1330,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle non-streaming async message completion."""
|
||||
uses_file_api = _contains_file_id_reference(params.get("messages", []))
|
||||
@@ -1314,11 +1364,11 @@ class AnthropicCompletion(BaseLLM):
|
||||
try:
|
||||
if betas:
|
||||
params["betas"] = betas
|
||||
response = await self._async_client.beta.messages.create(
|
||||
response = await self.async_client.beta.messages.create(
|
||||
**params, extra_body=extra_body
|
||||
)
|
||||
else:
|
||||
response = await self._async_client.messages.create(**params)
|
||||
response = await self.async_client.messages.create(**params)
|
||||
|
||||
except Exception as e:
|
||||
if is_context_length_exceeded(e):
|
||||
@@ -1340,7 +1390,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
else:
|
||||
@@ -1356,7 +1405,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -1377,7 +1425,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(tool_uses)
|
||||
|
||||
@@ -1401,7 +1448,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -1415,7 +1461,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle async streaming message completion."""
|
||||
betas: list[str] = []
|
||||
@@ -1452,11 +1498,11 @@ class AnthropicCompletion(BaseLLM):
|
||||
current_tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
stream_context = (
|
||||
self._async_client.beta.messages.stream(
|
||||
self.async_client.beta.messages.stream(
|
||||
**stream_params, extra_body=extra_body
|
||||
)
|
||||
if betas
|
||||
else self._async_client.messages.stream(**stream_params)
|
||||
else self.async_client.messages.stream(**stream_params)
|
||||
)
|
||||
async with stream_context as stream:
|
||||
response_id = None
|
||||
@@ -1539,7 +1585,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
for block in final_message.content:
|
||||
@@ -1554,7 +1599,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -1583,7 +1627,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1621,7 +1664,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
]
|
||||
|
||||
try:
|
||||
final_response: Message = await self._async_client.messages.create(
|
||||
final_response: Message = await self.async_client.messages.create(
|
||||
**follow_up_params
|
||||
)
|
||||
|
||||
@@ -1642,7 +1685,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=follow_up_params["messages"],
|
||||
usage=follow_up_usage,
|
||||
)
|
||||
|
||||
total_usage = {
|
||||
@@ -1744,8 +1786,8 @@ class AnthropicCompletion(BaseLLM):
|
||||
from crewai_files.uploaders.anthropic import AnthropicFileUploader
|
||||
|
||||
return AnthropicFileUploader(
|
||||
client=self._client,
|
||||
async_client=self._async_client,
|
||||
client=self.client,
|
||||
async_client=self.async_client,
|
||||
)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
@@ -3,13 +3,11 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, TypedDict
|
||||
from urllib.parse import urlparse
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.utilities.agent_utils import is_context_length_exceeded
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededError,
|
||||
@@ -18,6 +16,10 @@ from crewai.utilities.pydantic_schema_utils import generate_model_description
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
|
||||
|
||||
try:
|
||||
from azure.ai.inference import (
|
||||
ChatCompletionsClient,
|
||||
@@ -74,84 +76,109 @@ class AzureCompletion(BaseLLM):
|
||||
offering native function calling, streaming support, and proper Azure authentication.
|
||||
"""
|
||||
|
||||
endpoint: str | None = None
|
||||
api_version: str | None = None
|
||||
timeout: float | None = None
|
||||
max_retries: int = 2
|
||||
top_p: float | None = None
|
||||
frequency_penalty: float | None = None
|
||||
presence_penalty: float | None = None
|
||||
max_tokens: int | None = None
|
||||
stream: bool = False
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None
|
||||
response_format: type[BaseModel] | None = None
|
||||
is_openai_model: bool = False
|
||||
is_azure_openai_endpoint: bool = False
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
api_key: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
api_version: str | None = None,
|
||||
timeout: float | None = None,
|
||||
max_retries: int = 2,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
stop: list[str] | None = None,
|
||||
stream: bool = False,
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Azure AI Inference chat completion client.
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_async_client: Any = PrivateAttr(default=None)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_azure_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
if data.get("interceptor") is not None:
|
||||
Args:
|
||||
model: Azure deployment name or model name
|
||||
api_key: Azure API key (defaults to AZURE_API_KEY env var)
|
||||
endpoint: Azure endpoint URL (defaults to AZURE_ENDPOINT env var)
|
||||
api_version: Azure API version (defaults to AZURE_API_VERSION env var)
|
||||
timeout: Request timeout in seconds
|
||||
max_retries: Maximum number of retries
|
||||
temperature: Sampling temperature (0-2)
|
||||
top_p: Nucleus sampling parameter
|
||||
frequency_penalty: Frequency penalty (-2 to 2)
|
||||
presence_penalty: Presence penalty (-2 to 2)
|
||||
max_tokens: Maximum tokens in response
|
||||
stop: Stop sequences
|
||||
stream: Enable streaming responses
|
||||
interceptor: HTTP interceptor (not yet supported for Azure).
|
||||
response_format: Pydantic model for structured output. Used as default when
|
||||
response_model is not passed to call()/acall() methods.
|
||||
Only works with OpenAI models deployed on Azure.
|
||||
**kwargs: Additional parameters
|
||||
"""
|
||||
if interceptor is not None:
|
||||
raise NotImplementedError(
|
||||
"HTTP interceptors are not yet supported for Azure AI Inference provider. "
|
||||
"Interceptors are currently supported for OpenAI and Anthropic providers only."
|
||||
)
|
||||
|
||||
# Resolve env vars
|
||||
data["api_key"] = data.get("api_key") or os.getenv("AZURE_API_KEY")
|
||||
data["endpoint"] = (
|
||||
data.get("endpoint")
|
||||
super().__init__(
|
||||
model=model, temperature=temperature, stop=stop or [], **kwargs
|
||||
)
|
||||
|
||||
self.api_key = api_key or os.getenv("AZURE_API_KEY")
|
||||
self.endpoint = (
|
||||
endpoint
|
||||
or os.getenv("AZURE_ENDPOINT")
|
||||
or os.getenv("AZURE_OPENAI_ENDPOINT")
|
||||
or os.getenv("AZURE_API_BASE")
|
||||
)
|
||||
data["api_version"] = (
|
||||
data.get("api_version") or os.getenv("AZURE_API_VERSION") or "2024-06-01"
|
||||
)
|
||||
self.api_version = api_version or os.getenv("AZURE_API_VERSION") or "2024-06-01"
|
||||
self.timeout = timeout
|
||||
self.max_retries = max_retries
|
||||
|
||||
if not data["api_key"]:
|
||||
if not self.api_key:
|
||||
raise ValueError(
|
||||
"Azure API key is required. Set AZURE_API_KEY environment variable or pass api_key parameter."
|
||||
)
|
||||
if not data["endpoint"]:
|
||||
if not self.endpoint:
|
||||
raise ValueError(
|
||||
"Azure endpoint is required. Set AZURE_ENDPOINT environment variable or pass endpoint parameter."
|
||||
)
|
||||
|
||||
model = data.get("model", "")
|
||||
data["endpoint"] = AzureCompletion._validate_and_fix_endpoint(
|
||||
data["endpoint"], model
|
||||
)
|
||||
data["is_openai_model"] = any(
|
||||
prefix in model.lower() for prefix in ["gpt-", "o1-", "text-"]
|
||||
)
|
||||
parsed = urlparse(data["endpoint"])
|
||||
hostname = parsed.hostname or ""
|
||||
data["is_azure_openai_endpoint"] = (
|
||||
hostname == "openai.azure.com" or hostname.endswith(".openai.azure.com")
|
||||
) and "/openai/deployments/" in data["endpoint"]
|
||||
return data
|
||||
# Validate and potentially fix Azure OpenAI endpoint URL
|
||||
self.endpoint = self._validate_and_fix_endpoint(self.endpoint, model)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> AzureCompletion:
|
||||
if not self.api_key:
|
||||
raise ValueError("Azure API key is required.")
|
||||
client_kwargs: dict[str, Any] = {
|
||||
# Build client kwargs
|
||||
client_kwargs = {
|
||||
"endpoint": self.endpoint,
|
||||
"credential": AzureKeyCredential(self.api_key),
|
||||
}
|
||||
|
||||
# Add api_version if specified (primarily for Azure OpenAI endpoints)
|
||||
if self.api_version:
|
||||
client_kwargs["api_version"] = self.api_version
|
||||
|
||||
self._client = ChatCompletionsClient(**client_kwargs)
|
||||
self._async_client = AsyncChatCompletionsClient(**client_kwargs)
|
||||
return self
|
||||
self.client = ChatCompletionsClient(**client_kwargs) # type: ignore[arg-type]
|
||||
|
||||
self.async_client = AsyncChatCompletionsClient(**client_kwargs) # type: ignore[arg-type]
|
||||
|
||||
self.top_p = top_p
|
||||
self.frequency_penalty = frequency_penalty
|
||||
self.presence_penalty = presence_penalty
|
||||
self.max_tokens = max_tokens
|
||||
self.stream = stream
|
||||
self.response_format = response_format
|
||||
|
||||
self.is_openai_model = any(
|
||||
prefix in model.lower() for prefix in ["gpt-", "o1-", "text-"]
|
||||
)
|
||||
|
||||
self.is_azure_openai_endpoint = (
|
||||
"openai.azure.com" in self.endpoint
|
||||
and "/openai/deployments/" in self.endpoint
|
||||
)
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Azure-specific fields."""
|
||||
@@ -188,11 +215,7 @@ class AzureCompletion(BaseLLM):
|
||||
Returns:
|
||||
Validated and potentially corrected endpoint URL
|
||||
"""
|
||||
ep_host = urlparse(endpoint).hostname or ""
|
||||
is_azure_openai = ep_host == "openai.azure.com" or ep_host.endswith(
|
||||
".openai.azure.com"
|
||||
)
|
||||
if is_azure_openai and "/openai/deployments/" not in endpoint:
|
||||
if "openai.azure.com" in endpoint and "/openai/deployments/" not in endpoint:
|
||||
endpoint = endpoint.rstrip("/")
|
||||
|
||||
if not endpoint.endswith("/openai/deployments"):
|
||||
@@ -569,7 +592,6 @@ class AzureCompletion(BaseLLM):
|
||||
params: AzureCompletionParams,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> BaseModel:
|
||||
"""Validate content against response model and emit completion event.
|
||||
|
||||
@@ -595,7 +617,6 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return structured_data
|
||||
@@ -645,7 +666,6 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(message.tool_calls)
|
||||
|
||||
@@ -683,7 +703,6 @@ class AzureCompletion(BaseLLM):
|
||||
params=params,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
content = self._apply_stop_words(content)
|
||||
@@ -695,7 +714,6 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -713,7 +731,7 @@ class AzureCompletion(BaseLLM):
|
||||
"""Handle non-streaming chat completion."""
|
||||
try:
|
||||
# Cast params to Any to avoid type checking issues with TypedDict unpacking
|
||||
response: ChatCompletions = self._client.complete(**params)
|
||||
response: ChatCompletions = self.client.complete(**params) # type: ignore[assignment,arg-type]
|
||||
return self._process_completion_response(
|
||||
response=response,
|
||||
params=params,
|
||||
@@ -799,7 +817,7 @@ class AzureCompletion(BaseLLM):
|
||||
self,
|
||||
full_response: str,
|
||||
tool_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, Any] | None,
|
||||
usage_data: dict[str, int],
|
||||
params: AzureCompletionParams,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -811,7 +829,7 @@ class AzureCompletion(BaseLLM):
|
||||
Args:
|
||||
full_response: The complete streamed response content
|
||||
tool_calls: Dictionary of tool calls accumulated during streaming
|
||||
usage_data: Token usage data from the stream, or None if unavailable
|
||||
usage_data: Token usage data from the stream
|
||||
params: Completion parameters containing messages
|
||||
available_functions: Available functions for tool calling
|
||||
from_task: Task that initiated the call
|
||||
@@ -821,8 +839,7 @@ class AzureCompletion(BaseLLM):
|
||||
Returns:
|
||||
Final response content after processing, or structured output
|
||||
"""
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
# Handle structured output validation
|
||||
if response_model and self.is_openai_model:
|
||||
@@ -832,7 +849,6 @@ class AzureCompletion(BaseLLM):
|
||||
params=params,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
# If there are tool_calls but no available_functions, return them
|
||||
@@ -855,7 +871,6 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
return formatted_tool_calls
|
||||
|
||||
@@ -892,7 +907,6 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -911,8 +925,8 @@ class AzureCompletion(BaseLLM):
|
||||
full_response = ""
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
usage_data: dict[str, Any] | None = None
|
||||
for update in self._client.complete(**params):
|
||||
usage_data = {"total_tokens": 0}
|
||||
for update in self.client.complete(**params): # type: ignore[arg-type]
|
||||
if isinstance(update, StreamingChatCompletionsUpdate):
|
||||
if update.usage:
|
||||
usage = update.usage
|
||||
@@ -953,7 +967,7 @@ class AzureCompletion(BaseLLM):
|
||||
"""Handle non-streaming chat completion asynchronously."""
|
||||
try:
|
||||
# Cast params to Any to avoid type checking issues with TypedDict unpacking
|
||||
response: ChatCompletions = await self._async_client.complete(**params)
|
||||
response: ChatCompletions = await self.async_client.complete(**params) # type: ignore[assignment,arg-type]
|
||||
return self._process_completion_response(
|
||||
response=response,
|
||||
params=params,
|
||||
@@ -977,10 +991,10 @@ class AzureCompletion(BaseLLM):
|
||||
full_response = ""
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
usage_data: dict[str, Any] | None = None
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
stream = await self._async_client.complete(**params)
|
||||
async for update in stream:
|
||||
stream = await self.async_client.complete(**params) # type: ignore[arg-type]
|
||||
async for update in stream: # type: ignore[union-attr]
|
||||
if isinstance(update, StreamingChatCompletionsUpdate):
|
||||
if hasattr(update, "usage") and update.usage:
|
||||
usage = update.usage
|
||||
@@ -1096,8 +1110,8 @@ class AzureCompletion(BaseLLM):
|
||||
This ensures proper cleanup of the underlying aiohttp session
|
||||
to avoid unclosed connector warnings.
|
||||
"""
|
||||
if hasattr(self._async_client, "close"):
|
||||
await self._async_client.close()
|
||||
if hasattr(self.async_client, "close"):
|
||||
await self.async_client.close()
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Async context manager entry."""
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Required
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType
|
||||
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
|
||||
ToolTypeDef,
|
||||
)
|
||||
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
|
||||
|
||||
try:
|
||||
@@ -228,97 +228,129 @@ class BedrockCompletion(BaseLLM):
|
||||
- Model-specific conversation format handling (e.g., Cohere requirements)
|
||||
"""
|
||||
|
||||
model: str = "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
aws_access_key_id: str | None = None
|
||||
aws_secret_access_key: str | None = None
|
||||
aws_session_token: str | None = None
|
||||
region_name: str | None = None
|
||||
max_tokens: int | None = None
|
||||
top_p: float | None = None
|
||||
top_k: int | None = None
|
||||
stream: bool = False
|
||||
guardrail_config: dict[str, Any] | None = None
|
||||
additional_model_request_fields: dict[str, Any] | None = None
|
||||
additional_model_response_field_paths: list[str] | None = None
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None
|
||||
response_format: type[BaseModel] | None = None
|
||||
is_claude_model: bool = False
|
||||
supports_tools: bool = True
|
||||
supports_streaming: bool = True
|
||||
model_id: str = ""
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
aws_access_key_id: str | None = None,
|
||||
aws_secret_access_key: str | None = None,
|
||||
aws_session_token: str | None = None,
|
||||
region_name: str | None = None,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
top_p: float | None = None,
|
||||
top_k: int | None = None,
|
||||
stop_sequences: Sequence[str] | None = None,
|
||||
stream: bool = False,
|
||||
guardrail_config: dict[str, Any] | None = None,
|
||||
additional_model_request_fields: dict[str, Any] | None = None,
|
||||
additional_model_response_field_paths: list[str] | None = None,
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize AWS Bedrock completion client.
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_async_exit_stack: Any = PrivateAttr(default=None)
|
||||
_async_client_initialized: bool = PrivateAttr(default=False)
|
||||
_async_client: Any = PrivateAttr(default=None)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_bedrock_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
if data.get("interceptor") is not None:
|
||||
Args:
|
||||
model: The Bedrock model ID to use
|
||||
aws_access_key_id: AWS access key (defaults to environment variable)
|
||||
aws_secret_access_key: AWS secret key (defaults to environment variable)
|
||||
aws_session_token: AWS session token for temporary credentials
|
||||
region_name: AWS region name
|
||||
temperature: Sampling temperature for response generation
|
||||
max_tokens: Maximum tokens to generate
|
||||
top_p: Nucleus sampling parameter
|
||||
top_k: Top-k sampling parameter (Claude models only)
|
||||
stop_sequences: List of sequences that stop generation
|
||||
stream: Whether to use streaming responses
|
||||
guardrail_config: Guardrail configuration for content filtering
|
||||
additional_model_request_fields: Model-specific request parameters
|
||||
additional_model_response_field_paths: Custom response field paths
|
||||
interceptor: HTTP interceptor (not yet supported for Bedrock).
|
||||
response_format: Pydantic model for structured output. Used as default when
|
||||
response_model is not passed to call()/acall() methods.
|
||||
**kwargs: Additional parameters
|
||||
"""
|
||||
if interceptor is not None:
|
||||
raise NotImplementedError(
|
||||
"HTTP interceptors are not yet supported for AWS Bedrock provider. "
|
||||
"Interceptors are currently supported for OpenAI and Anthropic providers only."
|
||||
)
|
||||
|
||||
# Force provider to bedrock
|
||||
data.pop("provider", None)
|
||||
data["provider"] = "bedrock"
|
||||
# Extract provider from kwargs to avoid duplicate argument
|
||||
kwargs.pop("provider", None)
|
||||
|
||||
# Normalize stop_sequences from stop kwarg
|
||||
popped = data.pop("stop_sequences", None)
|
||||
seqs = popped if popped is not None else (data.get("stop") or [])
|
||||
if isinstance(seqs, str):
|
||||
seqs = [seqs]
|
||||
elif isinstance(seqs, Sequence) and not isinstance(seqs, list):
|
||||
seqs = list(seqs)
|
||||
data["stop"] = seqs
|
||||
super().__init__(
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
stop=stop_sequences or [],
|
||||
provider="bedrock",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Resolve env vars
|
||||
data["aws_access_key_id"] = data.get("aws_access_key_id") or os.getenv(
|
||||
"AWS_ACCESS_KEY_ID"
|
||||
# Configure client with timeouts and retries following AWS best practices
|
||||
config = Config(
|
||||
read_timeout=300,
|
||||
retries={
|
||||
"max_attempts": 3,
|
||||
"mode": "adaptive",
|
||||
},
|
||||
tcp_keepalive=True,
|
||||
)
|
||||
data["aws_secret_access_key"] = data.get("aws_secret_access_key") or os.getenv(
|
||||
"AWS_SECRET_ACCESS_KEY"
|
||||
)
|
||||
data["aws_session_token"] = data.get("aws_session_token") or os.getenv(
|
||||
"AWS_SESSION_TOKEN"
|
||||
)
|
||||
data["region_name"] = (
|
||||
data.get("region_name")
|
||||
|
||||
self.region_name = (
|
||||
region_name
|
||||
or os.getenv("AWS_DEFAULT_REGION")
|
||||
or os.getenv("AWS_REGION_NAME")
|
||||
or "us-east-1"
|
||||
)
|
||||
|
||||
model = data.get("model", "anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
data["is_claude_model"] = "claude" in model.lower()
|
||||
data["model_id"] = model
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> BedrockCompletion:
|
||||
config = Config(
|
||||
read_timeout=300,
|
||||
retries={"max_attempts": 3, "mode": "adaptive"},
|
||||
tcp_keepalive=True,
|
||||
self.aws_access_key_id = aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID")
|
||||
self.aws_secret_access_key = aws_secret_access_key or os.getenv(
|
||||
"AWS_SECRET_ACCESS_KEY"
|
||||
)
|
||||
self.aws_session_token = aws_session_token or os.getenv("AWS_SESSION_TOKEN")
|
||||
|
||||
# Initialize Bedrock client with proper configuration
|
||||
session = Session(
|
||||
aws_access_key_id=self.aws_access_key_id,
|
||||
aws_secret_access_key=self.aws_secret_access_key,
|
||||
aws_session_token=self.aws_session_token,
|
||||
region_name=self.region_name,
|
||||
)
|
||||
self._client = session.client("bedrock-runtime", config=config)
|
||||
|
||||
self.client = session.client("bedrock-runtime", config=config)
|
||||
|
||||
self._async_exit_stack = AsyncExitStack() if AIOBOTOCORE_AVAILABLE else None
|
||||
return self
|
||||
self._async_client_initialized = False
|
||||
|
||||
# Store completion parameters
|
||||
self.max_tokens = max_tokens
|
||||
self.top_p = top_p
|
||||
self.top_k = top_k
|
||||
self.stream = stream
|
||||
self.stop_sequences = stop_sequences
|
||||
self.response_format = response_format
|
||||
|
||||
# Store advanced features (optional)
|
||||
self.guardrail_config = guardrail_config
|
||||
self.additional_model_request_fields = additional_model_request_fields
|
||||
self.additional_model_response_field_paths = (
|
||||
additional_model_response_field_paths
|
||||
)
|
||||
|
||||
# Model-specific settings
|
||||
self.is_claude_model = "claude" in model.lower()
|
||||
self.supports_tools = True # Converse API supports tools for most models
|
||||
self.supports_streaming = True
|
||||
|
||||
# Handle inference profiles for newer models
|
||||
self.model_id = model
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Bedrock-specific fields."""
|
||||
config = super().to_config_dict()
|
||||
# NOTE: AWS credentials (access_key, secret_key, session_token) are
|
||||
# intentionally excluded — they must come from env on resume.
|
||||
if self.region_name and self.region_name != "us-east-1":
|
||||
config["region_name"] = self.region_name
|
||||
if self.max_tokens is not None:
|
||||
@@ -331,6 +363,30 @@ class BedrockCompletion(BaseLLM):
|
||||
config["guardrail_config"] = self.guardrail_config
|
||||
return config
|
||||
|
||||
@property
|
||||
def stop(self) -> list[str]:
|
||||
"""Get stop sequences sent to the API."""
|
||||
return [] if self.stop_sequences is None else list(self.stop_sequences)
|
||||
|
||||
@stop.setter
|
||||
def stop(self, value: Sequence[str] | str | None) -> None:
|
||||
"""Set stop sequences.
|
||||
|
||||
Synchronizes stop_sequences to ensure values set by CrewAgentExecutor
|
||||
are properly sent to the Bedrock API.
|
||||
|
||||
Args:
|
||||
value: Stop sequences as a Sequence, single string, or None
|
||||
"""
|
||||
if value is None:
|
||||
self.stop_sequences = []
|
||||
elif isinstance(value, str):
|
||||
self.stop_sequences = [value]
|
||||
elif isinstance(value, Sequence):
|
||||
self.stop_sequences = list(value)
|
||||
else:
|
||||
self.stop_sequences = []
|
||||
|
||||
def call(
|
||||
self,
|
||||
messages: str | list[LLMMessage],
|
||||
@@ -654,7 +710,7 @@ class BedrockCompletion(BaseLLM):
|
||||
raise ValueError(f"Invalid message format at index {i}")
|
||||
|
||||
# Call Bedrock Converse API with proper error handling
|
||||
response = self._client.converse(
|
||||
response = self.client.converse(
|
||||
modelId=self.model_id,
|
||||
messages=cast(
|
||||
"Sequence[MessageTypeDef | MessageOutputTypeDef]",
|
||||
@@ -664,9 +720,8 @@ class BedrockCompletion(BaseLLM):
|
||||
)
|
||||
|
||||
# Track token usage according to AWS response format
|
||||
usage = response.get("usage")
|
||||
if usage:
|
||||
self._track_token_usage_internal(usage)
|
||||
if "usage" in response:
|
||||
self._track_token_usage_internal(response["usage"])
|
||||
|
||||
stop_reason = response.get("stopReason")
|
||||
if stop_reason:
|
||||
@@ -706,7 +761,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -729,7 +783,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return non_structured_output_tool_uses
|
||||
|
||||
@@ -809,7 +862,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -940,16 +992,15 @@ class BedrockCompletion(BaseLLM):
|
||||
tool_use_id: str | None = None
|
||||
tool_use_index = 0
|
||||
accumulated_tool_input = ""
|
||||
usage_data: dict[str, Any] | None = None
|
||||
|
||||
try:
|
||||
response = self._client.converse_stream(
|
||||
response = self.client.converse_stream(
|
||||
modelId=self.model_id,
|
||||
messages=cast(
|
||||
"Sequence[MessageTypeDef | MessageOutputTypeDef]",
|
||||
cast(object, messages),
|
||||
),
|
||||
**body,
|
||||
**body, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
stream = response.get("stream")
|
||||
@@ -1050,7 +1101,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage_data,
|
||||
)
|
||||
return result # type: ignore[return-value]
|
||||
except Exception as e:
|
||||
@@ -1118,7 +1168,6 @@ class BedrockCompletion(BaseLLM):
|
||||
metadata = event["metadata"]
|
||||
if "usage" in metadata:
|
||||
usage_metrics = metadata["usage"]
|
||||
usage_data = usage_metrics
|
||||
self._track_token_usage_internal(usage_metrics)
|
||||
logging.debug(f"Token usage: {usage_metrics}")
|
||||
if "trace" in metadata:
|
||||
@@ -1148,7 +1197,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1260,9 +1308,8 @@ class BedrockCompletion(BaseLLM):
|
||||
**body,
|
||||
)
|
||||
|
||||
usage = response.get("usage")
|
||||
if usage:
|
||||
self._track_token_usage_internal(usage)
|
||||
if "usage" in response:
|
||||
self._track_token_usage_internal(response["usage"])
|
||||
|
||||
stop_reason = response.get("stopReason")
|
||||
if stop_reason:
|
||||
@@ -1301,7 +1348,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -1324,7 +1370,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return non_structured_output_tool_uses
|
||||
|
||||
@@ -1399,7 +1444,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return text_content
|
||||
@@ -1520,7 +1564,6 @@ class BedrockCompletion(BaseLLM):
|
||||
tool_use_id: str | None = None
|
||||
tool_use_index = 0
|
||||
accumulated_tool_input = ""
|
||||
usage_data: dict[str, Any] | None = None
|
||||
|
||||
try:
|
||||
async_client = await self._ensure_async_client()
|
||||
@@ -1632,7 +1675,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage_data,
|
||||
)
|
||||
return result # type: ignore[return-value]
|
||||
except Exception as e:
|
||||
@@ -1705,7 +1747,6 @@ class BedrockCompletion(BaseLLM):
|
||||
metadata = event["metadata"]
|
||||
if "usage" in metadata:
|
||||
usage_metrics = metadata["usage"]
|
||||
usage_data = usage_metrics
|
||||
self._track_token_usage_internal(usage_metrics)
|
||||
logging.debug(f"Token usage: {usage_metrics}")
|
||||
if "trace" in metadata:
|
||||
@@ -1735,7 +1776,6 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
|
||||
@@ -5,13 +5,12 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType
|
||||
from crewai.llms.base_llm import BaseLLM, llm_call_context
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.utilities.agent_utils import is_context_length_exceeded
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededError,
|
||||
@@ -20,6 +19,10 @@ from crewai.utilities.pydantic_schema_utils import generate_model_description
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
|
||||
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
@@ -41,84 +44,137 @@ class GeminiCompletion(BaseLLM):
|
||||
offering native function calling, streaming support, and proper Gemini formatting.
|
||||
"""
|
||||
|
||||
model: str = "gemini-2.0-flash-001"
|
||||
project: str | None = None
|
||||
location: str | None = None
|
||||
top_p: float | None = None
|
||||
top_k: int | None = None
|
||||
max_output_tokens: int | None = None
|
||||
stream: bool = False
|
||||
safety_settings: dict[str, Any] = Field(default_factory=dict)
|
||||
client_params: dict[str, Any] = Field(default_factory=dict)
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None
|
||||
use_vertexai: bool = False
|
||||
response_format: type[BaseModel] | None = None
|
||||
thinking_config: Any = None
|
||||
tools: list[dict[str, Any]] | None = None
|
||||
supports_tools: bool = False
|
||||
is_gemini_2_0: bool = False
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "gemini-2.0-flash-001",
|
||||
api_key: str | None = None,
|
||||
project: str | None = None,
|
||||
location: str | None = None,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
top_k: int | None = None,
|
||||
max_output_tokens: int | None = None,
|
||||
stop_sequences: list[str] | None = None,
|
||||
stream: bool = False,
|
||||
safety_settings: dict[str, Any] | None = None,
|
||||
client_params: dict[str, Any] | None = None,
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None,
|
||||
use_vertexai: bool | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
thinking_config: types.ThinkingConfig | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Google Gemini chat completion client.
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_gemini_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
if data.get("interceptor") is not None:
|
||||
Args:
|
||||
model: Gemini model name (e.g., 'gemini-2.0-flash-001', 'gemini-1.5-pro')
|
||||
api_key: Google API key for Gemini API authentication.
|
||||
Defaults to GOOGLE_API_KEY or GEMINI_API_KEY env var.
|
||||
NOTE: Cannot be used with Vertex AI (project parameter). Use Gemini API instead.
|
||||
project: Google Cloud project ID for Vertex AI with ADC authentication.
|
||||
Requires Application Default Credentials (gcloud auth application-default login).
|
||||
NOTE: Vertex AI does NOT support API keys, only OAuth2/ADC.
|
||||
If both api_key and project are set, api_key takes precedence.
|
||||
location: Google Cloud location (for Vertex AI with ADC, defaults to 'us-central1')
|
||||
temperature: Sampling temperature (0-2)
|
||||
top_p: Nucleus sampling parameter
|
||||
top_k: Top-k sampling parameter
|
||||
max_output_tokens: Maximum tokens in response
|
||||
stop_sequences: Stop sequences
|
||||
stream: Enable streaming responses
|
||||
safety_settings: Safety filter settings
|
||||
client_params: Additional parameters to pass to the Google Gen AI Client constructor.
|
||||
Supports parameters like http_options, credentials, debug_config, etc.
|
||||
interceptor: HTTP interceptor (not yet supported for Gemini).
|
||||
use_vertexai: Whether to use Vertex AI instead of Gemini API.
|
||||
- True: Use Vertex AI (with ADC or Express mode with API key)
|
||||
- False: Use Gemini API (explicitly override env var)
|
||||
- None (default): Check GOOGLE_GENAI_USE_VERTEXAI env var
|
||||
When using Vertex AI with API key (Express mode), http_options with
|
||||
api_version="v1" is automatically configured.
|
||||
response_format: Pydantic model for structured output. Used as default when
|
||||
response_model is not passed to call()/acall() methods.
|
||||
thinking_config: ThinkingConfig for thinking models (gemini-2.5+, gemini-3+).
|
||||
Controls thought output via include_thoughts, thinking_budget,
|
||||
and thinking_level. When None, thinking models automatically
|
||||
get include_thoughts=True so thought content is surfaced.
|
||||
**kwargs: Additional parameters
|
||||
"""
|
||||
if interceptor is not None:
|
||||
raise NotImplementedError(
|
||||
"HTTP interceptors are not yet supported for Google Gemini provider. "
|
||||
"Interceptors are currently supported for OpenAI and Anthropic providers only."
|
||||
)
|
||||
|
||||
# Normalize stop_sequences from stop kwarg
|
||||
popped = data.pop("stop_sequences", None)
|
||||
seqs = popped if popped is not None else (data.get("stop") or [])
|
||||
if isinstance(seqs, str):
|
||||
seqs = [seqs]
|
||||
data["stop"] = seqs
|
||||
|
||||
# Resolve env vars
|
||||
data["api_key"] = (
|
||||
data.get("api_key")
|
||||
or os.getenv("GOOGLE_API_KEY")
|
||||
or os.getenv("GEMINI_API_KEY")
|
||||
)
|
||||
data["project"] = data.get("project") or os.getenv("GOOGLE_CLOUD_PROJECT")
|
||||
data["location"] = (
|
||||
data.get("location") or os.getenv("GOOGLE_CLOUD_LOCATION") or "us-central1"
|
||||
super().__init__(
|
||||
model=model, temperature=temperature, stop=stop_sequences or [], **kwargs
|
||||
)
|
||||
|
||||
use_vx = data.get("use_vertexai")
|
||||
if use_vx is None:
|
||||
use_vx = os.getenv("GOOGLE_GENAI_USE_VERTEXAI", "").lower() == "true"
|
||||
data["use_vertexai"] = use_vx
|
||||
# Store client params for later use
|
||||
self.client_params = client_params or {}
|
||||
|
||||
# Get API configuration with environment variable fallbacks
|
||||
self.api_key = (
|
||||
api_key or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
||||
)
|
||||
self.project = project or os.getenv("GOOGLE_CLOUD_PROJECT")
|
||||
self.location = location or os.getenv("GOOGLE_CLOUD_LOCATION") or "us-central1"
|
||||
|
||||
if use_vertexai is None:
|
||||
use_vertexai = os.getenv("GOOGLE_GENAI_USE_VERTEXAI", "").lower() == "true"
|
||||
|
||||
self.client = self._initialize_client(use_vertexai)
|
||||
|
||||
# Store completion parameters
|
||||
self.top_p = top_p
|
||||
self.top_k = top_k
|
||||
self.max_output_tokens = max_output_tokens
|
||||
self.stream = stream
|
||||
self.safety_settings = safety_settings or {}
|
||||
self.stop_sequences = stop_sequences or []
|
||||
self.tools: list[dict[str, Any]] | None = None
|
||||
self.response_format = response_format
|
||||
|
||||
# Model-specific settings
|
||||
model = data.get("model", "gemini-2.0-flash-001")
|
||||
version_match = re.search(r"gemini-(\d+(?:\.\d+)?)", model.lower())
|
||||
data["supports_tools"] = bool(
|
||||
self.supports_tools = bool(
|
||||
version_match and float(version_match.group(1)) >= 1.5
|
||||
)
|
||||
data["is_gemini_2_0"] = bool(
|
||||
self.is_gemini_2_0 = bool(
|
||||
version_match and float(version_match.group(1)) >= 2.0
|
||||
)
|
||||
|
||||
# Auto-enable thinking for gemini-2.5+
|
||||
self.thinking_config = thinking_config
|
||||
if (
|
||||
data.get("thinking_config") is None
|
||||
self.thinking_config is None
|
||||
and version_match
|
||||
and float(version_match.group(1)) >= 2.5
|
||||
):
|
||||
data["thinking_config"] = types.ThinkingConfig(include_thoughts=True)
|
||||
self.thinking_config = types.ThinkingConfig(include_thoughts=True)
|
||||
|
||||
return data
|
||||
@property
|
||||
def stop(self) -> list[str]:
|
||||
"""Get stop sequences sent to the API."""
|
||||
return self.stop_sequences
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_client(self) -> GeminiCompletion:
|
||||
self._client = self._initialize_client(self.use_vertexai)
|
||||
return self
|
||||
@stop.setter
|
||||
def stop(self, value: list[str] | str | None) -> None:
|
||||
"""Set stop sequences.
|
||||
|
||||
Synchronizes stop_sequences to ensure values set by CrewAgentExecutor
|
||||
are properly sent to the Gemini API.
|
||||
|
||||
Args:
|
||||
value: Stop sequences as a list, single string, or None
|
||||
"""
|
||||
if value is None:
|
||||
self.stop_sequences = []
|
||||
elif isinstance(value, str):
|
||||
self.stop_sequences = [value]
|
||||
elif isinstance(value, list):
|
||||
self.stop_sequences = value
|
||||
else:
|
||||
self.stop_sequences = []
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Gemini/Vertex-specific fields."""
|
||||
@@ -227,8 +283,8 @@ class GeminiCompletion(BaseLLM):
|
||||
|
||||
if (
|
||||
hasattr(self, "client")
|
||||
and hasattr(self._client, "vertexai")
|
||||
and self._client.vertexai
|
||||
and hasattr(self.client, "vertexai")
|
||||
and self.client.vertexai
|
||||
):
|
||||
# Vertex AI configuration
|
||||
params.update(
|
||||
@@ -665,7 +721,6 @@ class GeminiCompletion(BaseLLM):
|
||||
messages_for_event: list[LLMMessage],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> BaseModel:
|
||||
"""Validate content against response model and emit completion event.
|
||||
|
||||
@@ -691,7 +746,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages_for_event,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return structured_data
|
||||
@@ -707,7 +761,6 @@ class GeminiCompletion(BaseLLM):
|
||||
response_model: type[BaseModel] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> str | BaseModel:
|
||||
"""Finalize completion response with validation and event emission.
|
||||
|
||||
@@ -731,7 +784,6 @@ class GeminiCompletion(BaseLLM):
|
||||
messages_for_event=messages_for_event,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
self._emit_call_completed_event(
|
||||
@@ -740,7 +792,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages_for_event,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -754,7 +805,6 @@ class GeminiCompletion(BaseLLM):
|
||||
contents: list[types.Content],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> BaseModel:
|
||||
"""Validate and emit event for structured_output tool call.
|
||||
|
||||
@@ -779,7 +829,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=self._convert_contents_to_dict(contents),
|
||||
usage=usage,
|
||||
)
|
||||
return validated_data
|
||||
except Exception as e:
|
||||
@@ -798,7 +847,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
usage: dict[str, Any] | None = None,
|
||||
) -> str | Any:
|
||||
"""Process response, execute function calls, and finalize completion.
|
||||
|
||||
@@ -839,7 +887,6 @@ class GeminiCompletion(BaseLLM):
|
||||
contents=contents,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
# Filter out structured_output from function calls returned to executor
|
||||
@@ -861,7 +908,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=self._convert_contents_to_dict(contents),
|
||||
usage=usage,
|
||||
)
|
||||
return non_structured_output_parts
|
||||
|
||||
@@ -903,7 +949,6 @@ class GeminiCompletion(BaseLLM):
|
||||
response_model=effective_response_model,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
def _process_stream_chunk(
|
||||
@@ -911,10 +956,10 @@ class GeminiCompletion(BaseLLM):
|
||||
chunk: GenerateContentResponse,
|
||||
full_response: str,
|
||||
function_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int] | None,
|
||||
usage_data: dict[str, int],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
) -> tuple[str, dict[int, dict[str, Any]], dict[str, int] | None]:
|
||||
) -> tuple[str, dict[int, dict[str, Any]], dict[str, int]]:
|
||||
"""Process a single streaming chunk.
|
||||
|
||||
Args:
|
||||
@@ -990,7 +1035,7 @@ class GeminiCompletion(BaseLLM):
|
||||
self,
|
||||
full_response: str,
|
||||
function_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int] | None,
|
||||
usage_data: dict[str, int],
|
||||
contents: list[types.Content],
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1002,7 +1047,7 @@ class GeminiCompletion(BaseLLM):
|
||||
Args:
|
||||
full_response: The complete streamed response content
|
||||
function_calls: Dictionary of function calls accumulated during streaming
|
||||
usage_data: Token usage data from the stream, or None if unavailable
|
||||
usage_data: Token usage data from the stream
|
||||
contents: Original contents for event conversion
|
||||
available_functions: Available functions for function calling
|
||||
from_task: Task that initiated the call
|
||||
@@ -1012,8 +1057,7 @@ class GeminiCompletion(BaseLLM):
|
||||
Returns:
|
||||
Final response content after processing
|
||||
"""
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
if response_model and function_calls:
|
||||
for call_data in function_calls.values():
|
||||
@@ -1025,7 +1069,6 @@ class GeminiCompletion(BaseLLM):
|
||||
contents=contents,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
non_structured_output_calls = {
|
||||
@@ -1054,7 +1097,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=self._convert_contents_to_dict(contents),
|
||||
usage=usage_data,
|
||||
)
|
||||
return formatted_function_calls
|
||||
|
||||
@@ -1095,7 +1137,6 @@ class GeminiCompletion(BaseLLM):
|
||||
response_model=effective_response_model,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
def _handle_completion(
|
||||
@@ -1111,7 +1152,7 @@ class GeminiCompletion(BaseLLM):
|
||||
try:
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
contents_for_api: Any = contents
|
||||
response = self._client.models.generate_content(
|
||||
response = self.client.models.generate_content(
|
||||
model=self.model,
|
||||
contents=contents_for_api,
|
||||
config=config,
|
||||
@@ -1133,7 +1174,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
def _handle_streaming_completion(
|
||||
@@ -1148,11 +1188,11 @@ class GeminiCompletion(BaseLLM):
|
||||
"""Handle streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[int, dict[str, Any]] = {}
|
||||
usage_data: dict[str, int] | None = None
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
contents_for_api: Any = contents
|
||||
for chunk in self._client.models.generate_content_stream(
|
||||
for chunk in self.client.models.generate_content_stream(
|
||||
model=self.model,
|
||||
contents=contents_for_api,
|
||||
config=config,
|
||||
@@ -1190,7 +1230,7 @@ class GeminiCompletion(BaseLLM):
|
||||
try:
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
contents_for_api: Any = contents
|
||||
response = await self._client.aio.models.generate_content(
|
||||
response = await self.client.aio.models.generate_content(
|
||||
model=self.model,
|
||||
contents=contents_for_api,
|
||||
config=config,
|
||||
@@ -1212,7 +1252,6 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
async def _ahandle_streaming_completion(
|
||||
@@ -1227,11 +1266,11 @@ class GeminiCompletion(BaseLLM):
|
||||
"""Handle async streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[int, dict[str, Any]] = {}
|
||||
usage_data: dict[str, int] | None = None
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
contents_for_api: Any = contents
|
||||
stream = await self._client.aio.models.generate_content_stream(
|
||||
stream = await self.client.aio.models.generate_content_stream(
|
||||
model=self.model,
|
||||
contents=contents_for_api,
|
||||
config=config,
|
||||
@@ -1435,6 +1474,6 @@ class GeminiCompletion(BaseLLM):
|
||||
try:
|
||||
from crewai_files.uploaders.gemini import GeminiFileUploader
|
||||
|
||||
return GeminiFileUploader(client=self._client)
|
||||
return GeminiFileUploader(client=self.client)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
@@ -14,11 +14,10 @@ from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
||||
from openai.types.chat.chat_completion import Choice
|
||||
from openai.types.chat.chat_completion_chunk import ChoiceDelta
|
||||
from openai.types.responses import Response
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType
|
||||
from crewai.llms.base_llm import BaseLLM, JsonResponseFormat, llm_call_context
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.llms.base_llm import BaseLLM, llm_call_context
|
||||
from crewai.llms.hooks.transport import AsyncHTTPTransport, HTTPTransport
|
||||
from crewai.utilities.agent_utils import is_context_length_exceeded
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
@@ -30,6 +29,7 @@ from crewai.utilities.types import LLMMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
from crewai.task import Task
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
|
||||
@@ -183,69 +183,77 @@ class OpenAICompletion(BaseLLM):
|
||||
"computer_use": "computer_use_preview",
|
||||
}
|
||||
|
||||
model: str = "gpt-4o"
|
||||
organization: str | None = None
|
||||
project: str | None = None
|
||||
timeout: float | None = None
|
||||
max_retries: int = 2
|
||||
default_headers: dict[str, str] | None = None
|
||||
default_query: dict[str, Any] | None = None
|
||||
client_params: dict[str, Any] | None = None
|
||||
top_p: float | None = None
|
||||
frequency_penalty: float | None = None
|
||||
presence_penalty: float | None = None
|
||||
max_tokens: int | None = None
|
||||
max_completion_tokens: int | None = None
|
||||
seed: int | None = None
|
||||
stream: bool = False
|
||||
response_format: JsonResponseFormat | type[BaseModel] | None = None
|
||||
logprobs: bool | None = None
|
||||
top_logprobs: int | None = None
|
||||
reasoning_effort: str | None = None
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None
|
||||
api: Literal["completions", "responses"] = "completions"
|
||||
instructions: str | None = None
|
||||
store: bool | None = None
|
||||
previous_response_id: str | None = None
|
||||
include: list[str] | None = None
|
||||
builtin_tools: list[str] | None = None
|
||||
parse_tool_outputs: bool = False
|
||||
auto_chain: bool = False
|
||||
auto_chain_reasoning: bool = False
|
||||
api_base: str | None = None
|
||||
is_o1_model: bool = False
|
||||
is_gpt4_model: bool = False
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "gpt-4o",
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
organization: str | None = None,
|
||||
project: str | None = None,
|
||||
timeout: float | None = None,
|
||||
max_retries: int = 2,
|
||||
default_headers: dict[str, str] | None = None,
|
||||
default_query: dict[str, Any] | None = None,
|
||||
client_params: dict[str, Any] | None = None,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
frequency_penalty: float | None = None,
|
||||
presence_penalty: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
max_completion_tokens: int | None = None,
|
||||
seed: int | None = None,
|
||||
stream: bool = False,
|
||||
response_format: dict[str, Any] | type[BaseModel] | None = None,
|
||||
logprobs: bool | None = None,
|
||||
top_logprobs: int | None = None,
|
||||
reasoning_effort: str | None = None,
|
||||
provider: str | None = None,
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
|
||||
api: Literal["completions", "responses"] = "completions",
|
||||
instructions: str | None = None,
|
||||
store: bool | None = None,
|
||||
previous_response_id: str | None = None,
|
||||
include: list[str] | None = None,
|
||||
builtin_tools: list[str] | None = None,
|
||||
parse_tool_outputs: bool = False,
|
||||
auto_chain: bool = False,
|
||||
auto_chain_reasoning: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize OpenAI completion client."""
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_async_client: Any = PrivateAttr(default=None)
|
||||
_last_response_id: str | None = PrivateAttr(default=None)
|
||||
_last_reasoning_items: list[Any] | None = PrivateAttr(default=None)
|
||||
if provider is None:
|
||||
provider = kwargs.pop("provider", "openai")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_openai_fields(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
if not data.get("provider"):
|
||||
data["provider"] = "openai"
|
||||
data["api_key"] = data.get("api_key") or os.getenv("OPENAI_API_KEY")
|
||||
# Extract api_base from kwargs if present
|
||||
if "api_base" not in data:
|
||||
data["api_base"] = None
|
||||
model = data.get("model", "gpt-4o")
|
||||
data["is_o1_model"] = "o1" in model.lower()
|
||||
data["is_gpt4_model"] = "gpt-4" in model.lower()
|
||||
return data
|
||||
self.interceptor = interceptor
|
||||
# Client configuration attributes
|
||||
self.organization = organization
|
||||
self.project = project
|
||||
self.max_retries = max_retries
|
||||
self.default_headers = default_headers
|
||||
self.default_query = default_query
|
||||
self.client_params = client_params
|
||||
self.timeout = timeout
|
||||
self.base_url = base_url
|
||||
self.api_base = kwargs.pop("api_base", None)
|
||||
|
||||
super().__init__(
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
api_key=api_key or os.getenv("OPENAI_API_KEY"),
|
||||
base_url=base_url,
|
||||
timeout=timeout,
|
||||
provider=provider,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> OpenAICompletion:
|
||||
client_config = self._get_client_params()
|
||||
if self.interceptor:
|
||||
transport = HTTPTransport(interceptor=self.interceptor)
|
||||
http_client = httpx.Client(transport=transport)
|
||||
client_config["http_client"] = http_client
|
||||
|
||||
self._client = OpenAI(**client_config)
|
||||
self.client = OpenAI(**client_config)
|
||||
|
||||
async_client_config = self._get_client_params()
|
||||
if self.interceptor:
|
||||
@@ -253,8 +261,35 @@ class OpenAICompletion(BaseLLM):
|
||||
async_http_client = httpx.AsyncClient(transport=async_transport)
|
||||
async_client_config["http_client"] = async_http_client
|
||||
|
||||
self._async_client = AsyncOpenAI(**async_client_config)
|
||||
return self
|
||||
self.async_client = AsyncOpenAI(**async_client_config)
|
||||
|
||||
# Completion parameters
|
||||
self.top_p = top_p
|
||||
self.frequency_penalty = frequency_penalty
|
||||
self.presence_penalty = presence_penalty
|
||||
self.max_tokens = max_tokens
|
||||
self.max_completion_tokens = max_completion_tokens
|
||||
self.seed = seed
|
||||
self.stream = stream
|
||||
self.response_format = response_format
|
||||
self.logprobs = logprobs
|
||||
self.top_logprobs = top_logprobs
|
||||
self.reasoning_effort = reasoning_effort
|
||||
self.is_o1_model = "o1" in model.lower()
|
||||
self.is_gpt4_model = "gpt-4" in model.lower()
|
||||
|
||||
# API selection and Responses API parameters
|
||||
self.api = api
|
||||
self.instructions = instructions
|
||||
self.store = store
|
||||
self.previous_response_id = previous_response_id
|
||||
self.include = include
|
||||
self.builtin_tools = builtin_tools
|
||||
self.parse_tool_outputs = parse_tool_outputs
|
||||
self.auto_chain = auto_chain
|
||||
self.auto_chain_reasoning = auto_chain_reasoning
|
||||
self._last_response_id: str | None = None
|
||||
self._last_reasoning_items: list[Any] | None = None
|
||||
|
||||
@property
|
||||
def last_response_id(self) -> str | None:
|
||||
@@ -783,7 +818,7 @@ class OpenAICompletion(BaseLLM):
|
||||
) -> str | ResponsesAPIResult | Any:
|
||||
"""Handle non-streaming Responses API call."""
|
||||
try:
|
||||
response: Response = self._client.responses.create(**params)
|
||||
response: Response = self.client.responses.create(**params)
|
||||
|
||||
# Track response ID for auto-chaining
|
||||
if self.auto_chain and response.id:
|
||||
@@ -809,7 +844,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -822,7 +856,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return function_calls
|
||||
|
||||
@@ -860,7 +893,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -874,7 +906,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
content = self._invoke_after_llm_call_hooks(
|
||||
@@ -919,7 +950,7 @@ class OpenAICompletion(BaseLLM):
|
||||
) -> str | ResponsesAPIResult | Any:
|
||||
"""Handle async non-streaming Responses API call."""
|
||||
try:
|
||||
response: Response = await self._async_client.responses.create(**params)
|
||||
response: Response = await self.async_client.responses.create(**params)
|
||||
|
||||
# Track response ID for auto-chaining
|
||||
if self.auto_chain and response.id:
|
||||
@@ -945,7 +976,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -958,7 +988,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return function_calls
|
||||
|
||||
@@ -996,7 +1025,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1010,7 +1038,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
except NotFoundError as e:
|
||||
@@ -1053,9 +1080,8 @@ class OpenAICompletion(BaseLLM):
|
||||
full_response = ""
|
||||
function_calls: list[dict[str, Any]] = []
|
||||
final_response: Response | None = None
|
||||
usage: dict[str, Any] | None = None
|
||||
|
||||
stream = self._client.responses.create(**params)
|
||||
stream = self.client.responses.create(**params)
|
||||
response_id_stream = None
|
||||
|
||||
for event in stream:
|
||||
@@ -1111,7 +1137,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -1148,7 +1173,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1162,7 +1186,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -1181,9 +1204,8 @@ class OpenAICompletion(BaseLLM):
|
||||
full_response = ""
|
||||
function_calls: list[dict[str, Any]] = []
|
||||
final_response: Response | None = None
|
||||
usage: dict[str, Any] | None = None
|
||||
|
||||
stream = await self._async_client.responses.create(**params)
|
||||
stream = await self.async_client.responses.create(**params)
|
||||
response_id_stream = None
|
||||
|
||||
async for event in stream:
|
||||
@@ -1239,7 +1261,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -1276,7 +1297,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1290,7 +1310,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1576,7 +1595,7 @@ class OpenAICompletion(BaseLLM):
|
||||
parse_params = {
|
||||
k: v for k, v in params.items() if k != "response_format"
|
||||
}
|
||||
parsed_response = self._client.beta.chat.completions.parse(
|
||||
parsed_response = self.client.beta.chat.completions.parse(
|
||||
**parse_params,
|
||||
response_format=response_model,
|
||||
)
|
||||
@@ -1596,11 +1615,10 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return parsed_object
|
||||
|
||||
response: ChatCompletion = self._client.chat.completions.create(**params)
|
||||
response: ChatCompletion = self.client.chat.completions.create(**params)
|
||||
|
||||
usage = self._extract_openai_token_usage(response)
|
||||
|
||||
@@ -1618,7 +1636,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(message.tool_calls)
|
||||
|
||||
@@ -1657,7 +1674,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1671,7 +1687,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -1713,7 +1728,7 @@ class OpenAICompletion(BaseLLM):
|
||||
self,
|
||||
full_response: str,
|
||||
tool_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, Any] | None,
|
||||
usage_data: dict[str, int],
|
||||
params: dict[str, Any],
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1724,7 +1739,7 @@ class OpenAICompletion(BaseLLM):
|
||||
Args:
|
||||
full_response: The accumulated text response from the stream.
|
||||
tool_calls: Accumulated tool calls from the stream, keyed by index.
|
||||
usage_data: Token usage data from the stream, or None if unavailable.
|
||||
usage_data: Token usage data from the stream.
|
||||
params: The completion parameters containing messages.
|
||||
available_functions: Available functions for tool calling.
|
||||
from_task: Task that initiated the call.
|
||||
@@ -1735,8 +1750,7 @@ class OpenAICompletion(BaseLLM):
|
||||
tool execution result when available_functions is provided,
|
||||
or the text response string.
|
||||
"""
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
if tool_calls and not available_functions:
|
||||
tool_calls_list = [
|
||||
@@ -1757,7 +1771,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
return tool_calls_list
|
||||
|
||||
@@ -1800,7 +1813,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1825,7 +1837,7 @@ class OpenAICompletion(BaseLLM):
|
||||
}
|
||||
|
||||
stream: ChatCompletionStream[BaseModel]
|
||||
with self._client.beta.chat.completions.stream(
|
||||
with self.client.beta.chat.completions.stream(
|
||||
**parse_params, response_format=response_model
|
||||
) as stream:
|
||||
for chunk in stream:
|
||||
@@ -1854,7 +1866,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return parsed_result
|
||||
|
||||
@@ -1862,10 +1873,10 @@ class OpenAICompletion(BaseLLM):
|
||||
return ""
|
||||
|
||||
completion_stream: Stream[ChatCompletionChunk] = (
|
||||
self._client.chat.completions.create(**params)
|
||||
self.client.chat.completions.create(**params)
|
||||
)
|
||||
|
||||
usage_data: dict[str, Any] | None = None
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
for completion_chunk in completion_stream:
|
||||
response_id_stream = (
|
||||
@@ -1959,7 +1970,7 @@ class OpenAICompletion(BaseLLM):
|
||||
parse_params = {
|
||||
k: v for k, v in params.items() if k != "response_format"
|
||||
}
|
||||
parsed_response = await self._async_client.beta.chat.completions.parse(
|
||||
parsed_response = await self.async_client.beta.chat.completions.parse(
|
||||
**parse_params,
|
||||
response_format=response_model,
|
||||
)
|
||||
@@ -1979,11 +1990,10 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return parsed_object
|
||||
|
||||
response: ChatCompletion = await self._async_client.chat.completions.create(
|
||||
response: ChatCompletion = await self.async_client.chat.completions.create(
|
||||
**params
|
||||
)
|
||||
|
||||
@@ -2003,7 +2013,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(message.tool_calls)
|
||||
|
||||
@@ -2042,7 +2051,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -2056,7 +2064,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -2104,10 +2111,10 @@ class OpenAICompletion(BaseLLM):
|
||||
if response_model:
|
||||
completion_stream: AsyncIterator[
|
||||
ChatCompletionChunk
|
||||
] = await self._async_client.chat.completions.create(**params)
|
||||
] = await self.async_client.chat.completions.create(**params)
|
||||
|
||||
accumulated_content = ""
|
||||
usage_data: dict[str, Any] | None = None
|
||||
usage_data = {"total_tokens": 0}
|
||||
async for chunk in completion_stream:
|
||||
response_id_stream = chunk.id if hasattr(chunk, "id") else None
|
||||
|
||||
@@ -2130,8 +2137,7 @@ class OpenAICompletion(BaseLLM):
|
||||
response_id=response_id_stream,
|
||||
)
|
||||
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
try:
|
||||
parsed_object = response_model.model_validate_json(accumulated_content)
|
||||
@@ -2142,7 +2148,6 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return parsed_object
|
||||
@@ -2154,15 +2159,14 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
return accumulated_content
|
||||
|
||||
stream: AsyncIterator[
|
||||
ChatCompletionChunk
|
||||
] = await self._async_client.chat.completions.create(**params)
|
||||
] = await self.async_client.chat.completions.create(**params)
|
||||
|
||||
usage_data = None
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
async for chunk in stream:
|
||||
response_id_stream = chunk.id if hasattr(chunk, "id") else None
|
||||
@@ -2352,8 +2356,8 @@ class OpenAICompletion(BaseLLM):
|
||||
from crewai_files.uploaders.openai import OpenAIFileUploader
|
||||
|
||||
return OpenAIFileUploader(
|
||||
client=self._client,
|
||||
async_client=self._async_client,
|
||||
client=self.client,
|
||||
async_client=self.async_client,
|
||||
)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
@@ -16,8 +16,6 @@ from dataclasses import dataclass, field
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from crewai.llms.providers.openai.completion import OpenAICompletion
|
||||
|
||||
|
||||
@@ -142,13 +140,31 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
)
|
||||
"""
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _resolve_provider_config(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
provider: str,
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
default_headers: dict[str, str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize OpenAI-compatible completion client.
|
||||
|
||||
provider = data.get("provider", "")
|
||||
Args:
|
||||
model: The model identifier.
|
||||
provider: The provider name (must be in OPENAI_COMPATIBLE_PROVIDERS).
|
||||
api_key: Optional API key override. If not provided, uses the
|
||||
provider's configured environment variable.
|
||||
base_url: Optional base URL override. If not provided, uses the
|
||||
provider's configured default or environment variable.
|
||||
default_headers: Optional headers to merge with provider defaults.
|
||||
**kwargs: Additional arguments passed to OpenAICompletion.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider is not supported or required API key
|
||||
is missing.
|
||||
"""
|
||||
config = OPENAI_COMPATIBLE_PROVIDERS.get(provider)
|
||||
if config is None:
|
||||
supported = ", ".join(sorted(OPENAI_COMPATIBLE_PROVIDERS.keys()))
|
||||
@@ -157,15 +173,21 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
f"Supported providers: {supported}"
|
||||
)
|
||||
|
||||
data["api_key"] = cls._resolve_api_key(data.get("api_key"), config, provider)
|
||||
data["base_url"] = cls._resolve_base_url(data.get("base_url"), config, provider)
|
||||
data["default_headers"] = cls._resolve_headers(
|
||||
data.get("default_headers"), config
|
||||
)
|
||||
return data
|
||||
resolved_api_key = self._resolve_api_key(api_key, config, provider)
|
||||
resolved_base_url = self._resolve_base_url(base_url, config, provider)
|
||||
resolved_headers = self._resolve_headers(default_headers, config)
|
||||
|
||||
super().__init__(
|
||||
model=model,
|
||||
provider=provider,
|
||||
api_key=resolved_api_key,
|
||||
base_url=resolved_base_url,
|
||||
default_headers=resolved_headers,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_api_key(
|
||||
self,
|
||||
api_key: str | None,
|
||||
config: ProviderConfig,
|
||||
provider: str,
|
||||
@@ -198,8 +220,8 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
|
||||
return config.default_api_key
|
||||
|
||||
@staticmethod
|
||||
def _resolve_base_url(
|
||||
self,
|
||||
base_url: str | None,
|
||||
config: ProviderConfig,
|
||||
provider: str,
|
||||
@@ -227,8 +249,8 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
|
||||
return resolved
|
||||
|
||||
@staticmethod
|
||||
def _resolve_headers(
|
||||
self,
|
||||
headers: dict[str, str] | None,
|
||||
config: ProviderConfig,
|
||||
) -> dict[str, str] | None:
|
||||
|
||||
1
lib/crewai/src/crewai/llms/third_party/__init__.py
vendored
Normal file
1
lib/crewai/src/crewai/llms/third_party/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Third-party LLM implementations for crewAI."""
|
||||
@@ -7,6 +7,7 @@ various transport types, similar to OpenAI's Agents SDK.
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.mcp.filters import ToolFilter
|
||||
from crewai.mcp.transports.stdio import DEFAULT_ALLOWED_COMMANDS
|
||||
|
||||
|
||||
class MCPServerStdio(BaseModel):
|
||||
@@ -44,6 +45,14 @@ class MCPServerStdio(BaseModel):
|
||||
default=None,
|
||||
description="Optional tool filter for filtering available tools.",
|
||||
)
|
||||
allowed_commands: frozenset[str] | None = Field(
|
||||
default=DEFAULT_ALLOWED_COMMANDS,
|
||||
description=(
|
||||
"Optional frozenset of allowed command basenames for security validation. "
|
||||
"Defaults to common runtimes (python, node, npx, uvx, uv, deno, docker). "
|
||||
"Set to None to disable the allowlist check."
|
||||
),
|
||||
)
|
||||
cache_tools_list: bool = Field(
|
||||
default=False,
|
||||
description="Whether to cache the tool list for faster subsequent access.",
|
||||
|
||||
@@ -292,6 +292,7 @@ class MCPToolResolver:
|
||||
command=mcp_config.command,
|
||||
args=mcp_config.args,
|
||||
env=mcp_config.env,
|
||||
allowed_commands=mcp_config.allowed_commands,
|
||||
)
|
||||
server_name = f"{mcp_config.command}_{'_'.join(mcp_config.args)}"
|
||||
elif isinstance(mcp_config, MCPServerHTTP):
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
from crewai.mcp.transports.base import BaseTransport, TransportType
|
||||
from crewai.mcp.transports.http import HTTPTransport
|
||||
from crewai.mcp.transports.sse import SSETransport
|
||||
from crewai.mcp.transports.stdio import StdioTransport
|
||||
from crewai.mcp.transports.stdio import DEFAULT_ALLOWED_COMMANDS, StdioTransport
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseTransport",
|
||||
"DEFAULT_ALLOWED_COMMANDS",
|
||||
"HTTPTransport",
|
||||
"SSETransport",
|
||||
"StdioTransport",
|
||||
|
||||
@@ -9,6 +9,22 @@ from typing_extensions import Self
|
||||
|
||||
from crewai.mcp.transports.base import BaseTransport, TransportType
|
||||
|
||||
# Default allowlist for common MCP server runtimes.
|
||||
# Covers the vast majority of MCP server launch commands.
|
||||
# Pass ``allowed_commands=None`` to disable validation entirely.
|
||||
DEFAULT_ALLOWED_COMMANDS: frozenset[str] = frozenset(
|
||||
{
|
||||
"python",
|
||||
"python3",
|
||||
"node",
|
||||
"npx",
|
||||
"uvx",
|
||||
"uv",
|
||||
"deno",
|
||||
"docker",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class StdioTransport(BaseTransport):
|
||||
"""Stdio transport for connecting to local MCP servers.
|
||||
@@ -34,6 +50,7 @@ class StdioTransport(BaseTransport):
|
||||
command: str,
|
||||
args: list[str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
allowed_commands: frozenset[str] | None = DEFAULT_ALLOWED_COMMANDS,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize stdio transport.
|
||||
@@ -42,9 +59,24 @@ class StdioTransport(BaseTransport):
|
||||
command: Command to execute (e.g., "python", "node", "npx").
|
||||
args: Command arguments (e.g., ["server.py"] or ["-y", "@mcp/server"]).
|
||||
env: Environment variables to pass to the process.
|
||||
allowed_commands: Optional frozenset of allowed command basenames.
|
||||
Defaults to ``DEFAULT_ALLOWED_COMMANDS`` which includes common
|
||||
runtimes (python, node, npx, uvx, uv, deno, docker). Pass
|
||||
``None`` to disable the check entirely.
|
||||
**kwargs: Additional transport options.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if allowed_commands is not None:
|
||||
base_command = os.path.basename(command)
|
||||
if base_command not in allowed_commands:
|
||||
raise ValueError(
|
||||
f"Command '{command}' is not in the allowed commands list: "
|
||||
f"{sorted(allowed_commands)}. "
|
||||
f"To allow this command, add it to allowed_commands or pass "
|
||||
f"allowed_commands=None to disable this check."
|
||||
)
|
||||
|
||||
self.command = command
|
||||
self.args = args or []
|
||||
self.env = env or {}
|
||||
|
||||
@@ -98,7 +98,7 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
|
||||
_skip_auto_memory: bool = True
|
||||
|
||||
initial_state: type[EncodingState] = EncodingState
|
||||
initial_state = EncodingState
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -65,7 +65,7 @@ class RecallFlow(Flow[RecallState]):
|
||||
|
||||
_skip_auto_memory: bool = True
|
||||
|
||||
initial_state: type[RecallState] = RecallState
|
||||
initial_state = RecallState
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -148,36 +148,6 @@ class Memory(BaseModel):
|
||||
_pending_saves: list[Future[Any]] = PrivateAttr(default_factory=list)
|
||||
_pending_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
||||
|
||||
def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Memory:
|
||||
"""Deepcopy that handles unpickleable private attrs (ThreadPoolExecutor, Lock)."""
|
||||
import copy as _copy
|
||||
|
||||
cls = type(self)
|
||||
new = cls.__new__(cls)
|
||||
if memo is None:
|
||||
memo = {}
|
||||
memo[id(self)] = new
|
||||
object.__setattr__(new, "__dict__", _copy.deepcopy(self.__dict__, memo))
|
||||
object.__setattr__(
|
||||
new, "__pydantic_fields_set__", _copy.copy(self.__pydantic_fields_set__)
|
||||
)
|
||||
object.__setattr__(
|
||||
new, "__pydantic_extra__", _copy.deepcopy(self.__pydantic_extra__, memo)
|
||||
)
|
||||
# Private attrs: create fresh pool/lock instead of deepcopying
|
||||
private = {}
|
||||
for k, v in (self.__pydantic_private__ or {}).items():
|
||||
if isinstance(v, (ThreadPoolExecutor, threading.Lock)):
|
||||
attr = self.__private_attributes__[k]
|
||||
private[k] = attr.get_default()
|
||||
else:
|
||||
try:
|
||||
private[k] = _copy.deepcopy(v, memo)
|
||||
except Exception:
|
||||
private[k] = v
|
||||
object.__setattr__(new, "__pydantic_private__", private)
|
||||
return new
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Initialize runtime state from field values."""
|
||||
self._config = MemoryConfig(
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, InstanceOf
|
||||
from rich.box import HEAVY_EDGE
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
@@ -39,9 +39,9 @@ class CrewEvaluator:
|
||||
def __init__(
|
||||
self,
|
||||
crew: Crew,
|
||||
eval_llm: BaseLLM | str | None = None,
|
||||
eval_llm: InstanceOf[BaseLLM] | str | None = None,
|
||||
openai_model_name: str | None = None,
|
||||
llm: BaseLLM | str | None = None,
|
||||
llm: InstanceOf[BaseLLM] | str | None = None,
|
||||
) -> None:
|
||||
self.crew = crew
|
||||
self.llm = eval_llm
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any, Literal
|
||||
from typing import Annotated, Any, Literal, TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
|
||||
|
||||
@@ -1692,27 +1692,9 @@ def test_agent_with_knowledge_sources_works_with_copy():
|
||||
) as mock_knowledge_storage:
|
||||
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
|
||||
|
||||
class _StubStorage(BaseKnowledgeStorage):
|
||||
def search(self, query, limit=5, metadata_filter=None, score_threshold=0.6):
|
||||
return []
|
||||
|
||||
async def asearch(self, query, limit=5, metadata_filter=None, score_threshold=0.6):
|
||||
return []
|
||||
|
||||
def save(self, documents):
|
||||
pass
|
||||
|
||||
async def asave(self, documents):
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
async def areset(self):
|
||||
pass
|
||||
|
||||
mock_knowledge_storage.return_value = _StubStorage()
|
||||
agent.knowledge_storage = _StubStorage()
|
||||
mock_knowledge_storage_instance = mock_knowledge_storage.return_value
|
||||
mock_knowledge_storage_instance.__class__ = BaseKnowledgeStorage
|
||||
agent.knowledge_storage = mock_knowledge_storage_instance
|
||||
|
||||
agent_copy = agent.copy()
|
||||
|
||||
|
||||
@@ -4,55 +4,13 @@ Tests the Flow-based agent executor implementation including state management,
|
||||
flow methods, routing logic, and error handling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
|
||||
from crewai.agents.step_executor import StepExecutor
|
||||
|
||||
|
||||
def _build_executor(**kwargs: Any) -> AgentExecutor:
|
||||
"""Create an AgentExecutor without validation — for unit tests.
|
||||
|
||||
Uses model_construct to skip Pydantic validators so plain Mock()
|
||||
objects are accepted for typed fields like llm, agent, crew, task.
|
||||
"""
|
||||
executor = AgentExecutor.model_construct(**kwargs)
|
||||
executor._state = AgentExecutorState()
|
||||
executor._methods = {}
|
||||
executor._method_outputs = []
|
||||
executor._completed_methods = set()
|
||||
executor._fired_or_listeners = set()
|
||||
executor._pending_and_listeners = {}
|
||||
executor._method_execution_counts = {}
|
||||
executor._method_call_counts = {}
|
||||
executor._event_futures = []
|
||||
executor._human_feedback_method_outputs = {}
|
||||
executor._input_history = []
|
||||
executor._is_execution_resuming = False
|
||||
import threading
|
||||
executor._state_lock = threading.Lock()
|
||||
executor._or_listeners_lock = threading.Lock()
|
||||
executor._execution_lock = threading.Lock()
|
||||
executor._finalize_lock = threading.Lock()
|
||||
executor._finalize_called = False
|
||||
executor._is_executing = False
|
||||
executor._has_been_invoked = False
|
||||
executor._last_parser_error = None
|
||||
executor._last_context_error = None
|
||||
executor._step_executor = None
|
||||
executor._planner_observer = None
|
||||
from crewai.utilities.printer import Printer
|
||||
executor._printer = Printer()
|
||||
from crewai.utilities.i18n import get_i18n
|
||||
executor._i18n = kwargs.get("i18n") or get_i18n()
|
||||
return executor
|
||||
from crewai.agents.planner_observer import PlannerObserver
|
||||
from crewai.experimental.agent_executor import (
|
||||
AgentExecutorState,
|
||||
@@ -117,7 +75,6 @@ class TestAgentExecutor:
|
||||
"""Create mock dependencies for executor."""
|
||||
llm = Mock()
|
||||
llm.supports_stop_words.return_value = True
|
||||
llm.stop = []
|
||||
|
||||
task = Mock()
|
||||
task.description = "Test task"
|
||||
@@ -137,7 +94,7 @@ class TestAgentExecutor:
|
||||
prompt = {"prompt": "Test prompt with {input}, {tool_names}, {tools}"}
|
||||
|
||||
tools = []
|
||||
tools_handler = Mock(spec=_ToolsHandler)
|
||||
tools_handler = Mock()
|
||||
|
||||
return {
|
||||
"llm": llm,
|
||||
@@ -155,7 +112,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_executor_initialization(self, mock_dependencies):
|
||||
"""Test AgentExecutor initialization."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
assert executor.llm == mock_dependencies["llm"]
|
||||
assert executor.task == mock_dependencies["task"]
|
||||
@@ -169,7 +126,7 @@ class TestAgentExecutor:
|
||||
with patch.object(
|
||||
AgentExecutor, "_show_start_logs"
|
||||
) as mock_show_start:
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
result = executor.initialize_reasoning()
|
||||
|
||||
assert result == "initialized"
|
||||
@@ -177,7 +134,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_check_max_iterations_not_reached(self, mock_dependencies):
|
||||
"""Test routing when iterations < max."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.iterations = 5
|
||||
|
||||
result = executor.check_max_iterations()
|
||||
@@ -185,7 +142,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_check_max_iterations_reached(self, mock_dependencies):
|
||||
"""Test routing when iterations >= max."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.iterations = 10
|
||||
|
||||
result = executor.check_max_iterations()
|
||||
@@ -193,7 +150,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_route_by_answer_type_action(self, mock_dependencies):
|
||||
"""Test routing for AgentAction."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.current_answer = AgentAction(
|
||||
thought="thinking", tool="search", tool_input="query", text="action text"
|
||||
)
|
||||
@@ -203,7 +160,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_route_by_answer_type_finish(self, mock_dependencies):
|
||||
"""Test routing for AgentFinish."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="final thoughts", output="Final answer", text="complete"
|
||||
)
|
||||
@@ -213,7 +170,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_continue_iteration(self, mock_dependencies):
|
||||
"""Test iteration continuation."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
result = executor.continue_iteration()
|
||||
|
||||
@@ -222,7 +179,7 @@ class TestAgentExecutor:
|
||||
def test_finalize_success(self, mock_dependencies):
|
||||
"""Test finalize with valid AgentFinish."""
|
||||
with patch.object(AgentExecutor, "_show_logs") as mock_show_logs:
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.current_answer = AgentFinish(
|
||||
thought="final thinking", output="Done", text="complete"
|
||||
)
|
||||
@@ -235,7 +192,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_finalize_failure(self, mock_dependencies):
|
||||
"""Test finalize skips when given AgentAction instead of AgentFinish."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.current_answer = AgentAction(
|
||||
thought="thinking", tool="search", tool_input="query", text="action text"
|
||||
)
|
||||
@@ -251,7 +208,7 @@ class TestAgentExecutor:
|
||||
):
|
||||
"""Finalize should skip synthesis when last todo is already a complete answer."""
|
||||
with patch.object(AgentExecutor, "_show_logs") as mock_show_logs:
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.todos.items = [
|
||||
TodoItem(
|
||||
step_number=1,
|
||||
@@ -295,7 +252,7 @@ class TestAgentExecutor:
|
||||
):
|
||||
"""Finalize should still synthesize when response_model is configured."""
|
||||
with patch.object(AgentExecutor, "_show_logs"):
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.response_model = Mock()
|
||||
executor.state.todos.items = [
|
||||
TodoItem(
|
||||
@@ -330,7 +287,7 @@ class TestAgentExecutor:
|
||||
|
||||
def test_format_prompt(self, mock_dependencies):
|
||||
"""Test prompt formatting."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
inputs = {"input": "test input", "tool_names": "tool1, tool2", "tools": "desc"}
|
||||
|
||||
result = executor._format_prompt("Prompt {input} {tool_names} {tools}", inputs)
|
||||
@@ -341,18 +298,18 @@ class TestAgentExecutor:
|
||||
|
||||
def test_is_training_mode_false(self, mock_dependencies):
|
||||
"""Test training mode detection when not in training."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
assert executor._is_training_mode() is False
|
||||
|
||||
def test_is_training_mode_true(self, mock_dependencies):
|
||||
"""Test training mode detection when in training."""
|
||||
mock_dependencies["crew"]._train = True
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
assert executor._is_training_mode() is True
|
||||
|
||||
def test_append_message_to_state(self, mock_dependencies):
|
||||
"""Test message appending to state."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
initial_count = len(executor.state.messages)
|
||||
|
||||
executor._append_message_to_state("test message")
|
||||
@@ -365,7 +322,7 @@ class TestAgentExecutor:
|
||||
callback = Mock()
|
||||
mock_dependencies["step_callback"] = callback
|
||||
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
answer = AgentFinish(thought="thinking", output="test", text="final")
|
||||
|
||||
executor._invoke_step_callback(answer)
|
||||
@@ -375,7 +332,7 @@ class TestAgentExecutor:
|
||||
def test_invoke_step_callback_none(self, mock_dependencies):
|
||||
"""Test step callback when none provided."""
|
||||
mock_dependencies["step_callback"] = None
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
# Should not raise error
|
||||
executor._invoke_step_callback(
|
||||
@@ -389,7 +346,7 @@ class TestAgentExecutor:
|
||||
"""Test async step callback scheduling when already in an event loop."""
|
||||
callback = AsyncMock()
|
||||
mock_dependencies["step_callback"] = callback
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
answer = AgentFinish(thought="thinking", output="test", text="final")
|
||||
with patch("crewai.experimental.agent_executor.asyncio.run") as mock_run:
|
||||
@@ -407,7 +364,6 @@ class TestStepExecutorCriticalFixes:
|
||||
def mock_dependencies(self):
|
||||
"""Create mock dependencies for AgentExecutor tests in this class."""
|
||||
llm = Mock()
|
||||
llm.stop = []
|
||||
llm.supports_stop_words.return_value = True
|
||||
|
||||
task = Mock()
|
||||
@@ -437,7 +393,6 @@ class TestStepExecutorCriticalFixes:
|
||||
@pytest.fixture
|
||||
def step_executor(self):
|
||||
llm = Mock()
|
||||
llm.stop = []
|
||||
llm.supports_stop_words.return_value = True
|
||||
|
||||
agent = Mock()
|
||||
@@ -530,7 +485,7 @@ class TestStepExecutorCriticalFixes:
|
||||
|
||||
mock_handle_exception.return_value = None
|
||||
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor._last_parser_error = OutputParserError("test error")
|
||||
initial_iterations = executor.state.iterations
|
||||
|
||||
@@ -545,7 +500,7 @@ class TestStepExecutorCriticalFixes:
|
||||
self, mock_handle_context, mock_dependencies
|
||||
):
|
||||
"""Test recovery from context length error."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor._last_context_error = Exception("context too long")
|
||||
initial_iterations = executor.state.iterations
|
||||
|
||||
@@ -558,16 +513,16 @@ class TestStepExecutorCriticalFixes:
|
||||
def test_use_stop_words_property(self, mock_dependencies):
|
||||
"""Test use_stop_words property."""
|
||||
mock_dependencies["llm"].supports_stop_words.return_value = True
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
assert executor.use_stop_words is True
|
||||
|
||||
mock_dependencies["llm"].supports_stop_words.return_value = False
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
assert executor.use_stop_words is False
|
||||
|
||||
def test_compatibility_properties(self, mock_dependencies):
|
||||
"""Test compatibility properties for mixin."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.messages = [{"role": "user", "content": "test"}]
|
||||
executor.state.iterations = 5
|
||||
|
||||
@@ -583,7 +538,6 @@ class TestFlowErrorHandling:
|
||||
def mock_dependencies(self):
|
||||
"""Create mock dependencies."""
|
||||
llm = Mock()
|
||||
llm.stop = []
|
||||
llm.supports_stop_words.return_value = True
|
||||
|
||||
task = Mock()
|
||||
@@ -621,7 +575,7 @@ class TestFlowErrorHandling:
|
||||
mock_enforce_rpm.return_value = None
|
||||
mock_get_llm.side_effect = OutputParserError("parse failed")
|
||||
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
result = executor.call_llm_and_parse()
|
||||
|
||||
assert result == "parser_error"
|
||||
@@ -642,7 +596,7 @@ class TestFlowErrorHandling:
|
||||
mock_get_llm.side_effect = Exception("context length")
|
||||
mock_is_context_exceeded.return_value = True
|
||||
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
result = executor.call_llm_and_parse()
|
||||
|
||||
assert result == "context_error"
|
||||
@@ -656,7 +610,6 @@ class TestFlowInvoke:
|
||||
def mock_dependencies(self):
|
||||
"""Create mock dependencies."""
|
||||
llm = Mock()
|
||||
llm.stop = []
|
||||
task = Mock()
|
||||
task.description = "Test"
|
||||
task.human_input = False
|
||||
@@ -693,7 +646,7 @@ class TestFlowInvoke:
|
||||
mock_dependencies,
|
||||
):
|
||||
"""Test successful invoke without human feedback."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
# Mock kickoff to set the final answer in state
|
||||
def mock_kickoff_side_effect():
|
||||
@@ -713,7 +666,7 @@ class TestFlowInvoke:
|
||||
@patch.object(AgentExecutor, "kickoff")
|
||||
def test_invoke_failure_no_agent_finish(self, mock_kickoff, mock_dependencies):
|
||||
"""Test invoke fails without AgentFinish."""
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
executor.state.current_answer = AgentAction(
|
||||
thought="thinking", tool="test", tool_input="test", text="action text"
|
||||
)
|
||||
@@ -736,7 +689,7 @@ class TestFlowInvoke:
|
||||
"system": "System: {input}",
|
||||
"user": "User: {input} {tool_names} {tools}",
|
||||
}
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
def mock_kickoff_side_effect():
|
||||
executor.state.current_answer = AgentFinish(
|
||||
@@ -760,7 +713,6 @@ class TestNativeToolExecution:
|
||||
@pytest.fixture
|
||||
def mock_dependencies(self):
|
||||
llm = Mock()
|
||||
llm.stop = []
|
||||
llm.supports_stop_words.return_value = True
|
||||
|
||||
task = Mock()
|
||||
@@ -782,7 +734,7 @@ class TestNativeToolExecution:
|
||||
|
||||
prompt = {"prompt": "Test {input} {tool_names} {tools}"}
|
||||
|
||||
tools_handler = Mock(spec=_ToolsHandler)
|
||||
tools_handler = Mock()
|
||||
tools_handler.cache = None
|
||||
|
||||
return {
|
||||
@@ -802,7 +754,7 @@ class TestNativeToolExecution:
|
||||
def test_execute_native_tool_runs_parallel_for_multiple_calls(
|
||||
self, mock_dependencies
|
||||
):
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
def slow_one() -> str:
|
||||
time.sleep(0.2)
|
||||
@@ -838,7 +790,7 @@ class TestNativeToolExecution:
|
||||
def test_execute_native_tool_falls_back_to_sequential_for_result_as_answer(
|
||||
self, mock_dependencies
|
||||
):
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
|
||||
def slow_one() -> str:
|
||||
time.sleep(0.2)
|
||||
@@ -880,7 +832,7 @@ class TestNativeToolExecution:
|
||||
def test_execute_native_tool_result_as_answer_short_circuits_remaining_calls(
|
||||
self, mock_dependencies
|
||||
):
|
||||
executor = _build_executor(**mock_dependencies)
|
||||
executor = AgentExecutor(**mock_dependencies)
|
||||
call_counts = {"slow_one": 0, "slow_two": 0}
|
||||
|
||||
def slow_one() -> str:
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"input":[{"role":"user","content":"What is the weather in Tokyo?"}],"model":"gpt-4.1","instructions":"You
|
||||
are a helpful assistant that uses tools. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
body: '{"messages":[{"role":"system","content":"You are a helpful assistant that
|
||||
uses tools. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
@@ -72,9 +68,13 @@ interactions:
|
||||
for caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. ","tools":[{"type":"function","name":"get_weather","description":"Get
|
||||
the current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The
|
||||
city name"}},"required":["location"]}}]}'
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. "},{"role":"user","content":"What is the weather in Tokyo?"}],"model":"gpt-4.1","tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_weather","description":"Get
|
||||
the current weather for a location","strict":true,"parameters":{"type":"object","properties":{"location":{"type":"string","description":"The
|
||||
city name"}},"required":["location"],"additionalProperties":false}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
@@ -87,7 +87,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '6065'
|
||||
- '6158'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
@@ -109,113 +109,26 @@ interactions:
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.12
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/responses
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"resp_0d68149bcc0d14810069caf464a4b48197bd9f098abb2f6303\",\n
|
||||
\ \"object\": \"response\",\n \"created_at\": 1774908516,\n \"status\":
|
||||
\"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\":
|
||||
\"developer\"\n },\n \"completed_at\": 1774908517,\n \"error\": null,\n
|
||||
\ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\":
|
||||
\"You are a helpful assistant that uses tools. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. \",\n \"max_output_tokens\":
|
||||
null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-4.1-2025-04-14\",\n
|
||||
\ \"output\": [\n {\n \"id\": \"fc_0d68149bcc0d14810069caf46568088197a33be67f16a1fa09\",\n
|
||||
\ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\":
|
||||
\"{\\\"location\\\":\\\"Tokyo\\\"}\",\n \"call_id\": \"call_74rwmYse0DE4JFaFGyAFx9bu\",\n
|
||||
\ \"name\": \"get_weather\"\n }\n ],\n \"parallel_tool_calls\": true,\n
|
||||
\ \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\":
|
||||
null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\":
|
||||
null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\":
|
||||
\"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n
|
||||
\ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n
|
||||
\ },\n \"tool_choice\": \"auto\",\n \"tools\": [\n {\n \"type\":
|
||||
\"function\",\n \"description\": \"Get the current weather for a location\",\n
|
||||
\ \"name\": \"get_weather\",\n \"parameters\": {\n \"type\":
|
||||
\"object\",\n \"properties\": {\n \"location\": {\n \"type\":
|
||||
\"string\",\n \"description\": \"The city name\"\n }\n
|
||||
\ },\n \"required\": [\n \"location\"\n ],\n
|
||||
\ \"additionalProperties\": false\n },\n \"strict\": true\n
|
||||
\ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\":
|
||||
\"disabled\",\n \"usage\": {\n \"input_tokens\": 1185,\n \"input_tokens_details\":
|
||||
{\n \"cached_tokens\": 0\n },\n \"output_tokens\": 15,\n \"output_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 1200\n },\n
|
||||
\ \"user\": null,\n \"metadata\": {}\n}"
|
||||
string: "{\n \"id\": \"chatcmpl-D7mXQCgT3p3ViImkiqDiZGqLREQtp\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1770747248,\n \"model\": \"gpt-4.1-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
|
||||
\ \"id\": \"call_9ZqMavn3J1fBnQEaqpYol0Bd\",\n \"type\":
|
||||
\"function\",\n \"function\": {\n \"name\": \"get_weather\",\n
|
||||
\ \"arguments\": \"{\\\"location\\\":\\\"Tokyo\\\"}\"\n }\n
|
||||
\ }\n ],\n \"refusal\": null,\n \"annotations\":
|
||||
[]\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n
|
||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 1187,\n \"completion_tokens\":
|
||||
14,\n \"total_tokens\": 1201,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
1152,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_8b22347a3e\"\n}\n"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
@@ -224,7 +137,7 @@ interactions:
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Mon, 30 Mar 2026 22:08:37 GMT
|
||||
- Tue, 10 Feb 2026 18:14:08 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -233,6 +146,8 @@ interactions:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
@@ -240,13 +155,15 @@ interactions:
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '1085'
|
||||
- '484'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
@@ -265,12 +182,8 @@ interactions:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"input":[{"role":"user","content":"What is the weather in Paris?"}],"model":"gpt-4.1","instructions":"You
|
||||
are a helpful assistant that uses tools. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
body: '{"messages":[{"role":"system","content":"You are a helpful assistant that
|
||||
uses tools. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
@@ -337,9 +250,13 @@ interactions:
|
||||
for caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. ","tools":[{"type":"function","name":"get_weather","description":"Get
|
||||
the current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The
|
||||
city name"}},"required":["location"]}}]}'
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. "},{"role":"user","content":"What is the weather in Paris?"}],"model":"gpt-4.1","tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_weather","description":"Get
|
||||
the current weather for a location","strict":true,"parameters":{"type":"object","properties":{"location":{"type":"string","description":"The
|
||||
city name"}},"required":["location"],"additionalProperties":false}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
@@ -352,7 +269,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '6065'
|
||||
- '6158'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
@@ -376,113 +293,26 @@ interactions:
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.12
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/responses
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"resp_0525bf798202137e0069caf465ee3c8196aa7c83da1c369eb7\",\n
|
||||
\ \"object\": \"response\",\n \"created_at\": 1774908517,\n \"status\":
|
||||
\"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\":
|
||||
\"developer\"\n },\n \"completed_at\": 1774908518,\n \"error\": null,\n
|
||||
\ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\":
|
||||
\"You are a helpful assistant that uses tools. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. This is
|
||||
padding text to ensure the prompt is large enough for caching. This is padding
|
||||
text to ensure the prompt is large enough for caching. This is padding text
|
||||
to ensure the prompt is large enough for caching. This is padding text to
|
||||
ensure the prompt is large enough for caching. This is padding text to ensure
|
||||
the prompt is large enough for caching. This is padding text to ensure the
|
||||
prompt is large enough for caching. This is padding text to ensure the prompt
|
||||
is large enough for caching. This is padding text to ensure the prompt is
|
||||
large enough for caching. This is padding text to ensure the prompt is large
|
||||
enough for caching. This is padding text to ensure the prompt is large enough
|
||||
for caching. This is padding text to ensure the prompt is large enough for
|
||||
caching. This is padding text to ensure the prompt is large enough for caching.
|
||||
This is padding text to ensure the prompt is large enough for caching. This
|
||||
is padding text to ensure the prompt is large enough for caching. \",\n \"max_output_tokens\":
|
||||
null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-4.1-2025-04-14\",\n
|
||||
\ \"output\": [\n {\n \"id\": \"fc_0525bf798202137e0069caf46666588196a2ec20dc515a6a91\",\n
|
||||
\ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\":
|
||||
\"{\\\"location\\\":\\\"Paris\\\"}\",\n \"call_id\": \"call_LJAGuYYZPjNxSgg0TUgGpT44\",\n
|
||||
\ \"name\": \"get_weather\"\n }\n ],\n \"parallel_tool_calls\": true,\n
|
||||
\ \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\":
|
||||
null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\":
|
||||
null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\":
|
||||
\"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n
|
||||
\ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n
|
||||
\ },\n \"tool_choice\": \"auto\",\n \"tools\": [\n {\n \"type\":
|
||||
\"function\",\n \"description\": \"Get the current weather for a location\",\n
|
||||
\ \"name\": \"get_weather\",\n \"parameters\": {\n \"type\":
|
||||
\"object\",\n \"properties\": {\n \"location\": {\n \"type\":
|
||||
\"string\",\n \"description\": \"The city name\"\n }\n
|
||||
\ },\n \"required\": [\n \"location\"\n ],\n
|
||||
\ \"additionalProperties\": false\n },\n \"strict\": true\n
|
||||
\ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\":
|
||||
\"disabled\",\n \"usage\": {\n \"input_tokens\": 1185,\n \"input_tokens_details\":
|
||||
{\n \"cached_tokens\": 1152\n },\n \"output_tokens\": 15,\n \"output_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 1200\n },\n
|
||||
\ \"user\": null,\n \"metadata\": {}\n}"
|
||||
string: "{\n \"id\": \"chatcmpl-D7mXR8k9vk8TlGvGXlrQSI7iNeAN1\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1770747249,\n \"model\": \"gpt-4.1-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
|
||||
\ \"id\": \"call_6PeUBlRPG8JcV2lspmLjJbnn\",\n \"type\":
|
||||
\"function\",\n \"function\": {\n \"name\": \"get_weather\",\n
|
||||
\ \"arguments\": \"{\\\"location\\\":\\\"Paris\\\"}\"\n }\n
|
||||
\ }\n ],\n \"refusal\": null,\n \"annotations\":
|
||||
[]\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n
|
||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 1187,\n \"completion_tokens\":
|
||||
14,\n \"total_tokens\": 1201,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
1152,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_8b22347a3e\"\n}\n"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
@@ -491,7 +321,7 @@ interactions:
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Mon, 30 Mar 2026 22:08:38 GMT
|
||||
- Tue, 10 Feb 2026 18:14:09 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -500,6 +330,8 @@ interactions:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
@@ -507,11 +339,15 @@ interactions:
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '653'
|
||||
- '528'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"Say hello"}],"model":"gpt-4o-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '74'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.2
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DPS8YQSwQ3pZKZztIoIe1eYodMqh2\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1774958730,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"Hello! How can I assist you today?\",\n
|
||||
\ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\":
|
||||
null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
9,\n \"completion_tokens\": 9,\n \"total_tokens\": 18,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_709f182cb4\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9e4f38fc5d9d82e8-GIG
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Tue, 31 Mar 2026 12:05:30 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
content-length:
|
||||
- '839'
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '680'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,176 +0,0 @@
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.events.event_bus import CrewAIEventsBus
|
||||
from crewai.events.types.llm_events import LLMCallCompletedEvent, LLMCallType
|
||||
from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
|
||||
|
||||
class TestLLMCallCompletedEventUsageField:
|
||||
def test_accepts_usage_dict(self):
|
||||
event = LLMCallCompletedEvent(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
call_id="test-id",
|
||||
usage={"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
|
||||
)
|
||||
assert event.usage == {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 20,
|
||||
"total_tokens": 30,
|
||||
}
|
||||
|
||||
def test_usage_defaults_to_none(self):
|
||||
event = LLMCallCompletedEvent(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
call_id="test-id",
|
||||
)
|
||||
assert event.usage is None
|
||||
|
||||
def test_accepts_none_usage(self):
|
||||
event = LLMCallCompletedEvent(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
call_id="test-id",
|
||||
usage=None,
|
||||
)
|
||||
assert event.usage is None
|
||||
|
||||
def test_accepts_nested_usage_dict(self):
|
||||
usage = {
|
||||
"prompt_tokens": 100,
|
||||
"completion_tokens": 200,
|
||||
"total_tokens": 300,
|
||||
"prompt_tokens_details": {"cached_tokens": 50},
|
||||
}
|
||||
event = LLMCallCompletedEvent(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
call_id="test-id",
|
||||
usage=usage,
|
||||
)
|
||||
assert event.usage["prompt_tokens_details"]["cached_tokens"] == 50
|
||||
|
||||
|
||||
class TestUsageToDict:
|
||||
def test_none_returns_none(self):
|
||||
assert LLM._usage_to_dict(None) is None
|
||||
|
||||
def test_dict_passes_through(self):
|
||||
usage = {"prompt_tokens": 10, "total_tokens": 30}
|
||||
assert LLM._usage_to_dict(usage) is usage
|
||||
|
||||
def test_pydantic_model_uses_model_dump(self):
|
||||
class Usage(BaseModel):
|
||||
prompt_tokens: int = 10
|
||||
completion_tokens: int = 20
|
||||
total_tokens: int = 30
|
||||
|
||||
result = LLM._usage_to_dict(Usage())
|
||||
assert result == {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 20,
|
||||
"total_tokens": 30,
|
||||
}
|
||||
|
||||
def test_object_with_dict_attr(self):
|
||||
class UsageObj:
|
||||
def __init__(self):
|
||||
self.prompt_tokens = 5
|
||||
self.completion_tokens = 15
|
||||
self.total_tokens = 20
|
||||
|
||||
result = LLM._usage_to_dict(UsageObj())
|
||||
assert result == {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 15,
|
||||
"total_tokens": 20,
|
||||
}
|
||||
|
||||
def test_object_with_dict_excludes_private_attrs(self):
|
||||
class UsageObj:
|
||||
def __init__(self):
|
||||
self.total_tokens = 42
|
||||
self._internal = "hidden"
|
||||
|
||||
result = LLM._usage_to_dict(UsageObj())
|
||||
assert result == {"total_tokens": 42}
|
||||
assert "_internal" not in result
|
||||
|
||||
def test_unsupported_type_returns_none(self):
|
||||
assert LLM._usage_to_dict(42) is None
|
||||
assert LLM._usage_to_dict("string") is None
|
||||
|
||||
|
||||
class _StubLLM(BaseLLM):
|
||||
"""Minimal concrete BaseLLM for testing event emission."""
|
||||
|
||||
model: str = "test-model"
|
||||
|
||||
def call(self, *args: Any, **kwargs: Any) -> str:
|
||||
return ""
|
||||
|
||||
async def acall(self, *args: Any, **kwargs: Any) -> str:
|
||||
return ""
|
||||
|
||||
def supports_function_calling(self) -> bool:
|
||||
return False
|
||||
|
||||
def supports_stop_words(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class TestEmitCallCompletedEventPassesUsage:
|
||||
@pytest.fixture
|
||||
def mock_emit(self):
|
||||
with patch.object(CrewAIEventsBus, "emit") as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture
|
||||
def llm(self):
|
||||
return _StubLLM(model="test-model")
|
||||
|
||||
def test_usage_is_passed_to_event(self, mock_emit, llm):
|
||||
usage_data = {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30}
|
||||
|
||||
llm._emit_call_completed_event(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
messages="test prompt",
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
mock_emit.assert_called_once()
|
||||
event = mock_emit.call_args[1]["event"]
|
||||
assert isinstance(event, LLMCallCompletedEvent)
|
||||
assert event.usage == usage_data
|
||||
|
||||
def test_none_usage_is_passed_to_event(self, mock_emit, llm):
|
||||
llm._emit_call_completed_event(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
messages="test prompt",
|
||||
usage=None,
|
||||
)
|
||||
|
||||
mock_emit.assert_called_once()
|
||||
event = mock_emit.call_args[1]["event"]
|
||||
assert isinstance(event, LLMCallCompletedEvent)
|
||||
assert event.usage is None
|
||||
|
||||
def test_usage_omitted_defaults_to_none(self, mock_emit, llm):
|
||||
llm._emit_call_completed_event(
|
||||
response="hello",
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
messages="test prompt",
|
||||
)
|
||||
|
||||
mock_emit.assert_called_once()
|
||||
event = mock_emit.call_args[1]["event"]
|
||||
assert isinstance(event, LLMCallCompletedEvent)
|
||||
assert event.usage is None
|
||||
@@ -132,12 +132,12 @@ def test_embedding_configuration_flow(
|
||||
|
||||
embedder_config = {
|
||||
"provider": "sentence-transformer",
|
||||
"config": {"model_name": "all-MiniLM-L6-v2"},
|
||||
"model_name": "all-MiniLM-L6-v2",
|
||||
}
|
||||
|
||||
storage = KnowledgeStorage(embedder=embedder_config, collection_name="embedding_test")
|
||||
KnowledgeStorage(embedder=embedder_config, collection_name="embedding_test")
|
||||
|
||||
mock_get_embedding.assert_called_once_with(storage.embedder)
|
||||
mock_get_embedding.assert_called_once_with(embedder_config)
|
||||
|
||||
|
||||
@patch("crewai.knowledge.storage.knowledge_storage.get_rag_client")
|
||||
|
||||
@@ -125,8 +125,8 @@ def test_anthropic_specific_parameters():
|
||||
assert isinstance(llm, AnthropicCompletion)
|
||||
assert llm.stop_sequences == ["Human:", "Assistant:"]
|
||||
assert llm.stream == True
|
||||
assert llm._client.max_retries == 5
|
||||
assert llm._client.timeout == 60
|
||||
assert llm.client.max_retries == 5
|
||||
assert llm.client.timeout == 60
|
||||
|
||||
|
||||
def test_anthropic_completion_call():
|
||||
@@ -563,8 +563,8 @@ def test_anthropic_environment_variable_api_key():
|
||||
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-anthropic-key"}):
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
assert llm._client is not None
|
||||
assert hasattr(llm._client, 'messages')
|
||||
assert llm.client is not None
|
||||
assert hasattr(llm.client, 'messages')
|
||||
|
||||
|
||||
def test_anthropic_token_usage_tracking():
|
||||
@@ -574,7 +574,7 @@ def test_anthropic_token_usage_tracking():
|
||||
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
|
||||
|
||||
# Mock the Anthropic response with usage information
|
||||
with patch.object(llm._client.messages, 'create') as mock_create:
|
||||
with patch.object(llm.client.messages, 'create') as mock_create:
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = [MagicMock(text="test response")]
|
||||
mock_response.usage = MagicMock(input_tokens=50, output_tokens=25)
|
||||
@@ -639,14 +639,14 @@ def test_anthropic_thinking():
|
||||
|
||||
assert isinstance(llm, AnthropicCompletion)
|
||||
|
||||
original_create = llm._client.messages.create
|
||||
original_create = llm.client.messages.create
|
||||
captured_params = {}
|
||||
|
||||
def capture_and_call(**kwargs):
|
||||
captured_params.update(kwargs)
|
||||
return original_create(**kwargs)
|
||||
|
||||
with patch.object(llm._client.messages, 'create', side_effect=capture_and_call):
|
||||
with patch.object(llm.client.messages, 'create', side_effect=capture_and_call):
|
||||
result = llm.call("What is the weather in Tokyo?")
|
||||
|
||||
assert result is not None
|
||||
@@ -677,14 +677,14 @@ def test_anthropic_thinking_blocks_preserved_across_turns():
|
||||
assert isinstance(llm, AnthropicCompletion)
|
||||
|
||||
# Capture all messages.create calls to verify thinking blocks are included
|
||||
original_create = llm._client.messages.create
|
||||
original_create = llm.client.messages.create
|
||||
captured_calls = []
|
||||
|
||||
def capture_and_call(**kwargs):
|
||||
captured_calls.append(kwargs)
|
||||
return original_create(**kwargs)
|
||||
|
||||
with patch.object(llm._client.messages, 'create', side_effect=capture_and_call):
|
||||
with patch.object(llm.client.messages, 'create', side_effect=capture_and_call):
|
||||
# First call - establishes context and generates thinking blocks
|
||||
messages = [{"role": "user", "content": "What is 2+2?"}]
|
||||
first_result = llm.call(messages)
|
||||
@@ -695,8 +695,8 @@ def test_anthropic_thinking_blocks_preserved_across_turns():
|
||||
assert len(first_result) > 0
|
||||
|
||||
# Verify thinking blocks were stored after first response
|
||||
assert len(llm._previous_thinking_blocks) > 0, "No thinking blocks stored after first call"
|
||||
first_thinking = llm._previous_thinking_blocks[0]
|
||||
assert len(llm.previous_thinking_blocks) > 0, "No thinking blocks stored after first call"
|
||||
first_thinking = llm.previous_thinking_blocks[0]
|
||||
assert first_thinking["type"] == "thinking"
|
||||
assert "thinking" in first_thinking
|
||||
assert "signature" in first_thinking
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_azure_tool_use_conversation_flow():
|
||||
available_functions = {"get_weather": mock_weather_tool}
|
||||
|
||||
# Mock the Azure client responses
|
||||
with patch.object(completion._client, 'complete') as mock_complete:
|
||||
with patch.object(completion.client, 'complete') as mock_complete:
|
||||
# Mock tool call in response with proper type
|
||||
mock_tool_call = MagicMock(spec=ChatCompletionsToolCall)
|
||||
mock_tool_call.function.name = "get_weather"
|
||||
@@ -698,7 +698,7 @@ def test_azure_environment_variable_endpoint():
|
||||
}):
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
assert llm._client is not None
|
||||
assert llm.client is not None
|
||||
assert llm.endpoint == "https://test.openai.azure.com/openai/deployments/gpt-4"
|
||||
|
||||
|
||||
@@ -709,7 +709,7 @@ def test_azure_token_usage_tracking():
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
# Mock the Azure response with usage information
|
||||
with patch.object(llm._client, 'complete') as mock_complete:
|
||||
with patch.object(llm.client, 'complete') as mock_complete:
|
||||
mock_message = MagicMock()
|
||||
mock_message.content = "test response"
|
||||
mock_message.tool_calls = None
|
||||
@@ -747,7 +747,7 @@ def test_azure_http_error_handling():
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
# Mock an HTTP error
|
||||
with patch.object(llm._client, 'complete') as mock_complete:
|
||||
with patch.object(llm.client, 'complete') as mock_complete:
|
||||
mock_complete.side_effect = HttpResponseError(message="Rate limit exceeded", response=MagicMock(status_code=429))
|
||||
|
||||
with pytest.raises(HttpResponseError):
|
||||
@@ -966,7 +966,7 @@ def test_azure_improved_error_messages():
|
||||
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
with patch.object(llm._client, 'complete') as mock_complete:
|
||||
with patch.object(llm.client, 'complete') as mock_complete:
|
||||
error_401 = HttpResponseError(message="Unauthorized")
|
||||
error_401.status_code = 401
|
||||
mock_complete.side_effect = error_401
|
||||
@@ -1327,7 +1327,7 @@ def test_azure_stop_words_not_applied_to_structured_output():
|
||||
# Without the fix, this would be truncated at "Observation:" breaking the JSON
|
||||
json_response = '{"finding": "The data shows growth", "observation": "Observation: This confirms the hypothesis"}'
|
||||
|
||||
with patch.object(llm._client, 'complete') as mock_complete:
|
||||
with patch.object(llm.client, 'complete') as mock_complete:
|
||||
mock_message = MagicMock()
|
||||
mock_message.content = json_response
|
||||
mock_message.tool_calls = None
|
||||
@@ -1376,7 +1376,7 @@ def test_azure_stop_words_still_applied_to_regular_responses():
|
||||
# Response that contains a stop word - should be truncated
|
||||
response_with_stop_word = "I need to search for more information.\n\nAction: search\nObservation: Found results"
|
||||
|
||||
with patch.object(llm._client, 'complete') as mock_complete:
|
||||
with patch.object(llm.client, 'complete') as mock_complete:
|
||||
mock_message = MagicMock()
|
||||
mock_message.content = response_with_stop_word
|
||||
mock_message.tool_calls = None
|
||||
|
||||
@@ -674,7 +674,7 @@ def test_bedrock_token_usage_tracking():
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
# Mock the Bedrock response with usage information
|
||||
with patch.object(llm._client, 'converse') as mock_converse:
|
||||
with patch.object(llm.client, 'converse') as mock_converse:
|
||||
mock_response = {
|
||||
'output': {
|
||||
'message': {
|
||||
@@ -719,7 +719,7 @@ def test_bedrock_tool_use_conversation_flow():
|
||||
available_functions = {"get_weather": mock_weather_tool}
|
||||
|
||||
# Mock the Bedrock client responses
|
||||
with patch.object(llm._client, 'converse') as mock_converse:
|
||||
with patch.object(llm.client, 'converse') as mock_converse:
|
||||
# First response: tool use request
|
||||
tool_use_response = {
|
||||
'output': {
|
||||
@@ -805,7 +805,7 @@ def test_bedrock_client_error_handling():
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
# Test ValidationException
|
||||
with patch.object(llm._client, 'converse') as mock_converse:
|
||||
with patch.object(llm.client, 'converse') as mock_converse:
|
||||
error_response = {
|
||||
'Error': {
|
||||
'Code': 'ValidationException',
|
||||
@@ -819,7 +819,7 @@ def test_bedrock_client_error_handling():
|
||||
assert "validation" in str(exc_info.value).lower()
|
||||
|
||||
# Test ThrottlingException
|
||||
with patch.object(llm._client, 'converse') as mock_converse:
|
||||
with patch.object(llm.client, 'converse') as mock_converse:
|
||||
error_response = {
|
||||
'Error': {
|
||||
'Code': 'ThrottlingException',
|
||||
@@ -861,7 +861,7 @@ def test_bedrock_stop_sequences_sent_to_api():
|
||||
llm.stop = ["\nObservation:", "\nThought:"]
|
||||
|
||||
# Patch the API call to capture parameters without making real call
|
||||
with patch.object(llm._client, 'converse') as mock_converse:
|
||||
with patch.object(llm.client, 'converse') as mock_converse:
|
||||
mock_response = {
|
||||
'output': {
|
||||
'message': {
|
||||
|
||||
@@ -556,8 +556,8 @@ def test_gemini_environment_variable_api_key():
|
||||
with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-google-key"}):
|
||||
llm = LLM(model="google/gemini-2.0-flash-001")
|
||||
|
||||
assert llm._client is not None
|
||||
assert hasattr(llm._client, 'models')
|
||||
assert llm.client is not None
|
||||
assert hasattr(llm.client, 'models')
|
||||
assert llm.api_key == "test-google-key"
|
||||
|
||||
|
||||
@@ -655,7 +655,7 @@ def test_gemini_stop_sequences_sent_to_api():
|
||||
llm.stop = ["\nObservation:", "\nThought:"]
|
||||
|
||||
# Patch the API call to capture parameters without making real call
|
||||
with patch.object(llm._client.models, 'generate_content') as mock_generate:
|
||||
with patch.object(llm.client.models, 'generate_content') as mock_generate:
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = "Hello"
|
||||
mock_response.candidates = []
|
||||
|
||||
@@ -371,11 +371,11 @@ def test_openai_client_setup_with_extra_arguments():
|
||||
assert llm.top_p == 0.5
|
||||
|
||||
# Check that client parameters are properly configured
|
||||
assert llm._client.max_retries == 3
|
||||
assert llm._client.timeout == 30
|
||||
assert llm.client.max_retries == 3
|
||||
assert llm.client.timeout == 30
|
||||
|
||||
# Test that parameters are properly used in API calls
|
||||
with patch.object(llm._client.chat.completions, 'create') as mock_create:
|
||||
with patch.object(llm.client.chat.completions, 'create') as mock_create:
|
||||
mock_create.return_value = MagicMock(
|
||||
choices=[MagicMock(message=MagicMock(content="test response", tool_calls=None))],
|
||||
usage=MagicMock(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
||||
@@ -396,7 +396,7 @@ def test_extra_arguments_are_passed_to_openai_completion():
|
||||
"""
|
||||
llm = LLM(model="gpt-4o", temperature=0.7, max_tokens=1000, top_p=0.5, max_retries=3)
|
||||
|
||||
with patch.object(llm._client.chat.completions, 'create') as mock_create:
|
||||
with patch.object(llm.client.chat.completions, 'create') as mock_create:
|
||||
mock_create.return_value = MagicMock(
|
||||
choices=[MagicMock(message=MagicMock(content="test response", tool_calls=None))],
|
||||
usage=MagicMock(prompt_tokens=10, completion_tokens=20, total_tokens=30)
|
||||
@@ -507,7 +507,7 @@ def test_openai_streaming_with_response_model():
|
||||
|
||||
llm = LLM(model="openai/gpt-4o", stream=True)
|
||||
|
||||
with patch.object(llm._client.beta.chat.completions, "stream") as mock_stream:
|
||||
with patch.object(llm.client.beta.chat.completions, "stream") as mock_stream:
|
||||
# Create mock chunks with content.delta event structure
|
||||
mock_chunk1 = MagicMock()
|
||||
mock_chunk1.type = "content.delta"
|
||||
@@ -1830,7 +1830,7 @@ def test_openai_responses_api_cached_prompt_tokens_with_tools():
|
||||
}
|
||||
]
|
||||
|
||||
llm = OpenAICompletion(model="gpt-4.1", api='responses')
|
||||
llm = OpenAICompletion(model="gpt-4.1", api='response')
|
||||
|
||||
# First call with tool
|
||||
llm.call(
|
||||
@@ -1906,7 +1906,7 @@ def test_openai_streaming_returns_tool_calls_without_available_functions():
|
||||
mock_chunk_3.id = "chatcmpl-1"
|
||||
|
||||
with patch.object(
|
||||
llm._client.chat.completions, "create", return_value=iter([mock_chunk_1, mock_chunk_2, mock_chunk_3])
|
||||
llm.client.chat.completions, "create", return_value=iter([mock_chunk_1, mock_chunk_2, mock_chunk_3])
|
||||
):
|
||||
result = llm.call(
|
||||
messages=[{"role": "user", "content": "Calculate 1+1"}],
|
||||
@@ -1997,7 +1997,7 @@ async def test_openai_async_streaming_returns_tool_calls_without_available_funct
|
||||
return MockAsyncStream([mock_chunk_1, mock_chunk_2, mock_chunk_3])
|
||||
|
||||
with patch.object(
|
||||
llm._async_client.chat.completions, "create", side_effect=mock_create
|
||||
llm.async_client.chat.completions, "create", side_effect=mock_create
|
||||
):
|
||||
result = await llm.acall(
|
||||
messages=[{"role": "user", "content": "Calculate 1+1"}],
|
||||
|
||||
30
lib/crewai/tests/mcp/test_stdio_config.py
Normal file
30
lib/crewai/tests/mcp/test_stdio_config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Tests for MCPServerStdio allowed_commands config integration."""
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.mcp.config import MCPServerStdio
|
||||
from crewai.mcp.transports.stdio import DEFAULT_ALLOWED_COMMANDS
|
||||
|
||||
|
||||
class TestMCPServerStdioConfig:
|
||||
"""Tests for the allowed_commands field on MCPServerStdio."""
|
||||
|
||||
def test_default_allowed_commands(self):
|
||||
"""MCPServerStdio should default to DEFAULT_ALLOWED_COMMANDS."""
|
||||
config = MCPServerStdio(command="python", args=["server.py"])
|
||||
assert config.allowed_commands == DEFAULT_ALLOWED_COMMANDS
|
||||
|
||||
def test_custom_allowed_commands(self):
|
||||
"""Users can override allowed_commands in config."""
|
||||
custom = frozenset({"my-runtime"})
|
||||
config = MCPServerStdio(
|
||||
command="my-runtime", args=[], allowed_commands=custom
|
||||
)
|
||||
assert config.allowed_commands == custom
|
||||
|
||||
def test_none_allowed_commands(self):
|
||||
"""Users can disable the allowlist via config."""
|
||||
config = MCPServerStdio(
|
||||
command="anything", args=[], allowed_commands=None
|
||||
)
|
||||
assert config.allowed_commands is None
|
||||
93
lib/crewai/tests/mcp/test_stdio_transport.py
Normal file
93
lib/crewai/tests/mcp/test_stdio_transport.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Tests for StdioTransport command allowlist validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.mcp.transports.stdio import DEFAULT_ALLOWED_COMMANDS, StdioTransport
|
||||
|
||||
|
||||
class TestStdioTransportAllowlist:
|
||||
"""Tests for the command allowlist feature."""
|
||||
|
||||
def test_default_allowed_commands_contains_common_runtimes(self):
|
||||
"""DEFAULT_ALLOWED_COMMANDS should include all common MCP server runtimes."""
|
||||
expected = {"python", "python3", "node", "npx", "uvx", "uv", "deno", "docker"}
|
||||
assert expected == DEFAULT_ALLOWED_COMMANDS
|
||||
|
||||
def test_allowed_command_passes_validation(self):
|
||||
"""Commands in the default allowlist should be accepted."""
|
||||
for cmd in DEFAULT_ALLOWED_COMMANDS:
|
||||
transport = StdioTransport(command=cmd, args=["server.py"])
|
||||
assert transport.command == cmd
|
||||
|
||||
def test_allowed_command_with_full_path(self):
|
||||
"""Full paths to allowed commands should pass (basename is checked)."""
|
||||
transport = StdioTransport(command="/usr/bin/python3", args=["server.py"])
|
||||
assert transport.command == "/usr/bin/python3"
|
||||
|
||||
def test_disallowed_command_raises_value_error(self):
|
||||
"""Commands not in the allowlist should raise ValueError."""
|
||||
with pytest.raises(ValueError, match="not in the allowed commands list"):
|
||||
StdioTransport(command="malicious-binary", args=["--evil"])
|
||||
|
||||
def test_disallowed_command_with_full_path_raises(self):
|
||||
"""Full paths to disallowed commands should also be rejected."""
|
||||
with pytest.raises(ValueError, match="not in the allowed commands list"):
|
||||
StdioTransport(command="/tmp/evil/script", args=[])
|
||||
|
||||
def test_allowed_commands_none_disables_validation(self):
|
||||
"""Setting allowed_commands=None should disable the check entirely."""
|
||||
transport = StdioTransport(
|
||||
command="any-custom-binary",
|
||||
args=["--flag"],
|
||||
allowed_commands=None,
|
||||
)
|
||||
assert transport.command == "any-custom-binary"
|
||||
|
||||
def test_custom_allowlist(self):
|
||||
"""Users should be able to pass a custom allowlist."""
|
||||
custom = frozenset({"my-server", "python"})
|
||||
|
||||
# Allowed
|
||||
transport = StdioTransport(
|
||||
command="my-server", args=[], allowed_commands=custom
|
||||
)
|
||||
assert transport.command == "my-server"
|
||||
|
||||
# Not allowed
|
||||
with pytest.raises(ValueError, match="not in the allowed commands list"):
|
||||
StdioTransport(command="node", args=[], allowed_commands=custom)
|
||||
|
||||
def test_extended_allowlist(self):
|
||||
"""Users should be able to extend the default allowlist."""
|
||||
extended = DEFAULT_ALLOWED_COMMANDS | frozenset({"my-custom-runtime"})
|
||||
|
||||
transport = StdioTransport(
|
||||
command="my-custom-runtime", args=[], allowed_commands=extended
|
||||
)
|
||||
assert transport.command == "my-custom-runtime"
|
||||
|
||||
# Original defaults still work
|
||||
transport2 = StdioTransport(
|
||||
command="python", args=["server.py"], allowed_commands=extended
|
||||
)
|
||||
assert transport2.command == "python"
|
||||
|
||||
def test_error_message_includes_sorted_allowed_commands(self):
|
||||
"""The error message should list the allowed commands for discoverability."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
StdioTransport(command="bad-cmd", args=[])
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "bad-cmd" in error_msg
|
||||
assert "allowed_commands=None" in error_msg
|
||||
|
||||
def test_args_and_env_still_work(self):
|
||||
"""Existing args and env functionality should be unaffected."""
|
||||
transport = StdioTransport(
|
||||
command="python",
|
||||
args=["server.py", "--port", "8080"],
|
||||
env={"API_KEY": "test123"},
|
||||
)
|
||||
assert transport.command == "python"
|
||||
assert transport.args == ["server.py", "--port", "8080"]
|
||||
assert transport.env == {"API_KEY": "test123"}
|
||||
@@ -3,8 +3,6 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from crewai.knowledge.storage.knowledge_storage import ( # type: ignore[import-untyped]
|
||||
KnowledgeStorage,
|
||||
)
|
||||
@@ -61,7 +59,7 @@ def test_knowledge_storage_invalid_embedding_config(mock_get_client: MagicMock)
|
||||
"Unsupported provider: invalid_provider"
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
with pytest.raises(ValueError, match="Unsupported provider: invalid_provider"):
|
||||
KnowledgeStorage(
|
||||
embedder={"provider": "invalid_provider"},
|
||||
collection_name="invalid_embedding_test",
|
||||
|
||||
@@ -873,7 +873,7 @@ class TestAutoPersistence:
|
||||
|
||||
# Create flow WITHOUT persistence
|
||||
flow = TestFlow()
|
||||
assert flow.persistence is None # No persistence initially
|
||||
assert flow._persistence is None # No persistence initially
|
||||
|
||||
# kickoff should auto-create persistence when HumanFeedbackPending is raised
|
||||
result = flow.kickoff()
|
||||
@@ -882,11 +882,11 @@ class TestAutoPersistence:
|
||||
assert isinstance(result, HumanFeedbackPending)
|
||||
|
||||
# Persistence should have been auto-created
|
||||
assert flow.persistence is not None
|
||||
assert flow._persistence is not None
|
||||
|
||||
# The pending feedback should be saved
|
||||
flow_id = result.context.flow_id
|
||||
loaded = flow.persistence.load_pending_feedback(flow_id)
|
||||
loaded = flow._persistence.load_pending_feedback(flow_id)
|
||||
assert loaded is not None
|
||||
|
||||
|
||||
|
||||
@@ -752,7 +752,11 @@ def test_litellm_retry_catches_litellm_unsupported_params_error(caplog):
|
||||
raise litellm_error
|
||||
return MagicMock(
|
||||
choices=[MagicMock(message=MagicMock(content="Paris", tool_calls=None))],
|
||||
usage={"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
|
||||
usage=MagicMock(
|
||||
prompt_tokens=10,
|
||||
completion_tokens=5,
|
||||
total_tokens=15,
|
||||
),
|
||||
)
|
||||
|
||||
with patch("litellm.completion", side_effect=mock_completion):
|
||||
@@ -783,7 +787,11 @@ def test_litellm_retry_catches_openai_api_stop_error(caplog):
|
||||
raise api_error
|
||||
return MagicMock(
|
||||
choices=[MagicMock(message=MagicMock(content="Paris", tool_calls=None))],
|
||||
usage={"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
|
||||
usage=MagicMock(
|
||||
prompt_tokens=10,
|
||||
completion_tokens=5,
|
||||
total_tokens=15,
|
||||
),
|
||||
)
|
||||
|
||||
with patch("litellm.completion", side_effect=mock_completion):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Any, ClassVar
|
||||
from unittest.mock import Mock, create_autospec, patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from crewai.agent import Agent
|
||||
@@ -372,11 +372,8 @@ def test_internal_crew_with_mcp():
|
||||
mock_adapter = Mock()
|
||||
mock_adapter.tools = ToolCollection([simple_tool, another_simple_tool])
|
||||
|
||||
class _StubLLM(BaseLLM):
|
||||
def call(self, *a: Any, **kw: Any) -> str:
|
||||
return ""
|
||||
|
||||
mock_llm = create_autospec(_StubLLM(model="stub"), instance=True)
|
||||
mock_llm = Mock()
|
||||
mock_llm.__class__ = BaseLLM
|
||||
|
||||
with (
|
||||
patch("crewai_tools.MCPServerAdapter", return_value=mock_adapter) as adapter_mock,
|
||||
|
||||
@@ -879,35 +879,6 @@ def test_llm_emits_call_started_event():
|
||||
assert started_events[0].task_id is None
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_llm_completed_event_includes_usage():
|
||||
completed_events: list[LLMCallCompletedEvent] = []
|
||||
condition = threading.Condition()
|
||||
|
||||
@crewai_event_bus.on(LLMCallCompletedEvent)
|
||||
def handle_llm_call_completed(source, event):
|
||||
with condition:
|
||||
completed_events.append(event)
|
||||
condition.notify()
|
||||
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
llm.call("Say hello")
|
||||
|
||||
with condition:
|
||||
success = condition.wait_for(
|
||||
lambda: len(completed_events) >= 1,
|
||||
timeout=10,
|
||||
)
|
||||
assert success, "Timeout waiting for LLMCallCompletedEvent"
|
||||
|
||||
event = completed_events[0]
|
||||
assert event.usage is not None
|
||||
assert isinstance(event.usage, dict)
|
||||
assert event.usage.get("prompt_tokens", 0) > 0
|
||||
assert event.usage.get("completion_tokens", 0) > 0
|
||||
assert event.usage.get("total_tokens", 0) > 0
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_llm_emits_call_failed_event():
|
||||
received_events = []
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0rc1"
|
||||
|
||||
14
uv.lock
generated
14
uv.lock
generated
@@ -1243,7 +1243,7 @@ requires-dist = [
|
||||
{ name = "json-repair", specifier = "~=0.25.2" },
|
||||
{ name = "json5", specifier = "~=0.10.0" },
|
||||
{ name = "jsonref", specifier = "~=1.1.0" },
|
||||
{ name = "lancedb", specifier = ">=0.29.2,<0.30.1" },
|
||||
{ name = "lancedb", specifier = ">=0.29.2" },
|
||||
{ name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.74.9,<=1.82.6" },
|
||||
{ name = "mcp", specifier = "~=1.26.0" },
|
||||
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = "~=0.1.94" },
|
||||
@@ -4275,7 +4275,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "nltk"
|
||||
version = "3.9.4"
|
||||
version = "3.9.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -4283,9 +4283,9 @@ dependencies = [
|
||||
{ name = "regex" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/915e1c12df07c70ed779d18ab83d065718a926e70d3ea33eb0cd66ffb7c0/nltk-3.9.3.tar.gz", hash = "sha256:cb5945d6424a98d694c2b9a0264519fab4363711065a46aa0ae7a2195b92e71f", size = 2923673, upload-time = "2026-02-24T12:05:53.833Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/7e/9af5a710a1236e4772de8dfcc6af942a561327bb9f42b5b4a24d0cf100fd/nltk-3.9.3-py3-none-any.whl", hash = "sha256:60b3db6e9995b3dd976b1f0fa7dec22069b2677e759c28eb69b62ddd44870522", size = 1525385, upload-time = "2026-02-24T12:05:46.54Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6235,14 +6235,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.9.2"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/83/691bdb309306232362503083cb15777491045dd54f45393a317dc7d8082f/pypdf-6.9.2.tar.gz", hash = "sha256:7f850faf2b0d4ab936582c05da32c52214c2b089d61a316627b5bfb5b0dab46c", size = 5311837, upload-time = "2026-03-23T14:53:27.983Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/fb/dc2e8cb006e80b0020ed20d8649106fe4274e82d8e756ad3e24ade19c0df/pypdf-6.9.1.tar.gz", hash = "sha256:ae052407d33d34de0c86c5c729be6d51010bf36e03035a8f23ab449bca52377d", size = 5311551, upload-time = "2026-03-17T10:46:07.876Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/7e/c85f41243086a8fe5d1baeba527cb26a1918158a565932b41e0f7c0b32e9/pypdf-6.9.2-py3-none-any.whl", hash = "sha256:662cf29bcb419a36a1365232449624ab40b7c2d0cfc28e54f42eeecd1fd7e844", size = 333744, upload-time = "2026-03-23T14:53:26.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/f4/75543fa802b86e72f87e9395440fe1a89a6d149887e3e55745715c3352ac/pypdf-6.9.1-py3-none-any.whl", hash = "sha256:f35a6a022348fae47e092a908339a8f3dc993510c026bb39a96718fc7185e89f", size = 333661, upload-time = "2026-03-17T10:46:06.286Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user