mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-02 00:58:16 +00:00
Compare commits
57 Commits
docs/amp-t
...
docs/conve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c75bc3d6 | ||
|
|
5ae24c2ff6 | ||
|
|
e14f84c724 | ||
|
|
7eb0b5c5e7 | ||
|
|
42c341b590 | ||
|
|
e4c86c05b0 | ||
|
|
c14abf1758 | ||
|
|
f10d320ddb | ||
|
|
258f31d44c | ||
|
|
68720fd4e5 | ||
|
|
3132910084 | ||
|
|
c8f3a96779 | ||
|
|
18ada25f01 | ||
|
|
146da8d73a | ||
|
|
98c6109214 | ||
|
|
54a9174c12 | ||
|
|
c26ae969b3 | ||
|
|
205555b786 | ||
|
|
d6714a0e60 | ||
|
|
107bc7f7be | ||
|
|
b1f49b1356 | ||
|
|
accae5ca43 | ||
|
|
68e943be68 | ||
|
|
3283a00e31 | ||
|
|
dfc0f9a317 | ||
|
|
ef79456968 | ||
|
|
6c7ea422e7 | ||
|
|
bb9bcd6823 | ||
|
|
ac14b9127e | ||
|
|
98b7626784 | ||
|
|
e21c506214 | ||
|
|
9fe0c15549 | ||
|
|
78d8ddb649 | ||
|
|
1b2062009a | ||
|
|
886aa4ba8f | ||
|
|
5bec000b21 | ||
|
|
2965384907 | ||
|
|
032ef06ef6 | ||
|
|
0ce9567cfc | ||
|
|
d7252bfee7 | ||
|
|
10fc3796bb | ||
|
|
52249683a7 | ||
|
|
6193e082e1 | ||
|
|
33f33c6fcc | ||
|
|
74976b157d | ||
|
|
bd03f6cf64 | ||
|
|
a91cd1a7d7 | ||
|
|
66dee3195f | ||
|
|
034f576dc0 | ||
|
|
918654318b | ||
|
|
371e6cfd11 | ||
|
|
6fd70ce6e5 | ||
|
|
c183b77991 | ||
|
|
b5a0d6e709 | ||
|
|
454156cff9 | ||
|
|
d86707da3d | ||
|
|
1956471086 |
50
.github/security.md
vendored
50
.github/security.md
vendored
@@ -1,50 +1,12 @@
|
||||
## CrewAI Security Policy
|
||||
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the CrewAI ecosystem. This policy explains how to report potential vulnerabilities and what you can expect from us when you do.
|
||||
|
||||
### Scope
|
||||
|
||||
We welcome reports for vulnerabilities that could impact:
|
||||
|
||||
- CrewAI-maintained source code and repositories
|
||||
- CrewAI-operated infrastructure and services
|
||||
- Official CrewAI releases, packages, and distributions
|
||||
|
||||
Issues affecting clearly unaffiliated third-party services or user-generated content are out of scope, unless you can demonstrate a direct impact on CrewAI systems or customers.
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the
|
||||
CrewAI ecosystem.
|
||||
|
||||
### How to Report
|
||||
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests, or social media.
|
||||
- Email detailed reports to **security@crewai.com** with the subject line `Security Report`.
|
||||
- If you need to share large files or sensitive artifacts, mention it in your email and we will coordinate a secure transfer method.
|
||||
Please submit reports to **crewai-vdp-ess@submit.bugcrowd.com**
|
||||
|
||||
### What to Include
|
||||
|
||||
Providing comprehensive information enables us to validate the issue quickly:
|
||||
|
||||
- **Vulnerability overview** — a concise description and classification (e.g., RCE, privilege escalation)
|
||||
- **Affected components** — repository, branch, tag, or deployed service along with relevant file paths or endpoints
|
||||
- **Reproduction steps** — detailed, step-by-step instructions; include logs, screenshots, or screen recordings when helpful
|
||||
- **Proof-of-concept** — exploit details or code that demonstrates the impact (if available)
|
||||
- **Impact analysis** — severity assessment, potential exploitation scenarios, and any prerequisites or special configurations
|
||||
|
||||
### Our Commitment
|
||||
|
||||
- **Acknowledgement:** We aim to acknowledge your report within two business days.
|
||||
- **Communication:** We will keep you informed about triage results, remediation progress, and planned release timelines.
|
||||
- **Resolution:** Confirmed vulnerabilities will be prioritized based on severity and fixed as quickly as possible.
|
||||
- **Recognition:** We currently do not run a bug bounty program; any rewards or recognition are issued at CrewAI's discretion.
|
||||
|
||||
### Coordinated Disclosure
|
||||
|
||||
We ask that you allow us a reasonable window to investigate and remediate confirmed issues before any public disclosure. We will coordinate publication timelines with you whenever possible.
|
||||
|
||||
### Safe Harbor
|
||||
|
||||
We will not pursue or support legal action against individuals who, in good faith:
|
||||
|
||||
- Follow this policy and refrain from violating any applicable laws
|
||||
- Avoid privacy violations, data destruction, or service disruption
|
||||
- Limit testing to systems in scope and respect rate limits and terms of service
|
||||
|
||||
If you are unsure whether your testing is covered, please contact us at **security@crewai.com** before proceeding.
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests,
|
||||
or social media
|
||||
- Reports submitted via channels other than this Bugcrowd submission email will not be reviewed and will be dismissed
|
||||
|
||||
15
.github/workflows/docs-broken-links.yml
vendored
15
.github/workflows/docs-broken-links.yml
vendored
@@ -4,13 +4,13 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "docs.json"
|
||||
- "docs/docs.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "docs.json"
|
||||
- "docs/docs.json"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -23,13 +23,14 @@ jobs:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
node-version: "22"
|
||||
|
||||
- name: Install libsecret for Mintlify CLI
|
||||
run: sudo apt-get update && sudo apt-get install -y libsecret-1-0
|
||||
|
||||
- name: Install Mintlify CLI
|
||||
run: npm i -g mintlify
|
||||
run: npm install -g mint@latest
|
||||
|
||||
- name: Run broken link checker
|
||||
run: |
|
||||
# Auto-answer the prompt with yes command
|
||||
yes "" | mintlify broken-links || test $? -eq 141
|
||||
working-directory: ./docs
|
||||
run: mint broken-links
|
||||
|
||||
@@ -4,6 +4,275 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="1 أبريل 2026">
|
||||
## v1.13.0a6
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a6)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الوثائق
|
||||
- إصلاح مستويات أذونات RBAC لتتوافق مع خيارات واجهة المستخدم الفعلية (#5210)
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0a5 (#5200)
|
||||
|
||||
### الأداء
|
||||
- تقليل عبء العمل على الإطار من خلال تنفيذ حافلة أحداث كسولة وتجاوز التتبع عند تعطيله (#5187)
|
||||
|
||||
## المساهمون
|
||||
|
||||
@alex-clawd, @joaomdmoura, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0a2
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 مارس 2026">
|
||||
## v1.13.0a2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- تحديث تلقائي لمستودع اختبار النشر أثناء الإصدار
|
||||
- تحسين مرونة إصدار المؤسسات وتجربة المستخدم
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار للإصدار v1.13.0a1
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 مارس 2026">
|
||||
## v1.13.0a1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح الروابط المعطلة في سير العمل الوثائقي عن طريق تثبيت Node على LTS 22
|
||||
- مسح ذاكرة التخزين المؤقت لـ uv للحزم المنشورة حديثًا في الإصدار المؤسسي
|
||||
|
||||
### الوثائق
|
||||
- إضافة مصفوفة شاملة لأذونات RBAC ودليل النشر
|
||||
- تحديث سجل التغييرات والإصدار للإصدار v1.12.2
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة مرحلة إصدار المؤسسات إلى إصدار أدوات المطورين
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- الحفاظ على قيمة إرجاع الطريقة كإخراج تدفق لـ @human_feedback مع emit
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.1
|
||||
- مراجعة سياسة الأمان وتعليمات الإبلاغ
|
||||
|
||||
## المساهمون
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة request_id إلى HumanFeedbackRequestedEvent
|
||||
- إضافة Qdrant Edge كخلفية تخزين لنظام الذاكرة
|
||||
- إضافة أمر docs-check لتحليل التغييرات وتوليد الوثائق مع الترجمات
|
||||
- إضافة دعم اللغة العربية إلى سجل التغييرات وأدوات الإصدار
|
||||
- إضافة ترجمة باللغة العربية الفصحى لجميع الوثائق
|
||||
- إضافة أمر تسجيل الخروج في واجهة سطر الأوامر
|
||||
- إضافة مهارات الوكيل
|
||||
- تنفيذ root_scope تلقائيًا لعزل الذاكرة الهيكلية
|
||||
- تنفيذ مزودين متوافقين مع OpenAI (OpenRouter، DeepSeek، Ollama، vLLM، Cerebras، Dashscope)
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح بيانات اعتماد غير صحيحة لدفع دفعات التتبع (404)
|
||||
- حل العديد من الأخطاء في نظام تدفق HITL
|
||||
- إصلاح حفظ ذاكرة الوكيل
|
||||
- حل جميع أخطاء mypy الصارمة عبر حزمة crewai
|
||||
- إصلاح استخدام __router_paths__ لطرق المستمع + الموجه في FlowMeta
|
||||
- إصلاح خطأ القيمة عند عدم دعم الملفات
|
||||
- تصحيح صياغة الحجر الصحي لـ litellm في الوثائق
|
||||
- إصلاح جميع أخطاء mypy في crewai-files وإضافة جميع الحزم إلى فحوصات النوع في CI
|
||||
- تثبيت الحد الأعلى لـ litellm على آخر إصدار تم اختباره (1.82.6)
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.0
|
||||
- إضافة CONTRIBUTING.md
|
||||
- إضافة دليل لاستخدام CrewAI بدون LiteLLM
|
||||
|
||||
## المساهمون
|
||||
|
||||
@akaKuruma، @alex-clawd، @greysonlalonde، @iris-clawd، @joaomdmoura، @lorenzejay، @lucasgomide، @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.0
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة واجهة تخزين Qdrant Edge لنظام الذاكرة
|
||||
- إضافة أمر docs-check لتحليل التغييرات وتوليد الوثائق مع الترجمات
|
||||
- إضافة دعم اللغة العربية لسجل التغييرات وأدوات الإصدار
|
||||
- إضافة ترجمة اللغة العربية الفصحى لجميع الوثائق
|
||||
- إضافة أمر تسجيل الخروج في واجهة سطر الأوامر
|
||||
- تنفيذ مهارات الوكيل
|
||||
- تنفيذ نطاق الجذر التلقائي لعزل الذاكرة الهرمية
|
||||
- تنفيذ موفري خدمات متوافقين مع OpenAI (OpenRouter، DeepSeek، Ollama، vLLM، Cerebras، Dashscope)
|
||||
|
||||
### إصلاح الأخطاء
|
||||
- إصلاح بيانات الاعتماد السيئة لدفع دفعات التتبع (404)
|
||||
- حل العديد من الأخطاء في نظام تدفق HITL
|
||||
- حل أخطاء mypy في crewai-files وإضافة جميع الحزم إلى فحوصات نوع CI
|
||||
- حل جميع أخطاء mypy الصارمة عبر حزمة crewai-tools
|
||||
- حل جميع أخطاء mypy عبر حزمة crewai
|
||||
- إصلاح حفظ الذاكرة في الوكيل
|
||||
- إصلاح استخدام __router_paths__ لطرق المستمع + الموجه في FlowMeta
|
||||
- رفع خطأ القيمة عند عدم دعم الملفات
|
||||
- تصحيح صياغة الحجر الصحي لـ litellm في الوثائق
|
||||
- استخدام فحص None بدلاً من isinstance للذاكرة في تعلم التغذية الراجعة البشرية
|
||||
- تثبيت الحد الأعلى لـ litellm على آخر إصدار تم اختباره (1.82.6)
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.0
|
||||
- إضافة CONTRIBUTING.md
|
||||
- إضافة دليل لاستخدام CrewAI بدون LiteLLM
|
||||
|
||||
### إعادة الهيكلة
|
||||
- إعادة هيكلة لتجنب تكرار تنفيذ المهام المتزامنة / غير المتزامنة وبدء التشغيل في الوكيل
|
||||
- تبسيط الأنابيب الداخلية من litellm (عد الرموز، ردود النداء، اكتشاف الميزات، الأخطاء)
|
||||
|
||||
## المساهمون
|
||||
|
||||
@akaKuruma، @alex-clawd، @greysonlalonde، @iris-clawd، @joaomdmoura، @lorenzejay، @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="26 مارس 2026">
|
||||
## v1.12.0a3
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0a3)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح بيانات الاعتماد الخاطئة لدفع دفعات التتبع (404)
|
||||
- حل العديد من الأخطاء في نظام تدفق HITL
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.0a2
|
||||
|
||||
## المساهمون
|
||||
|
||||
@akaKuruma, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.0a2
|
||||
|
||||
|
||||
147
docs/ar/concepts/agent-capabilities.mdx
Normal file
147
docs/ar/concepts/agent-capabilities.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
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,15 +1,217 @@
|
||||
---
|
||||
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` | لا | قائمة أدوات معتمدة مسبقًا مفصولة بمسافات. تجريبي. |
|
||||
|
||||
---
|
||||
|
||||
## هيكل المجلد
|
||||
|
||||
@@ -21,79 +223,25 @@ my-skill/
|
||||
└── assets/ # اختياري — ملفات ثابتة (إعدادات، بيانات)
|
||||
```
|
||||
|
||||
يجب أن يتطابق اسم المجلد مع حقل `name` في `SKILL.md`.
|
||||
يجب أن يتطابق اسم المجلد مع حقل `name` في `SKILL.md`. مجلدات `scripts/` و `references/` و `assets/` متاحة في مسار المهارة `path` للوكلاء الذين يحتاجون للإشارة إلى الملفات مباشرة.
|
||||
|
||||
## تنسيق 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",
|
||||
@@ -102,13 +250,57 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## كيف يتم تحميل المهارات
|
||||
|
||||
يتم تحميل المهارات تدريجيًا — فقط البيانات المطلوبة في كل مرحلة يتم قراءتها:
|
||||
تستخدم المهارات **الكشف التدريجي** — تحمّل فقط ما هو مطلوب في كل مرحلة:
|
||||
|
||||
| المرحلة | ما يتم تحميله | متى |
|
||||
| :--------------- | :------------------------------------------------ | :----------------- |
|
||||
| الاكتشاف | الاسم، الوصف، حقول البيانات الوصفية | `discover_skills()` |
|
||||
| التفعيل | نص محتوى SKILL.md الكامل | `activate_skill()` |
|
||||
| المرحلة | ما يتم تحميله | متى |
|
||||
| :--------- | :------------------------------------ | :------------------ |
|
||||
| الاكتشاف | الاسم، الوصف، حقول البيانات الوصفية | `discover_skills()` |
|
||||
| التفعيل | نص محتوى SKILL.md الكامل | `activate_skill()` |
|
||||
|
||||
أثناء التنفيذ العادي للوكيل، يتم اكتشاف المهارات وتفعيلها تلقائيًا. مجلدات `scripts/` و `references/` و `assets/` متاحة في مسار المهارة `path` للوكلاء الذين يحتاجون للإشارة إلى الملفات مباشرة.
|
||||
أثناء التنفيذ العادي للوكيل (تمرير مسارات المجلدات عبر `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>
|
||||
|
||||
@@ -10,6 +10,10 @@ 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 هي مهارة أو وظيفة يمكن للوكلاء استخدامها لأداء إجراءات مختلفة.
|
||||
|
||||
@@ -7,11 +7,13 @@ mode: "wide"
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
يتيح RBAC في CrewAI AMP إدارة وصول آمنة وقابلة للتوسع من خلال مزيج من الأدوار على مستوى المؤسسة وعناصر التحكم في الرؤية على مستوى الأتمتة.
|
||||
يتيح RBAC في CrewAI AMP إدارة وصول آمنة وقابلة للتوسع من خلال طبقتين:
|
||||
|
||||
1. **صلاحيات الميزات** — تتحكم في ما يمكن لكل دور القيام به عبر المنصة (إدارة، قراءة، أو بدون وصول)
|
||||
2. **صلاحيات على مستوى الكيان** — وصول دقيق للأتمتات الفردية ومتغيرات البيئة واتصالات LLM ومستودعات Git
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/users_and_roles.png" alt="نظرة عامة على RBAC في CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
## المستخدمون والأدوار
|
||||
@@ -39,6 +41,13 @@ mode: "wide"
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### الأدوار المحددة مسبقاً
|
||||
|
||||
| الدور | الوصف |
|
||||
| :---------- | :-------------------------------------------------------------------- |
|
||||
| **Owner** | وصول كامل لجميع الميزات والإعدادات. لا يمكن تقييده. |
|
||||
| **Member** | وصول للقراءة لمعظم الميزات، وصول إدارة لمتغيرات البيئة واتصالات LLM ومشاريع Studio. لا يمكنه تعديل إعدادات المؤسسة أو الإعدادات الافتراضية. |
|
||||
|
||||
### ملخص التهيئة
|
||||
|
||||
| المجال | مكان التهيئة | الخيارات |
|
||||
@@ -46,23 +55,80 @@ mode: "wide"
|
||||
| المستخدمون والأدوار | Settings → Roles | محددة مسبقاً: Owner، Member؛ أدوار مخصصة |
|
||||
| رؤية الأتمتة | Automation → Settings → Visibility | خاص؛ قائمة بيضاء للمستخدمين/الأدوار |
|
||||
|
||||
## التحكم في الوصول على مستوى الأتمتة
|
||||
---
|
||||
|
||||
بالإضافة إلى الأدوار على مستوى المؤسسة، تدعم أتمتات CrewAI إعدادات رؤية دقيقة تتيح لك تقييد الوصول إلى أتمتات محددة حسب المستخدم أو الدور.
|
||||
## مصفوفة صلاحيات الميزات
|
||||
|
||||
هذا مفيد لـ:
|
||||
لكل دور مستوى صلاحية لكل منطقة ميزة. المستويات الثلاثة هي:
|
||||
|
||||
- **إدارة (Manage)** — وصول كامل للقراءة/الكتابة (إنشاء، تعديل، حذف)
|
||||
- **قراءة (Read)** — وصول للعرض فقط
|
||||
- **بدون وصول (No access)** — الميزة مخفية/غير قابلة للوصول
|
||||
|
||||
| الميزة | Owner | Member (افتراضي) | المستويات المتاحة | الوصف |
|
||||
| :------------------------ | :------ | :--------------- | :--------------------------------- | :-------------------------------------------------------------- |
|
||||
| `usage_dashboards` | Manage | Read | Manage / Read / No access | عرض مقاييس وتحليلات الاستخدام |
|
||||
| `crews_dashboards` | Manage | Read | Manage / Read / No access | عرض لوحات النشر والوصول إلى تفاصيل الأتمتة |
|
||||
| `invitations` | Manage | Read | Manage / Read / No access | دعوة أعضاء جدد إلى المؤسسة |
|
||||
| `training_ui` | Manage | Read | Manage / Read / No access | الوصول إلى واجهات التدريب/الضبط الدقيق |
|
||||
| `tools` | Manage | Read | Manage / Read / No access | إنشاء وإدارة الأدوات |
|
||||
| `agents` | Manage | Read | Manage / Read / No access | إنشاء وإدارة الوكلاء |
|
||||
| `environment_variables` | Manage | Manage | Manage / No access | إنشاء وإدارة متغيرات البيئة |
|
||||
| `llm_connections` | Manage | Manage | Manage / No access | تهيئة اتصالات مزودي LLM |
|
||||
| `default_settings` | Manage | No access | Manage / No access | تعديل الإعدادات الافتراضية على مستوى المؤسسة |
|
||||
| `organization_settings` | Manage | No access | Manage / No access | إدارة الفوترة والخطط وتهيئة المؤسسة |
|
||||
| `studio_projects` | Manage | Manage | Manage / No access | إنشاء وتعديل المشاريع في Studio |
|
||||
|
||||
<Tip>
|
||||
عند إنشاء دور مخصص، يمكن ضبط معظم الميزات على **Manage** أو **Read** أو **No access**. ومع ذلك، فإن `environment_variables` و`llm_connections` و`default_settings` و`organization_settings` و`studio_projects` تدعم فقط **Manage** أو **No access** — لا يوجد خيار للقراءة فقط لهذه الميزات.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## النشر من GitHub أو Zip
|
||||
|
||||
من أكثر أسئلة RBAC شيوعاً: _"ما الصلاحيات التي يحتاجها عضو الفريق للنشر؟"_
|
||||
|
||||
### النشر من GitHub
|
||||
|
||||
لنشر أتمتة من مستودع GitHub، يحتاج المستخدم إلى:
|
||||
|
||||
1. **`crews_dashboards`**: على الأقل `Read` — مطلوب للوصول إلى لوحة الأتمتات حيث يتم إنشاء عمليات النشر
|
||||
2. **الوصول إلى مستودع Git** (إذا كان RBAC على مستوى الكيان لمستودعات Git مفعلاً): يجب منح دور المستخدم الوصول إلى مستودع Git المحدد عبر صلاحيات مستوى الكيان
|
||||
3. **`studio_projects`: `Manage`** — إذا كان يبني الطاقم في Studio قبل النشر
|
||||
|
||||
### النشر من Zip
|
||||
|
||||
لنشر أتمتة من ملف Zip، يحتاج المستخدم إلى:
|
||||
|
||||
1. **`crews_dashboards`**: على الأقل `Read` — مطلوب للوصول إلى لوحة الأتمتات
|
||||
2. **تفعيل نشر Zip**: يجب ألا تكون المؤسسة قد عطلت نشر Zip في إعدادات المؤسسة
|
||||
|
||||
### مرجع سريع: الحد الأدنى من الصلاحيات للنشر
|
||||
|
||||
| الإجراء | صلاحيات الميزات المطلوبة | متطلبات إضافية |
|
||||
| :------------------- | :----------------------------------- | :----------------------------------------------- |
|
||||
| النشر من GitHub | `crews_dashboards: Read` | وصول كيان مستودع Git (إذا كان Git RBAC مفعلاً) |
|
||||
| النشر من Zip | `crews_dashboards: Read` | يجب تفعيل نشر Zip على مستوى المؤسسة |
|
||||
| البناء في Studio | `studio_projects: Manage` | — |
|
||||
| تهيئة مفاتيح LLM | `llm_connections: Manage` | — |
|
||||
| ضبط متغيرات البيئة | `environment_variables: Manage` | وصول مستوى الكيان (إذا كان RBAC الكيان مفعلاً) |
|
||||
|
||||
---
|
||||
|
||||
## التحكم في الوصول على مستوى الأتمتة (صلاحيات الكيان)
|
||||
|
||||
بالإضافة إلى الأدوار على مستوى المؤسسة، يدعم CrewAI صلاحيات دقيقة على مستوى الكيان تقيد الوصول إلى موارد فردية.
|
||||
|
||||
### رؤية الأتمتة
|
||||
|
||||
تدعم الأتمتات إعدادات رؤية تقيد الوصول حسب المستخدم أو الدور. هذا مفيد لـ:
|
||||
|
||||
- الحفاظ على خصوصية الأتمتات الحساسة أو التجريبية
|
||||
- إدارة الرؤية عبر الفرق الكبيرة أو المتعاونين الخارجيين
|
||||
- اختبار الأتمتات في سياقات معزولة
|
||||
|
||||
يمكن تهيئة عمليات النشر كخاصة، مما يعني أن المستخدمين والأدوار المدرجين في القائمة البيضاء فقط سيتمكنون من:
|
||||
|
||||
- عرض عملية النشر
|
||||
- تشغيلها أو التفاعل مع API الخاص بها
|
||||
- الوصول إلى سجلاتها ومقاييسها وإعداداتها
|
||||
|
||||
يتمتع مالك المؤسسة دائماً بالوصول، بغض النظر عن إعدادات الرؤية.
|
||||
يمكن تهيئة عمليات النشر كخاصة، مما يعني أن المستخدمين والأدوار المدرجين في القائمة البيضاء فقط سيتمكنون من التفاعل معها.
|
||||
|
||||
يمكنك تهيئة التحكم في الوصول على مستوى الأتمتة في Automation → Settings → علامة تبويب Visibility.
|
||||
|
||||
@@ -99,9 +165,92 @@ mode: "wide"
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/visibility.png" alt="إعدادات رؤية الأتمتة في CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
### أنواع صلاحيات النشر
|
||||
|
||||
عند منح وصول على مستوى الكيان لأتمتة محددة، يمكنك تعيين أنواع الصلاحيات التالية:
|
||||
|
||||
| الصلاحية | ما تسمح به |
|
||||
| :------------------- | :-------------------------------------------------- |
|
||||
| `run` | تنفيذ الأتمتة واستخدام API الخاص بها |
|
||||
| `traces` | عرض تتبعات التنفيذ والسجلات |
|
||||
| `manage_settings` | تعديل، إعادة نشر، استرجاع، أو حذف الأتمتة |
|
||||
| `human_in_the_loop` | الرد على طلبات الإنسان في الحلقة (HITL) |
|
||||
| `full_access` | جميع ما سبق |
|
||||
|
||||
### RBAC على مستوى الكيان لموارد أخرى
|
||||
|
||||
عند تفعيل RBAC على مستوى الكيان، يمكن أيضاً التحكم في الوصول لهذه الموارد حسب المستخدم أو الدور:
|
||||
|
||||
| المورد | يتم التحكم فيه بواسطة | الوصف |
|
||||
| :-------------------- | :--------------------------------- | :------------------------------------------------------------- |
|
||||
| متغيرات البيئة | علامة ميزة RBAC الكيان | تقييد أي الأدوار/المستخدمين يمكنهم عرض أو إدارة متغيرات بيئة محددة |
|
||||
| اتصالات LLM | علامة ميزة RBAC الكيان | تقييد الوصول لتهيئات مزودي LLM محددة |
|
||||
| مستودعات Git | إعداد RBAC لمستودعات Git بالمؤسسة | تقييد أي الأدوار/المستخدمين يمكنهم الوصول لمستودعات متصلة محددة |
|
||||
|
||||
---
|
||||
|
||||
## أنماط الأدوار الشائعة
|
||||
|
||||
بينما يأتي CrewAI بدوري Owner وMember، تستفيد معظم الفرق من إنشاء أدوار مخصصة. إليك الأنماط الشائعة:
|
||||
|
||||
### دور المطور
|
||||
|
||||
دور لأعضاء الفريق الذين يبنون وينشرون الأتمتات لكن لا يديرون إعدادات المؤسسة.
|
||||
|
||||
| الميزة | الصلاحية |
|
||||
| :------------------------ | :---------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Read |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Manage |
|
||||
| `agents` | Manage |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Manage |
|
||||
|
||||
### دور المشاهد / أصحاب المصلحة
|
||||
|
||||
دور للمعنيين غير التقنيين الذين يحتاجون لمراقبة الأتمتات وعرض النتائج.
|
||||
|
||||
| الميزة | الصلاحية |
|
||||
| :------------------------ | :---------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Read |
|
||||
| `invitations` | No access |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | No access |
|
||||
| `llm_connections` | No access |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
### دور مسؤول العمليات / المنصة
|
||||
|
||||
دور لمشغلي المنصة الذين يديرون إعدادات البنية التحتية لكن قد لا يبنون الوكلاء.
|
||||
|
||||
| الميزة | الصلاحية |
|
||||
| :------------------------ | :---------- |
|
||||
| `usage_dashboards` | Manage |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Manage |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | Manage |
|
||||
| `organization_settings` | Read |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
---
|
||||
|
||||
<Card title="تحتاج مساعدة؟" icon="headset" href="mailto:support@crewai.com">
|
||||
تواصل مع فريق الدعم للمساعدة في أسئلة RBAC.
|
||||
</Card>
|
||||
|
||||
6336
docs/docs.json
6336
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,275 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Apr 01, 2026">
|
||||
## v1.13.0a6
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a6)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Fix RBAC permission levels to match actual UI options (#5210)
|
||||
- Update changelog and version for v1.13.0a5 (#5200)
|
||||
|
||||
### Performance
|
||||
- Reduce framework overhead by implementing a lazy event bus and skipping tracing when disabled (#5187)
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @joaomdmoura, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 27, 2026">
|
||||
## v1.13.0a2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Auto-update deployment test repo during release
|
||||
- Improve enterprise release resilience and UX
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a1
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 27, 2026">
|
||||
## v1.13.0a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Fix broken links in documentation workflow by pinning Node to LTS 22
|
||||
- Bust the uv cache for freshly published packages in enterprise release
|
||||
|
||||
### Documentation
|
||||
- Add comprehensive RBAC permissions matrix and deployment guide
|
||||
- Update changelog and version for v1.12.2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add enterprise release phase to devtools release
|
||||
|
||||
### Bug Fixes
|
||||
- Preserve method return value as flow output for @human_feedback with emit
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.1
|
||||
- Revise security policy and reporting instructions
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add request_id to HumanFeedbackRequestedEvent
|
||||
- Add Qdrant Edge storage backend for memory system
|
||||
- Add docs-check command to analyze changes and generate docs with translations
|
||||
- Add Arabic language support to changelog and release tooling
|
||||
- Add modern standard Arabic translation of all documentation
|
||||
- Add logout command in CLI
|
||||
- Add agent skills
|
||||
- Implement automatic root_scope for hierarchical memory isolation
|
||||
- Implement native OpenAI-compatible providers (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix bad credentials for traces batch push (404)
|
||||
- Resolve multiple bugs in HITL flow system
|
||||
- Fix agent memory saving
|
||||
- Resolve all strict mypy errors across crewai package
|
||||
- Fix use of __router_paths__ for listener+router methods in FlowMeta
|
||||
- Fix value error on no file support
|
||||
- Correct litellm quarantine wording in docs
|
||||
- Fix all mypy errors in crewai-files and add all packages to CI type checks
|
||||
- Pin litellm upper bound to last tested version (1.82.6)
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.0
|
||||
- Add CONTRIBUTING.md
|
||||
- Add guide for using CrewAI without LiteLLM
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add Qdrant Edge storage backend for memory system
|
||||
- Add docs-check command to analyze changes and generate docs with translations
|
||||
- Add Arabic language support to changelog and release tooling
|
||||
- Add modern standard Arabic translation of all documentation
|
||||
- Add logout command in CLI
|
||||
- Implement agent skills
|
||||
- Implement automatic root_scope for hierarchical memory isolation
|
||||
- Implement native OpenAI-compatible providers (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix bad credentials for traces batch push (404)
|
||||
- Resolve multiple bugs in HITL flow system
|
||||
- Resolve mypy errors in crewai-files and add all packages to CI type checks
|
||||
- Resolve all strict mypy errors across crewai-tools package
|
||||
- Resolve all mypy errors across crewai package
|
||||
- Fix memory saving in agent
|
||||
- Fix usage of __router_paths__ for listener+router methods in FlowMeta
|
||||
- Raise value error on no file support
|
||||
- Correct litellm quarantine wording in docs
|
||||
- Use None check instead of isinstance for memory in human feedback learn
|
||||
- Pin litellm upper bound to last tested version (1.82.6)
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.0
|
||||
- Add CONTRIBUTING.md
|
||||
- Add guide for using CrewAI without LiteLLM
|
||||
|
||||
### Refactoring
|
||||
- Refactor to deduplicate sync/async task execution and kickoff in agent
|
||||
- Simplify internal plumbing from litellm (token counting, callbacks, feature detection, errors)
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 26, 2026">
|
||||
## v1.12.0a3
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0a3)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Fix bad credentials for traces batch push (404)
|
||||
- Resolve multiple bugs in HITL flow system
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.0a2
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.0a2
|
||||
|
||||
|
||||
147
docs/en/concepts/agent-capabilities.mdx
Normal file
147
docs/en/concepts/agent-capabilities.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
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>
|
||||
@@ -572,6 +572,176 @@ The `third_method` and `fourth_method` listen to the output of the `second_metho
|
||||
|
||||
When you run this Flow, the output will change based on the random boolean value generated by the `start_method`.
|
||||
|
||||
### Conversational Flows (User Input)
|
||||
|
||||
The `self.ask()` method pauses flow execution to request input from a user inline, then returns their response as a string. This enables conversational, interactive flows where the AI can gather information, ask clarifying questions, or request approvals during execution.
|
||||
|
||||
#### Basic Usage
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
|
||||
class GreetingFlow(Flow):
|
||||
@start()
|
||||
def greet(self):
|
||||
name = self.ask("What's your name?")
|
||||
self.state["name"] = name
|
||||
|
||||
@listen(greet)
|
||||
def welcome(self):
|
||||
print(f"Welcome, {self.state['name']}!")
|
||||
|
||||
flow = GreetingFlow()
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
By default, `self.ask()` uses a `ConsoleProvider` that prompts via Python's built-in `input()`.
|
||||
|
||||
#### Multiple Asks in One Method
|
||||
|
||||
You can call `self.ask()` multiple times within a single method to gather several inputs:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start
|
||||
|
||||
class OnboardingFlow(Flow):
|
||||
@start()
|
||||
def collect_info(self):
|
||||
name = self.ask("What's your name?")
|
||||
role = self.ask("What's your role?")
|
||||
team = self.ask("Which team are you joining?")
|
||||
self.state["profile"] = {"name": name, "role": role, "team": team}
|
||||
print(f"Welcome {name}, {role} on {team}!")
|
||||
|
||||
flow = OnboardingFlow()
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
#### Timeout Support
|
||||
|
||||
Pass `timeout=` (in seconds) to avoid blocking indefinitely. If the user doesn't respond in time, `self.ask()` returns `None`:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start
|
||||
|
||||
class ApprovalFlow(Flow):
|
||||
@start()
|
||||
def request_approval(self):
|
||||
response = self.ask("Approve deployment? (yes/no)", timeout=120)
|
||||
|
||||
if response is None:
|
||||
print("No response received — timed out.")
|
||||
self.state["approved"] = False
|
||||
return
|
||||
|
||||
self.state["approved"] = response.strip().lower() == "yes"
|
||||
```
|
||||
|
||||
Use a `while` loop to retry on timeout:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start
|
||||
|
||||
class RetryFlow(Flow):
|
||||
@start()
|
||||
def ask_with_retry(self):
|
||||
answer = None
|
||||
while answer is None:
|
||||
answer = self.ask("Please confirm (yes/no):", timeout=60)
|
||||
if answer is None:
|
||||
print("Timed out, asking again...")
|
||||
self.state["confirmed"] = answer.strip().lower() == "yes"
|
||||
```
|
||||
|
||||
#### Metadata Support
|
||||
|
||||
The `metadata` parameter enables bidirectional context passing between the flow and the input provider. Send context to the provider, and receive structured context back:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start
|
||||
|
||||
class ContextualFlow(Flow):
|
||||
@start()
|
||||
def gather_feedback(self):
|
||||
response = self.ask(
|
||||
"Rate this output (1-5):",
|
||||
metadata={
|
||||
"step": "quality_review",
|
||||
"output_id": "abc-123",
|
||||
"options": ["1", "2", "3", "4", "5"],
|
||||
},
|
||||
)
|
||||
self.state["rating"] = int(response) if response else None
|
||||
```
|
||||
|
||||
When a custom provider returns an `InputResponse`, it can include its own metadata (e.g., user identity, timestamp, channel info) that your flow can process.
|
||||
|
||||
#### Custom InputProvider
|
||||
|
||||
For production use cases (Slack bots, web UIs, webhooks), implement the `InputProvider` protocol:
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai.flow.input_provider import InputProvider, InputResponse
|
||||
import requests
|
||||
|
||||
class SlackInputProvider(InputProvider):
|
||||
def __init__(self, channel_id: str, bot_token: str):
|
||||
self.channel_id = channel_id
|
||||
self.bot_token = bot_token
|
||||
|
||||
def request_input(self, message, flow, metadata=None):
|
||||
# Post the question to Slack
|
||||
requests.post(
|
||||
"https://slack.com/api/chat.postMessage",
|
||||
headers={"Authorization": f"Bearer {self.bot_token}"},
|
||||
json={"channel": self.channel_id, "text": message},
|
||||
)
|
||||
# Wait for and return the user's reply (simplified)
|
||||
reply = self.poll_for_reply()
|
||||
return InputResponse(
|
||||
value=reply["text"],
|
||||
metadata={"user": reply["user"], "ts": reply["ts"]},
|
||||
)
|
||||
|
||||
def poll_for_reply(self):
|
||||
# Your implementation to wait for a Slack reply
|
||||
...
|
||||
|
||||
# Use the custom provider
|
||||
flow = Flow(input_provider=SlackInputProvider(
|
||||
channel_id="C01ABC123",
|
||||
bot_token="xoxb-...",
|
||||
))
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
The `request_input` method can return:
|
||||
- A **string** — used directly as the user's response
|
||||
- An **`InputResponse`** — includes `value` (the response string) and optional `metadata`
|
||||
- **`None`** — treated as a timeout / no response
|
||||
|
||||
#### Auto-Checkpoint Behavior
|
||||
|
||||
<Note>
|
||||
When persistence is configured, the flow state is automatically saved **before** each `self.ask()` call. If the process restarts while waiting for input, the flow can resume from the checkpoint without losing progress.
|
||||
</Note>
|
||||
|
||||
#### `self.ask()` vs `@human_feedback`
|
||||
|
||||
| | `self.ask()` | `@human_feedback` |
|
||||
|---|---|---|
|
||||
| **Purpose** | Inline user input during execution | Approval gates and review feedback |
|
||||
| **Returns** | `str \| None` | `HumanFeedbackResult` with structured fields |
|
||||
| **Timeout** | Built-in `timeout=` parameter | Not built-in |
|
||||
| **Provider** | Pluggable `InputProvider` protocol | Console-based |
|
||||
| **Use when** | Gathering data, clarifications, confirmations | Review/approval workflows with structured feedback |
|
||||
| **Decorator** | None — call `self.ask()` anywhere | `@human_feedback` on the method |
|
||||
|
||||
<Note>
|
||||
Both features coexist — you can use `self.ask()` and `@human_feedback` in the same flow. Use `self.ask()` for inline data gathering and `@human_feedback` for structured review gates.
|
||||
</Note>
|
||||
|
||||
### Human in the Loop (human feedback)
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -1,27 +1,186 @@
|
||||
---
|
||||
title: Skills
|
||||
description: Filesystem-based skill packages that inject context into agent prompts.
|
||||
description: Filesystem-based skill packages that inject domain expertise and instructions into agent prompts.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
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 use **progressive disclosure** — metadata is loaded first, full instructions only when activated, and resource catalogs only when needed.
|
||||
When activated, a skill's instructions are injected directly into the agent's task prompt — giving the agent expertise without requiring any code changes.
|
||||
|
||||
## Directory Structure
|
||||
<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
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # Required — frontmatter + instructions
|
||||
├── scripts/ # Optional — executable scripts
|
||||
├── references/ # Optional — reference documents
|
||||
└── assets/ # Optional — static files (configs, data)
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # Required — instructions
|
||||
├── references/ # Optional — reference docs
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # Optional — executable scripts
|
||||
```
|
||||
|
||||
The directory name must match the `name` field in `SKILL.md`.
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## SKILL.md Format
|
||||
|
||||
@@ -34,7 +193,7 @@ compatibility: crewai>=0.1.0 # optional
|
||||
metadata: # optional
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # optional, space-delimited
|
||||
allowed-tools: web-search file-read # optional, experimental
|
||||
---
|
||||
|
||||
Instructions for the agent go here. This markdown body is injected
|
||||
@@ -43,57 +202,46 @@ into the agent's prompt when the skill is activated.
|
||||
|
||||
### Frontmatter Fields
|
||||
|
||||
| Field | Required | Constraints |
|
||||
| Field | Required | Description |
|
||||
| :-------------- | :------- | :----------------------------------------------------------------------- |
|
||||
| `name` | Yes | 1–64 chars. Lowercase alphanumeric and hyphens. No leading/trailing/consecutive hyphens. Must match directory name. |
|
||||
| `name` | Yes | 1–64 chars. Lowercase alphanumeric and 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
|
||||
---
|
||||
|
||||
### Agent-level Skills
|
||||
## Directory Structure
|
||||
|
||||
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
|
||||
)
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # Required — frontmatter + instructions
|
||||
├── scripts/ # Optional — executable scripts
|
||||
├── references/ # Optional — reference documents
|
||||
└── assets/ # Optional — static files (configs, data)
|
||||
```
|
||||
|
||||
### Crew-level Skills
|
||||
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.
|
||||
|
||||
Skill paths on a crew are merged into every agent:
|
||||
---
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
## Pre-loading Skills
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
skills=["./skills"],
|
||||
)
|
||||
```
|
||||
|
||||
### Pre-loaded Skills
|
||||
|
||||
You can also pass `Skill` objects directly:
|
||||
For more control, you can discover and activate skills programmatically:
|
||||
|
||||
```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",
|
||||
@@ -102,14 +250,57 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How Skills Are Loaded
|
||||
|
||||
Skills load progressively — only the data needed at each stage is read:
|
||||
Skills use **progressive disclosure** — only loading what's needed at each stage:
|
||||
|
||||
| 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, 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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,6 +10,10 @@ 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.
|
||||
|
||||
@@ -7,11 +7,13 @@ mode: "wide"
|
||||
|
||||
## Overview
|
||||
|
||||
RBAC in CrewAI AMP enables secure, scalable access management through a combination of organization‑level roles and automation‑level visibility controls.
|
||||
RBAC in CrewAI AMP enables secure, scalable access management through two layers:
|
||||
|
||||
1. **Feature permissions** — control what each role can do across the platform (manage, read, or no access)
|
||||
2. **Entity-level permissions** — fine-grained access on individual automations, environment variables, LLM connections, and Git repositories
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/users_and_roles.png" alt="RBAC overview in CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
## Users and Roles
|
||||
@@ -39,6 +41,13 @@ You can configure users and roles in Settings → Roles.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Predefined Roles
|
||||
|
||||
| Role | Description |
|
||||
| :--------- | :-------------------------------------------------------------------------- |
|
||||
| **Owner** | Full access to all features and settings. Cannot be restricted. |
|
||||
| **Member** | Read access to most features, manage access to environment variables, LLM connections, and Studio projects. Cannot modify organization or default settings. |
|
||||
|
||||
### Configuration summary
|
||||
|
||||
| Area | Where to configure | Options |
|
||||
@@ -46,23 +55,80 @@ You can configure users and roles in Settings → Roles.
|
||||
| Users & Roles | Settings → Roles | Predefined: Owner, Member; Custom roles |
|
||||
| Automation visibility | Automation → Settings → Visibility | Private; Whitelist users/roles |
|
||||
|
||||
## Automation‑level Access Control
|
||||
---
|
||||
|
||||
In addition to organization‑wide roles, CrewAI Automations support fine‑grained visibility settings that let you restrict access to specific automations by user or role.
|
||||
## Feature Permissions Matrix
|
||||
|
||||
This is useful for:
|
||||
Every role has a permission level for each feature area. The three levels are:
|
||||
|
||||
- **Manage** — full read/write access (create, edit, delete)
|
||||
- **Read** — view-only access
|
||||
- **No access** — feature is hidden/inaccessible
|
||||
|
||||
| Feature | Owner | Member (default) | Available levels | Description |
|
||||
| :------------------------ | :------ | :--------------- | :------------------------ | :-------------------------------------------------------------- |
|
||||
| `usage_dashboards` | Manage | Read | Manage / Read / No access | View usage metrics and analytics |
|
||||
| `crews_dashboards` | Manage | Read | Manage / Read / No access | View deployment dashboards, access automation details |
|
||||
| `invitations` | Manage | Read | Manage / Read / No access | Invite new members to the organization |
|
||||
| `training_ui` | Manage | Read | Manage / Read / No access | Access training/fine-tuning interfaces |
|
||||
| `tools` | Manage | Read | Manage / Read / No access | Create and manage tools |
|
||||
| `agents` | Manage | Read | Manage / Read / No access | Create and manage agents |
|
||||
| `environment_variables` | Manage | Manage | Manage / No access | Create and manage environment variables |
|
||||
| `llm_connections` | Manage | Manage | Manage / No access | Configure LLM provider connections |
|
||||
| `default_settings` | Manage | No access | Manage / No access | Modify organization-wide default settings |
|
||||
| `organization_settings` | Manage | No access | Manage / No access | Manage billing, plans, and organization configuration |
|
||||
| `studio_projects` | Manage | Manage | Manage / No access | Create and edit projects in Studio |
|
||||
|
||||
<Tip>
|
||||
When creating a custom role, most features can be set to **Manage**, **Read**, or **No access**. However, `environment_variables`, `llm_connections`, `default_settings`, `organization_settings`, and `studio_projects` only support **Manage** or **No access** — there is no read-only option for these features.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Deploying from GitHub or Zip
|
||||
|
||||
One of the most common RBAC questions is: _"What permissions does a team member need to deploy?"_
|
||||
|
||||
### Deploy from GitHub
|
||||
|
||||
To deploy an automation from a GitHub repository, a user needs:
|
||||
|
||||
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard where deployments are created
|
||||
2. **Git repository access** (if entity-level RBAC for Git repositories is enabled): the user's role must be granted access to the specific Git repository via entity-level permissions
|
||||
3. **`studio_projects`: `Manage`** — if building the crew in Studio before deploying
|
||||
|
||||
### Deploy from Zip
|
||||
|
||||
To deploy an automation from a Zip file upload, a user needs:
|
||||
|
||||
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard
|
||||
2. **Zip deployments enabled**: the organization must not have disabled zip deployments in organization settings
|
||||
|
||||
### Quick Reference: Minimum Permissions for Deployment
|
||||
|
||||
| Action | Required feature permissions | Additional requirements |
|
||||
| :------------------- | :------------------------------------ | :----------------------------------------------- |
|
||||
| Deploy from GitHub | `crews_dashboards: Read` | Git repo entity access (if Git RBAC is enabled) |
|
||||
| Deploy from Zip | `crews_dashboards: Read` | Zip deployments must be enabled at the org level |
|
||||
| Build in Studio | `studio_projects: Manage` | — |
|
||||
| Configure LLM keys | `llm_connections: Manage` | — |
|
||||
| Set environment vars | `environment_variables: Manage` | Entity-level access (if entity RBAC is enabled) |
|
||||
|
||||
---
|
||||
|
||||
## Automation‑level Access Control (Entity Permissions)
|
||||
|
||||
In addition to organization‑wide roles, CrewAI supports fine‑grained entity-level permissions that restrict access to individual resources.
|
||||
|
||||
### Automation Visibility
|
||||
|
||||
Automations support visibility settings that restrict access by user or role. This is useful for:
|
||||
|
||||
- Keeping sensitive or experimental automations private
|
||||
- Managing visibility across large teams or external collaborators
|
||||
- Testing automations in isolated contexts
|
||||
|
||||
Deployments can be configured as private, meaning only whitelisted users and roles will be able to:
|
||||
|
||||
- View the deployment
|
||||
- Run it or interact with its API
|
||||
- Access its logs, metrics, and settings
|
||||
|
||||
The organization owner always has access, regardless of visibility settings.
|
||||
Deployments can be configured as private, meaning only whitelisted users and roles will be able to interact with them.
|
||||
|
||||
You can configure automation‑level access control in Automation → Settings → Visibility tab.
|
||||
|
||||
@@ -99,9 +165,92 @@ You can configure automation‑level access control in Automation → Settings
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/visibility.png" alt="Automation Visibility settings in CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
### Deployment Permission Types
|
||||
|
||||
When granting entity-level access to a specific automation, you can assign these permission types:
|
||||
|
||||
| Permission | What it allows |
|
||||
| :------------------- | :-------------------------------------------------- |
|
||||
| `run` | Execute the automation and use its API |
|
||||
| `traces` | View execution traces and logs |
|
||||
| `manage_settings` | Edit, redeploy, rollback, or delete the automation |
|
||||
| `human_in_the_loop` | Respond to human-in-the-loop (HITL) requests |
|
||||
| `full_access` | All of the above |
|
||||
|
||||
### Entity-level RBAC for Other Resources
|
||||
|
||||
When entity-level RBAC is enabled, access to these resources can also be controlled per user or role:
|
||||
|
||||
| Resource | Controlled by | Description |
|
||||
| :--------------------- | :------------------------------- | :---------------------------------------------------- |
|
||||
| Environment variables | Entity RBAC feature flag | Restrict which roles/users can view or manage specific env vars |
|
||||
| LLM connections | Entity RBAC feature flag | Restrict access to specific LLM provider configurations |
|
||||
| Git repositories | Git repositories RBAC org setting | Restrict which roles/users can access specific connected repos |
|
||||
|
||||
---
|
||||
|
||||
## Common Role Patterns
|
||||
|
||||
While CrewAI ships with Owner and Member roles, most teams benefit from creating custom roles. Here are common patterns:
|
||||
|
||||
### Developer Role
|
||||
|
||||
A role for team members who build and deploy automations but don't manage organization settings.
|
||||
|
||||
| Feature | Permission |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Read |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Manage |
|
||||
| `agents` | Manage |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Manage |
|
||||
|
||||
### Viewer / Stakeholder Role
|
||||
|
||||
A role for non-technical stakeholders who need to monitor automations and view results.
|
||||
|
||||
| Feature | Permission |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Read |
|
||||
| `invitations` | No access |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | No access |
|
||||
| `llm_connections` | No access |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
### Ops / Platform Admin Role
|
||||
|
||||
A role for platform operators who manage infrastructure settings but may not build agents.
|
||||
|
||||
| Feature | Permission |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Manage |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Manage |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | Manage |
|
||||
| `organization_settings` | Read |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
---
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for assistance with RBAC questions.
|
||||
</Card>
|
||||
|
||||
550
docs/en/enterprise/features/sso.mdx
Normal file
550
docs/en/enterprise/features/sso.mdx
Normal file
@@ -0,0 +1,550 @@
|
||||
---
|
||||
title: Single Sign-On (SSO)
|
||||
icon: "key"
|
||||
description: Configure enterprise SSO authentication for CrewAI Platform — SaaS and Factory
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CrewAI Platform supports enterprise Single Sign-On (SSO) across both **SaaS (AMP)** and **Factory (self-hosted)** deployments. SSO enables your team to authenticate using your organization's existing identity provider, enforcing centralized access control, MFA policies, and user lifecycle management.
|
||||
|
||||
### Supported Providers
|
||||
|
||||
| Provider | SaaS | Factory | Protocol | CLI Support |
|
||||
|---|---|---|---|---|
|
||||
| **WorkOS** | ✅ (default) | ✅ | OAuth 2.0 / OIDC | ✅ |
|
||||
| **Microsoft Entra ID** (Azure AD) | ✅ (enterprise) | ✅ | OAuth 2.0 / SAML 2.0 | ✅ |
|
||||
| **Okta** | ✅ (enterprise) | ✅ | OAuth 2.0 / OIDC | ✅ |
|
||||
| **Auth0** | ✅ (enterprise) | ✅ | OAuth 2.0 / OIDC | ✅ |
|
||||
| **Keycloak** | — | ✅ | OAuth 2.0 / OIDC | ✅ |
|
||||
|
||||
### Key Capabilities
|
||||
|
||||
- **SAML 2.0 and OAuth 2.0 / OIDC** protocol support
|
||||
- **Device Authorization Grant** flow for CLI authentication
|
||||
- **Role-Based Access Control (RBAC)** with custom roles and per-resource permissions
|
||||
- **MFA enforcement** delegated to your identity provider
|
||||
- **User provisioning** through IdP assignment (users/groups)
|
||||
|
||||
---
|
||||
|
||||
## SaaS SSO
|
||||
|
||||
### Default Authentication
|
||||
|
||||
CrewAI's managed SaaS platform (AMP) uses **WorkOS** as the default authentication provider. When you sign up at [app.crewai.com](https://app.crewai.com), authentication is handled through `login.crewai.com` — no additional SSO configuration is required.
|
||||
|
||||
### Enterprise Custom SSO
|
||||
|
||||
Enterprise SaaS customers can configure SSO with their own identity provider (Entra ID, Okta, Auth0). Contact your CrewAI account team to enable custom SSO for your organization. Once configured:
|
||||
|
||||
1. Your team members authenticate through your organization's IdP
|
||||
2. Access control and MFA policies are enforced by your IdP
|
||||
3. The CrewAI CLI automatically detects your SSO configuration via `crewai enterprise configure`
|
||||
|
||||
### CLI Defaults (SaaS)
|
||||
|
||||
| Setting | Default Value |
|
||||
|---|---|
|
||||
| `enterprise_base_url` | `https://app.crewai.com` |
|
||||
| `oauth2_provider` | `workos` |
|
||||
| `oauth2_domain` | `login.crewai.com` |
|
||||
|
||||
---
|
||||
|
||||
## Factory SSO Setup
|
||||
|
||||
Factory (self-hosted) deployments require you to configure SSO by setting environment variables in your Helm `values.yaml` and registering an application in your identity provider.
|
||||
|
||||
### Microsoft Entra ID (Azure AD)
|
||||
|
||||
<Steps>
|
||||
<Step title="Register an Application">
|
||||
1. Go to [portal.azure.com](https://portal.azure.com) → **Microsoft Entra ID** → **App registrations** → **New registration**
|
||||
2. Configure:
|
||||
- **Name:** `CrewAI` (or your preferred name)
|
||||
- **Supported account types:** Accounts in this organizational directory only
|
||||
- **Redirect URI:** Select **Web**, enter `https://<your-domain>/auth/entra_id/callback`
|
||||
3. Click **Register**
|
||||
</Step>
|
||||
|
||||
<Step title="Collect Credentials">
|
||||
From the app overview page, copy:
|
||||
- **Application (client) ID** → `ENTRA_ID_CLIENT_ID`
|
||||
- **Directory (tenant) ID** → `ENTRA_ID_TENANT_ID`
|
||||
</Step>
|
||||
|
||||
<Step title="Create Client Secret">
|
||||
1. Navigate to **Certificates & Secrets** → **New client secret**
|
||||
2. Add a description and select expiration period
|
||||
3. Copy the secret value immediately (it won't be shown again) → `ENTRA_ID_CLIENT_SECRET`
|
||||
</Step>
|
||||
|
||||
<Step title="Grant Admin Consent">
|
||||
1. Go to **Enterprise applications** → select your app
|
||||
2. Under **Security** → **Permissions**, click **Grant admin consent**
|
||||
3. Ensure **Microsoft Graph → User.Read** is granted
|
||||
</Step>
|
||||
|
||||
<Step title="Configure App Roles (Recommended)">
|
||||
Under **App registrations** → your app → **App roles**, create:
|
||||
|
||||
| Display Name | Value | Allowed Member Types |
|
||||
|---|---|---|
|
||||
| Member | `member` | Users/Groups |
|
||||
| Factory Admin | `factory-admin` | Users/Groups |
|
||||
|
||||
<Note>
|
||||
The `member` role grants login access. The `factory-admin` role grants admin panel access. Roles are included in the JWT automatically.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Assign Users">
|
||||
1. Under **Properties**, set **Assignment required?** to **Yes**
|
||||
2. Under **Users and groups**, assign users/groups with the appropriate role
|
||||
</Step>
|
||||
|
||||
<Step title="Set Environment Variables">
|
||||
```yaml
|
||||
envVars:
|
||||
AUTH_PROVIDER: "entra_id"
|
||||
|
||||
secrets:
|
||||
ENTRA_ID_CLIENT_ID: "<Application (client) ID>"
|
||||
ENTRA_ID_CLIENT_SECRET: "<Client Secret>"
|
||||
ENTRA_ID_TENANT_ID: "<Directory (tenant) ID>"
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Enable CLI Support (Optional)">
|
||||
To allow `crewai login` via Device Authorization Grant:
|
||||
|
||||
1. Under **Authentication** → **Advanced settings**, enable **Allow public client flows**
|
||||
2. Under **Expose an API**, add an Application ID URI (e.g., `api://crewai-cli`)
|
||||
3. Add a scope (e.g., `read`) with **Admins and users** consent
|
||||
4. Under **Manifest**, set `accessTokenAcceptedVersion` to `2`
|
||||
5. Add environment variables:
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
ENTRA_ID_DEVICE_AUTHORIZATION_CLIENT_ID: "<Application (client) ID>"
|
||||
ENTRA_ID_CUSTOM_OPENID_SCOPE: "<scope URI, e.g. api://crewai-cli/read>"
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
### Okta
|
||||
|
||||
<Steps>
|
||||
<Step title="Create App Integration">
|
||||
1. Open Okta Admin Console → **Applications** → **Create App Integration**
|
||||
2. Select **OIDC - OpenID Connect** → **Web Application** → **Next**
|
||||
3. Configure:
|
||||
- **App integration name:** `CrewAI SSO`
|
||||
- **Sign-in redirect URI:** `https://<your-domain>/auth/okta/callback`
|
||||
- **Sign-out redirect URI:** `https://<your-domain>`
|
||||
- **Assignments:** Choose who can access (everyone or specific groups)
|
||||
4. Click **Save**
|
||||
</Step>
|
||||
|
||||
<Step title="Collect Credentials">
|
||||
From the app details page:
|
||||
- **Client ID** → `OKTA_CLIENT_ID`
|
||||
- **Client Secret** → `OKTA_CLIENT_SECRET`
|
||||
- **Okta URL** (top-right corner, under your username) → `OKTA_SITE`
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Authorization Server">
|
||||
1. Navigate to **Security** → **API**
|
||||
2. Select your authorization server (default: `default`)
|
||||
3. Under **Access Policies**, add a policy and rule:
|
||||
- In the rule, under **Scopes requested**, select **The following scopes** → **OIDC default scopes**
|
||||
4. Note the **Name** and **Audience** of the authorization server
|
||||
|
||||
<Warning>
|
||||
The authorization server name and audience must match `OKTA_AUTHORIZATION_SERVER` and `OKTA_AUDIENCE` exactly. Mismatches cause `401 Unauthorized` or `Invalid token: Signature verification failed` errors.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Set Environment Variables">
|
||||
```yaml
|
||||
envVars:
|
||||
AUTH_PROVIDER: "okta"
|
||||
|
||||
secrets:
|
||||
OKTA_CLIENT_ID: "<Okta app client ID>"
|
||||
OKTA_CLIENT_SECRET: "<Okta client secret>"
|
||||
OKTA_SITE: "https://your-domain.okta.com"
|
||||
OKTA_AUTHORIZATION_SERVER: "default"
|
||||
OKTA_AUDIENCE: "api://default"
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Enable CLI Support (Optional)">
|
||||
1. Create a **new** app integration: **OIDC** → **Native Application**
|
||||
2. Enable **Device Authorization** and **Refresh Token** grant types
|
||||
3. Allow everyone in your organization to access
|
||||
4. Add environment variable:
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
OKTA_DEVICE_AUTHORIZATION_CLIENT_ID: "<Native app client ID>"
|
||||
```
|
||||
|
||||
<Note>
|
||||
Device Authorization requires a **Native Application** — it cannot use the Web Application created for browser-based SSO.
|
||||
</Note>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
### Keycloak
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Client">
|
||||
1. Open Keycloak Admin Console → navigate to your realm
|
||||
2. **Clients** → **Create client**:
|
||||
- **Client type:** OpenID Connect
|
||||
- **Client ID:** `crewai-factory` (suggested)
|
||||
3. Capability config:
|
||||
- **Client authentication:** On
|
||||
- **Standard flow:** Checked
|
||||
4. Login settings:
|
||||
- **Root URL:** `https://<your-domain>`
|
||||
- **Valid redirect URIs:** `https://<your-domain>/auth/keycloak/callback`
|
||||
- **Valid post logout redirect URIs:** `https://<your-domain>`
|
||||
5. Click **Save**
|
||||
</Step>
|
||||
|
||||
<Step title="Collect Credentials">
|
||||
- **Client ID** → `KEYCLOAK_CLIENT_ID`
|
||||
- Under **Credentials** tab: **Client secret** → `KEYCLOAK_CLIENT_SECRET`
|
||||
- **Realm name** → `KEYCLOAK_REALM`
|
||||
- **Keycloak server URL** → `KEYCLOAK_SITE`
|
||||
</Step>
|
||||
|
||||
<Step title="Set Environment Variables">
|
||||
```yaml
|
||||
envVars:
|
||||
AUTH_PROVIDER: "keycloak"
|
||||
|
||||
secrets:
|
||||
KEYCLOAK_CLIENT_ID: "<client ID>"
|
||||
KEYCLOAK_CLIENT_SECRET: "<client secret>"
|
||||
KEYCLOAK_SITE: "https://keycloak.yourdomain.com"
|
||||
KEYCLOAK_REALM: "<realm name>"
|
||||
KEYCLOAK_AUDIENCE: "account"
|
||||
# Only set if using a custom base path (pre-v17 migrations):
|
||||
# KEYCLOAK_BASE_URL: "/auth"
|
||||
```
|
||||
|
||||
<Note>
|
||||
Keycloak includes `account` as the default audience in access tokens. For most installations, `KEYCLOAK_AUDIENCE=account` works without additional configuration. See [Keycloak audience documentation](https://www.keycloak.org/docs/latest/authorization_services/index.html) if you need a custom audience.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Enable CLI Support (Optional)">
|
||||
1. Create a **second** client:
|
||||
- **Client type:** OpenID Connect
|
||||
- **Client ID:** `crewai-factory-cli` (suggested)
|
||||
- **Client authentication:** Off (Device Authorization requires a public client)
|
||||
- **Authentication flow:** Check **only** OAuth 2.0 Device Authorization Grant
|
||||
2. Add environment variable:
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
KEYCLOAK_DEVICE_AUTHORIZATION_CLIENT_ID: "<CLI client ID>"
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
### WorkOS
|
||||
|
||||
<Steps>
|
||||
<Step title="Configure in WorkOS Dashboard">
|
||||
1. Create an application in the [WorkOS Dashboard](https://dashboard.workos.com)
|
||||
2. Configure the redirect URI: `https://<your-domain>/auth/workos/callback`
|
||||
3. Note the **Client ID** and **AuthKit domain**
|
||||
4. Set up organizations in the WorkOS dashboard
|
||||
</Step>
|
||||
|
||||
<Step title="Set Environment Variables">
|
||||
```yaml
|
||||
envVars:
|
||||
AUTH_PROVIDER: "workos"
|
||||
|
||||
secrets:
|
||||
WORKOS_CLIENT_ID: "<WorkOS client ID>"
|
||||
WORKOS_AUTHKIT_DOMAIN: "<your-authkit-domain.authkit.com>"
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
### Auth0
|
||||
|
||||
<Steps>
|
||||
<Step title="Create Application">
|
||||
1. In the [Auth0 Dashboard](https://manage.auth0.com), create a new **Regular Web Application**
|
||||
2. Configure:
|
||||
- **Allowed Callback URLs:** `https://<your-domain>/auth/auth0/callback`
|
||||
- **Allowed Logout URLs:** `https://<your-domain>`
|
||||
3. Note the **Domain**, **Client ID**, and **Client Secret**
|
||||
</Step>
|
||||
|
||||
<Step title="Set Environment Variables">
|
||||
```yaml
|
||||
envVars:
|
||||
AUTH_PROVIDER: "auth0"
|
||||
|
||||
secrets:
|
||||
AUTH0_CLIENT_ID: "<Auth0 client ID>"
|
||||
AUTH0_CLIENT_SECRET: "<Auth0 client secret>"
|
||||
AUTH0_DOMAIN: "<your-tenant.auth0.com>"
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Enable CLI Support (Optional)">
|
||||
1. Create a **Native** application in Auth0 for Device Authorization
|
||||
2. Enable the **Device Authorization** grant type under application settings
|
||||
3. Configure the CLI with the appropriate audience and client ID
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## CLI Authentication
|
||||
|
||||
The CrewAI CLI supports SSO authentication via the **Device Authorization Grant** flow. This allows developers to authenticate from their terminal without exposing credentials.
|
||||
|
||||
### Quick Setup
|
||||
|
||||
For Factory installations, the CLI can auto-configure all OAuth2 settings:
|
||||
|
||||
```bash
|
||||
crewai enterprise configure https://your-factory-url.app
|
||||
```
|
||||
|
||||
This command fetches the SSO configuration from your Factory instance and sets all required CLI parameters automatically.
|
||||
|
||||
Then authenticate:
|
||||
|
||||
```bash
|
||||
crewai login
|
||||
```
|
||||
|
||||
<Note>
|
||||
Requires CrewAI CLI version **1.6.0** or higher for Entra ID, **0.159.0** or higher for Okta, and **1.9.0** or higher for Keycloak.
|
||||
</Note>
|
||||
|
||||
### Manual CLI Configuration
|
||||
|
||||
If you need to configure the CLI manually, use `crewai config set`:
|
||||
|
||||
```bash
|
||||
# Set the provider
|
||||
crewai config set oauth2_provider okta
|
||||
|
||||
# Set provider-specific values
|
||||
crewai config set oauth2_domain your-domain.okta.com
|
||||
crewai config set oauth2_client_id your-client-id
|
||||
crewai config set oauth2_audience api://default
|
||||
|
||||
# Set the enterprise base URL
|
||||
crewai config set enterprise_base_url https://your-factory-url.app
|
||||
```
|
||||
|
||||
### CLI Configuration Reference
|
||||
|
||||
| Setting | Description | Example |
|
||||
|---|---|---|
|
||||
| `enterprise_base_url` | Your CrewAI instance URL | `https://crewai.yourcompany.com` |
|
||||
| `oauth2_provider` | Provider name | `workos`, `okta`, `auth0`, `entra_id`, `keycloak` |
|
||||
| `oauth2_domain` | Provider domain | `your-domain.okta.com` |
|
||||
| `oauth2_client_id` | OAuth2 client ID | `0oaqnwji7pGW7VT6T697` |
|
||||
| `oauth2_audience` | API audience identifier | `api://default` |
|
||||
|
||||
View current configuration:
|
||||
|
||||
```bash
|
||||
crewai config list
|
||||
```
|
||||
|
||||
### How Device Authorization Works
|
||||
|
||||
1. Run `crewai login` — the CLI requests a device code from your IdP
|
||||
2. A verification URL and code are displayed in your terminal
|
||||
3. Your browser opens to the verification URL
|
||||
4. Enter the code and authenticate with your IdP credentials
|
||||
5. The CLI receives an access token and stores it locally
|
||||
|
||||
---
|
||||
|
||||
## Role-Based Access Control (RBAC)
|
||||
|
||||
CrewAI Platform provides granular RBAC that integrates with your SSO provider.
|
||||
|
||||
### Permission Model
|
||||
|
||||
| Permission | Description |
|
||||
|---|---|
|
||||
| **Read** | View resources (dashboards, automations, logs) |
|
||||
| **Write** | Create and modify resources |
|
||||
| **Manage** | Full control including deletion and configuration |
|
||||
|
||||
### Resources
|
||||
|
||||
Permissions can be scoped to individual resources:
|
||||
|
||||
- **Usage Dashboard** — Platform usage metrics and analytics
|
||||
- **Automations Dashboard** — Crew and flow management
|
||||
- **Environment Variables** — Secret and configuration management
|
||||
- **Individual Automations** — Per-automation access control
|
||||
|
||||
### Roles
|
||||
|
||||
- **Predefined roles** come out of the box with standard permission sets
|
||||
- **Custom roles** can be created with any combination of permissions
|
||||
- **Per-resource assignment** — limit specific automations to individual users or roles
|
||||
|
||||
### Factory Admin Access
|
||||
|
||||
For Factory deployments using Entra ID, admin access is controlled via App Roles:
|
||||
|
||||
- Assign the `factory-admin` role to users who need admin panel access
|
||||
- Assign the `member` role for standard platform access
|
||||
- Roles are communicated via JWT claims — no additional configuration needed after IdP setup
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Invalid Redirect URI
|
||||
|
||||
**Symptom:** Authentication fails with a redirect URI mismatch error.
|
||||
|
||||
**Fix:** Ensure the redirect URI in your IdP exactly matches the expected callback URL:
|
||||
|
||||
| Provider | Callback URL |
|
||||
|---|---|
|
||||
| Entra ID | `https://<domain>/auth/entra_id/callback` |
|
||||
| Okta | `https://<domain>/auth/okta/callback` |
|
||||
| Keycloak | `https://<domain>/auth/keycloak/callback` |
|
||||
| WorkOS | `https://<domain>/auth/workos/callback` |
|
||||
| Auth0 | `https://<domain>/auth/auth0/callback` |
|
||||
|
||||
### CLI Login Fails (Device Authorization)
|
||||
|
||||
**Symptom:** `crewai login` returns an error or times out.
|
||||
|
||||
**Fix:**
|
||||
- Verify that Device Authorization Grant is enabled in your IdP
|
||||
- For Okta: ensure you have a **Native Application** (not Web) with Device Authorization grant
|
||||
- For Entra ID: ensure **Allow public client flows** is enabled
|
||||
- For Keycloak: ensure the CLI client has **Client authentication: Off** and only Device Authorization Grant enabled
|
||||
- Check that `*_DEVICE_AUTHORIZATION_CLIENT_ID` environment variable is set on the server
|
||||
|
||||
### Token Validation Errors
|
||||
|
||||
**Symptom:** `Invalid token: Signature verification failed` or `401 Unauthorized` after login.
|
||||
|
||||
**Fix:**
|
||||
- **Okta:** Verify `OKTA_AUTHORIZATION_SERVER` and `OKTA_AUDIENCE` match the authorization server's Name and Audience exactly
|
||||
- **Entra ID:** Ensure `accessTokenAcceptedVersion` is set to `2` in the app manifest
|
||||
- **Keycloak:** Verify `KEYCLOAK_AUDIENCE` matches the audience in your access tokens (default: `account`)
|
||||
|
||||
### Admin Consent Not Granted (Entra ID)
|
||||
|
||||
**Symptom:** Users can't log in, see "needs admin approval" message.
|
||||
|
||||
**Fix:** Go to **Enterprise applications** → your app → **Permissions** → **Grant admin consent**. Ensure `User.Read` is granted for Microsoft Graph.
|
||||
|
||||
### 403 Forbidden After Login
|
||||
|
||||
**Symptom:** User authenticates successfully but gets 403 errors.
|
||||
|
||||
**Fix:**
|
||||
- Check that the user is assigned to the application in your IdP
|
||||
- For Entra ID with **Assignment required = Yes**: ensure the user has a role assignment (Member or Factory Admin)
|
||||
- For Okta: verify the user or their group is assigned under the app's **Assignments** tab
|
||||
|
||||
### CLI Can't Reach Factory Instance
|
||||
|
||||
**Symptom:** `crewai enterprise configure` fails to connect.
|
||||
|
||||
**Fix:**
|
||||
- Verify the Factory URL is reachable from your machine
|
||||
- Check that `enterprise_base_url` is set correctly: `crewai config list`
|
||||
- Ensure TLS certificates are valid and trusted
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
### Common
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `AUTH_PROVIDER` | Authentication provider: `entra_id`, `okta`, `workos`, `auth0`, `keycloak`, `local` |
|
||||
|
||||
### Microsoft Entra ID
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `ENTRA_ID_CLIENT_ID` | ✅ | Application (client) ID from Azure |
|
||||
| `ENTRA_ID_CLIENT_SECRET` | ✅ | Client secret from Azure |
|
||||
| `ENTRA_ID_TENANT_ID` | ✅ | Directory (tenant) ID from Azure |
|
||||
| `ENTRA_ID_DEVICE_AUTHORIZATION_CLIENT_ID` | CLI only | Client ID for Device Authorization Grant |
|
||||
| `ENTRA_ID_CUSTOM_OPENID_SCOPE` | CLI only | Custom scope from "Expose an API" (e.g., `api://crewai-cli/read`) |
|
||||
|
||||
### Okta
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `OKTA_CLIENT_ID` | ✅ | Okta application client ID |
|
||||
| `OKTA_CLIENT_SECRET` | ✅ | Okta client secret |
|
||||
| `OKTA_SITE` | ✅ | Okta organization URL (e.g., `https://your-domain.okta.com`) |
|
||||
| `OKTA_AUTHORIZATION_SERVER` | ✅ | Authorization server name (e.g., `default`) |
|
||||
| `OKTA_AUDIENCE` | ✅ | Authorization server audience (e.g., `api://default`) |
|
||||
| `OKTA_DEVICE_AUTHORIZATION_CLIENT_ID` | CLI only | Native app client ID for Device Authorization |
|
||||
|
||||
### WorkOS
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `WORKOS_CLIENT_ID` | ✅ | WorkOS application client ID |
|
||||
| `WORKOS_AUTHKIT_DOMAIN` | ✅ | AuthKit domain (e.g., `your-domain.authkit.com`) |
|
||||
|
||||
### Auth0
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `AUTH0_CLIENT_ID` | ✅ | Auth0 application client ID |
|
||||
| `AUTH0_CLIENT_SECRET` | ✅ | Auth0 client secret |
|
||||
| `AUTH0_DOMAIN` | ✅ | Auth0 tenant domain (e.g., `your-tenant.auth0.com`) |
|
||||
|
||||
### Keycloak
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `KEYCLOAK_CLIENT_ID` | ✅ | Keycloak client ID |
|
||||
| `KEYCLOAK_CLIENT_SECRET` | ✅ | Keycloak client secret |
|
||||
| `KEYCLOAK_SITE` | ✅ | Keycloak server URL |
|
||||
| `KEYCLOAK_REALM` | ✅ | Keycloak realm name |
|
||||
| `KEYCLOAK_AUDIENCE` | ✅ | Token audience (default: `account`) |
|
||||
| `KEYCLOAK_BASE_URL` | Optional | Base URL path (e.g., `/auth` for pre-v17 migrations) |
|
||||
| `KEYCLOAK_DEVICE_AUTHORIZATION_CLIENT_ID` | CLI only | Public client ID for Device Authorization |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Installation Guide](/installation) — Get started with CrewAI
|
||||
- [Quickstart](/quickstart) — Build your first crew
|
||||
- [RBAC Setup](/enterprise/features/rbac) — Detailed role and permission management
|
||||
344
docs/en/learn/a2ui.mdx
Normal file
344
docs/en/learn/a2ui.mdx
Normal file
@@ -0,0 +1,344 @@
|
||||
---
|
||||
title: Agent-to-UI (A2UI) Protocol
|
||||
description: Enable agents to generate declarative UI surfaces for rich client rendering via the A2UI extension.
|
||||
icon: window-restore
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## A2UI Overview
|
||||
|
||||
A2UI is a declarative UI protocol extension for [A2A](/en/learn/a2a-agent-delegation) that lets agents emit structured JSON messages describing interactive surfaces. Clients receive these messages and render them as rich UI components — forms, cards, lists, modals, and more — without the agent needing to know anything about the client's rendering stack.
|
||||
|
||||
A2UI is built on the A2A extension mechanism and identified by the URI `https://a2ui.org/a2a-extension/a2ui/v0.8`.
|
||||
|
||||
<Note>
|
||||
A2UI requires the `a2a-sdk` package. Install with: `uv add 'crewai[a2a]'` or `pip install 'crewai[a2a]'`
|
||||
</Note>
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The **server extension** scans agent output for A2UI JSON objects
|
||||
2. Valid messages are wrapped as `DataPart` entries with the `application/json+a2ui` MIME type
|
||||
3. The **client extension** augments the agent's system prompt with A2UI instructions and the component catalog
|
||||
4. The client tracks surface state (active surfaces and data models) across conversation turns
|
||||
|
||||
## Server Setup
|
||||
|
||||
Add `A2UIServerExtension` to your `A2AServerConfig` to enable A2UI output:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.a2a import A2AServerConfig
|
||||
from crewai.a2a.extensions.a2ui import A2UIServerExtension
|
||||
|
||||
agent = Agent(
|
||||
role="Dashboard Agent",
|
||||
goal="Present data through interactive UI surfaces",
|
||||
backstory="Expert at building clear, actionable dashboards",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AServerConfig(
|
||||
url="https://your-server.com",
|
||||
server_extensions=[A2UIServerExtension()],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Server Extension Options
|
||||
|
||||
<ParamField path="catalog_ids" type="list[str] | None" default="None">
|
||||
Component catalog identifiers the server supports. When set, only these catalogs are advertised to clients.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="accept_inline_catalogs" type="bool" default="False">
|
||||
Whether to accept inline catalog definitions from clients in addition to named catalogs.
|
||||
</ParamField>
|
||||
|
||||
## Client Setup
|
||||
|
||||
Add `A2UIClientExtension` to your `A2AClientConfig` to enable A2UI rendering:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.extensions.a2ui import A2UIClientExtension
|
||||
|
||||
agent = Agent(
|
||||
role="UI Coordinator",
|
||||
goal="Coordinate tasks and render agent responses as rich UI",
|
||||
backstory="Expert at presenting agent output in interactive formats",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://dashboard-agent.example.com/.well-known/agent-card.json",
|
||||
client_extensions=[A2UIClientExtension()],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Client Extension Options
|
||||
|
||||
<ParamField path="catalog_id" type="str | None" default="None">
|
||||
Preferred component catalog identifier. Defaults to `"standard (v0.8)"` when not set.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="allowed_components" type="list[str] | None" default="None">
|
||||
Restrict which components the agent may use. When `None`, all 18 standard catalog components are available.
|
||||
</ParamField>
|
||||
|
||||
## Message Types
|
||||
|
||||
A2UI defines four server-to-client message types. Each message targets a **surface** identified by `surfaceId`.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="beginRendering">
|
||||
Initializes a new surface with a root component and optional styles.
|
||||
|
||||
```json
|
||||
{
|
||||
"beginRendering": {
|
||||
"surfaceId": "dashboard-1",
|
||||
"root": "main-column",
|
||||
"catalogId": "standard (v0.8)",
|
||||
"styles": {
|
||||
"primaryColor": "#EB6658"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="surfaceUpdate">
|
||||
Sends or updates one or more components on an existing surface.
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "dashboard-1",
|
||||
"components": [
|
||||
{
|
||||
"id": "main-column",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": { "explicitList": ["title", "content"] },
|
||||
"alignment": "start"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "title",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "literalString": "Dashboard" },
|
||||
"usageHint": "h1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="dataModelUpdate">
|
||||
Updates the data model bound to a surface, enabling dynamic content.
|
||||
|
||||
```json
|
||||
{
|
||||
"dataModelUpdate": {
|
||||
"surfaceId": "dashboard-1",
|
||||
"path": "/data/model",
|
||||
"contents": [
|
||||
{
|
||||
"key": "userName",
|
||||
"valueString": "Alice"
|
||||
},
|
||||
{
|
||||
"key": "score",
|
||||
"valueNumber": 42
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="deleteSurface">
|
||||
Removes a surface and all its components.
|
||||
|
||||
```json
|
||||
{
|
||||
"deleteSurface": {
|
||||
"surfaceId": "dashboard-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Component Catalog
|
||||
|
||||
A2UI ships with 18 standard components organized into three categories:
|
||||
|
||||
### Content
|
||||
|
||||
| Component | Description | Required Fields |
|
||||
|-----------|-------------|-----------------|
|
||||
| **Text** | Renders text with optional heading/body hints | `text` (StringBinding) |
|
||||
| **Image** | Displays an image with fit and size options | `url` (StringBinding) |
|
||||
| **Icon** | Renders a named icon from a set of 47 icons | `name` (IconBinding) |
|
||||
| **Video** | Embeds a video player | `url` (StringBinding) |
|
||||
| **AudioPlayer** | Embeds an audio player with optional description | `url` (StringBinding) |
|
||||
|
||||
### Layout
|
||||
|
||||
| Component | Description | Required Fields |
|
||||
|-----------|-------------|-----------------|
|
||||
| **Row** | Horizontal flex container | `children` (ChildrenDef) |
|
||||
| **Column** | Vertical flex container | `children` (ChildrenDef) |
|
||||
| **List** | Scrollable list (vertical or horizontal) | `children` (ChildrenDef) |
|
||||
| **Card** | Elevated container for a single child | `child` (str) |
|
||||
| **Tabs** | Tabbed container | `tabItems` (list of TabItem) |
|
||||
| **Divider** | Visual separator (horizontal or vertical) | — |
|
||||
| **Modal** | Overlay triggered by an entry point | `entryPointChild`, `contentChild` (str) |
|
||||
|
||||
### Interactive
|
||||
|
||||
| Component | Description | Required Fields |
|
||||
|-----------|-------------|-----------------|
|
||||
| **Button** | Clickable button that triggers an action | `child` (str), `action` (Action) |
|
||||
| **CheckBox** | Boolean toggle with a label | `label` (StringBinding), `value` (BooleanBinding) |
|
||||
| **TextField** | Text input with type and validation options | `label` (StringBinding) |
|
||||
| **DateTimeInput** | Date and/or time picker | `value` (StringBinding) |
|
||||
| **MultipleChoice** | Selection from a list of options | `selections` (ArrayBinding), `options` (list) |
|
||||
| **Slider** | Numeric range slider | `value` (NumberBinding) |
|
||||
|
||||
## Data Binding
|
||||
|
||||
Components reference values through **bindings** rather than raw literals. This allows surfaces to update dynamically when the data model changes.
|
||||
|
||||
There are two ways to bind a value:
|
||||
|
||||
- **Literal values** — hardcoded directly in the component definition
|
||||
- **Path references** — point to a key in the surface's data model
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "profile-1",
|
||||
"components": [
|
||||
{
|
||||
"id": "greeting",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "path": "/data/model/userName" },
|
||||
"usageHint": "h2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "status",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "literalString": "Online" },
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, `greeting` reads the user's name from the data model (updated via `dataModelUpdate`), while `status` uses a hardcoded literal.
|
||||
|
||||
## Handling User Actions
|
||||
|
||||
Interactive components like `Button` trigger `userAction` events that flow back to the server. Each action includes a `name`, the originating `surfaceId` and `sourceComponentId`, and an optional `context` with key-value pairs.
|
||||
|
||||
```json
|
||||
{
|
||||
"userAction": {
|
||||
"name": "submitForm",
|
||||
"surfaceId": "form-1",
|
||||
"sourceComponentId": "submit-btn",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
"context": {
|
||||
"selectedOption": "optionA"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Action context values can also use path bindings to send current data model values back to the server:
|
||||
|
||||
```json
|
||||
{
|
||||
"Button": {
|
||||
"child": "confirm-label",
|
||||
"action": {
|
||||
"name": "confirm",
|
||||
"context": [
|
||||
{
|
||||
"key": "currentScore",
|
||||
"value": { "path": "/data/model/score" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Use `validate_a2ui_message` to validate server-to-client messages and `validate_a2ui_event` for client-to-server events:
|
||||
|
||||
```python Code
|
||||
from crewai.a2a.extensions.a2ui import validate_a2ui_message
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
validate_a2ui_event,
|
||||
A2UIValidationError,
|
||||
)
|
||||
|
||||
# Validate a server message
|
||||
try:
|
||||
msg = validate_a2ui_message({"beginRendering": {"surfaceId": "s1", "root": "r1"}})
|
||||
except A2UIValidationError as exc:
|
||||
print(exc.errors)
|
||||
|
||||
# Validate a client event
|
||||
try:
|
||||
event = validate_a2ui_event({
|
||||
"userAction": {
|
||||
"name": "click",
|
||||
"surfaceId": "s1",
|
||||
"sourceComponentId": "btn-1",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
}
|
||||
})
|
||||
except A2UIValidationError as exc:
|
||||
print(exc.errors)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Start Simple" icon="play">
|
||||
Begin with a `beginRendering` message and a single `surfaceUpdate`. Add data binding and interactivity once the basic flow works.
|
||||
</Card>
|
||||
|
||||
<Card title="Use Data Binding for Dynamic Content" icon="arrows-rotate">
|
||||
Prefer path bindings over literal values for content that changes. Use `dataModelUpdate` to push new values without resending the full component tree.
|
||||
</Card>
|
||||
|
||||
<Card title="Filter Components" icon="filter">
|
||||
Use the `allowed_components` option on `A2UIClientExtension` to restrict which components the agent may emit, reducing prompt size and keeping output predictable.
|
||||
</Card>
|
||||
|
||||
<Card title="Validate Messages" icon="check">
|
||||
Use `validate_a2ui_message` and `validate_a2ui_event` to catch malformed payloads early, especially when building custom integrations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Learn More
|
||||
|
||||
- [A2A Agent Delegation](/en/learn/a2a-agent-delegation) — configure agents for remote delegation via the A2A protocol
|
||||
- [A2A Protocol Documentation](https://a2a-protocol.org) — official protocol specification
|
||||
@@ -4,6 +4,275 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 4월 1일">
|
||||
## v1.13.0a6
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a6)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- 실제 UI 옵션에 맞게 RBAC 권한 수준 수정 (#5210)
|
||||
- v1.13.0a5에 대한 변경 로그 및 버전 업데이트 (#5200)
|
||||
|
||||
### 성능
|
||||
- 지연 이벤트 버스를 구현하고 비활성화 시 추적을 건너뛰어 프레임워크 오버헤드 감소 (#5187)
|
||||
|
||||
## 기여자
|
||||
|
||||
@alex-clawd, @joaomdmoura, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- v1.13.0a2의 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 27일">
|
||||
## v1.13.0a2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 릴리스 중 자동 업데이트 배포 테스트 리포지토리
|
||||
- 기업 릴리스의 복원력 및 사용자 경험 개선
|
||||
|
||||
### 문서
|
||||
- v1.13.0a1에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 27일">
|
||||
## v1.13.0a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- Node를 LTS 22로 고정하여 문서 작업 흐름의 끊어진 링크 수정
|
||||
- 기업 릴리스에서 새로 게시된 패키지의 uv 캐시 초기화
|
||||
|
||||
### 문서
|
||||
- 포괄적인 RBAC 권한 매트릭스 및 배포 가이드 추가
|
||||
- v1.12.2에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- devtools 릴리스에 기업 릴리스 단계 추가
|
||||
|
||||
### 버그 수정
|
||||
- @human_feedback과 함께 emit을 사용할 때 메서드 반환 값을 흐름 출력으로 유지
|
||||
|
||||
### 문서
|
||||
- v1.12.1에 대한 변경 로그 및 버전 업데이트
|
||||
- 보안 정책 및 보고 지침 수정
|
||||
|
||||
## 기여자
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- HumanFeedbackRequestedEvent에 request_id 추가
|
||||
- 메모리 시스템을 위한 Qdrant Edge 저장소 백엔드 추가
|
||||
- 변경 사항을 분석하고 번역된 문서와 함께 문서를 생성하는 docs-check 명령어 추가
|
||||
- 변경 로그 및 릴리스 도구에 아랍어 지원 추가
|
||||
- 모든 문서에 대한 현대 표준 아랍어 번역 추가
|
||||
- CLI에 로그아웃 명령어 추가
|
||||
- 에이전트 기술 추가
|
||||
- 계층적 메모리 격리를 위한 자동 root_scope 구현
|
||||
- OpenAI 호환 네이티브 제공자 구현 (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### 버그 수정
|
||||
- 트레이스 배치 푸시에 대한 잘못된 자격 증명 수정 (404)
|
||||
- HITL 흐름 시스템의 여러 버그 해결
|
||||
- 에이전트 메모리 저장 수정
|
||||
- crewai 패키지 전반에 걸쳐 모든 엄격한 mypy 오류 해결
|
||||
- FlowMeta의 listener+router 메서드에 대한 __router_paths__ 사용 수정
|
||||
- 파일 지원이 없는 경우 값 오류 수정
|
||||
- 문서에서 litellm 격리 단어 수정
|
||||
- crewai-files의 모든 mypy 오류 수정 및 모든 패키지를 CI 유형 검사에 추가
|
||||
- litellm의 상한을 마지막 테스트된 버전 (1.82.6)으로 고정
|
||||
|
||||
### 문서
|
||||
- v1.12.0에 대한 변경 로그 및 버전 업데이트
|
||||
- CONTRIBUTING.md 추가
|
||||
- LiteLLM 없이 CrewAI를 사용하는 가이드 추가
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 메모리 시스템을 위한 Qdrant Edge 스토리지 백엔드 추가
|
||||
- 변경 사항을 분석하고 번역된 문서와 함께 문서를 생성하는 docs-check 명령어 추가
|
||||
- 변경 로그 및 릴리스 도구에 아랍어 지원 추가
|
||||
- 모든 문서의 현대 표준 아랍어 번역 추가
|
||||
- CLI에 로그아웃 명령어 추가
|
||||
- 에이전트 기술 구현
|
||||
- 계층적 메모리 격리를 위한 자동 root_scope 구현
|
||||
- OpenAI 호환 네이티브 제공자 구현 (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### 버그 수정
|
||||
- 트레이스 배치 푸시에 대한 잘못된 자격 증명 수정 (404)
|
||||
- HITL 흐름 시스템의 여러 버그 해결
|
||||
- crewai-files의 mypy 오류 해결 및 모든 패키지를 CI 타입 검사에 추가
|
||||
- crewai-tools 패키지 전반의 모든 엄격한 mypy 오류 해결
|
||||
- crewai 패키지 전반의 모든 mypy 오류 해결
|
||||
- 에이전트의 메모리 절약 수정
|
||||
- FlowMeta에서 listener+router 메서드의 __router_paths__ 사용 수정
|
||||
- 파일 지원이 없을 때 값 오류 발생
|
||||
- 문서에서 litellm 격리 단어 수정
|
||||
- 인간 피드백 학습에서 메모리에 대한 isinstance 대신 None 체크 사용
|
||||
- litellm의 상한을 마지막 테스트된 버전(1.82.6)으로 고정
|
||||
|
||||
### 문서
|
||||
- v1.12.0에 대한 변경 로그 및 버전 업데이트
|
||||
- CONTRIBUTING.md 추가
|
||||
- LiteLLM 없이 CrewAI를 사용하는 가이드 추가
|
||||
|
||||
### 리팩토링
|
||||
- 에이전트에서 동기/비동기 작업 실행 및 시작을 중복 제거하도록 리팩토링
|
||||
- litellm의 내부 플러밍 단순화 (토큰 카운팅, 콜백, 기능 감지, 오류)
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 26일">
|
||||
## v1.12.0a3
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0a3)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- 트레이스 배치 푸시에 대한 잘못된 자격 증명 수정 (404)
|
||||
- HITL 흐름 시스템의 여러 버그 해결
|
||||
|
||||
### 문서
|
||||
- v1.12.0a2에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.0a2
|
||||
|
||||
|
||||
147
docs/ko/concepts/agent-capabilities.mdx
Normal file
147
docs/ko/concepts/agent-capabilities.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
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,27 +1,186 @@
|
||||
---
|
||||
title: 스킬
|
||||
description: 에이전트 프롬프트에 컨텍스트를 주입하는 파일 시스템 기반 스킬 패키지.
|
||||
description: 에이전트 프롬프트에 도메인 전문성과 지침을 주입하는 파일 시스템 기반 스킬 패키지.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
스킬은 에이전트에게 도메인별 지침, 참조 자료, 에셋을 제공하는 자체 포함 디렉터리입니다. 각 스킬은 YAML 프론트매터와 마크다운 본문이 포함된 `SKILL.md` 파일로 정의됩니다.
|
||||
스킬은 에이전트에게 **도메인별 지침, 가이드라인 및 참조 자료**를 제공하는 자체 포함 디렉터리입니다. 각 스킬은 YAML 프론트매터와 마크다운 본문이 포함된 `SKILL.md` 파일로 정의됩니다.
|
||||
|
||||
스킬은 **점진적 공개**를 사용합니다 — 메타데이터가 먼저 로드되고, 활성화 시에만 전체 지침이 로드되며, 필요할 때만 리소스 카탈로그가 로드됩니다.
|
||||
활성화되면 스킬의 지침이 에이전트의 작업 프롬프트에 직접 주입됩니다 — 코드 변경 없이 에이전트에게 전문성을 부여합니다.
|
||||
|
||||
## 디렉터리 구조
|
||||
<Note type="info" title="스킬 vs 도구 — 핵심 구분">
|
||||
**스킬은 도구가 아닙니다.** 이것이 가장 흔한 혼동 포인트입니다.
|
||||
|
||||
- **스킬**은 에이전트의 프롬프트에 *지침과 컨텍스트*를 주입합니다. 에이전트에게 문제에 대해 *어떻게 생각할지*를 알려줍니다.
|
||||
- **도구**는 에이전트에게 행동을 취할 수 있는 *호출 가능한 함수*를 제공합니다 (검색, 파일 읽기, API 호출).
|
||||
|
||||
흔히 **둘 다** 필요합니다: 전문성을 위한 스킬과 행동을 위한 도구. 이들은 독립적으로 구성되며 서로 보완합니다.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 1. 스킬 디렉터리 생성
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # 필수 — 프론트매터 + 지침
|
||||
├── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
├── references/ # 선택 — 참조 문서
|
||||
└── assets/ # 선택 — 정적 파일 (설정, 데이터)
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # 필수 — 지침
|
||||
├── references/ # 선택 — 참조 문서
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
```
|
||||
|
||||
디렉터리 이름은 `SKILL.md`의 `name` 필드와 일치해야 합니다.
|
||||
### 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 형식
|
||||
|
||||
@@ -34,7 +193,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 # 선택, 실험적
|
||||
---
|
||||
|
||||
에이전트를 위한 지침이 여기에 들어갑니다. 이 마크다운 본문은
|
||||
@@ -43,57 +202,46 @@ allowed-tools: web-search file-read # 선택, 공백으로 구분
|
||||
|
||||
### 프론트매터 필드
|
||||
|
||||
| 필드 | 필수 | 제약 조건 |
|
||||
| 필드 | 필수 | 설명 |
|
||||
| :-------------- | :----- | :----------------------------------------------------------------------- |
|
||||
| `name` | 예 | 1–64자. 소문자 영숫자와 하이픈. 선행/후행/연속 하이픈 불가. 디렉터리 이름과 일치 필수. |
|
||||
| `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"], # 이 디렉터리의 모든 스킬을 검색
|
||||
)
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # 필수 — 프론트매터 + 지침
|
||||
├── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
├── references/ # 선택 — 참조 문서
|
||||
└── assets/ # 선택 — 정적 파일 (설정, 데이터)
|
||||
```
|
||||
|
||||
### 크루 레벨 스킬
|
||||
디렉터리 이름은 `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",
|
||||
@@ -102,13 +250,57 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 스킬 로드 방식
|
||||
|
||||
스킬은 점진적으로 로드됩니다 — 각 단계에서 필요한 데이터만 읽습니다:
|
||||
스킬은 **점진적 공개**를 사용합니다 — 각 단계에서 필요한 것만 로드합니다:
|
||||
|
||||
| 단계 | 로드되는 내용 | 시점 |
|
||||
| :--------------- | :------------------------------------------------ | :----------------- |
|
||||
| 검색 | 이름, 설명, 프론트매터 필드 | `discover_skills()` |
|
||||
| 활성화 | 전체 SKILL.md 본문 텍스트 | `activate_skill()` |
|
||||
| 단계 | 로드되는 내용 | 시점 |
|
||||
| :------- | :------------------------------------ | :------------------ |
|
||||
| 검색 | 이름, 설명, 프론트매터 필드 | `discover_skills()` |
|
||||
| 활성화 | 전체 SKILL.md 본문 텍스트 | `activate_skill()` |
|
||||
|
||||
일반적인 에이전트 실행 중에 스킬은 자동으로 검색되고 활성화됩니다. `scripts/`, `references/`, `assets/` 디렉터리는 파일을 직접 참조해야 하는 에이전트를 위해 스킬의 `path`에서 사용할 수 있습니다.
|
||||
일반적인 에이전트 실행 중(`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>
|
||||
|
||||
@@ -10,6 +10,10 @@ 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은 에이전트가 다양한 작업을 수행하기 위해 활용할 수 있는 기술 또는 기능입니다.
|
||||
|
||||
@@ -1,108 +1,260 @@
|
||||
---
|
||||
title: "역할 기반 접근 제어 (RBAC)"
|
||||
description: "역할과 자동화별 가시성으로 crews, 도구, 데이터 접근을 제어합니다."
|
||||
description: "역할, 범위, 세분화된 권한으로 crews, 도구, 데이터 접근을 제어합니다."
|
||||
icon: "shield"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
CrewAI AOP의 RBAC는 **조직 수준 역할**과 **자동화(Automation) 수준 가시성**을 결합하여 안전하고 확장 가능한 접근 제어를 제공합니다.
|
||||
CrewAI AMP의 RBAC는 두 가지 계층을 통해 안전하고 확장 가능한 접근 관리를 제공합니다:
|
||||
|
||||
1. **기능 권한** — 플랫폼 전반에서 각 역할이 수행할 수 있는 작업을 제어합니다 (관리, 읽기 또는 접근 불가)
|
||||
2. **엔티티 수준 권한** — 개별 자동화, 환경 변수, LLM 연결, Git 저장소에 대한 세분화된 접근 제어
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/users_and_roles.png" alt="CrewAI AMP RBAC 개요" />
|
||||
|
||||
</Frame>
|
||||
|
||||
## 사용자와 역할
|
||||
|
||||
워크스페이스의 각 구성원은 역할이 있으며, 이는 기능 접근 범위를 결정합니다.
|
||||
CrewAI 워크스페이스의 각 구성원에게는 역할이 할당되며, 이를 통해 다양한 기능에 대한 접근 범위가 결정됩니다.
|
||||
|
||||
가능한 작업:
|
||||
|
||||
- 사전 정의된 역할 사용 (Owner, Member)
|
||||
- 권한을 세분화한 커스텀 역할 생성
|
||||
- 설정 화면에서 언제든 역할 할당/변경
|
||||
- 특정 권한에 맞춘 커스텀 역할 생성
|
||||
- 설정 패널에서 언제든지 역할 할당
|
||||
|
||||
설정 위치: Settings → Roles
|
||||
|
||||
<Steps>
|
||||
<Step title="Roles 열기">
|
||||
<b>Settings → Roles</b>로 이동합니다.
|
||||
<Step title="Roles 설정 열기">
|
||||
CrewAI AMP에서 <b>Settings → Roles</b>로 이동합니다.
|
||||
</Step>
|
||||
<Step title="역할 선택">
|
||||
<b>Owner</b> 또는 <b>Member</b>를 사용하거나 <b>Create role</b>로 커스텀
|
||||
역할을 만듭니다.
|
||||
<Step title="역할 유형 선택">
|
||||
사전 정의된 역할(<b>Owner</b>, <b>Member</b>)을 사용하거나{" "}
|
||||
<b>Create role</b>을 클릭하여 커스텀 역할을 만듭니다.
|
||||
</Step>
|
||||
<Step title="멤버에 할당">
|
||||
사용자들을 선택하여 역할을 지정합니다. 언제든 변경할 수 있습니다.
|
||||
사용자를 선택하고 역할을 할당합니다. 언제든지 변경할 수 있습니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### 사전 정의된 역할
|
||||
|
||||
| 역할 | 설명 |
|
||||
| :--------- | :------------------------------------------------------------------- |
|
||||
| **Owner** | 모든 기능 및 설정에 대한 전체 접근 권한. 제한할 수 없습니다. |
|
||||
| **Member** | 대부분의 기능에 대한 읽기 접근, 환경 변수, LLM 연결, Studio 프로젝트에 대한 관리 접근. 조직 설정이나 기본 설정은 수정할 수 없습니다. |
|
||||
|
||||
### 구성 요약
|
||||
|
||||
| 영역 | 위치 | 옵션 |
|
||||
| 영역 | 설정 위치 | 옵션 |
|
||||
| :------------ | :--------------------------------- | :-------------------------------- |
|
||||
| 사용자 & 역할 | Settings → Roles | Owner, Member; 커스텀 역할 |
|
||||
| 사용자 & 역할 | Settings → Roles | 사전 정의: Owner, Member; 커스텀 역할 |
|
||||
| 자동화 가시성 | Automation → Settings → Visibility | Private; 사용자/역할 화이트리스트 |
|
||||
|
||||
## 자동화 수준 접근 제어
|
||||
---
|
||||
|
||||
조직 역할과 별개로, **Automations**는 사용자/역할별로 특정 자동화 접근을 제한하는 가시성 설정을 제공합니다.
|
||||
## 기능 권한 매트릭스
|
||||
|
||||
유용한 경우:
|
||||
각 역할에는 기능 영역별 권한 수준이 있습니다. 세 가지 수준은 다음과 같습니다:
|
||||
|
||||
- 민감/실험 자동화를 비공개로 유지
|
||||
- 대규모 팀/외부 협업에서 가시성 관리
|
||||
- **Manage** — 전체 읽기/쓰기 접근 (생성, 편집, 삭제)
|
||||
- **Read** — 읽기 전용 접근
|
||||
- **No access** — 기능이 숨겨지거나 접근 불가
|
||||
|
||||
| 기능 | Owner | Member (기본값) | 사용 가능한 수준 | 설명 |
|
||||
| :-------------------------- | :------ | :--------------- | :------------------------- | :------------------------------------------------------------- |
|
||||
| `usage_dashboards` | Manage | Read | Manage / Read / No access | 사용 메트릭 및 분석 보기 |
|
||||
| `crews_dashboards` | Manage | Read | Manage / Read / No access | 배포 대시보드 보기, 자동화 세부 정보 접근 |
|
||||
| `invitations` | Manage | Read | Manage / Read / No access | 조직에 새 멤버 초대 |
|
||||
| `training_ui` | Manage | Read | Manage / Read / No access | 훈련/파인튜닝 인터페이스 접근 |
|
||||
| `tools` | Manage | Read | Manage / Read / No access | 도구 생성 및 관리 |
|
||||
| `agents` | Manage | Read | Manage / Read / No access | 에이전트 생성 및 관리 |
|
||||
| `environment_variables` | Manage | Manage | Manage / No access | 환경 변수 생성 및 관리 |
|
||||
| `llm_connections` | Manage | Manage | Manage / No access | LLM 제공자 연결 구성 |
|
||||
| `default_settings` | Manage | No access | Manage / No access | 조직 전체 기본 설정 수정 |
|
||||
| `organization_settings` | Manage | No access | Manage / No access | 결제, 플랜 및 조직 구성 관리 |
|
||||
| `studio_projects` | Manage | Manage | Manage / No access | Studio에서 프로젝트 생성 및 편집 |
|
||||
|
||||
<Tip>
|
||||
커스텀 역할을 만들 때 대부분의 기능은 **Manage**, **Read** 또는 **No access**로 설정할 수 있습니다. 그러나 `environment_variables`, `llm_connections`, `default_settings`, `organization_settings`, `studio_projects`는 **Manage** 또는 **No access**만 지원합니다 — 이 기능들에는 읽기 전용 옵션이 없습니다.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## GitHub 또는 Zip에서 배포
|
||||
|
||||
가장 흔한 RBAC 질문 중 하나: _"팀원이 배포하려면 어떤 권한이 필요한가요?"_
|
||||
|
||||
### GitHub에서 배포
|
||||
|
||||
GitHub 저장소에서 자동화를 배포하려면 사용자에게 다음이 필요합니다:
|
||||
|
||||
1. **`crews_dashboards`**: 최소 `Read` — 배포가 생성되는 자동화 대시보드에 접근하는 데 필요
|
||||
2. **Git 저장소 접근** (Git 저장소에 대한 엔티티 수준 RBAC가 활성화된 경우): 사용자의 역할에 엔티티 수준 권한을 통해 특정 Git 저장소에 대한 접근이 부여되어야 함
|
||||
3. **`studio_projects`: `Manage`** — 배포 전에 Studio에서 crew를 빌드하는 경우
|
||||
|
||||
### Zip에서 배포
|
||||
|
||||
Zip 파일 업로드로 자동화를 배포하려면 사용자에게 다음이 필요합니다:
|
||||
|
||||
1. **`crews_dashboards`**: 최소 `Read` — 자동화 대시보드에 접근하는 데 필요
|
||||
2. **Zip 배포 활성화**: 조직이 조직 설정에서 Zip 배포를 비활성화하지 않아야 함
|
||||
|
||||
### 빠른 참조: 배포에 필요한 최소 권한
|
||||
|
||||
| 작업 | 필요한 기능 권한 | 추가 요구사항 |
|
||||
| :------------------- | :----------------------------------- | :----------------------------------------------- |
|
||||
| GitHub에서 배포 | `crews_dashboards: Read` | Git 저장소 엔티티 접근 (Git RBAC 활성화 시) |
|
||||
| Zip에서 배포 | `crews_dashboards: Read` | 조직 수준에서 Zip 배포가 활성화되어야 함 |
|
||||
| Studio에서 빌드 | `studio_projects: Manage` | — |
|
||||
| LLM 키 구성 | `llm_connections: Manage` | — |
|
||||
| 환경 변수 설정 | `environment_variables: Manage` | 엔티티 수준 접근 (엔티티 RBAC 활성화 시) |
|
||||
|
||||
---
|
||||
|
||||
## 자동화 수준 접근 제어 (엔티티 권한)
|
||||
|
||||
조직 전체 역할 외에도, CrewAI는 개별 리소스에 대한 접근을 제한하는 세분화된 엔티티 수준 권한을 지원합니다.
|
||||
|
||||
### 자동화 가시성
|
||||
|
||||
자동화는 사용자 또는 역할별로 접근을 제한하는 가시성 설정을 지원합니다. 다음과 같은 경우에 유용합니다:
|
||||
|
||||
- 민감하거나 실험적인 자동화를 비공개로 유지
|
||||
- 대규모 팀이나 외부 협업자의 가시성 관리
|
||||
- 격리된 컨텍스트에서 자동화 테스트
|
||||
|
||||
Private 모드에서는 화이트리스트에 포함된 사용자/역할만 다음 작업이 가능합니다:
|
||||
배포를 비공개로 구성할 수 있으며, 이 경우 화이트리스트에 포함된 사용자와 역할만 상호작용할 수 있습니다.
|
||||
|
||||
- 자동화 보기
|
||||
- 실행/API 사용
|
||||
- 로그, 메트릭, 설정 접근
|
||||
|
||||
조직 Owner는 항상 접근 가능하며, 가시성 설정에 영향을 받지 않습니다.
|
||||
|
||||
설정 위치: Automation → Settings → Visibility
|
||||
설정 위치: Automation → Settings → Visibility 탭
|
||||
|
||||
<Steps>
|
||||
<Step title="Visibility 탭 열기">
|
||||
<b>Automation → Settings → Visibility</b>로 이동합니다.
|
||||
</Step>
|
||||
<Step title="가시성 설정">
|
||||
<b>Private</b>를 선택합니다. Owner는 항상 접근 가능합니다.
|
||||
접근을 제한하려면 <b>Private</b>를 선택합니다. 조직 Owner는 항상
|
||||
접근 권한을 유지합니다.
|
||||
</Step>
|
||||
<Step title="허용 대상 추가">
|
||||
보기/실행/로그·메트릭·설정 접근이 가능한 사용자/역할을 추가합니다.
|
||||
<Step title="접근 허용 대상 추가">
|
||||
보기, 실행, 로그/메트릭/설정 접근이 허용된 특정 사용자와 역할을
|
||||
추가합니다.
|
||||
</Step>
|
||||
<Step title="저장 및 확인">
|
||||
저장 후, 목록에 없는 사용자가 보거나 실행할 수 없는지 확인합니다.
|
||||
변경 사항을 저장한 후, 화이트리스트에 없는 사용자가 자동화를 보거나 실행할 수
|
||||
없는지 확인합니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Private 모드 접근 결과
|
||||
### Private 가시성: 접근 결과
|
||||
|
||||
| 동작 | Owner | 화이트리스트 사용자/역할 | 비포함 |
|
||||
| :--------------- | :---- | :----------------------- | :----- |
|
||||
| 자동화 보기 | ✓ | ✓ | ✗ |
|
||||
| 실행/API | ✓ | ✓ | ✗ |
|
||||
| 로그/메트릭/설정 | ✓ | ✓ | ✗ |
|
||||
| 동작 | Owner | 화이트리스트 사용자/역할 | 비포함 |
|
||||
| :--------------------- | :---- | :----------------------- | :----- |
|
||||
| 자동화 보기 | ✓ | ✓ | ✗ |
|
||||
| 자동화/API 실행 | ✓ | ✓ | ✗ |
|
||||
| 로그/메트릭/설정 접근 | ✓ | ✓ | ✗ |
|
||||
|
||||
<Tip>
|
||||
Owner는 항상 접근 가능하며, Private 모드에서는 화이트리스트에 포함된
|
||||
사용자/역할만 권한이 부여됩니다.
|
||||
조직 Owner는 항상 접근 권한이 있습니다. Private 모드에서는 화이트리스트에 포함된
|
||||
사용자/역할만 보기, 실행, 로그/메트릭/설정에 접근할 수 있습니다.
|
||||
</Tip>
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/visibility.png" alt="CrewAI AMP 가시성 설정" />
|
||||
|
||||
<img src="/images/enterprise/visibility.png" alt="CrewAI AMP 자동화 가시성 설정" />
|
||||
</Frame>
|
||||
|
||||
### 배포 권한 유형
|
||||
|
||||
특정 자동화에 엔티티 수준 접근을 부여할 때 다음 권한 유형을 할당할 수 있습니다:
|
||||
|
||||
| 권한 | 허용 범위 |
|
||||
| :------------------- | :-------------------------------------------------- |
|
||||
| `run` | 자동화 실행 및 API 사용 |
|
||||
| `traces` | 실행 트레이스 및 로그 보기 |
|
||||
| `manage_settings` | 자동화 편집, 재배포, 롤백 또는 삭제 |
|
||||
| `human_in_the_loop` | HITL(human-in-the-loop) 요청에 응답 |
|
||||
| `full_access` | 위의 모든 권한 |
|
||||
|
||||
### 기타 리소스에 대한 엔티티 수준 RBAC
|
||||
|
||||
엔티티 수준 RBAC가 활성화되면 다음 리소스에 대한 접근도 사용자 또는 역할별로 제어할 수 있습니다:
|
||||
|
||||
| 리소스 | 제어 방식 | 설명 |
|
||||
| :----------------- | :---------------------------------- | :------------------------------------------------------------ |
|
||||
| 환경 변수 | 엔티티 RBAC 기능 플래그 | 특정 환경 변수를 보거나 관리할 수 있는 역할/사용자 제한 |
|
||||
| LLM 연결 | 엔티티 RBAC 기능 플래그 | 특정 LLM 제공자 구성에 대한 접근 제한 |
|
||||
| Git 저장소 | Git 저장소 RBAC 조직 설정 | 특정 연결된 저장소에 접근할 수 있는 역할/사용자 제한 |
|
||||
|
||||
---
|
||||
|
||||
## 일반적인 역할 패턴
|
||||
|
||||
CrewAI는 Owner와 Member 역할을 기본 제공하지만, 대부분의 팀은 커스텀 역할을 만들어 활용합니다. 일반적인 패턴은 다음과 같습니다:
|
||||
|
||||
### Developer 역할
|
||||
|
||||
자동화를 빌드하고 배포하지만 조직 설정을 관리하지 않는 팀원을 위한 역할입니다.
|
||||
|
||||
| 기능 | 권한 |
|
||||
| :-------------------------- | :---------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Read |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Manage |
|
||||
| `agents` | Manage |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Manage |
|
||||
|
||||
### Viewer / Stakeholder 역할
|
||||
|
||||
자동화를 모니터링하고 결과를 확인해야 하는 비기술 이해관계자를 위한 역할입니다.
|
||||
|
||||
| 기능 | 권한 |
|
||||
| :-------------------------- | :---------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Read |
|
||||
| `invitations` | No access |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | No access |
|
||||
| `llm_connections` | No access |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
### Ops / Platform Admin 역할
|
||||
|
||||
인프라 설정을 관리하지만 에이전트를 빌드하지 않을 수 있는 플랫폼 운영자를 위한 역할입니다.
|
||||
|
||||
| 기능 | 권한 |
|
||||
| :-------------------------- | :---------- |
|
||||
| `usage_dashboards` | Manage |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Manage |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | Manage |
|
||||
| `organization_settings` | Read |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
---
|
||||
|
||||
<Card
|
||||
title="도움이 필요하신가요?"
|
||||
icon="headset"
|
||||
href="mailto:support@crewai.com"
|
||||
>
|
||||
RBAC 구성과 점검에 대한 지원이 필요하면 연락해 주세요.
|
||||
RBAC 관련 질문은 지원팀에 문의해 주세요.
|
||||
</Card>
|
||||
|
||||
@@ -4,6 +4,275 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="01 abr 2026">
|
||||
## v1.13.0a6
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a6)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Corrigir níveis de permissão RBAC para corresponder às opções reais da interface do usuário (#5210)
|
||||
- Atualizar changelog e versão para v1.13.0a5 (#5200)
|
||||
|
||||
### Desempenho
|
||||
- Reduzir a sobrecarga do framework implementando um barramento de eventos preguiçoso e pulando o rastreamento quando desativado (#5187)
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @joaomdmoura, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a2
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 mar 2026">
|
||||
## v1.13.0a2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Repositório de teste de implantação de autoatualização durante o lançamento
|
||||
- Melhorar a resiliência e a experiência do usuário na versão empresarial
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a1
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 mar 2026">
|
||||
## v1.13.0a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir links quebrados no fluxo de documentação fixando o Node na LTS 22
|
||||
- Limpar o cache uv para pacotes recém-publicados na versão empresarial
|
||||
|
||||
### Documentação
|
||||
- Adicionar uma matriz abrangente de permissões RBAC e guia de implantação
|
||||
- Atualizar o changelog e a versão para v1.12.2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar fase de lançamento empresarial ao lançamento do devtools
|
||||
|
||||
### Correções de Bugs
|
||||
- Preservar o valor de retorno do método como saída de fluxo para @human_feedback com emit
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.1
|
||||
- Revisar política de segurança e instruções de relatório
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar request_id ao HumanFeedbackRequestedEvent
|
||||
- Adicionar backend de armazenamento Qdrant Edge para sistema de memória
|
||||
- Adicionar comando docs-check para analisar mudanças e gerar documentação com traduções
|
||||
- Adicionar suporte ao idioma árabe para changelog e ferramentas de lançamento
|
||||
- Adicionar tradução em árabe padrão moderno de toda a documentação
|
||||
- Adicionar comando de logout na CLI
|
||||
- Adicionar habilidades de agente
|
||||
- Implementar root_scope automático para isolamento hierárquico de memória
|
||||
- Implementar provedores nativos compatíveis com OpenAI (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir credenciais incorretas para envio em lote de traces (404)
|
||||
- Resolver múltiplos bugs no sistema de fluxo HITL
|
||||
- Corrigir salvamento de memória do agente
|
||||
- Resolver todos os erros estritos do mypy no pacote crewai
|
||||
- Corrigir uso de __router_paths__ para métodos listener+router em FlowMeta
|
||||
- Corrigir erro de valor em caso de suporte a nenhum arquivo
|
||||
- Corrigir redação da quarentena do litellm na documentação
|
||||
- Corrigir todos os erros do mypy em crewai-files e adicionar todos os pacotes às verificações de tipo do CI
|
||||
- Fixar limite superior do litellm na última versão testada (1.82.6)
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.0
|
||||
- Adicionar CONTRIBUTING.md
|
||||
- Adicionar guia para usar CrewAI sem LiteLLM
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar backend de armazenamento Qdrant Edge para sistema de memória
|
||||
- Adicionar comando docs-check para analisar mudanças e gerar documentos com traduções
|
||||
- Adicionar suporte ao idioma árabe para changelog e ferramentas de lançamento
|
||||
- Adicionar tradução em árabe padrão moderno de toda a documentação
|
||||
- Adicionar comando de logout na CLI
|
||||
- Implementar habilidades de agente
|
||||
- Implementar root_scope automático para isolamento hierárquico de memória
|
||||
- Implementar provedores nativos compatíveis com OpenAI (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir credenciais inválidas para envio em lote de rastros (404)
|
||||
- Resolver múltiplos bugs no sistema de fluxo HITL
|
||||
- Resolver erros do mypy em crewai-files e adicionar todos os pacotes às verificações de tipo do CI
|
||||
- Resolver todos os erros estritos do mypy no pacote crewai-tools
|
||||
- Resolver todos os erros do mypy no pacote crewai
|
||||
- Corrigir economia de memória no agente
|
||||
- Corrigir uso de __router_paths__ para métodos listener+router em FlowMeta
|
||||
- Levantar erro de valor em caso de suporte a arquivos inexistente
|
||||
- Corrigir a redação da quarentena do litellm na documentação
|
||||
- Usar verificação de None em vez de isinstance para memória no aprendizado de feedback humano
|
||||
- Fixar limite superior do litellm na última versão testada (1.82.6)
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.0
|
||||
- Adicionar CONTRIBUTING.md
|
||||
- Adicionar guia para usar CrewAI sem LiteLLM
|
||||
|
||||
### Refatoração
|
||||
- Refatorar para desduplicar execução de tarefas síncronas/assíncronas e início no agente
|
||||
- Simplificar a infraestrutura interna do litellm (contagem de tokens, callbacks, detecção de recursos, erros)
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="26 mar 2026">
|
||||
## v1.12.0a3
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.0a3)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir credenciais inválidas para envio em lote de rastros (404)
|
||||
- Resolver múltiplos bugs no sistema de fluxo HITL
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.0a2
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.0a2
|
||||
|
||||
|
||||
147
docs/pt-BR/concepts/agent-capabilities.mdx
Normal file
147
docs/pt-BR/concepts/agent-capabilities.mdx
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
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,27 +1,186 @@
|
||||
---
|
||||
title: Skills
|
||||
description: Pacotes de skills baseados em sistema de arquivos que injetam contexto nos prompts dos agentes.
|
||||
description: Pacotes de skills baseados em sistema de arquivos que injetam expertise de domínio e instruções nos prompts dos agentes.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
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.
|
||||
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 usam **divulgação progressiva** — metadados são carregados primeiro, instruções completas apenas quando ativadas, e catálogos de recursos apenas quando necessário.
|
||||
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.
|
||||
|
||||
## Estrutura de Diretó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
|
||||
|
||||
```
|
||||
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)
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # Obrigatório — instruções
|
||||
├── references/ # Opcional — documentos de referência
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # Opcional — scripts executáveis
|
||||
```
|
||||
|
||||
O nome do diretório deve corresponder ao campo `name` no `SKILL.md`.
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## Formato do SKILL.md
|
||||
|
||||
@@ -34,7 +193,7 @@ compatibility: crewai>=0.1.0 # opcional
|
||||
metadata: # opcional
|
||||
author: your-name
|
||||
version: "1.0"
|
||||
allowed-tools: web-search file-read # opcional, delimitado por espaços
|
||||
allowed-tools: web-search file-read # opcional, experimental
|
||||
---
|
||||
|
||||
Instruções para o agente vão aqui. Este corpo em markdown é injetado
|
||||
@@ -43,57 +202,46 @@ no prompt do agente quando a skill é ativada.
|
||||
|
||||
### Campos do Frontmatter
|
||||
|
||||
| Campo | Obrigatório | Restrições |
|
||||
| Campo | Obrigatório | Descrição |
|
||||
| :-------------- | :---------- | :----------------------------------------------------------------------- |
|
||||
| `name` | Sim | 1–64 chars. Alfanumérico minúsculo e hifens. Sem hifens iniciais/finais/consecutivos. Deve corresponder ao nome do diretório. |
|
||||
| `name` | Sim | 1–64 chars. Alfanumérico minúsculo e hifens. 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
|
||||
---
|
||||
|
||||
### Skills no Nível do Agente
|
||||
## Estrutura de Diretório
|
||||
|
||||
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
|
||||
)
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
### Skills no Nível do Crew
|
||||
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.
|
||||
|
||||
Caminhos de skills no crew são mesclados em todos os agentes:
|
||||
---
|
||||
|
||||
```python
|
||||
from crewai import Crew
|
||||
## Skills Pré-carregadas
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
skills=["./skills"],
|
||||
)
|
||||
```
|
||||
|
||||
### Skills Pré-carregadas
|
||||
|
||||
Você também pode passar objetos `Skill` diretamente:
|
||||
Para mais controle, você pode descobrir e ativar skills programaticamente:
|
||||
|
||||
```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",
|
||||
@@ -102,13 +250,57 @@ agent = Agent(
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Como as Skills São Carregadas
|
||||
|
||||
Skills carregam progressivamente — apenas os dados necessários em cada etapa são lidos:
|
||||
Skills usam **divulgação progressiva** — carregando apenas o necessário em cada estágio:
|
||||
|
||||
| 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()` |
|
||||
| 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()` |
|
||||
|
||||
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.
|
||||
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>
|
||||
|
||||
@@ -10,6 +10,10 @@ 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.
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
---
|
||||
title: "Controle de Acesso Baseado em Funções (RBAC)"
|
||||
description: "Controle o acesso a crews, ferramentas e dados com funções e visibilidade por automação."
|
||||
description: "Controle o acesso a crews, ferramentas e dados com funções, escopos e permissões granulares."
|
||||
icon: "shield"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O RBAC no CrewAI AMP permite gerenciar acesso de forma segura e escalável combinando **funções em nível de organização** com **controles de visibilidade em nível de automação**.
|
||||
O RBAC no CrewAI AMP permite gerenciamento de acesso seguro e escalável através de duas camadas:
|
||||
|
||||
1. **Permissões de funcionalidade** — controlam o que cada função pode fazer na plataforma (gerenciar, ler ou sem acesso)
|
||||
2. **Permissões em nível de entidade** — acesso granular em automações individuais, variáveis de ambiente, conexões LLM e repositórios Git
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/users_and_roles.png" alt="Visão geral de RBAC no CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
## Usuários e Funções
|
||||
|
||||
Cada membro da sua workspace possui uma função, que determina o acesso aos recursos.
|
||||
Cada membro da sua workspace CrewAI recebe uma função, que determina seu acesso aos diversos recursos.
|
||||
|
||||
Você pode:
|
||||
|
||||
@@ -31,14 +33,21 @@ A configuração de usuários e funções é feita em Settings → Roles.
|
||||
Vá em <b>Settings → Roles</b> no CrewAI AMP.
|
||||
</Step>
|
||||
<Step title="Escolher a função">
|
||||
Use <b>Owner</b> ou <b>Member</b>, ou clique em <b>Create role</b> para
|
||||
criar uma função personalizada.
|
||||
Use uma função pré-definida (<b>Owner</b>, <b>Member</b>) ou clique em{" "}
|
||||
<b>Create role</b> para criar uma personalizada.
|
||||
</Step>
|
||||
<Step title="Atribuir aos membros">
|
||||
Selecione os usuários e atribua a função. Você pode alterar depois.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Funções Pré-definidas
|
||||
|
||||
| Função | Descrição |
|
||||
| :--------- | :------------------------------------------------------------------------ |
|
||||
| **Owner** | Acesso total a todas as funcionalidades e configurações. Não pode ser restrito. |
|
||||
| **Member** | Acesso de leitura à maioria das funcionalidades, acesso de gerenciamento a variáveis de ambiente, conexões LLM e projetos Studio. Não pode modificar configurações da organização ou padrões. |
|
||||
|
||||
### Resumo de configuração
|
||||
|
||||
| Área | Onde configurar | Opções |
|
||||
@@ -46,35 +55,93 @@ A configuração de usuários e funções é feita em Settings → Roles.
|
||||
| Usuários & Funções | Settings → Roles | Pré-definidas: Owner, Member; Funções personalizadas |
|
||||
| Visibilidade da automação | Automation → Settings → Visibility | Private; Lista de usuários/funções |
|
||||
|
||||
## Controle de Acesso em Nível de Automação
|
||||
---
|
||||
|
||||
Além das funções na organização, as **Automations** suportam visibilidade refinada para restringir acesso por usuário ou função.
|
||||
## Matriz de Permissões de Funcionalidades
|
||||
|
||||
Útil para:
|
||||
Cada função possui um nível de permissão para cada área de funcionalidade. Os três níveis são:
|
||||
|
||||
- Manter automações sensíveis/experimentais privadas
|
||||
- **Manage** — acesso total de leitura/escrita (criar, editar, excluir)
|
||||
- **Read** — acesso somente leitura
|
||||
- **No access** — funcionalidade oculta/inacessível
|
||||
|
||||
| Funcionalidade | Owner | Member (padrão) | Níveis disponíveis | Descrição |
|
||||
| :------------------------ | :------ | :--------------- | :------------------------ | :-------------------------------------------------------------- |
|
||||
| `usage_dashboards` | Manage | Read | Manage / Read / No access | Visualizar métricas e análises de uso |
|
||||
| `crews_dashboards` | Manage | Read | Manage / Read / No access | Visualizar dashboards de deploy, acessar detalhes de automações |
|
||||
| `invitations` | Manage | Read | Manage / Read / No access | Convidar novos membros para a organização |
|
||||
| `training_ui` | Manage | Read | Manage / Read / No access | Acessar interfaces de treinamento/fine-tuning |
|
||||
| `tools` | Manage | Read | Manage / Read / No access | Criar e gerenciar ferramentas |
|
||||
| `agents` | Manage | Read | Manage / Read / No access | Criar e gerenciar agentes |
|
||||
| `environment_variables` | Manage | Manage | Manage / No access | Criar e gerenciar variáveis de ambiente |
|
||||
| `llm_connections` | Manage | Manage | Manage / No access | Configurar conexões de provedores LLM |
|
||||
| `default_settings` | Manage | No access | Manage / No access | Modificar configurações padrão da organização |
|
||||
| `organization_settings` | Manage | No access | Manage / No access | Gerenciar cobrança, planos e configuração da organização |
|
||||
| `studio_projects` | Manage | Manage | Manage / No access | Criar e editar projetos no Studio |
|
||||
|
||||
<Tip>
|
||||
Ao criar uma função personalizada, a maioria das funcionalidades pode ser definida como **Manage**, **Read** ou **No access**. No entanto, `environment_variables`, `llm_connections`, `default_settings`, `organization_settings` e `studio_projects` suportam apenas **Manage** ou **No access** — não há opção somente leitura para essas funcionalidades.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Deploy via GitHub ou Zip
|
||||
|
||||
Uma das perguntas mais comuns sobre RBAC é: _"Quais permissões um membro da equipe precisa para fazer deploy?"_
|
||||
|
||||
### Deploy via GitHub
|
||||
|
||||
Para fazer deploy de uma automação a partir de um repositório GitHub, o usuário precisa de:
|
||||
|
||||
1. **`crews_dashboards`**: pelo menos `Read` — necessário para acessar o dashboard de automações onde os deploys são criados
|
||||
2. **Acesso ao repositório Git** (se RBAC em nível de entidade para repositórios Git estiver habilitado): a função do usuário deve ter acesso ao repositório Git específico via permissões de entidade
|
||||
3. **`studio_projects`: `Manage`** — se estiver construindo o crew no Studio antes do deploy
|
||||
|
||||
### Deploy via Zip
|
||||
|
||||
Para fazer deploy de uma automação via upload de arquivo Zip, o usuário precisa de:
|
||||
|
||||
1. **`crews_dashboards`**: pelo menos `Read` — necessário para acessar o dashboard de automações
|
||||
2. **Deploys via Zip habilitados**: a organização não deve ter desabilitado deploys via Zip nas configurações da organização
|
||||
|
||||
### Referência Rápida: Permissões Mínimas para Deploy
|
||||
|
||||
| Ação | Permissões de funcionalidade necessárias | Requisitos adicionais |
|
||||
| :------------------------- | :--------------------------------------- | :------------------------------------------------ |
|
||||
| Deploy via GitHub | `crews_dashboards: Read` | Acesso à entidade do repositório Git (se habilitado) |
|
||||
| Deploy via Zip | `crews_dashboards: Read` | Deploys via Zip devem estar habilitados na organização |
|
||||
| Construir no Studio | `studio_projects: Manage` | — |
|
||||
| Configurar chaves LLM | `llm_connections: Manage` | — |
|
||||
| Definir variáveis de ambiente | `environment_variables: Manage` | Acesso em nível de entidade (se habilitado) |
|
||||
|
||||
---
|
||||
|
||||
## Controle de Acesso em Nível de Automação (Permissões de Entidade)
|
||||
|
||||
Além das funções em nível de organização, o CrewAI suporta permissões granulares em nível de entidade que restringem o acesso a recursos individuais.
|
||||
|
||||
### Visibilidade da Automação
|
||||
|
||||
Automações suportam configurações de visibilidade que restringem acesso por usuário ou função. Útil para:
|
||||
|
||||
- Manter automações sensíveis ou experimentais privadas
|
||||
- Gerenciar visibilidade em equipes grandes ou colaboradores externos
|
||||
- Testar automações em contexto isolado
|
||||
|
||||
Em modo privado, somente usuários/funções na whitelist poderão:
|
||||
Deploys podem ser configurados como privados, significando que apenas usuários e funções na whitelist poderão interagir com eles.
|
||||
|
||||
- Ver a automação
|
||||
- Executar/usar a API
|
||||
- Acessar logs, métricas e configurações
|
||||
|
||||
O owner da organização sempre tem acesso, independente da visibilidade.
|
||||
|
||||
Configure em Automation → Settings → Visibility.
|
||||
Configure em Automation → Settings → aba Visibility.
|
||||
|
||||
<Steps>
|
||||
<Step title="Abrir a aba Visibility">
|
||||
Acesse <b>Automation → Settings → Visibility</b>.
|
||||
</Step>
|
||||
<Step title="Definir visibilidade">
|
||||
Selecione <b>Private</b> para restringir o acesso. O owner mantém acesso.
|
||||
Selecione <b>Private</b> para restringir o acesso. O owner da organização
|
||||
mantém acesso sempre.
|
||||
</Step>
|
||||
<Step title="Permitir acesso">
|
||||
Adicione usuários e funções que poderão ver/executar e acessar
|
||||
Adicione usuários e funções que poderão ver, executar e acessar
|
||||
logs/métricas/configurações.
|
||||
</Step>
|
||||
<Step title="Salvar e verificar">
|
||||
@@ -97,9 +164,92 @@ Configure em Automation → Settings → Visibility.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/visibility.png" alt="Configuração de visibilidade no CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
### Tipos de Permissão de Deploy
|
||||
|
||||
Ao conceder acesso em nível de entidade a uma automação específica, você pode atribuir estes tipos de permissão:
|
||||
|
||||
| Permissão | O que permite |
|
||||
| :------------------- | :-------------------------------------------------- |
|
||||
| `run` | Executar a automação e usar sua API |
|
||||
| `traces` | Visualizar traces de execução e logs |
|
||||
| `manage_settings` | Editar, reimplantar, reverter ou excluir a automação |
|
||||
| `human_in_the_loop` | Responder a solicitações human-in-the-loop (HITL) |
|
||||
| `full_access` | Todos os anteriores |
|
||||
|
||||
### RBAC em Nível de Entidade para Outros Recursos
|
||||
|
||||
Quando o RBAC em nível de entidade está habilitado, o acesso a estes recursos também pode ser controlado por usuário ou função:
|
||||
|
||||
| Recurso | Controlado por | Descrição |
|
||||
| :--------------------- | :------------------------------------- | :------------------------------------------------------------- |
|
||||
| Variáveis de ambiente | Flag de funcionalidade RBAC de entidade | Restringir quais funções/usuários podem ver ou gerenciar variáveis específicas |
|
||||
| Conexões LLM | Flag de funcionalidade RBAC de entidade | Restringir acesso a configurações de provedores LLM específicos |
|
||||
| Repositórios Git | Configuração RBAC de repositórios Git | Restringir quais funções/usuários podem acessar repositórios conectados específicos |
|
||||
|
||||
---
|
||||
|
||||
## Padrões Comuns de Funções
|
||||
|
||||
Embora o CrewAI venha com as funções Owner e Member, a maioria das equipes se beneficia da criação de funções personalizadas. Aqui estão os padrões comuns:
|
||||
|
||||
### Função Developer
|
||||
|
||||
Uma função para membros da equipe que constroem e fazem deploy de automações, mas não gerenciam configurações da organização.
|
||||
|
||||
| Funcionalidade | Permissão |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Read |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Manage |
|
||||
| `agents` | Manage |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Manage |
|
||||
|
||||
### Função Viewer / Stakeholder
|
||||
|
||||
Uma função para stakeholders não técnicos que precisam monitorar automações e visualizar resultados.
|
||||
|
||||
| Funcionalidade | Permissão |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Read |
|
||||
| `invitations` | No access |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | No access |
|
||||
| `llm_connections` | No access |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
### Função Ops / Platform Admin
|
||||
|
||||
Uma função para operadores de plataforma que gerenciam configurações de infraestrutura, mas podem não construir agentes.
|
||||
|
||||
| Funcionalidade | Permissão |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Manage |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Manage |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | Manage |
|
||||
| `organization_settings` | Read |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
---
|
||||
|
||||
<Card title="Precisa de Ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Fale com o nosso time para suporte em configuração e auditoria de RBAC.
|
||||
Fale com o nosso time para suporte em configuração de RBAC.
|
||||
</Card>
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.12.0a2"
|
||||
__version__ = "1.13.0a6"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.12.0a2",
|
||||
"crewai==1.13.0a6",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.12.0a2"
|
||||
__version__ = "1.13.0a6"
|
||||
|
||||
@@ -14281,10 +14281,349 @@
|
||||
],
|
||||
"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": {},
|
||||
"properties": {
|
||||
"llm": {
|
||||
"$ref": "#/$defs/LLM"
|
||||
}
|
||||
},
|
||||
"title": "OCRTool",
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies = [
|
||||
"uv~=0.9.13",
|
||||
"aiosqlite~=0.21.0",
|
||||
"pyyaml~=6.0",
|
||||
"lancedb>=0.29.2",
|
||||
"lancedb>=0.29.2,<0.30.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.12.0a2",
|
||||
"crewai-tools==1.13.0a6",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -4,6 +4,8 @@ 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
|
||||
@@ -42,7 +44,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.12.0a2"
|
||||
__version__ = "1.13.0a6"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
@@ -93,6 +95,38 @@ 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",
|
||||
|
||||
148
lib/crewai/src/crewai/a2a/extensions/a2ui/__init__.py
Normal file
148
lib/crewai/src/crewai/a2a/extensions/a2ui/__init__.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""A2UI (Agent to UI) declarative UI protocol support for CrewAI."""
|
||||
|
||||
from crewai.a2a.extensions.a2ui.catalog import (
|
||||
AudioPlayer,
|
||||
Button,
|
||||
Card,
|
||||
CheckBox,
|
||||
Column,
|
||||
DateTimeInput,
|
||||
Divider,
|
||||
Icon,
|
||||
Image,
|
||||
List,
|
||||
Modal,
|
||||
MultipleChoice,
|
||||
Row,
|
||||
Slider,
|
||||
Tabs,
|
||||
Text,
|
||||
TextField,
|
||||
Video,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.client_extension import A2UIClientExtension
|
||||
from crewai.a2a.extensions.a2ui.models import (
|
||||
A2UIEvent,
|
||||
A2UIMessage,
|
||||
A2UIResponse,
|
||||
BeginRendering,
|
||||
DataModelUpdate,
|
||||
DeleteSurface,
|
||||
SurfaceUpdate,
|
||||
UserAction,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.server_extension import (
|
||||
A2UI_STANDARD_CATALOG_ID,
|
||||
A2UI_V09_BASIC_CATALOG_ID,
|
||||
A2UI_V09_EXTENSION_URI,
|
||||
A2UIServerExtension,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.v0_9 import (
|
||||
A2UIEventV09,
|
||||
A2UIMessageV09,
|
||||
ActionEvent,
|
||||
ActionV09,
|
||||
AudioPlayerV09,
|
||||
ButtonV09,
|
||||
CardV09,
|
||||
CheckBoxV09,
|
||||
ChoicePickerV09,
|
||||
ClientDataModel,
|
||||
ClientErrorV09,
|
||||
ColumnV09,
|
||||
CreateSurface,
|
||||
DateTimeInputV09,
|
||||
DeleteSurfaceV09,
|
||||
DividerV09,
|
||||
IconV09,
|
||||
ImageV09,
|
||||
ListV09,
|
||||
ModalV09,
|
||||
RowV09,
|
||||
SliderV09,
|
||||
TabsV09,
|
||||
TextFieldV09,
|
||||
TextV09,
|
||||
Theme,
|
||||
UpdateComponents,
|
||||
UpdateDataModel,
|
||||
VideoV09,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
validate_a2ui_event,
|
||||
validate_a2ui_event_v09,
|
||||
validate_a2ui_message,
|
||||
validate_a2ui_message_v09,
|
||||
validate_catalog_components,
|
||||
validate_catalog_components_v09,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"A2UI_STANDARD_CATALOG_ID",
|
||||
"A2UI_V09_BASIC_CATALOG_ID",
|
||||
"A2UI_V09_EXTENSION_URI",
|
||||
"A2UIClientExtension",
|
||||
"A2UIEvent",
|
||||
"A2UIEventV09",
|
||||
"A2UIMessage",
|
||||
"A2UIMessageV09",
|
||||
"A2UIResponse",
|
||||
"A2UIServerExtension",
|
||||
"ActionEvent",
|
||||
"ActionV09",
|
||||
"AudioPlayer",
|
||||
"AudioPlayerV09",
|
||||
"BeginRendering",
|
||||
"Button",
|
||||
"ButtonV09",
|
||||
"Card",
|
||||
"CardV09",
|
||||
"CheckBox",
|
||||
"CheckBoxV09",
|
||||
"ChoicePickerV09",
|
||||
"ClientDataModel",
|
||||
"ClientErrorV09",
|
||||
"Column",
|
||||
"ColumnV09",
|
||||
"CreateSurface",
|
||||
"DataModelUpdate",
|
||||
"DateTimeInput",
|
||||
"DateTimeInputV09",
|
||||
"DeleteSurface",
|
||||
"DeleteSurfaceV09",
|
||||
"Divider",
|
||||
"DividerV09",
|
||||
"Icon",
|
||||
"IconV09",
|
||||
"Image",
|
||||
"ImageV09",
|
||||
"List",
|
||||
"ListV09",
|
||||
"Modal",
|
||||
"ModalV09",
|
||||
"MultipleChoice",
|
||||
"Row",
|
||||
"RowV09",
|
||||
"Slider",
|
||||
"SliderV09",
|
||||
"SurfaceUpdate",
|
||||
"Tabs",
|
||||
"TabsV09",
|
||||
"Text",
|
||||
"TextField",
|
||||
"TextFieldV09",
|
||||
"TextV09",
|
||||
"Theme",
|
||||
"UpdateComponents",
|
||||
"UpdateDataModel",
|
||||
"UserAction",
|
||||
"Video",
|
||||
"VideoV09",
|
||||
"validate_a2ui_event",
|
||||
"validate_a2ui_event_v09",
|
||||
"validate_a2ui_message",
|
||||
"validate_a2ui_message_v09",
|
||||
"validate_catalog_components",
|
||||
"validate_catalog_components_v09",
|
||||
]
|
||||
467
lib/crewai/src/crewai/a2a/extensions/a2ui/catalog.py
Normal file
467
lib/crewai/src/crewai/a2a/extensions/a2ui/catalog.py
Normal file
@@ -0,0 +1,467 @@
|
||||
"""Typed helpers for A2UI standard catalog components.
|
||||
|
||||
These models provide optional type safety for standard catalog components.
|
||||
Agents can also use raw dicts validated against the JSON schema.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class StringBinding(BaseModel):
|
||||
"""A string value: literal or data-model path."""
|
||||
|
||||
literal_string: str | None = Field(
|
||||
default=None, alias="literalString", description="Literal string value."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class NumberBinding(BaseModel):
|
||||
"""A numeric value: literal or data-model path."""
|
||||
|
||||
literal_number: float | None = Field(
|
||||
default=None, alias="literalNumber", description="Literal numeric value."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class BooleanBinding(BaseModel):
|
||||
"""A boolean value: literal or data-model path."""
|
||||
|
||||
literal_boolean: bool | None = Field(
|
||||
default=None, alias="literalBoolean", description="Literal boolean value."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ArrayBinding(BaseModel):
|
||||
"""An array value: literal or data-model path."""
|
||||
|
||||
literal_array: list[str] | None = Field(
|
||||
default=None, alias="literalArray", description="Literal array of strings."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ChildrenDef(BaseModel):
|
||||
"""Children definition for layout components."""
|
||||
|
||||
explicit_list: list[str] | None = Field(
|
||||
default=None,
|
||||
alias="explicitList",
|
||||
description="Explicit list of child component IDs.",
|
||||
)
|
||||
template: ChildTemplate | None = Field(
|
||||
default=None, description="Template for generating dynamic children."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ChildTemplate(BaseModel):
|
||||
"""Template for generating dynamic children from a data model list."""
|
||||
|
||||
component_id: str = Field(
|
||||
alias="componentId", description="ID of the component to repeat."
|
||||
)
|
||||
data_binding: str = Field(
|
||||
alias="dataBinding", description="Data-model path to bind the template to."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ActionContextEntry(BaseModel):
|
||||
"""A key-value pair in an action context payload."""
|
||||
|
||||
key: str = Field(description="Context entry key.")
|
||||
value: ActionBoundValue = Field(description="Context entry value.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ActionBoundValue(BaseModel):
|
||||
"""A value in an action context: literal or data-model path."""
|
||||
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
literal_string: str | None = Field(
|
||||
default=None, alias="literalString", description="Literal string value."
|
||||
)
|
||||
literal_number: float | None = Field(
|
||||
default=None, alias="literalNumber", description="Literal numeric value."
|
||||
)
|
||||
literal_boolean: bool | None = Field(
|
||||
default=None, alias="literalBoolean", description="Literal boolean value."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Action(BaseModel):
|
||||
"""Client-side action dispatched by interactive components."""
|
||||
|
||||
name: str = Field(description="Action name dispatched on interaction.")
|
||||
context: list[ActionContextEntry] | None = Field(
|
||||
default=None, description="Key-value pairs sent with the action."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TabItem(BaseModel):
|
||||
"""A single tab definition."""
|
||||
|
||||
title: StringBinding = Field(description="Tab title text.")
|
||||
child: str = Field(description="Component ID rendered as the tab content.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class MultipleChoiceOption(BaseModel):
|
||||
"""A single option in a MultipleChoice component."""
|
||||
|
||||
label: StringBinding = Field(description="Display label for the option.")
|
||||
value: str = Field(description="Value submitted when the option is selected.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Text(BaseModel):
|
||||
"""Displays text content."""
|
||||
|
||||
text: StringBinding = Field(description="Text content to display.")
|
||||
usage_hint: Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"] | None = Field(
|
||||
default=None, alias="usageHint", description="Semantic hint for text styling."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Image(BaseModel):
|
||||
"""Displays an image."""
|
||||
|
||||
url: StringBinding = Field(description="Image source URL.")
|
||||
fit: Literal["contain", "cover", "fill", "none", "scale-down"] | None = Field(
|
||||
default=None, description="Object-fit behavior for the image."
|
||||
)
|
||||
usage_hint: (
|
||||
Literal[
|
||||
"icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"
|
||||
]
|
||||
| None
|
||||
) = Field(
|
||||
default=None, alias="usageHint", description="Semantic hint for image sizing."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
IconName = Literal[
|
||||
"accountCircle",
|
||||
"add",
|
||||
"arrowBack",
|
||||
"arrowForward",
|
||||
"attachFile",
|
||||
"calendarToday",
|
||||
"call",
|
||||
"camera",
|
||||
"check",
|
||||
"close",
|
||||
"delete",
|
||||
"download",
|
||||
"edit",
|
||||
"event",
|
||||
"error",
|
||||
"favorite",
|
||||
"favoriteOff",
|
||||
"folder",
|
||||
"help",
|
||||
"home",
|
||||
"info",
|
||||
"locationOn",
|
||||
"lock",
|
||||
"lockOpen",
|
||||
"mail",
|
||||
"menu",
|
||||
"moreVert",
|
||||
"moreHoriz",
|
||||
"notificationsOff",
|
||||
"notifications",
|
||||
"payment",
|
||||
"person",
|
||||
"phone",
|
||||
"photo",
|
||||
"print",
|
||||
"refresh",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share",
|
||||
"shoppingCart",
|
||||
"star",
|
||||
"starHalf",
|
||||
"starOff",
|
||||
"upload",
|
||||
"visibility",
|
||||
"visibilityOff",
|
||||
"warning",
|
||||
]
|
||||
|
||||
|
||||
class IconBinding(BaseModel):
|
||||
"""Icon name: literal enum or data-model path."""
|
||||
|
||||
literal_string: IconName | None = Field(
|
||||
default=None, alias="literalString", description="Literal icon name."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Icon(BaseModel):
|
||||
"""Displays a named icon."""
|
||||
|
||||
name: IconBinding = Field(description="Icon name binding.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Video(BaseModel):
|
||||
"""Displays a video player."""
|
||||
|
||||
url: StringBinding = Field(description="Video source URL.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class AudioPlayer(BaseModel):
|
||||
"""Displays an audio player."""
|
||||
|
||||
url: StringBinding = Field(description="Audio source URL.")
|
||||
description: StringBinding | None = Field(
|
||||
default=None, description="Accessible description of the audio content."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Row(BaseModel):
|
||||
"""Horizontal layout container."""
|
||||
|
||||
children: ChildrenDef = Field(description="Child components in this row.")
|
||||
distribution: (
|
||||
Literal["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start"]
|
||||
| None
|
||||
) = Field(
|
||||
default=None, description="How children are distributed along the main axis."
|
||||
)
|
||||
alignment: Literal["start", "center", "end", "stretch"] | None = Field(
|
||||
default=None, description="How children are aligned on the cross axis."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Column(BaseModel):
|
||||
"""Vertical layout container."""
|
||||
|
||||
children: ChildrenDef = Field(description="Child components in this column.")
|
||||
distribution: (
|
||||
Literal["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly"]
|
||||
| None
|
||||
) = Field(
|
||||
default=None, description="How children are distributed along the main axis."
|
||||
)
|
||||
alignment: Literal["center", "end", "start", "stretch"] | None = Field(
|
||||
default=None, description="How children are aligned on the cross axis."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class List(BaseModel):
|
||||
"""Scrollable list container."""
|
||||
|
||||
children: ChildrenDef = Field(description="Child components in this list.")
|
||||
direction: Literal["vertical", "horizontal"] | None = Field(
|
||||
default=None, description="Scroll direction of the list."
|
||||
)
|
||||
alignment: Literal["start", "center", "end", "stretch"] | None = Field(
|
||||
default=None, description="How children are aligned on the cross axis."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Card(BaseModel):
|
||||
"""Card container wrapping a single child."""
|
||||
|
||||
child: str = Field(description="Component ID of the card content.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Tabs(BaseModel):
|
||||
"""Tabbed navigation container."""
|
||||
|
||||
tab_items: list[TabItem] = Field(
|
||||
alias="tabItems", description="List of tab definitions."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Divider(BaseModel):
|
||||
"""A visual divider line."""
|
||||
|
||||
axis: Literal["horizontal", "vertical"] | None = Field(
|
||||
default=None, description="Orientation of the divider."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class Modal(BaseModel):
|
||||
"""A modal dialog with an entry point trigger and content."""
|
||||
|
||||
entry_point_child: str = Field(
|
||||
alias="entryPointChild", description="Component ID that triggers the modal."
|
||||
)
|
||||
content_child: str = Field(
|
||||
alias="contentChild", description="Component ID rendered inside the modal."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Button(BaseModel):
|
||||
"""An interactive button with an action."""
|
||||
|
||||
child: str = Field(description="Component ID of the button label.")
|
||||
primary: bool | None = Field(
|
||||
default=None, description="Whether the button uses primary styling."
|
||||
)
|
||||
action: Action = Field(description="Action dispatched when the button is clicked.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class CheckBox(BaseModel):
|
||||
"""A checkbox input."""
|
||||
|
||||
label: StringBinding = Field(description="Label text for the checkbox.")
|
||||
value: BooleanBinding = Field(
|
||||
description="Boolean value binding for the checkbox state."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TextField(BaseModel):
|
||||
"""A text input field."""
|
||||
|
||||
label: StringBinding = Field(description="Label text for the input.")
|
||||
text: StringBinding | None = Field(
|
||||
default=None, description="Current text value binding."
|
||||
)
|
||||
text_field_type: (
|
||||
Literal["date", "longText", "number", "shortText", "obscured"] | None
|
||||
) = Field(default=None, alias="textFieldType", description="Input type variant.")
|
||||
validation_regexp: str | None = Field(
|
||||
default=None,
|
||||
alias="validationRegexp",
|
||||
description="Regex pattern for client-side validation.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DateTimeInput(BaseModel):
|
||||
"""A date and/or time picker."""
|
||||
|
||||
value: StringBinding = Field(description="ISO date/time string value binding.")
|
||||
enable_date: bool | None = Field(
|
||||
default=None,
|
||||
alias="enableDate",
|
||||
description="Whether the date picker is enabled.",
|
||||
)
|
||||
enable_time: bool | None = Field(
|
||||
default=None,
|
||||
alias="enableTime",
|
||||
description="Whether the time picker is enabled.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class MultipleChoice(BaseModel):
|
||||
"""A multiple-choice selection component."""
|
||||
|
||||
selections: ArrayBinding = Field(description="Array binding for selected values.")
|
||||
options: list[MultipleChoiceOption] = Field(description="Available choices.")
|
||||
max_allowed_selections: int | None = Field(
|
||||
default=None,
|
||||
alias="maxAllowedSelections",
|
||||
description="Maximum number of selections allowed.",
|
||||
)
|
||||
variant: Literal["checkbox", "chips"] | None = Field(
|
||||
default=None, description="Visual variant for the selection UI."
|
||||
)
|
||||
filterable: bool | None = Field(
|
||||
default=None, description="Whether options can be filtered by typing."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Slider(BaseModel):
|
||||
"""A numeric slider input."""
|
||||
|
||||
value: NumberBinding = Field(
|
||||
description="Numeric value binding for the slider position."
|
||||
)
|
||||
min_value: float | None = Field(
|
||||
default=None, alias="minValue", description="Minimum slider value."
|
||||
)
|
||||
max_value: float | None = Field(
|
||||
default=None, alias="maxValue", description="Maximum slider value."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
STANDARD_CATALOG_COMPONENTS: frozenset[str] = frozenset(
|
||||
{
|
||||
"Text",
|
||||
"Image",
|
||||
"Icon",
|
||||
"Video",
|
||||
"AudioPlayer",
|
||||
"Row",
|
||||
"Column",
|
||||
"List",
|
||||
"Card",
|
||||
"Tabs",
|
||||
"Divider",
|
||||
"Modal",
|
||||
"Button",
|
||||
"CheckBox",
|
||||
"TextField",
|
||||
"DateTimeInput",
|
||||
"MultipleChoice",
|
||||
"Slider",
|
||||
}
|
||||
)
|
||||
496
lib/crewai/src/crewai/a2a/extensions/a2ui/client_extension.py
Normal file
496
lib/crewai/src/crewai/a2a/extensions/a2ui/client_extension.py
Normal file
@@ -0,0 +1,496 @@
|
||||
"""A2UI client extension for the A2A protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
from typing_extensions import TypeIs, TypedDict
|
||||
|
||||
from crewai.a2a.extensions.a2ui.models import extract_a2ui_json_objects
|
||||
from crewai.a2a.extensions.a2ui.prompt import (
|
||||
build_a2ui_system_prompt,
|
||||
build_a2ui_v09_system_prompt,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.server_extension import (
|
||||
A2UI_MIME_TYPE,
|
||||
A2UI_STANDARD_CATALOG_ID,
|
||||
A2UI_V09_BASIC_CATALOG_ID,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.v0_9 import extract_a2ui_v09_json_objects
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
A2UIValidationError,
|
||||
validate_a2ui_message,
|
||||
validate_a2ui_message_v09,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Message
|
||||
|
||||
from crewai.agent.core import Agent
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StylesDict(TypedDict, total=False):
|
||||
"""Serialized surface styling."""
|
||||
|
||||
font: str
|
||||
primaryColor: str
|
||||
|
||||
|
||||
class ComponentEntryDict(TypedDict, total=False):
|
||||
"""Serialized component entry in a surface update."""
|
||||
|
||||
id: str
|
||||
weight: float
|
||||
component: dict[str, Any]
|
||||
|
||||
|
||||
class BeginRenderingDict(TypedDict, total=False):
|
||||
"""Serialized beginRendering payload."""
|
||||
|
||||
surfaceId: str
|
||||
root: str
|
||||
catalogId: str
|
||||
styles: StylesDict
|
||||
|
||||
|
||||
class SurfaceUpdateDict(TypedDict, total=False):
|
||||
"""Serialized surfaceUpdate payload."""
|
||||
|
||||
surfaceId: str
|
||||
components: list[ComponentEntryDict]
|
||||
|
||||
|
||||
class DataEntryDict(TypedDict, total=False):
|
||||
"""Serialized data model entry."""
|
||||
|
||||
key: str
|
||||
valueString: str
|
||||
valueNumber: float
|
||||
valueBoolean: bool
|
||||
valueMap: list[DataEntryDict]
|
||||
|
||||
|
||||
class DataModelUpdateDict(TypedDict, total=False):
|
||||
"""Serialized dataModelUpdate payload."""
|
||||
|
||||
surfaceId: str
|
||||
path: str
|
||||
contents: list[DataEntryDict]
|
||||
|
||||
|
||||
class DeleteSurfaceDict(TypedDict):
|
||||
"""Serialized deleteSurface payload."""
|
||||
|
||||
surfaceId: str
|
||||
|
||||
|
||||
class A2UIMessageDict(TypedDict, total=False):
|
||||
"""Serialized A2UI v0.8 server-to-client message with exactly one key set."""
|
||||
|
||||
beginRendering: BeginRenderingDict
|
||||
surfaceUpdate: SurfaceUpdateDict
|
||||
dataModelUpdate: DataModelUpdateDict
|
||||
deleteSurface: DeleteSurfaceDict
|
||||
|
||||
|
||||
class ThemeDict(TypedDict, total=False):
|
||||
"""Serialized v0.9 theme."""
|
||||
|
||||
primaryColor: str
|
||||
iconUrl: str
|
||||
agentDisplayName: str
|
||||
|
||||
|
||||
class CreateSurfaceDict(TypedDict, total=False):
|
||||
"""Serialized createSurface payload."""
|
||||
|
||||
surfaceId: str
|
||||
catalogId: str
|
||||
theme: ThemeDict
|
||||
sendDataModel: bool
|
||||
|
||||
|
||||
class UpdateComponentsDict(TypedDict, total=False):
|
||||
"""Serialized updateComponents payload."""
|
||||
|
||||
surfaceId: str
|
||||
components: list[dict[str, Any]]
|
||||
|
||||
|
||||
class UpdateDataModelDict(TypedDict, total=False):
|
||||
"""Serialized updateDataModel payload."""
|
||||
|
||||
surfaceId: str
|
||||
path: str
|
||||
value: Any
|
||||
|
||||
|
||||
class DeleteSurfaceV09Dict(TypedDict):
|
||||
"""Serialized v0.9 deleteSurface payload."""
|
||||
|
||||
surfaceId: str
|
||||
|
||||
|
||||
class A2UIMessageV09Dict(TypedDict, total=False):
|
||||
"""Serialized A2UI v0.9 server-to-client message with version and exactly one key set."""
|
||||
|
||||
version: Literal["v0.9"]
|
||||
createSurface: CreateSurfaceDict
|
||||
updateComponents: UpdateComponentsDict
|
||||
updateDataModel: UpdateDataModelDict
|
||||
deleteSurface: DeleteSurfaceV09Dict
|
||||
|
||||
|
||||
A2UIAnyMessageDict = A2UIMessageDict | A2UIMessageV09Dict
|
||||
|
||||
|
||||
def is_v09_message(msg: A2UIAnyMessageDict) -> TypeIs[A2UIMessageV09Dict]:
|
||||
"""Narrow a message dict to the v0.9 variant."""
|
||||
return msg.get("version") == "v0.9"
|
||||
|
||||
|
||||
def is_v08_message(msg: A2UIAnyMessageDict) -> TypeIs[A2UIMessageDict]:
|
||||
"""Narrow a message dict to the v0.8 variant."""
|
||||
return "version" not in msg
|
||||
|
||||
|
||||
@dataclass
|
||||
class A2UIConversationState:
|
||||
"""Tracks active A2UI surfaces and data models across a conversation."""
|
||||
|
||||
active_surfaces: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
||||
data_models: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
|
||||
last_a2ui_messages: list[A2UIAnyMessageDict] = Field(default_factory=list)
|
||||
initialized_surfaces: set[str] = Field(default_factory=set)
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
"""Return True when at least one surface has been initialized via beginRendering."""
|
||||
return bool(self.initialized_surfaces)
|
||||
|
||||
|
||||
class A2UIClientExtension:
|
||||
"""A2A client extension that adds A2UI support to agents.
|
||||
|
||||
Implements the ``A2AExtension`` protocol to inject A2UI prompt
|
||||
instructions, track UI state across conversations, and validate
|
||||
A2UI messages in responses.
|
||||
|
||||
Example::
|
||||
|
||||
A2AClientConfig(
|
||||
endpoint="...",
|
||||
extensions=["https://a2ui.org/a2a-extension/a2ui/v0.8"],
|
||||
client_extensions=[A2UIClientExtension()],
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
catalog_id: str | None = None,
|
||||
allowed_components: list[str] | None = None,
|
||||
version: str = "v0.8",
|
||||
) -> None:
|
||||
"""Initialize the A2UI client extension.
|
||||
|
||||
Args:
|
||||
catalog_id: Catalog identifier to use for prompt generation.
|
||||
allowed_components: Subset of component names to expose to the agent.
|
||||
version: Protocol version, ``"v0.8"`` or ``"v0.9"``.
|
||||
"""
|
||||
self._catalog_id = catalog_id
|
||||
self._allowed_components = allowed_components
|
||||
self._version = version
|
||||
|
||||
def inject_tools(self, agent: Agent) -> None:
|
||||
"""No-op — A2UI uses prompt augmentation rather than tool injection."""
|
||||
|
||||
def extract_state_from_history(
|
||||
self, conversation_history: Sequence[Message]
|
||||
) -> A2UIConversationState | None:
|
||||
"""Scan conversation history for A2UI DataParts and track surface state.
|
||||
|
||||
When ``catalog_id`` is set, only surfaces matching that catalog are tracked.
|
||||
"""
|
||||
state = A2UIConversationState()
|
||||
|
||||
for message in conversation_history:
|
||||
for part in message.parts:
|
||||
root = part.root
|
||||
if root.kind != "data":
|
||||
continue
|
||||
metadata = root.metadata or {}
|
||||
mime_type = metadata.get("mimeType", "")
|
||||
if mime_type != A2UI_MIME_TYPE:
|
||||
continue
|
||||
|
||||
data = root.data
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
surface_id = _get_surface_id(data)
|
||||
if not surface_id:
|
||||
continue
|
||||
|
||||
if self._catalog_id and "beginRendering" in data:
|
||||
catalog_id = data["beginRendering"].get("catalogId")
|
||||
if catalog_id and catalog_id != self._catalog_id:
|
||||
continue
|
||||
if self._catalog_id and "createSurface" in data:
|
||||
catalog_id = data["createSurface"].get("catalogId")
|
||||
if catalog_id and catalog_id != self._catalog_id:
|
||||
continue
|
||||
|
||||
if "deleteSurface" in data:
|
||||
state.active_surfaces.pop(surface_id, None)
|
||||
state.data_models.pop(surface_id, None)
|
||||
state.initialized_surfaces.discard(surface_id)
|
||||
elif "beginRendering" in data:
|
||||
state.initialized_surfaces.add(surface_id)
|
||||
state.active_surfaces[surface_id] = data["beginRendering"]
|
||||
elif "createSurface" in data:
|
||||
state.initialized_surfaces.add(surface_id)
|
||||
state.active_surfaces[surface_id] = data["createSurface"]
|
||||
elif "surfaceUpdate" in data:
|
||||
if surface_id not in state.initialized_surfaces:
|
||||
logger.warning(
|
||||
"surfaceUpdate for uninitialized surface %s",
|
||||
surface_id,
|
||||
)
|
||||
state.active_surfaces[surface_id] = data["surfaceUpdate"]
|
||||
elif "updateComponents" in data:
|
||||
if surface_id not in state.initialized_surfaces:
|
||||
logger.warning(
|
||||
"updateComponents for uninitialized surface %s",
|
||||
surface_id,
|
||||
)
|
||||
state.active_surfaces[surface_id] = data["updateComponents"]
|
||||
elif "dataModelUpdate" in data:
|
||||
contents = data["dataModelUpdate"].get("contents", [])
|
||||
state.data_models.setdefault(surface_id, []).extend(contents)
|
||||
elif "updateDataModel" in data:
|
||||
update = data["updateDataModel"]
|
||||
state.data_models.setdefault(surface_id, []).append(update)
|
||||
|
||||
if not state.active_surfaces and not state.data_models:
|
||||
return None
|
||||
return state
|
||||
|
||||
def augment_prompt(
|
||||
self,
|
||||
base_prompt: str,
|
||||
_conversation_state: A2UIConversationState | None,
|
||||
) -> str:
|
||||
"""Append A2UI system prompt instructions to the base prompt."""
|
||||
if self._version == "v0.9":
|
||||
a2ui_prompt = build_a2ui_v09_system_prompt(
|
||||
catalog_id=self._catalog_id,
|
||||
allowed_components=self._allowed_components,
|
||||
)
|
||||
else:
|
||||
a2ui_prompt = build_a2ui_system_prompt(
|
||||
catalog_id=self._catalog_id,
|
||||
allowed_components=self._allowed_components,
|
||||
)
|
||||
return f"{base_prompt}\n\n{a2ui_prompt}"
|
||||
|
||||
def process_response(
|
||||
self,
|
||||
agent_response: Any,
|
||||
conversation_state: A2UIConversationState | None,
|
||||
) -> Any:
|
||||
"""Extract and validate A2UI JSON from agent output.
|
||||
|
||||
When ``allowed_components`` is set, components not in the allowlist are
|
||||
logged and stripped from surface updates. Stores extracted A2UI messages
|
||||
on the conversation state and returns the original response unchanged.
|
||||
"""
|
||||
text = (
|
||||
agent_response if isinstance(agent_response, str) else str(agent_response)
|
||||
)
|
||||
results: list[A2UIAnyMessageDict]
|
||||
if self._version == "v0.9":
|
||||
results = list(_extract_and_validate_v09(text))
|
||||
if self._allowed_components:
|
||||
allowed = set(self._allowed_components)
|
||||
results = [
|
||||
_filter_components_v09(m, allowed)
|
||||
for m in results
|
||||
if is_v09_message(m)
|
||||
]
|
||||
else:
|
||||
results = list(_extract_and_validate(text))
|
||||
if self._allowed_components:
|
||||
allowed = set(self._allowed_components)
|
||||
results = [
|
||||
_filter_components(msg, allowed)
|
||||
for msg in results
|
||||
if is_v08_message(msg)
|
||||
]
|
||||
|
||||
if results and conversation_state is not None:
|
||||
conversation_state.last_a2ui_messages = results
|
||||
|
||||
return agent_response
|
||||
|
||||
def prepare_message_metadata(
|
||||
self,
|
||||
_conversation_state: A2UIConversationState | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Inject a2uiClientCapabilities into outbound A2A message metadata.
|
||||
|
||||
Per the A2UI extension spec, clients must declare supported catalog
|
||||
IDs in every outbound message's metadata. v0.9 nests capabilities
|
||||
under a ``"v0.9"`` key per ``client_capabilities.json``.
|
||||
"""
|
||||
if self._version == "v0.9":
|
||||
default_catalog = A2UI_V09_BASIC_CATALOG_ID
|
||||
catalog_ids = [default_catalog]
|
||||
if self._catalog_id and self._catalog_id != default_catalog:
|
||||
catalog_ids.append(self._catalog_id)
|
||||
return {
|
||||
"a2uiClientCapabilities": {
|
||||
"v0.9": {
|
||||
"supportedCatalogIds": catalog_ids,
|
||||
},
|
||||
},
|
||||
}
|
||||
catalog_ids = [A2UI_STANDARD_CATALOG_ID]
|
||||
if self._catalog_id and self._catalog_id != A2UI_STANDARD_CATALOG_ID:
|
||||
catalog_ids.append(self._catalog_id)
|
||||
return {
|
||||
"a2uiClientCapabilities": {
|
||||
"supportedCatalogIds": catalog_ids,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
_ALL_SURFACE_ID_KEYS = (
|
||||
"beginRendering",
|
||||
"surfaceUpdate",
|
||||
"dataModelUpdate",
|
||||
"deleteSurface",
|
||||
"createSurface",
|
||||
"updateComponents",
|
||||
"updateDataModel",
|
||||
)
|
||||
|
||||
|
||||
def _get_surface_id(data: dict[str, Any]) -> str | None:
|
||||
"""Extract surfaceId from any A2UI v0.8 or v0.9 message type."""
|
||||
for key in _ALL_SURFACE_ID_KEYS:
|
||||
inner = data.get(key)
|
||||
if isinstance(inner, dict):
|
||||
sid = inner.get("surfaceId")
|
||||
if isinstance(sid, str):
|
||||
return sid
|
||||
return None
|
||||
|
||||
|
||||
def _filter_components(msg: A2UIMessageDict, allowed: set[str]) -> A2UIMessageDict:
|
||||
"""Strip components whose type is not in *allowed* from a surfaceUpdate."""
|
||||
surface_update = msg.get("surfaceUpdate")
|
||||
if not isinstance(surface_update, dict):
|
||||
return msg
|
||||
|
||||
components = surface_update.get("components")
|
||||
if not isinstance(components, list):
|
||||
return msg
|
||||
|
||||
filtered = []
|
||||
for entry in components:
|
||||
component = entry.get("component", {})
|
||||
component_types = set(component.keys())
|
||||
disallowed = component_types - allowed
|
||||
if disallowed:
|
||||
logger.debug(
|
||||
"Stripping disallowed component type(s) %s from surface update",
|
||||
disallowed,
|
||||
)
|
||||
continue
|
||||
filtered.append(entry)
|
||||
|
||||
if len(filtered) == len(components):
|
||||
return msg
|
||||
|
||||
return {**msg, "surfaceUpdate": {**surface_update, "components": filtered}}
|
||||
|
||||
|
||||
def _filter_components_v09(
|
||||
msg: A2UIMessageV09Dict, allowed: set[str]
|
||||
) -> A2UIMessageV09Dict:
|
||||
"""Strip v0.9 components whose type is not in *allowed* from updateComponents.
|
||||
|
||||
v0.9 components use a flat structure where ``component`` is a type-name string.
|
||||
"""
|
||||
update = msg.get("updateComponents")
|
||||
if not isinstance(update, dict):
|
||||
return msg
|
||||
|
||||
components = update.get("components")
|
||||
if not isinstance(components, list):
|
||||
return msg
|
||||
|
||||
filtered = []
|
||||
for entry in components:
|
||||
comp_type = entry.get("component") if isinstance(entry, dict) else None
|
||||
if isinstance(comp_type, str) and comp_type not in allowed:
|
||||
logger.debug("Stripping disallowed v0.9 component type %s", comp_type)
|
||||
continue
|
||||
filtered.append(entry)
|
||||
|
||||
if len(filtered) == len(components):
|
||||
return msg
|
||||
|
||||
return {**msg, "updateComponents": {**update, "components": filtered}}
|
||||
|
||||
|
||||
def _extract_and_validate(text: str) -> list[A2UIMessageDict]:
|
||||
"""Extract A2UI v0.8 JSON objects from text and validate them."""
|
||||
return [
|
||||
dumped
|
||||
for candidate in extract_a2ui_json_objects(text)
|
||||
if (dumped := _try_validate(candidate)) is not None
|
||||
]
|
||||
|
||||
|
||||
def _try_validate(candidate: dict[str, Any]) -> A2UIMessageDict | None:
|
||||
"""Validate a single v0.8 A2UI candidate, returning None on failure."""
|
||||
try:
|
||||
msg = validate_a2ui_message(candidate)
|
||||
except A2UIValidationError:
|
||||
logger.debug(
|
||||
"Skipping invalid A2UI candidate in agent output",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
return cast(A2UIMessageDict, msg.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
def _extract_and_validate_v09(text: str) -> list[A2UIMessageV09Dict]:
|
||||
"""Extract and validate v0.9 A2UI JSON objects from text."""
|
||||
return [
|
||||
dumped
|
||||
for candidate in extract_a2ui_v09_json_objects(text)
|
||||
if (dumped := _try_validate_v09(candidate)) is not None
|
||||
]
|
||||
|
||||
|
||||
def _try_validate_v09(candidate: dict[str, Any]) -> A2UIMessageV09Dict | None:
|
||||
"""Validate a single v0.9 A2UI candidate, returning None on failure."""
|
||||
try:
|
||||
msg = validate_a2ui_message_v09(candidate)
|
||||
except A2UIValidationError:
|
||||
logger.debug(
|
||||
"Skipping invalid A2UI v0.9 candidate in agent output",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
return cast(A2UIMessageV09Dict, msg.model_dump(by_alias=True, exclude_none=True))
|
||||
277
lib/crewai/src/crewai/a2a/extensions/a2ui/models.py
Normal file
277
lib/crewai/src/crewai/a2a/extensions/a2ui/models.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""Pydantic models for A2UI server-to-client messages and client-to-server events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class BoundValue(BaseModel):
|
||||
"""A value that can be a literal or a data-model path reference."""
|
||||
|
||||
literal_string: str | None = Field(
|
||||
default=None, alias="literalString", description="Literal string value."
|
||||
)
|
||||
literal_number: float | None = Field(
|
||||
default=None, alias="literalNumber", description="Literal numeric value."
|
||||
)
|
||||
literal_boolean: bool | None = Field(
|
||||
default=None, alias="literalBoolean", description="Literal boolean value."
|
||||
)
|
||||
literal_array: list[str] | None = Field(
|
||||
default=None, alias="literalArray", description="Literal array of strings."
|
||||
)
|
||||
path: str | None = Field(default=None, description="Data-model path reference.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class MapEntry(BaseModel):
|
||||
"""A single entry in a valueMap adjacency list, supporting recursive nesting."""
|
||||
|
||||
key: str = Field(description="Entry key.")
|
||||
value_string: str | None = Field(
|
||||
default=None, alias="valueString", description="String value."
|
||||
)
|
||||
value_number: float | None = Field(
|
||||
default=None, alias="valueNumber", description="Numeric value."
|
||||
)
|
||||
value_boolean: bool | None = Field(
|
||||
default=None, alias="valueBoolean", description="Boolean value."
|
||||
)
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DataEntry(BaseModel):
|
||||
"""A data model entry with a key and exactly one typed value."""
|
||||
|
||||
key: str = Field(description="Entry key.")
|
||||
value_string: str | None = Field(
|
||||
default=None, alias="valueString", description="String value."
|
||||
)
|
||||
value_number: float | None = Field(
|
||||
default=None, alias="valueNumber", description="Numeric value."
|
||||
)
|
||||
value_boolean: bool | None = Field(
|
||||
default=None, alias="valueBoolean", description="Boolean value."
|
||||
)
|
||||
value_map: list[MapEntry] | None = Field(
|
||||
default=None, alias="valueMap", description="Nested map entries."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
_HEX_COLOR_PATTERN: re.Pattern[str] = re.compile(r"^#[0-9a-fA-F]{6}$")
|
||||
|
||||
|
||||
class Styles(BaseModel):
|
||||
"""Surface styling information."""
|
||||
|
||||
font: str | None = Field(default=None, description="Font family name.")
|
||||
primary_color: str | None = Field(
|
||||
default=None,
|
||||
alias="primaryColor",
|
||||
pattern=_HEX_COLOR_PATTERN.pattern,
|
||||
description="Primary color as a hex string.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class ComponentEntry(BaseModel):
|
||||
"""A single component in a UI widget tree.
|
||||
|
||||
The ``component`` dict must contain exactly one key — the component type
|
||||
name (e.g. ``"Text"``, ``"Button"``) — whose value holds the component
|
||||
properties. Component internals are left as ``dict[str, Any]`` because
|
||||
they are catalog-dependent; use the typed helpers in ``catalog.py`` for
|
||||
the standard catalog.
|
||||
"""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
weight: float | None = Field(
|
||||
default=None, description="Flex weight for layout distribution."
|
||||
)
|
||||
component: dict[str, Any] = Field(
|
||||
description="Component type name mapped to its properties."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class BeginRendering(BaseModel):
|
||||
"""Signals the client to begin rendering a surface."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Unique surface identifier.")
|
||||
root: str = Field(description="Component ID of the root element.")
|
||||
catalog_id: str | None = Field(
|
||||
default=None,
|
||||
alias="catalogId",
|
||||
description="Catalog identifier for the surface.",
|
||||
)
|
||||
styles: Styles | None = Field(
|
||||
default=None, description="Surface styling overrides."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class SurfaceUpdate(BaseModel):
|
||||
"""Updates a surface with a new set of components."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
|
||||
components: list[ComponentEntry] = Field(
|
||||
min_length=1, description="Components to render on the surface."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DataModelUpdate(BaseModel):
|
||||
"""Updates the data model for a surface."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
|
||||
path: str | None = Field(
|
||||
default=None, description="Data-model path prefix for the update."
|
||||
)
|
||||
contents: list[DataEntry] = Field(
|
||||
description="Data entries to merge into the model."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DeleteSurface(BaseModel):
|
||||
"""Signals the client to delete a surface."""
|
||||
|
||||
surface_id: str = Field(
|
||||
alias="surfaceId", description="Surface identifier to delete."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class A2UIMessage(BaseModel):
|
||||
"""Union wrapper for the four server-to-client A2UI message types.
|
||||
|
||||
Exactly one of the fields must be set.
|
||||
"""
|
||||
|
||||
begin_rendering: BeginRendering | None = Field(
|
||||
default=None,
|
||||
alias="beginRendering",
|
||||
description="Begin rendering a new surface.",
|
||||
)
|
||||
surface_update: SurfaceUpdate | None = Field(
|
||||
default=None,
|
||||
alias="surfaceUpdate",
|
||||
description="Update components on a surface.",
|
||||
)
|
||||
data_model_update: DataModelUpdate | None = Field(
|
||||
default=None,
|
||||
alias="dataModelUpdate",
|
||||
description="Update the surface data model.",
|
||||
)
|
||||
delete_surface: DeleteSurface | None = Field(
|
||||
default=None, alias="deleteSurface", description="Delete an existing surface."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> A2UIMessage:
|
||||
"""Enforce the spec's exactly-one-of constraint."""
|
||||
fields = [
|
||||
self.begin_rendering,
|
||||
self.surface_update,
|
||||
self.data_model_update,
|
||||
self.delete_surface,
|
||||
]
|
||||
count = sum(f is not None for f in fields)
|
||||
if count != 1:
|
||||
raise ValueError(f"Exactly one A2UI message type must be set, got {count}")
|
||||
return self
|
||||
|
||||
|
||||
class UserAction(BaseModel):
|
||||
"""Reports a user-initiated action from a component."""
|
||||
|
||||
name: str = Field(description="Action name.")
|
||||
surface_id: str = Field(alias="surfaceId", description="Source surface identifier.")
|
||||
source_component_id: str = Field(
|
||||
alias="sourceComponentId", description="Component that triggered the action."
|
||||
)
|
||||
timestamp: str = Field(description="ISO 8601 timestamp of the action.")
|
||||
context: dict[str, Any] = Field(description="Action context payload.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class ClientError(BaseModel):
|
||||
"""Reports a client-side error."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class A2UIEvent(BaseModel):
|
||||
"""Union wrapper for client-to-server events."""
|
||||
|
||||
user_action: UserAction | None = Field(
|
||||
default=None, alias="userAction", description="User-initiated action event."
|
||||
)
|
||||
error: ClientError | None = Field(
|
||||
default=None, description="Client-side error report."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> A2UIEvent:
|
||||
"""Enforce the spec's exactly-one-of constraint."""
|
||||
fields = [self.user_action, self.error]
|
||||
count = sum(f is not None for f in fields)
|
||||
if count != 1:
|
||||
raise ValueError(f"Exactly one A2UI event type must be set, got {count}")
|
||||
return self
|
||||
|
||||
|
||||
class A2UIResponse(BaseModel):
|
||||
"""Typed wrapper for responses containing A2UI messages."""
|
||||
|
||||
text: str = Field(description="Raw text content of the response.")
|
||||
a2ui_parts: list[dict[str, Any]] = Field(
|
||||
default_factory=list, description="A2UI DataParts extracted from the response."
|
||||
)
|
||||
a2ui_messages: list[dict[str, Any]] = Field(
|
||||
default_factory=list, description="Validated A2UI message dicts."
|
||||
)
|
||||
|
||||
|
||||
_A2UI_KEYS = {"beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface"}
|
||||
|
||||
|
||||
def extract_a2ui_json_objects(text: str) -> list[dict[str, Any]]:
|
||||
"""Extract JSON objects containing A2UI keys from text.
|
||||
|
||||
Uses ``json.JSONDecoder.raw_decode`` for robust parsing that correctly
|
||||
handles braces inside string literals.
|
||||
"""
|
||||
decoder = json.JSONDecoder()
|
||||
results: list[dict[str, Any]] = []
|
||||
idx = 0
|
||||
while idx < len(text):
|
||||
idx = text.find("{", idx)
|
||||
if idx == -1:
|
||||
break
|
||||
try:
|
||||
obj, end_idx = decoder.raw_decode(text, idx)
|
||||
if isinstance(obj, dict) and _A2UI_KEYS & obj.keys():
|
||||
results.append(obj)
|
||||
idx = end_idx
|
||||
except json.JSONDecodeError:
|
||||
idx += 1
|
||||
return results
|
||||
150
lib/crewai/src/crewai/a2a/extensions/a2ui/prompt.py
Normal file
150
lib/crewai/src/crewai/a2a/extensions/a2ui/prompt.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""System prompt generation for A2UI-capable agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from crewai.a2a.extensions.a2ui.catalog import STANDARD_CATALOG_COMPONENTS
|
||||
from crewai.a2a.extensions.a2ui.schema import load_schema
|
||||
from crewai.a2a.extensions.a2ui.server_extension import (
|
||||
A2UI_EXTENSION_URI,
|
||||
A2UI_V09_BASIC_CATALOG_ID,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.v0_9 import (
|
||||
BASIC_CATALOG_COMPONENTS as V09_CATALOG_COMPONENTS,
|
||||
BASIC_CATALOG_FUNCTIONS,
|
||||
)
|
||||
|
||||
|
||||
def build_a2ui_system_prompt(
|
||||
catalog_id: str | None = None,
|
||||
allowed_components: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Build a v0.8 system prompt fragment instructing the LLM to produce A2UI output.
|
||||
|
||||
Args:
|
||||
catalog_id: Catalog identifier to reference. Defaults to the
|
||||
standard catalog version derived from ``A2UI_EXTENSION_URI``.
|
||||
allowed_components: Subset of component names to expose. When
|
||||
``None``, all standard catalog components are available.
|
||||
|
||||
Returns:
|
||||
A system prompt string to append to the agent's instructions.
|
||||
"""
|
||||
components = sorted(
|
||||
allowed_components
|
||||
if allowed_components is not None
|
||||
else STANDARD_CATALOG_COMPONENTS
|
||||
)
|
||||
|
||||
catalog_label = catalog_id or f"standard ({A2UI_EXTENSION_URI.rsplit('/', 1)[-1]})"
|
||||
|
||||
resolved_schema = load_schema(
|
||||
"server_to_client_with_standard_catalog", version="v0.8"
|
||||
)
|
||||
schema_json = json.dumps(resolved_schema, indent=2)
|
||||
|
||||
return f"""\
|
||||
<A2UI_INSTRUCTIONS>
|
||||
You can generate rich, declarative UI by emitting A2UI JSON messages.
|
||||
|
||||
CATALOG: {catalog_label}
|
||||
AVAILABLE COMPONENTS: {", ".join(components)}
|
||||
|
||||
MESSAGE TYPES (emit exactly ONE per message):
|
||||
- beginRendering: Initialize a new surface with a root component and optional styles.
|
||||
- surfaceUpdate: Send/update components for a surface. Each component has a unique id \
|
||||
and a "component" wrapper containing exactly one component-type key.
|
||||
- dataModelUpdate: Update the data model for a surface. Data entries have a key and \
|
||||
one typed value (valueString, valueNumber, valueBoolean, valueMap).
|
||||
- deleteSurface: Remove a surface.
|
||||
|
||||
DATA BINDING:
|
||||
- Use {{"literalString": "..."}} for inline string values.
|
||||
- Use {{"literalNumber": ...}} for inline numeric values.
|
||||
- Use {{"literalBoolean": ...}} for inline boolean values.
|
||||
- Use {{"literalArray": ["...", "..."]}} for inline array values.
|
||||
- Use {{"path": "/data/model/path"}} to bind to data model values.
|
||||
|
||||
ACTIONS:
|
||||
- Interactive components (Button, etc.) have an "action" with a "name" and optional \
|
||||
"context" array of key/value pairs.
|
||||
- Values in action context can use data binding (path or literal).
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Emit each A2UI message as a valid JSON object. When generating UI, produce a \
|
||||
beginRendering message first, then surfaceUpdate messages with components, and \
|
||||
optionally dataModelUpdate messages to populate data-bound values.
|
||||
|
||||
SCHEMA:
|
||||
{schema_json}
|
||||
</A2UI_INSTRUCTIONS>"""
|
||||
|
||||
|
||||
def build_a2ui_v09_system_prompt(
|
||||
catalog_id: str | None = None,
|
||||
allowed_components: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Build a v0.9 system prompt fragment instructing the LLM to produce A2UI output.
|
||||
|
||||
Args:
|
||||
catalog_id: Catalog identifier to reference. Defaults to the
|
||||
v0.9 basic catalog.
|
||||
allowed_components: Subset of component names to expose. When
|
||||
``None``, all basic catalog components are available.
|
||||
|
||||
Returns:
|
||||
A system prompt string to append to the agent's instructions.
|
||||
"""
|
||||
components = sorted(
|
||||
allowed_components if allowed_components is not None else V09_CATALOG_COMPONENTS
|
||||
)
|
||||
|
||||
catalog_label = catalog_id or A2UI_V09_BASIC_CATALOG_ID
|
||||
functions = sorted(BASIC_CATALOG_FUNCTIONS)
|
||||
|
||||
envelope_schema = load_schema("server_to_client", version="v0.9")
|
||||
schema_json = json.dumps(envelope_schema, indent=2)
|
||||
|
||||
return f"""\
|
||||
<A2UI_INSTRUCTIONS>
|
||||
You can generate rich, declarative UI by emitting A2UI v0.9 JSON messages.
|
||||
Every message MUST include "version": "v0.9".
|
||||
|
||||
CATALOG: {catalog_label}
|
||||
AVAILABLE COMPONENTS: {", ".join(components)}
|
||||
AVAILABLE FUNCTIONS: {", ".join(functions)}
|
||||
|
||||
MESSAGE TYPES (emit exactly ONE per message alongside "version": "v0.9"):
|
||||
- createSurface: Create a new surface. Requires surfaceId and catalogId. \
|
||||
Optionally includes theme (primaryColor, iconUrl, agentDisplayName) and \
|
||||
sendDataModel (boolean).
|
||||
- updateComponents: Send/update components for a surface. Each component is a flat \
|
||||
object with "id", "component" (type name string), and type-specific properties at the \
|
||||
top level. One component MUST have id "root".
|
||||
- updateDataModel: Update the data model. Uses "path" (JSON Pointer) and "value" \
|
||||
(any JSON type). Omit "value" to delete the key at path.
|
||||
- deleteSurface: Remove a surface by surfaceId.
|
||||
|
||||
COMPONENT FORMAT (flat, NOT nested):
|
||||
{{"id": "myText", "component": "Text", "text": "Hello world", "variant": "h1"}}
|
||||
{{"id": "myBtn", "component": "Button", "child": "myText", "action": {{"event": \
|
||||
{{"name": "click"}}}}}}
|
||||
|
||||
DATA BINDING:
|
||||
- Use plain values for literals: "text": "Hello" or "value": 42
|
||||
- Use {{"path": "/data/model/path"}} to bind to data model values.
|
||||
- Use {{"call": "functionName", "args": {{...}}}} for client-side functions.
|
||||
|
||||
ACTIONS:
|
||||
- Server event: {{"event": {{"name": "actionName", "context": {{"key": "value"}}}}}}
|
||||
- Local function: {{"functionCall": {{"call": "openUrl", "args": {{"url": "..."}}}}}}
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Emit each A2UI message as a valid JSON object. When generating UI, first emit a \
|
||||
createSurface message with the catalogId, then updateComponents messages with \
|
||||
components (one must have id "root"), and optionally updateDataModel messages.
|
||||
|
||||
ENVELOPE SCHEMA:
|
||||
{schema_json}
|
||||
</A2UI_INSTRUCTIONS>"""
|
||||
74
lib/crewai/src/crewai/a2a/extensions/a2ui/schema/__init__.py
Normal file
74
lib/crewai/src/crewai/a2a/extensions/a2ui/schema/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Schema loading utilities for vendored A2UI JSON schemas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
_V08_DIR = Path(__file__).parent / "v0_8"
|
||||
_V09_DIR = Path(__file__).parent / "v0_9"
|
||||
|
||||
_SCHEMA_CACHE: dict[str, dict[str, Any]] = {}
|
||||
|
||||
SCHEMA_NAMES: frozenset[str] = frozenset(
|
||||
{
|
||||
"server_to_client",
|
||||
"client_to_server",
|
||||
"standard_catalog_definition",
|
||||
"server_to_client_with_standard_catalog",
|
||||
}
|
||||
)
|
||||
|
||||
V09_SCHEMA_NAMES: frozenset[str] = frozenset(
|
||||
{
|
||||
"server_to_client",
|
||||
"client_to_server",
|
||||
"common_types",
|
||||
"basic_catalog",
|
||||
"client_capabilities",
|
||||
"server_capabilities",
|
||||
"client_data_model",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def load_schema(name: str, *, version: str = "v0.8") -> dict[str, Any]:
|
||||
"""Load a vendored A2UI JSON schema by name and version.
|
||||
|
||||
Args:
|
||||
name: Schema name without extension, e.g. ``"server_to_client"``.
|
||||
version: Protocol version, ``"v0.8"`` or ``"v0.9"``.
|
||||
|
||||
Returns:
|
||||
Parsed JSON schema dict.
|
||||
|
||||
Raises:
|
||||
ValueError: If the schema name or version is not recognized.
|
||||
FileNotFoundError: If the schema file is missing from the package.
|
||||
"""
|
||||
if version == "v0.8":
|
||||
valid_names = SCHEMA_NAMES
|
||||
schema_dir = _V08_DIR
|
||||
elif version == "v0.9":
|
||||
valid_names = V09_SCHEMA_NAMES
|
||||
schema_dir = _V09_DIR
|
||||
else:
|
||||
raise ValueError(f"Unknown version {version!r}. Available: v0.8, v0.9")
|
||||
|
||||
if name not in valid_names:
|
||||
raise ValueError(
|
||||
f"Unknown schema {name!r} for {version}. Available: {sorted(valid_names)}"
|
||||
)
|
||||
|
||||
cache_key = f"{version}/{name}"
|
||||
if cache_key in _SCHEMA_CACHE:
|
||||
return _SCHEMA_CACHE[cache_key]
|
||||
|
||||
path = schema_dir / f"{name}.json"
|
||||
with path.open() as f:
|
||||
schema: dict[str, Any] = json.load(f)
|
||||
|
||||
_SCHEMA_CACHE[cache_key] = schema
|
||||
return schema
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"title": "A2UI (Agent to UI) Client-to-Server Event Schema",
|
||||
"description": "Describes a JSON payload for a client-to-server event message.",
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"properties": {
|
||||
"userAction": {
|
||||
"type": "object",
|
||||
"description": "Reports a user-initiated action from a component.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the action, taken from the component's action.name property."
|
||||
},
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The id of the surface where the event originated."
|
||||
},
|
||||
"sourceComponentId": {
|
||||
"type": "string",
|
||||
"description": "The id of the component that triggered the event."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "An ISO 8601 timestamp of when the event occurred."
|
||||
},
|
||||
"context": {
|
||||
"type": "object",
|
||||
"description": "A JSON object containing the key-value pairs from the component's action.context, after resolving all data bindings.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"surfaceId",
|
||||
"sourceComponentId",
|
||||
"timestamp",
|
||||
"context"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"description": "Reports a client-side error. The content is flexible.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{ "required": ["userAction"] },
|
||||
{ "required": ["error"] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"title": "A2UI Message Schema",
|
||||
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"beginRendering": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be rendered."
|
||||
},
|
||||
"catalogId": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the component catalog to use for this surface. If omitted, the client MUST default to the standard catalog for this A2UI version (https://a2ui.org/specification/v0_8/standard_catalog_definition.json)."
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"description": "The ID of the root component to render."
|
||||
},
|
||||
"styles": {
|
||||
"type": "object",
|
||||
"description": "Styling information for the UI.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["root", "surfaceId"]
|
||||
},
|
||||
"surfaceUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates a surface with a new set of components.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "A list containing all UI components for the surface.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this component."
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column."
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"description": "A wrapper object that MUST contain exactly one key, which is the name of the component type. The value is an object containing the properties for that specific component.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["id", "component"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "components"]
|
||||
},
|
||||
"dataModelUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates the data model for a surface.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface this data model update applies to."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced."
|
||||
},
|
||||
"contents": {
|
||||
"type": "array",
|
||||
"description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key for this data entry."
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"valueMap": {
|
||||
"description": "Represents a map as an adjacency list.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["contents", "surfaceId"]
|
||||
},
|
||||
"deleteSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to delete the surface identified by 'surfaceId'.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be deleted."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
{
|
||||
"title": "A2UI Message Schema",
|
||||
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"beginRendering": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be rendered."
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"description": "The ID of the root component to render."
|
||||
},
|
||||
"styles": {
|
||||
"type": "object",
|
||||
"description": "Styling information for the UI.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"description": "The primary font for the UI."
|
||||
},
|
||||
"primaryColor": {
|
||||
"type": "string",
|
||||
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["root", "surfaceId"]
|
||||
},
|
||||
"surfaceUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates a surface with a new set of components.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "A list containing all UI components for the surface.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this component."
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column."
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"Text": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.",
|
||||
"enum": [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"caption",
|
||||
"body"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"Image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fit": {
|
||||
"type": "string",
|
||||
"description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.",
|
||||
"enum": [
|
||||
"contain",
|
||||
"cover",
|
||||
"fill",
|
||||
"none",
|
||||
"scale-down"
|
||||
]
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.",
|
||||
"enum": [
|
||||
"icon",
|
||||
"avatar",
|
||||
"smallFeature",
|
||||
"mediumFeature",
|
||||
"largeFeature",
|
||||
"header"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Icon": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "object",
|
||||
"description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"accountCircle",
|
||||
"add",
|
||||
"arrowBack",
|
||||
"arrowForward",
|
||||
"attachFile",
|
||||
"calendarToday",
|
||||
"call",
|
||||
"camera",
|
||||
"check",
|
||||
"close",
|
||||
"delete",
|
||||
"download",
|
||||
"edit",
|
||||
"event",
|
||||
"error",
|
||||
"favorite",
|
||||
"favoriteOff",
|
||||
"folder",
|
||||
"help",
|
||||
"home",
|
||||
"info",
|
||||
"locationOn",
|
||||
"lock",
|
||||
"lockOpen",
|
||||
"mail",
|
||||
"menu",
|
||||
"moreVert",
|
||||
"moreHoriz",
|
||||
"notificationsOff",
|
||||
"notifications",
|
||||
"payment",
|
||||
"person",
|
||||
"phone",
|
||||
"photo",
|
||||
"print",
|
||||
"refresh",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share",
|
||||
"shoppingCart",
|
||||
"star",
|
||||
"starHalf",
|
||||
"starOff",
|
||||
"upload",
|
||||
"visibility",
|
||||
"visibilityOff",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"Video": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"AudioPlayer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Row": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.",
|
||||
"enum": [
|
||||
"center",
|
||||
"end",
|
||||
"spaceAround",
|
||||
"spaceBetween",
|
||||
"spaceEvenly",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Column": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.",
|
||||
"enum": [
|
||||
"start",
|
||||
"center",
|
||||
"end",
|
||||
"spaceBetween",
|
||||
"spaceAround",
|
||||
"spaceEvenly"
|
||||
]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.",
|
||||
"enum": ["center", "end", "start", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"List": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "The direction in which the list items are laid out.",
|
||||
"enum": ["vertical", "horizontal"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis.",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Card": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be rendered inside the card."
|
||||
}
|
||||
},
|
||||
"required": ["child"]
|
||||
},
|
||||
"Tabs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabItems": {
|
||||
"type": "array",
|
||||
"description": "An array of objects, where each object defines a tab with a title and a child component.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "object",
|
||||
"description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "child"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tabItems"]
|
||||
},
|
||||
"Divider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"axis": {
|
||||
"type": "string",
|
||||
"description": "The orientation of the divider.",
|
||||
"enum": ["horizontal", "vertical"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Modal": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"entryPointChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component that opens the modal when interacted with (e.g., a button)."
|
||||
},
|
||||
"contentChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be displayed inside the modal."
|
||||
}
|
||||
},
|
||||
"required": ["entryPointChild", "contentChild"]
|
||||
},
|
||||
"Button": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to display in the button, typically a Text component."
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if this button should be styled as the primary action."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"literalNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"literalBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["child", "action"]
|
||||
},
|
||||
"CheckBox": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
},
|
||||
"TextField": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"textFieldType": {
|
||||
"type": "string",
|
||||
"description": "The type of input field to display.",
|
||||
"enum": [
|
||||
"date",
|
||||
"longText",
|
||||
"number",
|
||||
"shortText",
|
||||
"obscured"
|
||||
]
|
||||
},
|
||||
"validationRegexp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used for client-side validation of the input."
|
||||
}
|
||||
},
|
||||
"required": ["label"]
|
||||
},
|
||||
"DateTimeInput": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The selected date and/or time value in ISO 8601 format. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableDate": {
|
||||
"type": "boolean",
|
||||
"description": "If true, allows the user to select a date."
|
||||
},
|
||||
"enableTime": {
|
||||
"type": "boolean",
|
||||
"description": "If true, allows the user to select a time."
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
},
|
||||
"MultipleChoice": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"selections": {
|
||||
"type": "object",
|
||||
"description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalArray": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "An array of available options for the user to choose from.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to be associated with this option when selected."
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
}
|
||||
},
|
||||
"maxAllowedSelections": {
|
||||
"type": "integer",
|
||||
"description": "The maximum number of options that the user is allowed to select."
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["checkbox", "chips"],
|
||||
"description": "The visual variant for the selection UI."
|
||||
},
|
||||
"filterable": {
|
||||
"type": "boolean",
|
||||
"description": "Whether options can be filtered by typing."
|
||||
}
|
||||
},
|
||||
"required": ["selections", "options"]
|
||||
},
|
||||
"Slider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minValue": {
|
||||
"type": "number",
|
||||
"description": "The minimum value of the slider."
|
||||
},
|
||||
"maxValue": {
|
||||
"type": "number",
|
||||
"description": "The maximum value of the slider."
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "component"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "components"]
|
||||
},
|
||||
"dataModelUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates the data model for a surface.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface this data model update applies to."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced."
|
||||
},
|
||||
"contents": {
|
||||
"type": "array",
|
||||
"description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key for this data entry."
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"valueMap": {
|
||||
"description": "Represents a map as an adjacency list.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["contents", "surfaceId"]
|
||||
},
|
||||
"deleteSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to delete the surface identified by 'surfaceId'.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be deleted."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
{
|
||||
"components": {
|
||||
"Text": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the base text style.",
|
||||
"enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"]
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"Image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the image to display.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"fit": {
|
||||
"type": "string",
|
||||
"description": "Specifies how the image should be resized to fit its container.",
|
||||
"enum": ["contain", "cover", "fill", "none", "scale-down"]
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the image size and style.",
|
||||
"enum": ["icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"]
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Icon": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "object",
|
||||
"description": "The name of the icon to display.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"accountCircle", "add", "arrowBack", "arrowForward", "attachFile",
|
||||
"calendarToday", "call", "camera", "check", "close", "delete",
|
||||
"download", "edit", "event", "error", "favorite", "favoriteOff",
|
||||
"folder", "help", "home", "info", "locationOn", "lock", "lockOpen",
|
||||
"mail", "menu", "moreVert", "moreHoriz", "notificationsOff",
|
||||
"notifications", "payment", "person", "phone", "photo", "print",
|
||||
"refresh", "search", "send", "settings", "share", "shoppingCart",
|
||||
"star", "starHalf", "starOff", "upload", "visibility",
|
||||
"visibilityOff", "warning"
|
||||
]
|
||||
},
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"Video": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the video to display.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"AudioPlayer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the audio to be played.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the audio, such as a title or summary.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Row": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": { "type": "array", "items": { "type": "string" } },
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": { "type": "string" },
|
||||
"dataBinding": { "type": "string" }
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"enum": ["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Column": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": { "type": "array", "items": { "type": "string" } },
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": { "type": "string" },
|
||||
"dataBinding": { "type": "string" }
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"enum": ["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"enum": ["center", "end", "start", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"List": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"explicitList": { "type": "array", "items": { "type": "string" } },
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"componentId": { "type": "string" },
|
||||
"dataBinding": { "type": "string" }
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"enum": ["vertical", "horizontal"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Card": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be rendered inside the card."
|
||||
}
|
||||
},
|
||||
"required": ["child"]
|
||||
},
|
||||
"Tabs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tabItems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"child": { "type": "string" }
|
||||
},
|
||||
"required": ["title", "child"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tabItems"]
|
||||
},
|
||||
"Divider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"axis": {
|
||||
"type": "string",
|
||||
"enum": ["horizontal", "vertical"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Modal": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"entryPointChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component that opens the modal when interacted with."
|
||||
},
|
||||
"contentChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be displayed inside the modal."
|
||||
}
|
||||
},
|
||||
"required": ["entryPointChild", "contentChild"]
|
||||
},
|
||||
"Button": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to display in the button."
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if this button should be styled as the primary action."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "type": "string" },
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": { "type": "string" },
|
||||
"literalString": { "type": "string" },
|
||||
"literalNumber": { "type": "number" },
|
||||
"literalBoolean": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["child", "action"]
|
||||
},
|
||||
"CheckBox": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalBoolean": { "type": "boolean" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
},
|
||||
"TextField": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"textFieldType": {
|
||||
"type": "string",
|
||||
"enum": ["date", "longText", "number", "shortText", "obscured"]
|
||||
},
|
||||
"validationRegexp": { "type": "string" }
|
||||
},
|
||||
"required": ["label"]
|
||||
},
|
||||
"DateTimeInput": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"enableDate": { "type": "boolean" },
|
||||
"enableTime": { "type": "boolean" }
|
||||
},
|
||||
"required": ["value"]
|
||||
},
|
||||
"MultipleChoice": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"selections": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalArray": { "type": "array", "items": { "type": "string" } },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalString": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"value": { "type": "string" }
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
}
|
||||
},
|
||||
"maxAllowedSelections": { "type": "integer" },
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["checkbox", "chips"]
|
||||
},
|
||||
"filterable": { "type": "boolean" }
|
||||
},
|
||||
"required": ["selections", "options"]
|
||||
},
|
||||
"Slider": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"literalNumber": { "type": "number" },
|
||||
"path": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"minValue": { "type": "number" },
|
||||
"maxValue": { "type": "number" }
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
},
|
||||
"styles": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"description": "The primary font for the UI."
|
||||
},
|
||||
"primaryColor": {
|
||||
"type": "string",
|
||||
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://a2ui.org/specification/v0_9/client_capabilities.json",
|
||||
"title": "A2UI Client Capabilities Schema",
|
||||
"description": "A schema for the a2uiClientCapabilities object, which is sent from the client to the server as part of the A2A metadata to describe the client's UI rendering capabilities.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"v0.9": {
|
||||
"type": "object",
|
||||
"description": "The capabilities structure for version 0.9 of the A2UI protocol.",
|
||||
"properties": {
|
||||
"supportedCatalogIds": {
|
||||
"type": "array",
|
||||
"description": "The URI of each of the component and function catalogs that is supported by the client.",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"inlineCatalogs": {
|
||||
"type": "array",
|
||||
"description": "An array of inline catalog definitions, which can contain both components and functions. This should only be provided if the agent declares 'acceptsInlineCatalogs: true' in its capabilities.",
|
||||
"items": { "$ref": "#/$defs/Catalog" }
|
||||
}
|
||||
},
|
||||
"required": ["supportedCatalogIds"]
|
||||
}
|
||||
},
|
||||
"required": ["v0.9"],
|
||||
"$defs": {
|
||||
"FunctionDefinition": {
|
||||
"type": "object",
|
||||
"description": "Describes a function's interface.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The unique name of the function."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A human-readable description of what the function does and how to use it."
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"description": "A JSON Schema describing the expected arguments (args) for this function.",
|
||||
"$ref": "https://json-schema.org/draft/2020-12/schema"
|
||||
},
|
||||
"returnType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
"any",
|
||||
"void"
|
||||
],
|
||||
"description": "The type of value this function returns."
|
||||
}
|
||||
},
|
||||
"required": ["name", "parameters", "returnType"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Catalog": {
|
||||
"type": "object",
|
||||
"description": "A collection of component and function definitions.",
|
||||
"properties": {
|
||||
"catalogId": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for this catalog."
|
||||
},
|
||||
"components": {
|
||||
"type": "object",
|
||||
"description": "Definitions for UI components supported by this catalog.",
|
||||
"additionalProperties": {
|
||||
"$ref": "https://json-schema.org/draft/2020-12/schema"
|
||||
}
|
||||
},
|
||||
"functions": {
|
||||
"type": "array",
|
||||
"description": "Definitions for functions supported by this catalog.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/FunctionDefinition"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "A2UI Theme",
|
||||
"description": "A schema that defines a catalog of A2UI theme properties. Each key is a theme property name (e.g. 'primaryColor'), and each value is the JSON schema for that property.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "https://json-schema.org/draft/2020-12/schema"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["catalogId"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://a2ui.org/specification/v0_9/client_data_model.json",
|
||||
"title": "A2UI Client Data Model Schema",
|
||||
"description": "Schema for attaching the client data model to A2A message metadata. This object should be placed in the `a2uiClientDataModel` field of the metadata.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "v0.9"
|
||||
},
|
||||
"surfaces": {
|
||||
"type": "object",
|
||||
"description": "A map of surface IDs to their current data models.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"description": "The current data model for the surface, as a standard JSON object."
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["version", "surfaces"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"title": "A2UI (Agent to UI) Client-to-Server Event Schema",
|
||||
"description": "Describes a JSON payload for a client-to-server event message.",
|
||||
"type": "object",
|
||||
"minProperties": 2,
|
||||
"maxProperties": 2,
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "v0.9"
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"description": "Reports a user-initiated action from a component.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the action, taken from the component's action.event.name property."
|
||||
},
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The id of the surface where the event originated."
|
||||
},
|
||||
"sourceComponentId": {
|
||||
"type": "string",
|
||||
"description": "The id of the component that triggered the event."
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "An ISO 8601 timestamp of when the event occurred."
|
||||
},
|
||||
"context": {
|
||||
"type": "object",
|
||||
"description": "A JSON object containing the key-value pairs from the component's action.event.context, after resolving all data bindings.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"surfaceId",
|
||||
"sourceComponentId",
|
||||
"timestamp",
|
||||
"context"
|
||||
]
|
||||
},
|
||||
"error": {
|
||||
"description": "Reports a client-side error.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Validation Failed Error",
|
||||
"properties": {
|
||||
"code": {
|
||||
"const": "VALIDATION_FAILED"
|
||||
},
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The id of the surface where the error occurred."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The JSON pointer to the field that failed validation (e.g. '/components/0/text')."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "A short one or two sentence description of why validation failed."
|
||||
}
|
||||
},
|
||||
"required": ["code", "path", "message", "surfaceId"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "Generic Error",
|
||||
"properties": {
|
||||
"code": {
|
||||
"not": {
|
||||
"const": "VALIDATION_FAILED"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "A short one or two sentence description of why the error occurred."
|
||||
},
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The id of the surface where the error occurred."
|
||||
}
|
||||
},
|
||||
"required": ["code", "surfaceId", "message"],
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["action", "version"]
|
||||
},
|
||||
{
|
||||
"required": ["error", "version"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://a2ui.org/specification/v0_9/common_types.json",
|
||||
"title": "A2UI Common Types",
|
||||
"description": "Common type definitions used across A2UI schemas.",
|
||||
"$defs": {
|
||||
"ComponentId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for a component, used for both definitions and references within the same surface."
|
||||
},
|
||||
"AccessibilityAttributes": {
|
||||
"type": "object",
|
||||
"description": "Attributes to enhance accessibility when using assistive technologies like screen readers.",
|
||||
"properties": {
|
||||
"label": {
|
||||
"$ref": "#/$defs/DynamicString",
|
||||
"description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'."
|
||||
},
|
||||
"description": {
|
||||
"$ref": "#/$defs/DynamicString",
|
||||
"description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComponentCommon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/ComponentId"
|
||||
},
|
||||
"accessibility": {
|
||||
"$ref": "#/$defs/AccessibilityAttributes"
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
},
|
||||
"ChildList": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ComponentId"
|
||||
},
|
||||
"description": "A static list of child component IDs."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.",
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"$ref": "#/$defs/ComponentId"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path to the list of component property objects in the data model."
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "path"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"DataBinding": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "A JSON Pointer path to a value in the data model."
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DynamicValue": {
|
||||
"description": "A value that can be a literal, a path, or a function call returning any type.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DataBinding"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/FunctionCall"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicString": {
|
||||
"description": "Represents a string",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DataBinding"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/FunctionCall"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"returnType": {
|
||||
"const": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicNumber": {
|
||||
"description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DataBinding"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/FunctionCall"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"returnType": {
|
||||
"const": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicBoolean": {
|
||||
"description": "A boolean value that can be a literal, a path, or a function call returning a boolean.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DataBinding"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/FunctionCall"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"returnType": {
|
||||
"const": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"DynamicStringList": {
|
||||
"description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DataBinding"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/FunctionCall"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"returnType": {
|
||||
"const": "array"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"FunctionCall": {
|
||||
"type": "object",
|
||||
"description": "Invokes a named function on the client.",
|
||||
"properties": {
|
||||
"call": {
|
||||
"type": "string",
|
||||
"description": "The name of the function to call."
|
||||
},
|
||||
"args": {
|
||||
"type": "object",
|
||||
"description": "Arguments passed to the function.",
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/DynamicValue"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "A literal object argument (e.g. configuration)."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"returnType": {
|
||||
"type": "string",
|
||||
"description": "The expected return type of the function call.",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
"any",
|
||||
"void"
|
||||
],
|
||||
"default": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["call"],
|
||||
"oneOf": [
|
||||
{ "$ref": "basic_catalog.json#/$defs/anyFunction" }
|
||||
]
|
||||
},
|
||||
"CheckRule": {
|
||||
"type": "object",
|
||||
"description": "A single validation rule applied to an input component.",
|
||||
"properties": {
|
||||
"condition": {
|
||||
"$ref": "#/$defs/DynamicBoolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The error message to display if the check fails."
|
||||
}
|
||||
},
|
||||
"required": ["condition", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Checkable": {
|
||||
"description": "Properties for components that support client-side checks.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"checks": {
|
||||
"type": "array",
|
||||
"description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/CheckRule"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Action": {
|
||||
"description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Triggers a server-side event.",
|
||||
"properties": {
|
||||
"event": {
|
||||
"type": "object",
|
||||
"description": "The event to dispatch to the server.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the action to be dispatched to the server."
|
||||
},
|
||||
"context": {
|
||||
"type": "object",
|
||||
"description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/DynamicValue"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["event"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Executes a local client-side function.",
|
||||
"properties": {
|
||||
"functionCall": {
|
||||
"$ref": "#/$defs/FunctionCall"
|
||||
}
|
||||
},
|
||||
"required": ["functionCall"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://a2ui.org/specification/v0_9/server_capabilities.json",
|
||||
"title": "A2UI Server Capabilities Schema",
|
||||
"description": "A schema for the server capabilities object, which is used by an A2UI server (or Agent) to advertise its supported UI features to clients. This can be embedded in an Agent Card for A2A or used in other transport protocols like MCP.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"v0.9": {
|
||||
"type": "object",
|
||||
"description": "The server capabilities structure for version 0.9 of the A2UI protocol.",
|
||||
"properties": {
|
||||
"supportedCatalogIds": {
|
||||
"type": "array",
|
||||
"description": "An array of strings, where each string is an ID identifying a Catalog Definition Schema that the server can generate. This is not necessarily a resolvable URI.",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"acceptsInlineCatalogs": {
|
||||
"type": "boolean",
|
||||
"description": "A boolean indicating if the server can accept an 'inlineCatalogs' array in the client's a2uiClientCapabilities. If omitted, this defaults to false.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["v0.9"]
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://a2ui.org/specification/v0_9/server_to_client.json",
|
||||
"title": "A2UI Message Schema",
|
||||
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.",
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/CreateSurfaceMessage" },
|
||||
{ "$ref": "#/$defs/UpdateComponentsMessage" },
|
||||
{ "$ref": "#/$defs/UpdateDataModelMessage" },
|
||||
{ "$ref": "#/$defs/DeleteSurfaceMessage" }
|
||||
],
|
||||
"$defs": {
|
||||
"CreateSurfaceMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "v0.9"
|
||||
},
|
||||
"createSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be rendered."
|
||||
},
|
||||
"catalogId": {
|
||||
"description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.",
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"$ref": "basic_catalog.json#/$defs/theme",
|
||||
"description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog."
|
||||
},
|
||||
"sendDataModel": {
|
||||
"type": "boolean",
|
||||
"description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "catalogId"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["createSurface", "version"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UpdateComponentsMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "v0.9"
|
||||
},
|
||||
"updateComponents": {
|
||||
"type": "object",
|
||||
"description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be updated."
|
||||
},
|
||||
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "A list containing all UI components for the surface.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "basic_catalog.json#/$defs/anyComponent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "components"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["updateComponents", "version"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"UpdateDataModelMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "v0.9"
|
||||
},
|
||||
"updateDataModel": {
|
||||
"type": "object",
|
||||
"description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface this data model update applies to."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model."
|
||||
},
|
||||
"value": {
|
||||
"description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["updateDataModel", "version"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DeleteSurfaceMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "v0.9"
|
||||
},
|
||||
"deleteSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be deleted."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["deleteSurface", "version"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
160
lib/crewai/src/crewai/a2a/extensions/a2ui/server_extension.py
Normal file
160
lib/crewai/src/crewai/a2a/extensions/a2ui/server_extension.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""A2UI server extension for the A2A protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from crewai.a2a.extensions.a2ui.models import A2UIResponse, extract_a2ui_json_objects
|
||||
from crewai.a2a.extensions.a2ui.v0_9 import (
|
||||
extract_a2ui_v09_json_objects,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.validator import (
|
||||
A2UIValidationError,
|
||||
validate_a2ui_message,
|
||||
validate_a2ui_message_v09,
|
||||
)
|
||||
from crewai.a2a.extensions.server import ExtensionContext, ServerExtension
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
A2UI_MIME_TYPE = "application/json+a2ui"
|
||||
A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.8"
|
||||
A2UI_STANDARD_CATALOG_ID = (
|
||||
"https://a2ui.org/specification/v0_8/standard_catalog_definition.json"
|
||||
)
|
||||
A2UI_V09_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9"
|
||||
A2UI_V09_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/basic_catalog.json"
|
||||
|
||||
|
||||
class A2UIServerExtension(ServerExtension):
|
||||
"""A2A server extension that enables A2UI declarative UI generation.
|
||||
|
||||
Supports both v0.8 and v0.9 of the A2UI protocol via the ``version``
|
||||
parameter. When activated by a client, this extension:
|
||||
|
||||
* Negotiates catalog preferences during ``on_request``.
|
||||
* Wraps A2UI messages in the agent response as A2A DataParts with
|
||||
``application/json+a2ui`` MIME type during ``on_response``.
|
||||
|
||||
Example::
|
||||
|
||||
A2AServerConfig
|
||||
server_extensions=[A2UIServerExtension],
|
||||
default_output_modes=["text/plain", "application/json+a2ui"],
|
||||
"""
|
||||
|
||||
uri: str = A2UI_EXTENSION_URI
|
||||
required: bool = False
|
||||
description: str = "A2UI declarative UI generation"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
catalog_ids: list[str] | None = None,
|
||||
accept_inline_catalogs: bool = False,
|
||||
version: str = "v0.8",
|
||||
) -> None:
|
||||
"""Initialize the A2UI server extension.
|
||||
|
||||
Args:
|
||||
catalog_ids: Catalog identifiers this server supports.
|
||||
accept_inline_catalogs: Whether inline catalog definitions are accepted.
|
||||
version: Protocol version, ``"v0.8"`` or ``"v0.9"``.
|
||||
"""
|
||||
self._catalog_ids = catalog_ids or []
|
||||
self._accept_inline_catalogs = accept_inline_catalogs
|
||||
self._version = version
|
||||
if version == "v0.9":
|
||||
self.uri = A2UI_V09_EXTENSION_URI
|
||||
|
||||
@property
|
||||
def params(self) -> dict[str, Any]:
|
||||
"""Extension parameters advertised in the AgentCard."""
|
||||
result: dict[str, Any] = {}
|
||||
if self._catalog_ids:
|
||||
result["supportedCatalogIds"] = self._catalog_ids
|
||||
result["acceptsInlineCatalogs"] = self._accept_inline_catalogs
|
||||
return result
|
||||
|
||||
async def on_request(self, context: ExtensionContext) -> None:
|
||||
"""Extract A2UI catalog preferences from the client request.
|
||||
|
||||
Stores the negotiated catalog in ``context.state`` under
|
||||
``"a2ui_catalog_id"`` for downstream use.
|
||||
"""
|
||||
if not self.is_active(context):
|
||||
return
|
||||
|
||||
catalog_id = context.get_extension_metadata(self.uri, "catalogId")
|
||||
if isinstance(catalog_id, str):
|
||||
context.state["a2ui_catalog_id"] = catalog_id
|
||||
elif self._catalog_ids:
|
||||
context.state["a2ui_catalog_id"] = self._catalog_ids[0]
|
||||
|
||||
context.state["a2ui_active"] = True
|
||||
|
||||
async def on_response(self, context: ExtensionContext, result: Any) -> Any:
|
||||
"""Wrap A2UI messages in the result as A2A DataParts.
|
||||
|
||||
Scans the result for A2UI JSON payloads and converts them into
|
||||
DataParts with ``application/json+a2ui`` MIME type and A2UI metadata.
|
||||
Dispatches to the correct extractor and validator based on version.
|
||||
"""
|
||||
if not context.state.get("a2ui_active"):
|
||||
return result
|
||||
|
||||
if not isinstance(result, str):
|
||||
return result
|
||||
|
||||
if self._version == "v0.9":
|
||||
a2ui_messages = extract_a2ui_v09_json_objects(result)
|
||||
else:
|
||||
a2ui_messages = extract_a2ui_json_objects(result)
|
||||
|
||||
if not a2ui_messages:
|
||||
return result
|
||||
|
||||
build_fn = _build_data_part_v09 if self._version == "v0.9" else _build_data_part
|
||||
data_parts = [
|
||||
part
|
||||
for part in (build_fn(msg_data) for msg_data in a2ui_messages)
|
||||
if part is not None
|
||||
]
|
||||
|
||||
if not data_parts:
|
||||
return result
|
||||
|
||||
return A2UIResponse(text=result, a2ui_parts=data_parts)
|
||||
|
||||
|
||||
def _build_data_part(msg_data: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Validate a v0.8 A2UI message and wrap it as a DataPart dict."""
|
||||
try:
|
||||
validated = validate_a2ui_message(msg_data)
|
||||
except A2UIValidationError:
|
||||
logger.warning("Skipping invalid A2UI message in response", exc_info=True)
|
||||
return None
|
||||
return {
|
||||
"kind": "data",
|
||||
"data": validated.model_dump(by_alias=True, exclude_none=True),
|
||||
"metadata": {
|
||||
"mimeType": A2UI_MIME_TYPE,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _build_data_part_v09(msg_data: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Validate a v0.9 A2UI message and wrap it as a DataPart dict."""
|
||||
try:
|
||||
validated = validate_a2ui_message_v09(msg_data)
|
||||
except A2UIValidationError:
|
||||
logger.warning("Skipping invalid A2UI v0.9 message in response", exc_info=True)
|
||||
return None
|
||||
return {
|
||||
"kind": "data",
|
||||
"data": validated.model_dump(by_alias=True, exclude_none=True),
|
||||
"metadata": {
|
||||
"mimeType": A2UI_MIME_TYPE,
|
||||
},
|
||||
}
|
||||
831
lib/crewai/src/crewai/a2a/extensions/a2ui/v0_9.py
Normal file
831
lib/crewai/src/crewai/a2a/extensions/a2ui/v0_9.py
Normal file
@@ -0,0 +1,831 @@
|
||||
"""Pydantic models for A2UI v0.9 protocol messages and types.
|
||||
|
||||
This module provides v0.9 counterparts to the v0.8 models in ``models.py``.
|
||||
Key differences from v0.8:
|
||||
|
||||
* ``beginRendering`` → ``createSurface`` — adds ``theme``, ``sendDataModel``,
|
||||
requires ``catalogId``.
|
||||
* ``surfaceUpdate`` → ``updateComponents`` — component structure is flat:
|
||||
``component`` is a type-name string, properties live at the top level.
|
||||
* ``dataModelUpdate`` → ``updateDataModel`` — ``contents`` adjacency list
|
||||
replaced by a single ``value`` of any JSON type; ``path`` uses JSON Pointers.
|
||||
* All messages carry a ``version: "v0.9"`` discriminator.
|
||||
* Data binding uses plain JSON values, ``DataBinding`` objects, or
|
||||
``FunctionCall`` objects instead of ``literalString`` / ``path`` wrappers.
|
||||
* ``MultipleChoice`` is replaced by ``ChoicePicker``.
|
||||
* ``Styles`` is replaced by ``Theme`` — adds ``iconUrl``, ``agentDisplayName``.
|
||||
* Client-to-server ``userAction`` is renamed to ``action``; ``error`` gains
|
||||
structured ``code`` / ``path`` fields.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
ComponentName = Literal[
|
||||
"Text",
|
||||
"Image",
|
||||
"Icon",
|
||||
"Video",
|
||||
"AudioPlayer",
|
||||
"Row",
|
||||
"Column",
|
||||
"List",
|
||||
"Card",
|
||||
"Tabs",
|
||||
"Modal",
|
||||
"Divider",
|
||||
"Button",
|
||||
"TextField",
|
||||
"CheckBox",
|
||||
"ChoicePicker",
|
||||
"Slider",
|
||||
"DateTimeInput",
|
||||
]
|
||||
|
||||
BASIC_CATALOG_COMPONENTS: frozenset[ComponentName] = frozenset(get_args(ComponentName))
|
||||
|
||||
FunctionName = Literal[
|
||||
"required",
|
||||
"regex",
|
||||
"length",
|
||||
"numeric",
|
||||
"email",
|
||||
"formatString",
|
||||
"formatNumber",
|
||||
"formatCurrency",
|
||||
"formatDate",
|
||||
"pluralize",
|
||||
"openUrl",
|
||||
"and",
|
||||
"or",
|
||||
"not",
|
||||
]
|
||||
|
||||
BASIC_CATALOG_FUNCTIONS: frozenset[FunctionName] = frozenset(get_args(FunctionName))
|
||||
|
||||
IconNameV09 = Literal[
|
||||
"accountCircle",
|
||||
"add",
|
||||
"arrowBack",
|
||||
"arrowForward",
|
||||
"attachFile",
|
||||
"calendarToday",
|
||||
"call",
|
||||
"camera",
|
||||
"check",
|
||||
"close",
|
||||
"delete",
|
||||
"download",
|
||||
"edit",
|
||||
"event",
|
||||
"error",
|
||||
"fastForward",
|
||||
"favorite",
|
||||
"favoriteOff",
|
||||
"folder",
|
||||
"help",
|
||||
"home",
|
||||
"info",
|
||||
"locationOn",
|
||||
"lock",
|
||||
"lockOpen",
|
||||
"mail",
|
||||
"menu",
|
||||
"moreVert",
|
||||
"moreHoriz",
|
||||
"notificationsOff",
|
||||
"notifications",
|
||||
"pause",
|
||||
"payment",
|
||||
"person",
|
||||
"phone",
|
||||
"photo",
|
||||
"play",
|
||||
"print",
|
||||
"refresh",
|
||||
"rewind",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share",
|
||||
"shoppingCart",
|
||||
"skipNext",
|
||||
"skipPrevious",
|
||||
"star",
|
||||
"starHalf",
|
||||
"starOff",
|
||||
"stop",
|
||||
"upload",
|
||||
"visibility",
|
||||
"visibilityOff",
|
||||
"volumeDown",
|
||||
"volumeMute",
|
||||
"volumeOff",
|
||||
"volumeUp",
|
||||
"warning",
|
||||
]
|
||||
|
||||
V09_ICON_NAMES: frozenset[IconNameV09] = frozenset(get_args(IconNameV09))
|
||||
|
||||
|
||||
class DataBinding(BaseModel):
|
||||
"""JSON Pointer path reference to the data model."""
|
||||
|
||||
path: str = Field(description="A JSON Pointer path to a value in the data model.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class FunctionCall(BaseModel):
|
||||
"""Client-side function invocation."""
|
||||
|
||||
call: str = Field(description="The name of the function to call.")
|
||||
args: dict[str, DynamicValue] | None = Field(
|
||||
default=None, description="Arguments passed to the function."
|
||||
)
|
||||
return_type: (
|
||||
Literal["string", "number", "boolean", "array", "object", "any", "void"] | None
|
||||
) = Field(
|
||||
default=None,
|
||||
alias="returnType",
|
||||
description="Expected return type of the function call.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
DynamicValue = str | float | int | bool | list[Any] | DataBinding | FunctionCall
|
||||
DynamicString = str | DataBinding | FunctionCall
|
||||
DynamicNumber = float | int | DataBinding | FunctionCall
|
||||
DynamicBoolean = bool | DataBinding | FunctionCall
|
||||
DynamicStringList = list[str] | DataBinding | FunctionCall
|
||||
|
||||
|
||||
class CheckRule(BaseModel):
|
||||
"""A single validation rule for an input component."""
|
||||
|
||||
condition: DynamicBoolean = Field(
|
||||
description="Condition that must evaluate to true for the check to pass."
|
||||
)
|
||||
message: str = Field(description="Error message displayed if the check fails.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class AccessibilityAttributes(BaseModel):
|
||||
"""Accessibility attributes for assistive technologies."""
|
||||
|
||||
label: DynamicString | None = Field(
|
||||
default=None, description="Short label for screen readers."
|
||||
)
|
||||
description: DynamicString | None = Field(
|
||||
default=None, description="Extended description for screen readers."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ChildTemplate(BaseModel):
|
||||
"""Template for generating dynamic children from a data model list."""
|
||||
|
||||
component_id: str = Field(
|
||||
alias="componentId", description="Component to repeat per list item."
|
||||
)
|
||||
path: str = Field(description="Data model path to the list of items.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
ChildListV09 = list[str] | ChildTemplate
|
||||
|
||||
|
||||
class EventAction(BaseModel):
|
||||
"""Server-side event triggered by a component interaction."""
|
||||
|
||||
name: str = Field(description="Action name dispatched to the server.")
|
||||
context: dict[str, DynamicValue] | None = Field(
|
||||
default=None, description="Key-value pairs sent with the event."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ActionV09(BaseModel):
|
||||
"""Interaction handler: server event or local function call.
|
||||
|
||||
Exactly one of ``event`` or ``function_call`` must be set.
|
||||
"""
|
||||
|
||||
event: EventAction | None = Field(
|
||||
default=None, description="Triggers a server-side event."
|
||||
)
|
||||
function_call: FunctionCall | None = Field(
|
||||
default=None,
|
||||
alias="functionCall",
|
||||
description="Executes a local client-side function.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> ActionV09:
|
||||
"""Enforce exactly one of event or functionCall."""
|
||||
count = sum(f is not None for f in (self.event, self.function_call))
|
||||
if count != 1:
|
||||
raise ValueError(
|
||||
f"Exactly one of event or functionCall must be set, got {count}"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class TextV09(BaseModel):
|
||||
"""Displays text content."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Text"] = "Text"
|
||||
text: DynamicString = Field(description="Text content to display.")
|
||||
variant: Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"] | None = Field(
|
||||
default=None, description="Semantic text style hint."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ImageV09(BaseModel):
|
||||
"""Displays an image."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Image"] = "Image"
|
||||
url: DynamicString = Field(description="Image source URL.")
|
||||
description: DynamicString | None = Field(
|
||||
default=None, description="Accessibility text."
|
||||
)
|
||||
fit: Literal["contain", "cover", "fill", "none", "scaleDown"] | None = Field(
|
||||
default=None, description="Object-fit behavior."
|
||||
)
|
||||
variant: (
|
||||
Literal[
|
||||
"icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"
|
||||
]
|
||||
| None
|
||||
) = Field(default=None, description="Image size hint.")
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class IconV09(BaseModel):
|
||||
"""Displays a named icon."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Icon"] = "Icon"
|
||||
name: IconNameV09 | DataBinding = Field(description="Icon name or data binding.")
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class VideoV09(BaseModel):
|
||||
"""Displays a video player."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Video"] = "Video"
|
||||
url: DynamicString = Field(description="Video source URL.")
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class AudioPlayerV09(BaseModel):
|
||||
"""Displays an audio player."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["AudioPlayer"] = "AudioPlayer"
|
||||
url: DynamicString = Field(description="Audio source URL.")
|
||||
description: DynamicString | None = Field(
|
||||
default=None, description="Audio content description."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class RowV09(BaseModel):
|
||||
"""Horizontal layout container."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Row"] = "Row"
|
||||
children: ChildListV09 = Field(description="Child components.")
|
||||
justify: (
|
||||
Literal[
|
||||
"center",
|
||||
"end",
|
||||
"spaceAround",
|
||||
"spaceBetween",
|
||||
"spaceEvenly",
|
||||
"start",
|
||||
"stretch",
|
||||
]
|
||||
| None
|
||||
) = Field(default=None, description="Main-axis distribution.")
|
||||
align: Literal["start", "center", "end", "stretch"] | None = Field(
|
||||
default=None, description="Cross-axis alignment."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ColumnV09(BaseModel):
|
||||
"""Vertical layout container."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Column"] = "Column"
|
||||
children: ChildListV09 = Field(description="Child components.")
|
||||
justify: (
|
||||
Literal[
|
||||
"start",
|
||||
"center",
|
||||
"end",
|
||||
"spaceBetween",
|
||||
"spaceAround",
|
||||
"spaceEvenly",
|
||||
"stretch",
|
||||
]
|
||||
| None
|
||||
) = Field(default=None, description="Main-axis distribution.")
|
||||
align: Literal["center", "end", "start", "stretch"] | None = Field(
|
||||
default=None, description="Cross-axis alignment."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ListV09(BaseModel):
|
||||
"""Scrollable list container."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["List"] = "List"
|
||||
children: ChildListV09 = Field(description="Child components.")
|
||||
direction: Literal["vertical", "horizontal"] | None = Field(
|
||||
default=None, description="Scroll direction."
|
||||
)
|
||||
align: Literal["start", "center", "end", "stretch"] | None = Field(
|
||||
default=None, description="Cross-axis alignment."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class CardV09(BaseModel):
|
||||
"""Card container wrapping a single child."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Card"] = "Card"
|
||||
child: str = Field(description="ID of the child component.")
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TabItemV09(BaseModel):
|
||||
"""A single tab definition."""
|
||||
|
||||
title: DynamicString = Field(description="Tab title.")
|
||||
child: str = Field(description="ID of the tab content component.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TabsV09(BaseModel):
|
||||
"""Tabbed navigation container."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Tabs"] = "Tabs"
|
||||
tabs: list[TabItemV09] = Field(min_length=1, description="Tab definitions.")
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ModalV09(BaseModel):
|
||||
"""Modal dialog with a trigger and content."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Modal"] = "Modal"
|
||||
trigger: str = Field(description="ID of the component that opens the modal.")
|
||||
content: str = Field(description="ID of the component inside the modal.")
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class DividerV09(BaseModel):
|
||||
"""Visual divider line."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Divider"] = "Divider"
|
||||
axis: Literal["horizontal", "vertical"] | None = Field(
|
||||
default=None, description="Divider orientation."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ButtonV09(BaseModel):
|
||||
"""Interactive button."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Button"] = "Button"
|
||||
child: str = Field(description="ID of the button label component.")
|
||||
action: ActionV09 = Field(description="Action dispatched on click.")
|
||||
variant: Literal["default", "primary", "borderless"] | None = Field(
|
||||
default=None, description="Button style variant."
|
||||
)
|
||||
checks: list[CheckRule] | None = Field(
|
||||
default=None, description="Validation rules."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class TextFieldV09(BaseModel):
|
||||
"""Text input field."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["TextField"] = "TextField"
|
||||
label: DynamicString = Field(description="Input label.")
|
||||
value: DynamicString | None = Field(default=None, description="Current text value.")
|
||||
variant: Literal["longText", "number", "shortText", "obscured"] | None = Field(
|
||||
default=None, description="Input type variant."
|
||||
)
|
||||
validation_regexp: str | None = Field(
|
||||
default=None,
|
||||
alias="validationRegexp",
|
||||
description="Regex for client-side validation.",
|
||||
)
|
||||
checks: list[CheckRule] | None = Field(
|
||||
default=None, description="Validation rules."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class CheckBoxV09(BaseModel):
|
||||
"""Checkbox input."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["CheckBox"] = "CheckBox"
|
||||
label: DynamicString = Field(description="Checkbox label.")
|
||||
value: DynamicBoolean = Field(description="Checked state.")
|
||||
checks: list[CheckRule] | None = Field(
|
||||
default=None, description="Validation rules."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ChoicePickerOption(BaseModel):
|
||||
"""A single option in a ChoicePicker."""
|
||||
|
||||
label: DynamicString = Field(description="Display label.")
|
||||
value: str = Field(description="Value when selected.")
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ChoicePickerV09(BaseModel):
|
||||
"""Selection component replacing v0.8 MultipleChoice."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["ChoicePicker"] = "ChoicePicker"
|
||||
options: list[ChoicePickerOption] = Field(description="Available choices.")
|
||||
value: DynamicStringList = Field(description="Currently selected values.")
|
||||
label: DynamicString | None = Field(default=None, description="Group label.")
|
||||
variant: Literal["multipleSelection", "mutuallyExclusive"] | None = Field(
|
||||
default=None, description="Selection behavior."
|
||||
)
|
||||
display_style: Literal["checkbox", "chips"] | None = Field(
|
||||
default=None, alias="displayStyle", description="Visual display style."
|
||||
)
|
||||
filterable: bool | None = Field(
|
||||
default=None, description="Whether options can be filtered."
|
||||
)
|
||||
checks: list[CheckRule] | None = Field(
|
||||
default=None, description="Validation rules."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class SliderV09(BaseModel):
|
||||
"""Numeric slider input."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["Slider"] = "Slider"
|
||||
value: DynamicNumber = Field(description="Current slider value.")
|
||||
max: float = Field(description="Maximum slider value.")
|
||||
min: float | None = Field(default=None, description="Minimum slider value.")
|
||||
label: DynamicString | None = Field(default=None, description="Slider label.")
|
||||
checks: list[CheckRule] | None = Field(
|
||||
default=None, description="Validation rules."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class DateTimeInputV09(BaseModel):
|
||||
"""Date and/or time picker."""
|
||||
|
||||
id: str = Field(description="Unique component identifier.")
|
||||
component: Literal["DateTimeInput"] = "DateTimeInput"
|
||||
value: DynamicString = Field(description="ISO 8601 date/time value.")
|
||||
enable_date: bool | None = Field(
|
||||
default=None, alias="enableDate", description="Enable date selection."
|
||||
)
|
||||
enable_time: bool | None = Field(
|
||||
default=None, alias="enableTime", description="Enable time selection."
|
||||
)
|
||||
min: DynamicString | None = Field(
|
||||
default=None, description="Minimum allowed date/time."
|
||||
)
|
||||
max: DynamicString | None = Field(
|
||||
default=None, description="Maximum allowed date/time."
|
||||
)
|
||||
label: DynamicString | None = Field(default=None, description="Input label.")
|
||||
checks: list[CheckRule] | None = Field(
|
||||
default=None, description="Validation rules."
|
||||
)
|
||||
weight: float | None = Field(default=None, description="Flex weight.")
|
||||
accessibility: AccessibilityAttributes | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class Theme(BaseModel):
|
||||
"""Surface theme configuration for v0.9.
|
||||
|
||||
Replaces v0.8 ``Styles``. Adds ``iconUrl`` and ``agentDisplayName``
|
||||
for agent attribution; drops ``font``.
|
||||
"""
|
||||
|
||||
primary_color: str | None = Field(
|
||||
default=None,
|
||||
alias="primaryColor",
|
||||
pattern=r"^#[0-9a-fA-F]{6}$",
|
||||
description="Primary brand color as a hex string.",
|
||||
)
|
||||
icon_url: str | None = Field(
|
||||
default=None,
|
||||
alias="iconUrl",
|
||||
description="URL for an image identifying the agent or tool.",
|
||||
)
|
||||
agent_display_name: str | None = Field(
|
||||
default=None,
|
||||
alias="agentDisplayName",
|
||||
description="Text label identifying the agent or tool.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
||||
|
||||
|
||||
class CreateSurface(BaseModel):
|
||||
"""Signals the client to create a new surface and begin rendering.
|
||||
|
||||
Replaces v0.8 ``BeginRendering``. ``catalogId`` is now required and
|
||||
``theme`` / ``sendDataModel`` are new.
|
||||
"""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Unique surface identifier.")
|
||||
catalog_id: str = Field(
|
||||
alias="catalogId", description="Catalog identifier for this surface."
|
||||
)
|
||||
theme: Theme | None = Field(default=None, description="Theme parameters.")
|
||||
send_data_model: bool | None = Field(
|
||||
default=None,
|
||||
alias="sendDataModel",
|
||||
description="If true, client sends data model in action metadata.",
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class UpdateComponents(BaseModel):
|
||||
"""Updates a surface with a new set of components.
|
||||
|
||||
Replaces v0.8 ``SurfaceUpdate``. Components use a flat structure where
|
||||
``component`` is a type-name string and properties sit at the top level.
|
||||
"""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
|
||||
components: list[dict[str, Any]] = Field(
|
||||
min_length=1, description="Components to render on the surface."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class UpdateDataModel(BaseModel):
|
||||
"""Updates the data model for a surface.
|
||||
|
||||
Replaces v0.8 ``DataModelUpdate``. The ``contents`` adjacency list is
|
||||
replaced by a single ``value`` of any JSON type. ``path`` uses JSON
|
||||
Pointer syntax — e.g. ``/user/name``.
|
||||
"""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
|
||||
path: str | None = Field(
|
||||
default=None, description="JSON Pointer path for the update."
|
||||
)
|
||||
value: Any = Field(
|
||||
default=None, description="Value to set. Omit to delete the key."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class DeleteSurfaceV09(BaseModel):
|
||||
"""Signals the client to delete a surface."""
|
||||
|
||||
surface_id: str = Field(alias="surfaceId", description="Surface to delete.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
|
||||
class A2UIMessageV09(BaseModel):
|
||||
"""Union wrapper for v0.9 server-to-client message types.
|
||||
|
||||
Exactly one message field must be set alongside the ``version`` field.
|
||||
"""
|
||||
|
||||
version: Literal["v0.9"] = "v0.9"
|
||||
create_surface: CreateSurface | None = Field(
|
||||
default=None, alias="createSurface", description="Create a new surface."
|
||||
)
|
||||
update_components: UpdateComponents | None = Field(
|
||||
default=None,
|
||||
alias="updateComponents",
|
||||
description="Update components on a surface.",
|
||||
)
|
||||
update_data_model: UpdateDataModel | None = Field(
|
||||
default=None,
|
||||
alias="updateDataModel",
|
||||
description="Update the surface data model.",
|
||||
)
|
||||
delete_surface: DeleteSurfaceV09 | None = Field(
|
||||
default=None, alias="deleteSurface", description="Delete a surface."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> A2UIMessageV09:
|
||||
"""Enforce the spec's exactly-one-of constraint."""
|
||||
fields = [
|
||||
self.create_surface,
|
||||
self.update_components,
|
||||
self.update_data_model,
|
||||
self.delete_surface,
|
||||
]
|
||||
count = sum(f is not None for f in fields)
|
||||
if count != 1:
|
||||
raise ValueError(
|
||||
f"Exactly one A2UI v0.9 message type must be set, got {count}"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class ActionEvent(BaseModel):
|
||||
"""User-initiated action from a component.
|
||||
|
||||
Replaces v0.8 ``UserAction``. The event field is renamed from
|
||||
``userAction`` to ``action``.
|
||||
"""
|
||||
|
||||
name: str = Field(description="Action name.")
|
||||
surface_id: str = Field(alias="surfaceId", description="Source surface identifier.")
|
||||
source_component_id: str = Field(
|
||||
alias="sourceComponentId",
|
||||
description="Component that triggered the action.",
|
||||
)
|
||||
timestamp: str = Field(description="ISO 8601 timestamp of the action.")
|
||||
context: dict[str, Any] = Field(description="Resolved action context payload.")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class ClientErrorV09(BaseModel):
|
||||
"""Structured client-side error report.
|
||||
|
||||
Replaces v0.8's flexible ``ClientError`` with required ``code``,
|
||||
``surfaceId``, and ``message`` fields.
|
||||
"""
|
||||
|
||||
code: str = Field(description="Error code (e.g. VALIDATION_FAILED).")
|
||||
surface_id: str = Field(
|
||||
alias="surfaceId", description="Surface where the error occurred."
|
||||
)
|
||||
message: str = Field(description="Human-readable error description.")
|
||||
path: str | None = Field(
|
||||
default=None, description="JSON Pointer to the failing field."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
||||
|
||||
|
||||
class A2UIEventV09(BaseModel):
|
||||
"""Union wrapper for v0.9 client-to-server events."""
|
||||
|
||||
version: Literal["v0.9"] = "v0.9"
|
||||
action: ActionEvent | None = Field(
|
||||
default=None, description="User-initiated action event."
|
||||
)
|
||||
error: ClientErrorV09 | None = Field(
|
||||
default=None, description="Client-side error report."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_exactly_one(self) -> A2UIEventV09:
|
||||
"""Enforce the spec's exactly-one-of constraint."""
|
||||
fields = [self.action, self.error]
|
||||
count = sum(f is not None for f in fields)
|
||||
if count != 1:
|
||||
raise ValueError(
|
||||
f"Exactly one A2UI v0.9 event type must be set, got {count}"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class ClientDataModel(BaseModel):
|
||||
"""Client data model payload for A2A message metadata.
|
||||
|
||||
When ``sendDataModel`` is ``true`` on ``createSurface``, the client
|
||||
attaches this object to every outbound A2A message as
|
||||
``a2uiClientDataModel`` in the metadata.
|
||||
"""
|
||||
|
||||
version: Literal["v0.9"] = "v0.9"
|
||||
surfaces: dict[str, dict[str, Any]] = Field(
|
||||
description="Map of surface IDs to their current data models."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
_V09_KEYS = {"createSurface", "updateComponents", "updateDataModel", "deleteSurface"}
|
||||
|
||||
|
||||
def extract_a2ui_v09_json_objects(text: str) -> list[dict[str, Any]]:
|
||||
"""Extract JSON objects containing A2UI v0.9 keys from text.
|
||||
|
||||
Uses ``json.JSONDecoder.raw_decode`` for robust parsing that correctly
|
||||
handles braces inside string literals.
|
||||
"""
|
||||
decoder = json.JSONDecoder()
|
||||
results: list[dict[str, Any]] = []
|
||||
idx = 0
|
||||
while idx < len(text):
|
||||
idx = text.find("{", idx)
|
||||
if idx == -1:
|
||||
break
|
||||
try:
|
||||
obj, end_idx = decoder.raw_decode(text, idx)
|
||||
if isinstance(obj, dict) and _V09_KEYS & obj.keys():
|
||||
results.append(obj)
|
||||
idx = end_idx
|
||||
except json.JSONDecodeError:
|
||||
idx += 1
|
||||
return results
|
||||
285
lib/crewai/src/crewai/a2a/extensions/a2ui/validator.py
Normal file
285
lib/crewai/src/crewai/a2a/extensions/a2ui/validator.py
Normal file
@@ -0,0 +1,285 @@
|
||||
"""Validate A2UI message dicts via Pydantic models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from crewai.a2a.extensions.a2ui.catalog import (
|
||||
AudioPlayer,
|
||||
Button,
|
||||
Card,
|
||||
CheckBox,
|
||||
Column,
|
||||
DateTimeInput,
|
||||
Divider,
|
||||
Icon,
|
||||
Image,
|
||||
List,
|
||||
Modal,
|
||||
MultipleChoice,
|
||||
Row,
|
||||
Slider,
|
||||
Tabs,
|
||||
Text,
|
||||
TextField,
|
||||
Video,
|
||||
)
|
||||
from crewai.a2a.extensions.a2ui.models import A2UIEvent, A2UIMessage
|
||||
from crewai.a2a.extensions.a2ui.v0_9 import (
|
||||
A2UIEventV09,
|
||||
A2UIMessageV09,
|
||||
AudioPlayerV09,
|
||||
ButtonV09,
|
||||
CardV09,
|
||||
CheckBoxV09,
|
||||
ChoicePickerV09,
|
||||
ColumnV09,
|
||||
DateTimeInputV09,
|
||||
DividerV09,
|
||||
IconV09,
|
||||
ImageV09,
|
||||
ListV09,
|
||||
ModalV09,
|
||||
RowV09,
|
||||
SliderV09,
|
||||
TabsV09,
|
||||
TextFieldV09,
|
||||
TextV09,
|
||||
VideoV09,
|
||||
)
|
||||
|
||||
|
||||
_STANDARD_CATALOG_MODELS: dict[str, type[BaseModel]] = {
|
||||
"AudioPlayer": AudioPlayer,
|
||||
"Button": Button,
|
||||
"Card": Card,
|
||||
"CheckBox": CheckBox,
|
||||
"Column": Column,
|
||||
"DateTimeInput": DateTimeInput,
|
||||
"Divider": Divider,
|
||||
"Icon": Icon,
|
||||
"Image": Image,
|
||||
"List": List,
|
||||
"Modal": Modal,
|
||||
"MultipleChoice": MultipleChoice,
|
||||
"Row": Row,
|
||||
"Slider": Slider,
|
||||
"Tabs": Tabs,
|
||||
"Text": Text,
|
||||
"TextField": TextField,
|
||||
"Video": Video,
|
||||
}
|
||||
|
||||
|
||||
class A2UIValidationError(Exception):
|
||||
"""Raised when an A2UI message fails validation."""
|
||||
|
||||
def __init__(self, message: str, errors: list[Any] | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.errors = errors or []
|
||||
|
||||
|
||||
def validate_a2ui_message(
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
validate_catalog: bool = False,
|
||||
) -> A2UIMessage:
|
||||
"""Parse and validate an A2UI server-to-client message.
|
||||
|
||||
Args:
|
||||
data: Raw JSON-decoded message dict.
|
||||
validate_catalog: If True, also validate component properties
|
||||
against the standard catalog.
|
||||
|
||||
Returns:
|
||||
Validated ``A2UIMessage`` instance.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If the data does not conform to the A2UI schema.
|
||||
"""
|
||||
try:
|
||||
message = A2UIMessage.model_validate(data)
|
||||
except ValidationError as exc:
|
||||
raise A2UIValidationError(
|
||||
f"Invalid A2UI message: {exc.error_count()} validation error(s)",
|
||||
errors=exc.errors(),
|
||||
) from exc
|
||||
|
||||
if validate_catalog:
|
||||
validate_catalog_components(message)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def validate_a2ui_event(data: dict[str, Any]) -> A2UIEvent:
|
||||
"""Parse and validate an A2UI client-to-server event.
|
||||
|
||||
Args:
|
||||
data: Raw JSON-decoded event dict.
|
||||
|
||||
Returns:
|
||||
Validated ``A2UIEvent`` instance.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If the data does not conform to the A2UI event schema.
|
||||
"""
|
||||
try:
|
||||
return A2UIEvent.model_validate(data)
|
||||
except ValidationError as exc:
|
||||
raise A2UIValidationError(
|
||||
f"Invalid A2UI event: {exc.error_count()} validation error(s)",
|
||||
errors=exc.errors(),
|
||||
) from exc
|
||||
|
||||
|
||||
def validate_a2ui_message_v09(data: dict[str, Any]) -> A2UIMessageV09:
|
||||
"""Parse and validate an A2UI v0.9 server-to-client message.
|
||||
|
||||
Args:
|
||||
data: Raw JSON-decoded message dict.
|
||||
|
||||
Returns:
|
||||
Validated ``A2UIMessageV09`` instance.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If the data does not conform to the v0.9 schema.
|
||||
"""
|
||||
try:
|
||||
return A2UIMessageV09.model_validate(data)
|
||||
except ValidationError as exc:
|
||||
raise A2UIValidationError(
|
||||
f"Invalid A2UI v0.9 message: {exc.error_count()} validation error(s)",
|
||||
errors=exc.errors(),
|
||||
) from exc
|
||||
|
||||
|
||||
def validate_a2ui_event_v09(data: dict[str, Any]) -> A2UIEventV09:
|
||||
"""Parse and validate an A2UI v0.9 client-to-server event.
|
||||
|
||||
Args:
|
||||
data: Raw JSON-decoded event dict.
|
||||
|
||||
Returns:
|
||||
Validated ``A2UIEventV09`` instance.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If the data does not conform to the v0.9 schema.
|
||||
"""
|
||||
try:
|
||||
return A2UIEventV09.model_validate(data)
|
||||
except ValidationError as exc:
|
||||
raise A2UIValidationError(
|
||||
f"Invalid A2UI v0.9 event: {exc.error_count()} validation error(s)",
|
||||
errors=exc.errors(),
|
||||
) from exc
|
||||
|
||||
|
||||
def validate_catalog_components(message: A2UIMessage) -> None:
|
||||
"""Validate component properties in a surfaceUpdate against the standard catalog.
|
||||
|
||||
Only applies to surfaceUpdate messages. Components whose type is not
|
||||
in the standard catalog are skipped without error.
|
||||
|
||||
Args:
|
||||
message: A validated A2UIMessage.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If any component fails catalog validation.
|
||||
"""
|
||||
if message.surface_update is None:
|
||||
return
|
||||
|
||||
errors: list[Any] = []
|
||||
for entry in message.surface_update.components:
|
||||
for type_name, props in entry.component.items():
|
||||
model = _STANDARD_CATALOG_MODELS.get(type_name)
|
||||
if model is None:
|
||||
continue
|
||||
try:
|
||||
model.model_validate(props)
|
||||
except ValidationError as exc:
|
||||
errors.extend(
|
||||
{
|
||||
"component_id": entry.id,
|
||||
"component_type": type_name,
|
||||
**err,
|
||||
}
|
||||
for err in exc.errors()
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise A2UIValidationError(
|
||||
f"Catalog validation failed: {len(errors)} error(s)",
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
_V09_BASIC_CATALOG_MODELS: dict[str, type[BaseModel]] = {
|
||||
"AudioPlayer": AudioPlayerV09,
|
||||
"Button": ButtonV09,
|
||||
"Card": CardV09,
|
||||
"CheckBox": CheckBoxV09,
|
||||
"ChoicePicker": ChoicePickerV09,
|
||||
"Column": ColumnV09,
|
||||
"DateTimeInput": DateTimeInputV09,
|
||||
"Divider": DividerV09,
|
||||
"Icon": IconV09,
|
||||
"Image": ImageV09,
|
||||
"List": ListV09,
|
||||
"Modal": ModalV09,
|
||||
"Row": RowV09,
|
||||
"Slider": SliderV09,
|
||||
"Tabs": TabsV09,
|
||||
"Text": TextV09,
|
||||
"TextField": TextFieldV09,
|
||||
"Video": VideoV09,
|
||||
}
|
||||
|
||||
|
||||
def validate_catalog_components_v09(message: A2UIMessageV09) -> None:
|
||||
"""Validate component properties in an updateComponents against the basic catalog.
|
||||
|
||||
v0.9 components use a flat structure where ``component`` is a type-name
|
||||
string and properties sit at the top level of the component dict.
|
||||
|
||||
Only applies to updateComponents messages. Components whose type is not
|
||||
in the basic catalog are skipped without error.
|
||||
|
||||
Args:
|
||||
message: A validated A2UIMessageV09.
|
||||
|
||||
Raises:
|
||||
A2UIValidationError: If any component fails catalog validation.
|
||||
"""
|
||||
if message.update_components is None:
|
||||
return
|
||||
|
||||
errors: list[Any] = []
|
||||
for entry in message.update_components.components:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
type_name = entry.get("component")
|
||||
if not isinstance(type_name, str):
|
||||
continue
|
||||
model = _V09_BASIC_CATALOG_MODELS.get(type_name)
|
||||
if model is None:
|
||||
continue
|
||||
try:
|
||||
model.model_validate(entry)
|
||||
except ValidationError as exc:
|
||||
errors.extend(
|
||||
{
|
||||
"component_id": entry.get("id", "<unknown>"),
|
||||
"component_type": type_name,
|
||||
**err,
|
||||
}
|
||||
for err in exc.errors()
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise A2UIValidationError(
|
||||
f"v0.9 catalog validation failed: {len(errors)} error(s)",
|
||||
errors=errors,
|
||||
)
|
||||
@@ -150,6 +150,23 @@ class A2AExtension(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
def prepare_message_metadata(
|
||||
self,
|
||||
conversation_state: ConversationState | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prepare extension-specific metadata for outbound A2A messages.
|
||||
|
||||
Called when constructing A2A messages to inject extension-specific
|
||||
metadata such as client capabilities declarations.
|
||||
|
||||
Args:
|
||||
conversation_state: Extension-specific state from extract_state_from_history.
|
||||
|
||||
Returns:
|
||||
Dict of metadata key-value pairs to merge into the message metadata.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class ExtensionRegistry:
|
||||
"""Registry for managing A2A extensions.
|
||||
@@ -236,3 +253,21 @@ class ExtensionRegistry:
|
||||
state = extension_states.get(type(extension))
|
||||
processed = extension.process_response(processed, state)
|
||||
return processed
|
||||
|
||||
def prepare_all_metadata(
|
||||
self,
|
||||
extension_states: dict[type[A2AExtension], ConversationState],
|
||||
) -> dict[str, Any]:
|
||||
"""Collect metadata from all registered extensions for outbound messages.
|
||||
|
||||
Args:
|
||||
extension_states: Mapping of extension types to conversation states.
|
||||
|
||||
Returns:
|
||||
Merged metadata dict from all extensions.
|
||||
"""
|
||||
metadata: dict[str, Any] = {}
|
||||
for extension in self._extensions:
|
||||
state = extension_states.get(type(extension))
|
||||
metadata.update(extension.prepare_message_metadata(state))
|
||||
return metadata
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import uuid
|
||||
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
@@ -18,7 +18,7 @@ from a2a.types import (
|
||||
TaskStatusUpdateEvent,
|
||||
TextPart,
|
||||
)
|
||||
from typing_extensions import NotRequired
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
|
||||
@@ -7,12 +7,11 @@ from typing import (
|
||||
Any,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypedDict,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from pydantic import BeforeValidator, HttpUrl, TypeAdapter
|
||||
from typing_extensions import NotRequired
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, TypedDict
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol
|
||||
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class CommonParams(NamedTuple):
|
||||
|
||||
@@ -28,6 +28,7 @@ APPLICATION_PDF: Literal["application/pdf"] = "application/pdf"
|
||||
APPLICATION_OCTET_STREAM: Literal["application/octet-stream"] = (
|
||||
"application/octet-stream"
|
||||
)
|
||||
APPLICATION_A2UI_JSON: Literal["application/json+a2ui"] = "application/json+a2ui"
|
||||
|
||||
DEFAULT_CLIENT_INPUT_MODES: Final[list[Literal["text/plain", "application/json"]]] = [
|
||||
TEXT_PLAIN,
|
||||
@@ -311,6 +312,10 @@ def get_part_content_type(part: Part) -> str:
|
||||
if root.kind == "text":
|
||||
return TEXT_PLAIN
|
||||
if root.kind == "data":
|
||||
metadata = root.metadata or {}
|
||||
mime = metadata.get("mimeType", "")
|
||||
if mime == APPLICATION_A2UI_JSON:
|
||||
return APPLICATION_A2UI_JSON
|
||||
return APPLICATION_JSON
|
||||
if root.kind == "file":
|
||||
return root.file.mime_type or APPLICATION_OCTET_STREAM
|
||||
|
||||
@@ -10,7 +10,7 @@ from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, TypedDict, cast
|
||||
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from a2a.server.agent_execution import RequestContext
|
||||
@@ -38,6 +38,7 @@ from a2a.utils import (
|
||||
from a2a.utils.errors import ServerError
|
||||
from aiocache import SimpleMemoryCache, caches # type: ignore[import-untyped]
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from crewai.a2a.utils.agent_card import _get_server_config
|
||||
from crewai.a2a.utils.content_type import validate_message_parts
|
||||
|
||||
@@ -1273,6 +1273,15 @@ def _delegate_to_a2a(
|
||||
for turn_num in range(ctx.max_turns):
|
||||
agent_branch, accepted_output_modes = _get_turn_context(ctx.agent_config)
|
||||
|
||||
merged_metadata = dict(ctx.metadata) if ctx.metadata else {}
|
||||
if _extension_registry and conversation_history:
|
||||
_ext_states = _extension_registry.extract_all_states(
|
||||
conversation_history
|
||||
)
|
||||
merged_metadata.update(
|
||||
_extension_registry.prepare_all_metadata(_ext_states)
|
||||
)
|
||||
|
||||
a2a_result = execute_a2a_delegation(
|
||||
endpoint=ctx.agent_config.endpoint,
|
||||
auth=ctx.agent_config.auth,
|
||||
@@ -1281,7 +1290,7 @@ def _delegate_to_a2a(
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=ctx.metadata,
|
||||
metadata=merged_metadata or None,
|
||||
extensions=ctx.extensions,
|
||||
conversation_history=conversation_history,
|
||||
agent_id=ctx.agent_id,
|
||||
@@ -1619,6 +1628,15 @@ async def _adelegate_to_a2a(
|
||||
for turn_num in range(ctx.max_turns):
|
||||
agent_branch, accepted_output_modes = _get_turn_context(ctx.agent_config)
|
||||
|
||||
merged_metadata = dict(ctx.metadata) if ctx.metadata else {}
|
||||
if _extension_registry and conversation_history:
|
||||
_ext_states = _extension_registry.extract_all_states(
|
||||
conversation_history
|
||||
)
|
||||
merged_metadata.update(
|
||||
_extension_registry.prepare_all_metadata(_ext_states)
|
||||
)
|
||||
|
||||
a2a_result = await aexecute_a2a_delegation(
|
||||
endpoint=ctx.agent_config.endpoint,
|
||||
auth=ctx.agent_config.auth,
|
||||
@@ -1627,7 +1645,7 @@ async def _adelegate_to_a2a(
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=ctx.metadata,
|
||||
metadata=merged_metadata or None,
|
||||
extensions=ctx.extensions,
|
||||
conversation_history=conversation_history,
|
||||
agent_id=ctx.agent_id,
|
||||
|
||||
@@ -25,7 +25,6 @@ from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
@@ -167,10 +166,10 @@ class Agent(BaseAgent):
|
||||
default=True,
|
||||
description="Use system prompt for the agent.",
|
||||
)
|
||||
llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
llm: str | BaseLLM | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
function_calling_llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
function_calling_llm: str | BaseLLM | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
system_template: str | None = Field(
|
||||
@@ -1012,7 +1011,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 = stop_words
|
||||
self.agent_executor.stop_words = 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,7 +12,6 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
@@ -185,7 +184,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default=None,
|
||||
description="Knowledge sources for the agent.",
|
||||
)
|
||||
knowledge_storage: InstanceOf[BaseKnowledgeStorage] | None = Field(
|
||||
knowledge_storage: BaseKnowledgeStorage | None = Field(
|
||||
default=None,
|
||||
description="Custom knowledge storage for the agent.",
|
||||
)
|
||||
|
||||
@@ -73,6 +73,7 @@ class PlusAPI:
|
||||
description: str | None,
|
||||
encoded_file: str,
|
||||
available_exports: list[dict[str, Any]] | None = None,
|
||||
tools_metadata: list[dict[str, Any]] | None = None,
|
||||
) -> httpx.Response:
|
||||
params = {
|
||||
"handle": handle,
|
||||
@@ -81,6 +82,9 @@ class PlusAPI:
|
||||
"file": encoded_file,
|
||||
"description": description,
|
||||
"available_exports": available_exports,
|
||||
"tools_metadata": {"package": handle, "tools": tools_metadata}
|
||||
if tools_metadata is not None
|
||||
else None,
|
||||
}
|
||||
return self._make_request("POST", f"{self.TOOLS_RESOURCE}", json=params)
|
||||
|
||||
@@ -196,6 +200,16 @@ class PlusAPI:
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
def mark_ephemeral_trace_batch_as_failed(
|
||||
self, trace_batch_id: str, error_message: str
|
||||
) -> httpx.Response:
|
||||
return self._make_request(
|
||||
"PATCH",
|
||||
f"{self.EPHEMERAL_TRACING_RESOURCE}/batches/{trace_batch_id}",
|
||||
json={"status": "failed", "failure_reason": error_message},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
def get_mcp_configs(self, slugs: list[str]) -> httpx.Response:
|
||||
"""Get MCP server configurations for the given slugs."""
|
||||
return self._make_request(
|
||||
|
||||
@@ -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.12.0a2"
|
||||
"crewai[tools]==1.13.0a6"
|
||||
]
|
||||
|
||||
[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.12.0a2"
|
||||
"crewai[tools]==1.13.0a6"
|
||||
]
|
||||
|
||||
[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.12.0a2"
|
||||
"crewai[tools]==1.13.0a6"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -17,6 +17,7 @@ from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai.cli.utils import (
|
||||
build_env_with_tool_repository_credentials,
|
||||
extract_available_exports,
|
||||
extract_tools_metadata,
|
||||
get_project_description,
|
||||
get_project_name,
|
||||
get_project_version,
|
||||
@@ -101,6 +102,18 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
console.print(
|
||||
f"[green]Found these tools to publish: {', '.join([e['name'] for e in available_exports])}[/green]"
|
||||
)
|
||||
|
||||
console.print("[bold blue]Extracting tool metadata...[/bold blue]")
|
||||
try:
|
||||
tools_metadata = extract_tools_metadata()
|
||||
except Exception as e:
|
||||
console.print(
|
||||
f"[yellow]Warning: Could not extract tool metadata: {e}[/yellow]\n"
|
||||
f"Publishing will continue without detailed metadata."
|
||||
)
|
||||
tools_metadata = []
|
||||
|
||||
self._print_tools_preview(tools_metadata)
|
||||
self._print_current_organization()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_build_dir:
|
||||
@@ -118,7 +131,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
"Project build failed. Please ensure that the command `uv build --sdist` completes successfully.",
|
||||
style="bold red",
|
||||
)
|
||||
raise SystemExit
|
||||
raise SystemExit(1)
|
||||
|
||||
tarball_path = os.path.join(temp_build_dir, tarball_filename)
|
||||
with open(tarball_path, "rb") as file:
|
||||
@@ -134,6 +147,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
description=project_description,
|
||||
encoded_file=f"data:application/x-gzip;base64,{encoded_tarball}",
|
||||
available_exports=available_exports,
|
||||
tools_metadata=tools_metadata,
|
||||
)
|
||||
|
||||
self._validate_response(publish_response)
|
||||
@@ -246,6 +260,55 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
)
|
||||
raise SystemExit
|
||||
|
||||
def _print_tools_preview(self, tools_metadata: list[dict[str, Any]]) -> None:
|
||||
if not tools_metadata:
|
||||
console.print("[yellow]No tool metadata extracted.[/yellow]")
|
||||
return
|
||||
|
||||
console.print(
|
||||
f"\n[bold]Tools to be published ({len(tools_metadata)}):[/bold]\n"
|
||||
)
|
||||
|
||||
for tool in tools_metadata:
|
||||
console.print(f" [bold cyan]{tool.get('name', 'Unknown')}[/bold cyan]")
|
||||
if tool.get("module"):
|
||||
console.print(f" Module: {tool.get('module')}")
|
||||
console.print(f" Name: {tool.get('humanized_name', 'N/A')}")
|
||||
console.print(
|
||||
f" Description: {tool.get('description', 'N/A')[:80]}{'...' if len(tool.get('description', '')) > 80 else ''}"
|
||||
)
|
||||
|
||||
init_params = tool.get("init_params_schema", {}).get("properties", {})
|
||||
if init_params:
|
||||
required = tool.get("init_params_schema", {}).get("required", [])
|
||||
console.print(" Init parameters:")
|
||||
for param_name, param_info in init_params.items():
|
||||
param_type = param_info.get("type", "any")
|
||||
is_required = param_name in required
|
||||
req_marker = "[red]*[/red]" if is_required else ""
|
||||
default = (
|
||||
f" = {param_info['default']}" if "default" in param_info else ""
|
||||
)
|
||||
console.print(
|
||||
f" - {param_name}: {param_type}{default} {req_marker}"
|
||||
)
|
||||
|
||||
env_vars = tool.get("env_vars", [])
|
||||
if env_vars:
|
||||
console.print(" Environment variables:")
|
||||
for env_var in env_vars:
|
||||
req_marker = "[red]*[/red]" if env_var.get("required") else ""
|
||||
default = (
|
||||
f" (default: {env_var['default']})"
|
||||
if env_var.get("default")
|
||||
else ""
|
||||
)
|
||||
console.print(
|
||||
f" - {env_var['name']}: {env_var.get('description', 'N/A')}{default} {req_marker}"
|
||||
)
|
||||
|
||||
console.print()
|
||||
|
||||
def _print_current_organization(self) -> None:
|
||||
settings = Settings()
|
||||
if settings.org_uuid:
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from functools import reduce
|
||||
from collections.abc import Generator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache, reduce
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import inspect
|
||||
from inspect import getmro, isclass, isfunction, ismethod
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
from typing import Any, cast, get_type_hints
|
||||
|
||||
import click
|
||||
@@ -544,43 +549,62 @@ def build_env_with_tool_repository_credentials(
|
||||
return env
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _load_module_from_file(
|
||||
init_file: Path, module_name: str | None = None
|
||||
) -> Generator[types.ModuleType | None, None, None]:
|
||||
"""
|
||||
Context manager for loading a module from file with automatic cleanup.
|
||||
|
||||
Yields the loaded module or None if loading fails.
|
||||
"""
|
||||
if module_name is None:
|
||||
module_name = (
|
||||
f"temp_module_{hashlib.sha256(str(init_file).encode()).hexdigest()[:8]}"
|
||||
)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_name, init_file)
|
||||
if not spec or not spec.loader:
|
||||
yield None
|
||||
return
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
yield module
|
||||
finally:
|
||||
sys.modules.pop(module_name, None)
|
||||
|
||||
|
||||
def _load_tools_from_init(init_file: Path) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Load and validate tools from a given __init__.py file.
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location("temp_module", init_file)
|
||||
|
||||
if not spec or not spec.loader:
|
||||
return []
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["temp_module"] = module
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
with _load_module_from_file(init_file) as module:
|
||||
if module is None:
|
||||
return []
|
||||
|
||||
if not hasattr(module, "__all__"):
|
||||
console.print(
|
||||
f"Warning: No __all__ defined in {init_file}",
|
||||
style="bold yellow",
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
}
|
||||
for name in module.__all__
|
||||
if hasattr(module, name) and is_valid_tool(getattr(module, name))
|
||||
]
|
||||
if not hasattr(module, "__all__"):
|
||||
console.print(
|
||||
f"Warning: No __all__ defined in {init_file}",
|
||||
style="bold yellow",
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
return [
|
||||
{"name": name}
|
||||
for name in module.__all__
|
||||
if hasattr(module, name) and is_valid_tool(getattr(module, name))
|
||||
]
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
console.print(f"[red]Warning: Could not load {init_file}: {e!s}[/red]")
|
||||
raise SystemExit(1) from e
|
||||
|
||||
finally:
|
||||
sys.modules.pop("temp_module", None)
|
||||
|
||||
|
||||
def _print_no_tools_warning() -> None:
|
||||
"""
|
||||
@@ -610,3 +634,242 @@ def _print_no_tools_warning() -> None:
|
||||
" # ... implementation\n"
|
||||
" return result\n"
|
||||
)
|
||||
|
||||
|
||||
def extract_tools_metadata(dir_path: str = "src") -> list[dict[str, Any]]:
|
||||
"""
|
||||
Extract rich metadata from tool classes in the project.
|
||||
|
||||
Returns a list of tool metadata dictionaries containing:
|
||||
- name: Class name
|
||||
- humanized_name: From name field default
|
||||
- description: From description field default
|
||||
- run_params_schema: JSON Schema for _run() params (from args_schema)
|
||||
- init_params_schema: JSON Schema for __init__ params (filtered)
|
||||
- env_vars: List of environment variable dicts
|
||||
"""
|
||||
tools_metadata: list[dict[str, Any]] = []
|
||||
|
||||
for init_file in Path(dir_path).glob("**/__init__.py"):
|
||||
tools = _extract_tool_metadata_from_init(init_file)
|
||||
tools_metadata.extend(tools)
|
||||
|
||||
return tools_metadata
|
||||
|
||||
|
||||
def _extract_tool_metadata_from_init(init_file: Path) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Load module from init file and extract metadata from valid tool classes.
|
||||
"""
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
|
||||
try:
|
||||
with _load_module_from_file(init_file) as module:
|
||||
if module is None:
|
||||
return []
|
||||
|
||||
exported_names = getattr(module, "__all__", None)
|
||||
if not exported_names:
|
||||
return []
|
||||
|
||||
tools_metadata = []
|
||||
for name in exported_names:
|
||||
obj = getattr(module, name, None)
|
||||
if obj is None or not (
|
||||
inspect.isclass(obj) and issubclass(obj, BaseTool)
|
||||
):
|
||||
continue
|
||||
if tool_info := _extract_single_tool_metadata(obj):
|
||||
tools_metadata.append(tool_info)
|
||||
|
||||
return tools_metadata
|
||||
except Exception as e:
|
||||
console.print(
|
||||
f"[yellow]Warning: Could not extract metadata from {init_file}: {e}[/yellow]"
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
def _extract_single_tool_metadata(tool_class: type) -> dict[str, Any] | None:
|
||||
"""
|
||||
Extract metadata from a single tool class.
|
||||
"""
|
||||
try:
|
||||
core_schema = cast(Any, tool_class).__pydantic_core_schema__
|
||||
if not core_schema:
|
||||
return None
|
||||
|
||||
schema = _unwrap_schema(core_schema)
|
||||
fields = schema.get("schema", {}).get("fields", {})
|
||||
|
||||
try:
|
||||
file_path = inspect.getfile(tool_class)
|
||||
relative_path = Path(file_path).relative_to(Path.cwd())
|
||||
module_path = relative_path.with_suffix("")
|
||||
if module_path.parts[0] == "src":
|
||||
module_path = Path(*module_path.parts[1:])
|
||||
if module_path.name == "__init__":
|
||||
module_path = module_path.parent
|
||||
module = ".".join(module_path.parts)
|
||||
except (TypeError, ValueError):
|
||||
module = tool_class.__module__
|
||||
|
||||
return {
|
||||
"name": tool_class.__name__,
|
||||
"module": module,
|
||||
"humanized_name": _extract_field_default(
|
||||
fields.get("name"), fallback=tool_class.__name__
|
||||
),
|
||||
"description": str(
|
||||
_extract_field_default(fields.get("description"))
|
||||
).strip(),
|
||||
"run_params_schema": _extract_run_params_schema(fields.get("args_schema")),
|
||||
"init_params_schema": _extract_init_params_schema(tool_class),
|
||||
"env_vars": _extract_env_vars(fields.get("env_vars")),
|
||||
}
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _unwrap_schema(schema: Mapping[str, Any] | dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Unwrap nested schema structures to get to the actual schema definition.
|
||||
"""
|
||||
result: dict[str, Any] = dict(schema)
|
||||
while (
|
||||
result.get("type")
|
||||
in {"function-after", "function-before", "function-wrap", "default"}
|
||||
and "schema" in result
|
||||
):
|
||||
result = dict(result["schema"])
|
||||
if result.get("type") == "definitions" and "schema" in result:
|
||||
result = dict(result["schema"])
|
||||
return result
|
||||
|
||||
|
||||
def _extract_field_default(
|
||||
field: dict[str, Any] | None, fallback: str | list[Any] = ""
|
||||
) -> str | list[Any] | int:
|
||||
"""
|
||||
Extract the default value from a field schema.
|
||||
"""
|
||||
if not field:
|
||||
return fallback
|
||||
|
||||
schema = field.get("schema", {})
|
||||
default = schema.get("default")
|
||||
return default if isinstance(default, (list, str, int)) else fallback
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_schema_generator() -> type:
|
||||
"""Get a SchemaGenerator that omits non-serializable defaults."""
|
||||
from pydantic.json_schema import GenerateJsonSchema
|
||||
from pydantic_core import PydanticOmit
|
||||
|
||||
class SchemaGenerator(GenerateJsonSchema):
|
||||
def handle_invalid_for_json_schema(
|
||||
self, schema: Any, error_info: Any
|
||||
) -> dict[str, Any]:
|
||||
raise PydanticOmit
|
||||
|
||||
return SchemaGenerator
|
||||
|
||||
|
||||
def _extract_run_params_schema(
|
||||
args_schema_field: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Extract JSON Schema for the tool's run parameters from args_schema field.
|
||||
"""
|
||||
from pydantic import BaseModel
|
||||
|
||||
if not args_schema_field:
|
||||
return {}
|
||||
|
||||
args_schema_class = args_schema_field.get("schema", {}).get("default")
|
||||
if not (
|
||||
inspect.isclass(args_schema_class) and issubclass(args_schema_class, BaseModel)
|
||||
):
|
||||
return {}
|
||||
|
||||
try:
|
||||
return args_schema_class.model_json_schema(
|
||||
schema_generator=_get_schema_generator()
|
||||
)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
_IGNORED_INIT_PARAMS = frozenset(
|
||||
{
|
||||
"name",
|
||||
"description",
|
||||
"env_vars",
|
||||
"args_schema",
|
||||
"description_updated",
|
||||
"cache_function",
|
||||
"result_as_answer",
|
||||
"max_usage_count",
|
||||
"current_usage_count",
|
||||
"package_dependencies",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _extract_init_params_schema(tool_class: type) -> dict[str, Any]:
|
||||
"""
|
||||
Extract JSON Schema for the tool's __init__ parameters, filtering out base fields.
|
||||
"""
|
||||
try:
|
||||
json_schema: dict[str, Any] = cast(Any, tool_class).model_json_schema(
|
||||
schema_generator=_get_schema_generator(), mode="serialization"
|
||||
)
|
||||
filtered_properties = {
|
||||
key: value
|
||||
for key, value in json_schema.get("properties", {}).items()
|
||||
if key not in _IGNORED_INIT_PARAMS
|
||||
}
|
||||
json_schema["properties"] = filtered_properties
|
||||
if "required" in json_schema:
|
||||
json_schema["required"] = [
|
||||
key for key in json_schema["required"] if key in filtered_properties
|
||||
]
|
||||
return json_schema
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_env_vars(env_vars_field: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Extract environment variable definitions from env_vars field.
|
||||
"""
|
||||
from crewai.tools.base_tool import EnvVar
|
||||
|
||||
if not env_vars_field:
|
||||
return []
|
||||
|
||||
schema = env_vars_field.get("schema", {})
|
||||
default = schema.get("default")
|
||||
if default is None:
|
||||
default_factory = schema.get("default_factory")
|
||||
if callable(default_factory):
|
||||
try:
|
||||
default = default_factory()
|
||||
except Exception:
|
||||
default = []
|
||||
|
||||
if not isinstance(default, list):
|
||||
return []
|
||||
|
||||
return [
|
||||
{
|
||||
"name": env_var.name,
|
||||
"description": env_var.description,
|
||||
"required": env_var.required,
|
||||
"default": env_var.default,
|
||||
}
|
||||
for env_var in default
|
||||
if isinstance(env_var, EnvVar)
|
||||
]
|
||||
|
||||
@@ -22,7 +22,6 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
Json,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
@@ -176,7 +175,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
_rpm_controller: RPMController = PrivateAttr()
|
||||
_logger: Logger = PrivateAttr()
|
||||
_file_handler: FileHandler = PrivateAttr()
|
||||
_cache_handler: InstanceOf[CacheHandler] = PrivateAttr(default_factory=CacheHandler)
|
||||
_cache_handler: CacheHandler = PrivateAttr(default_factory=CacheHandler)
|
||||
_memory: Memory | MemoryScope | MemorySlice | None = PrivateAttr(default=None)
|
||||
_train: bool | None = PrivateAttr(default=False)
|
||||
_train_iteration: int | None = PrivateAttr()
|
||||
@@ -210,13 +209,13 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=None,
|
||||
description="Metrics for the LLM usage during all tasks execution.",
|
||||
)
|
||||
manager_llm: str | InstanceOf[BaseLLM] | None = Field(
|
||||
manager_llm: str | 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 | InstanceOf[LLM] | None = Field(
|
||||
function_calling_llm: str | 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)
|
||||
@@ -267,7 +266,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=False,
|
||||
description="Plan the crew execution and add the plan to the crew.",
|
||||
)
|
||||
planning_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
planning_llm: str | BaseLLM | Any | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Language model that will run the AgentPlanner if planning is True."
|
||||
@@ -288,7 +287,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
"knowledge object."
|
||||
),
|
||||
)
|
||||
chat_llm: str | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
chat_llm: str | BaseLLM | Any | None = Field(
|
||||
default=None,
|
||||
description="LLM used to handle chatting with the crew.",
|
||||
)
|
||||
@@ -1800,7 +1799,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
def test(
|
||||
self,
|
||||
n_iterations: int,
|
||||
eval_llm: str | InstanceOf[BaseLLM],
|
||||
eval_llm: str | BaseLLM,
|
||||
inputs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Test and evaluate the Crew with the given inputs for n iterations.
|
||||
|
||||
@@ -85,6 +85,8 @@ class CrewAIEventsBus:
|
||||
_shutting_down: bool
|
||||
_pending_futures: set[Future[Any]]
|
||||
_futures_lock: threading.Lock
|
||||
_executor_initialized: bool
|
||||
_has_pending_events: bool
|
||||
|
||||
def __new__(cls) -> Self:
|
||||
"""Create or return the singleton instance.
|
||||
@@ -102,8 +104,9 @@ class CrewAIEventsBus:
|
||||
def _initialize(self) -> None:
|
||||
"""Initialize the event bus internal state.
|
||||
|
||||
Creates handler dictionaries and starts a dedicated background
|
||||
event loop for async handler execution.
|
||||
Creates handler dictionaries. The thread pool executor and event loop
|
||||
are lazily initialized on first emit() to avoid overhead when events
|
||||
are never emitted.
|
||||
"""
|
||||
self._shutting_down = False
|
||||
self._rwlock = RWLock()
|
||||
@@ -115,19 +118,37 @@ class CrewAIEventsBus:
|
||||
type[BaseEvent], dict[Handler, list[Depends[Any]]]
|
||||
] = {}
|
||||
self._execution_plan_cache: dict[type[BaseEvent], ExecutionPlan] = {}
|
||||
self._sync_executor = ThreadPoolExecutor(
|
||||
max_workers=10,
|
||||
thread_name_prefix="CrewAISyncHandler",
|
||||
)
|
||||
self._console = ConsoleFormatter()
|
||||
# Lazy initialization flags - executor and loop created on first emit
|
||||
self._executor_initialized = False
|
||||
self._has_pending_events = False
|
||||
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._loop_thread = threading.Thread(
|
||||
target=self._run_loop,
|
||||
name="CrewAIEventsLoop",
|
||||
daemon=True,
|
||||
)
|
||||
self._loop_thread.start()
|
||||
def _ensure_executor_initialized(self) -> None:
|
||||
"""Lazily initialize the thread pool executor and event loop.
|
||||
|
||||
Called on first emit() to avoid startup overhead when events are never used.
|
||||
Thread-safe via double-checked locking.
|
||||
"""
|
||||
if self._executor_initialized:
|
||||
return
|
||||
|
||||
with self._instance_lock:
|
||||
if self._executor_initialized:
|
||||
return
|
||||
|
||||
self._sync_executor = ThreadPoolExecutor(
|
||||
max_workers=10,
|
||||
thread_name_prefix="CrewAISyncHandler",
|
||||
)
|
||||
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._loop_thread = threading.Thread(
|
||||
target=self._run_loop,
|
||||
name="CrewAIEventsLoop",
|
||||
daemon=True,
|
||||
)
|
||||
self._loop_thread.start()
|
||||
self._executor_initialized = True
|
||||
|
||||
def _track_future(self, future: Future[Any]) -> Future[Any]:
|
||||
"""Track a future and set up automatic cleanup when it completes.
|
||||
@@ -431,6 +452,15 @@ class CrewAIEventsBus:
|
||||
sync_handlers = self._sync_handlers.get(event_type, frozenset())
|
||||
async_handlers = self._async_handlers.get(event_type, frozenset())
|
||||
|
||||
# Skip executor initialization if no handlers exist for this event
|
||||
if not sync_handlers and not async_handlers:
|
||||
return None
|
||||
|
||||
# Lazily initialize executor and event loop only when handlers exist
|
||||
self._ensure_executor_initialized()
|
||||
# Track that we have pending events for flush optimization
|
||||
self._has_pending_events = True
|
||||
|
||||
if has_dependencies:
|
||||
return self._track_future(
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
@@ -474,6 +504,10 @@ class CrewAIEventsBus:
|
||||
Returns:
|
||||
True if all handlers completed, False if timeout occurred.
|
||||
"""
|
||||
# Skip flush entirely if no events were ever emitted
|
||||
if not self._has_pending_events:
|
||||
return True
|
||||
|
||||
with self._futures_lock:
|
||||
futures_to_wait = list(self._pending_futures)
|
||||
|
||||
@@ -629,6 +663,9 @@ class CrewAIEventsBus:
|
||||
|
||||
with self._rwlock.w_locked():
|
||||
self._shutting_down = True
|
||||
# Check if executor was ever initialized (lazy init optimization)
|
||||
if not self._executor_initialized:
|
||||
return
|
||||
loop = getattr(self, "_loop", None)
|
||||
|
||||
if loop is None or loop.is_closed():
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
import uuid
|
||||
import webbrowser
|
||||
@@ -100,20 +101,50 @@ class FirstTimeTraceHandler:
|
||||
user_context=user_context,
|
||||
execution_metadata=execution_metadata,
|
||||
use_ephemeral=True,
|
||||
skip_context_check=True,
|
||||
)
|
||||
|
||||
if not self.batch_manager.trace_batch_id:
|
||||
self._gracefully_fail(
|
||||
"Backend batch creation failed, cannot send events."
|
||||
)
|
||||
self._reset_batch_state()
|
||||
return
|
||||
|
||||
self.batch_manager.backend_initialized = True
|
||||
|
||||
if self.batch_manager.event_buffer:
|
||||
self.batch_manager._send_events_to_backend()
|
||||
# Capture values before send/finalize consume them
|
||||
events_count = len(self.batch_manager.event_buffer)
|
||||
batch_id = self.batch_manager.trace_batch_id
|
||||
# Read duration non-destructively — _finalize_backend_batch will consume it
|
||||
start_time = self.batch_manager.execution_start_times.get("execution")
|
||||
duration_ms = (
|
||||
int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
if start_time
|
||||
else 0
|
||||
)
|
||||
|
||||
self.batch_manager.finalize_batch()
|
||||
if self.batch_manager.event_buffer:
|
||||
send_status = self.batch_manager._send_events_to_backend()
|
||||
if send_status == 500 and self.batch_manager.trace_batch_id:
|
||||
self.batch_manager._mark_batch_as_failed(
|
||||
self.batch_manager.trace_batch_id,
|
||||
"Error sending events to backend",
|
||||
)
|
||||
self._reset_batch_state()
|
||||
return
|
||||
|
||||
self.batch_manager._finalize_backend_batch(events_count)
|
||||
self.ephemeral_url = self.batch_manager.ephemeral_trace_url
|
||||
|
||||
if not self.ephemeral_url:
|
||||
self._show_local_trace_message()
|
||||
self._show_local_trace_message(events_count, duration_ms, batch_id)
|
||||
|
||||
self._reset_batch_state()
|
||||
|
||||
except Exception as e:
|
||||
self._gracefully_fail(f"Backend initialization failed: {e}")
|
||||
self._reset_batch_state()
|
||||
|
||||
def _display_ephemeral_trace_link(self) -> None:
|
||||
"""Display the ephemeral trace link to the user and automatically open browser."""
|
||||
@@ -185,6 +216,19 @@ To enable tracing later, do any one of these:
|
||||
console.print(panel)
|
||||
console.print()
|
||||
|
||||
def _reset_batch_state(self) -> None:
|
||||
"""Reset batch manager state to allow future executions to re-initialize."""
|
||||
if not self.batch_manager:
|
||||
return
|
||||
self.batch_manager.batch_owner_type = None
|
||||
self.batch_manager.batch_owner_id = None
|
||||
self.batch_manager.current_batch = None
|
||||
self.batch_manager.event_buffer.clear()
|
||||
self.batch_manager.trace_batch_id = None
|
||||
self.batch_manager.is_current_batch_ephemeral = False
|
||||
self.batch_manager.backend_initialized = False
|
||||
self.batch_manager._cleanup_batch_data()
|
||||
|
||||
def _gracefully_fail(self, error_message: str) -> None:
|
||||
"""Handle errors gracefully without disrupting user experience."""
|
||||
console = Console()
|
||||
@@ -192,7 +236,9 @@ To enable tracing later, do any one of these:
|
||||
|
||||
logger.debug(f"First-time trace error: {error_message}")
|
||||
|
||||
def _show_local_trace_message(self) -> None:
|
||||
def _show_local_trace_message(
|
||||
self, events_count: int = 0, duration_ms: int = 0, batch_id: str | None = None
|
||||
) -> None:
|
||||
"""Show message when traces were collected locally but couldn't be uploaded."""
|
||||
if self.batch_manager is None:
|
||||
return
|
||||
@@ -203,9 +249,9 @@ To enable tracing later, do any one of these:
|
||||
📊 Your execution traces were collected locally!
|
||||
|
||||
Unfortunately, we couldn't upload them to the server right now, but here's what we captured:
|
||||
• {len(self.batch_manager.event_buffer)} trace events
|
||||
• Execution duration: {self.batch_manager.calculate_duration("execution")}ms
|
||||
• Batch ID: {self.batch_manager.trace_batch_id}
|
||||
• {events_count} trace events
|
||||
• Execution duration: {duration_ms}ms
|
||||
• Batch ID: {batch_id}
|
||||
|
||||
✅ Tracing has been enabled for future runs!
|
||||
Your preference has been saved. Future Crew/Flow executions will automatically collect traces.
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from logging import getLogger
|
||||
from threading import Condition, Lock
|
||||
import time
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
@@ -98,7 +99,7 @@ class TraceBatchManager:
|
||||
self._initialize_backend_batch(
|
||||
user_context, execution_metadata, use_ephemeral
|
||||
)
|
||||
self.backend_initialized = True
|
||||
self.backend_initialized = self.trace_batch_id is not None
|
||||
|
||||
self._batch_ready_cv.notify_all()
|
||||
return self.current_batch
|
||||
@@ -108,14 +109,15 @@ class TraceBatchManager:
|
||||
user_context: dict[str, str],
|
||||
execution_metadata: dict[str, Any],
|
||||
use_ephemeral: bool = False,
|
||||
skip_context_check: bool = False,
|
||||
) -> None:
|
||||
"""Send batch initialization to backend"""
|
||||
|
||||
if not is_tracing_enabled_in_context():
|
||||
return
|
||||
if not skip_context_check and not is_tracing_enabled_in_context():
|
||||
return None
|
||||
|
||||
if not self.plus_api or not self.current_batch:
|
||||
return
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = {
|
||||
@@ -142,19 +144,53 @@ class TraceBatchManager:
|
||||
payload["ephemeral_trace_id"] = self.current_batch.batch_id
|
||||
payload["user_identifier"] = get_user_id()
|
||||
|
||||
response = (
|
||||
self.plus_api.initialize_ephemeral_trace_batch(payload)
|
||||
if use_ephemeral
|
||||
else self.plus_api.initialize_trace_batch(payload)
|
||||
)
|
||||
max_retries = 1
|
||||
response = None
|
||||
|
||||
try:
|
||||
for attempt in range(max_retries + 1):
|
||||
response = (
|
||||
self.plus_api.initialize_ephemeral_trace_batch(payload)
|
||||
if use_ephemeral
|
||||
else self.plus_api.initialize_trace_batch(payload)
|
||||
)
|
||||
if response is not None and response.status_code < 500:
|
||||
break
|
||||
if attempt < max_retries:
|
||||
logger.debug(
|
||||
f"Trace batch init attempt {attempt + 1} failed "
|
||||
f"(status={response.status_code if response else 'None'}), retrying..."
|
||||
)
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error initializing trace batch: {e}. Continuing without tracing."
|
||||
)
|
||||
self.trace_batch_id = None
|
||||
return None
|
||||
|
||||
if response is None:
|
||||
logger.warning(
|
||||
"Trace batch initialization failed gracefully. Continuing without tracing."
|
||||
)
|
||||
return
|
||||
self.trace_batch_id = None
|
||||
return None
|
||||
|
||||
# Fall back to ephemeral on auth failure (expired/revoked token)
|
||||
if response.status_code in [401, 403] and not use_ephemeral:
|
||||
logger.warning(
|
||||
"Auth rejected by server, falling back to ephemeral tracing."
|
||||
)
|
||||
self.is_current_batch_ephemeral = True
|
||||
return self._initialize_backend_batch(
|
||||
user_context,
|
||||
execution_metadata,
|
||||
use_ephemeral=True,
|
||||
skip_context_check=skip_context_check,
|
||||
)
|
||||
|
||||
if response.status_code in [201, 200]:
|
||||
self.is_current_batch_ephemeral = use_ephemeral
|
||||
response_data = response.json()
|
||||
self.trace_batch_id = (
|
||||
response_data["trace_id"]
|
||||
@@ -165,11 +201,22 @@ class TraceBatchManager:
|
||||
logger.warning(
|
||||
f"Trace batch initialization returned status {response.status_code}. Continuing without tracing."
|
||||
)
|
||||
self.trace_batch_id = None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error initializing trace batch: {e}. Continuing without tracing."
|
||||
)
|
||||
self.trace_batch_id = None
|
||||
|
||||
def _mark_batch_as_failed(self, trace_batch_id: str, error_message: str) -> None:
|
||||
"""Mark a trace batch as failed, routing to the correct endpoint."""
|
||||
if self.is_current_batch_ephemeral:
|
||||
self.plus_api.mark_ephemeral_trace_batch_as_failed(
|
||||
trace_batch_id, error_message
|
||||
)
|
||||
else:
|
||||
self.plus_api.mark_trace_batch_as_failed(trace_batch_id, error_message)
|
||||
|
||||
def begin_event_processing(self) -> None:
|
||||
"""Mark that an event handler started processing (for synchronization)."""
|
||||
@@ -260,7 +307,7 @@ class TraceBatchManager:
|
||||
logger.error(
|
||||
"Event handler timeout - marking batch as failed due to incomplete events"
|
||||
)
|
||||
self.plus_api.mark_trace_batch_as_failed(
|
||||
self._mark_batch_as_failed(
|
||||
self.trace_batch_id,
|
||||
"Timeout waiting for event handlers - events incomplete",
|
||||
)
|
||||
@@ -284,7 +331,7 @@ class TraceBatchManager:
|
||||
events_sent_to_backend_status = self._send_events_to_backend()
|
||||
self.event_buffer = original_buffer
|
||||
if events_sent_to_backend_status == 500 and self.trace_batch_id:
|
||||
self.plus_api.mark_trace_batch_as_failed(
|
||||
self._mark_batch_as_failed(
|
||||
self.trace_batch_id, "Error sending events to backend"
|
||||
)
|
||||
return None
|
||||
@@ -364,13 +411,16 @@ class TraceBatchManager:
|
||||
logger.error(
|
||||
f"❌ Failed to finalize trace batch: {response.status_code} - {response.text}"
|
||||
)
|
||||
self.plus_api.mark_trace_batch_as_failed(
|
||||
self.trace_batch_id, response.text
|
||||
)
|
||||
self._mark_batch_as_failed(self.trace_batch_id, response.text)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error finalizing trace batch: {e}")
|
||||
self.plus_api.mark_trace_batch_as_failed(self.trace_batch_id, str(e))
|
||||
try:
|
||||
self._mark_batch_as_failed(self.trace_batch_id, str(e))
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Could not mark trace batch as failed (network unavailable)"
|
||||
)
|
||||
|
||||
def _cleanup_batch_data(self) -> None:
|
||||
"""Clean up batch data after successful finalization to free memory"""
|
||||
|
||||
@@ -17,7 +17,10 @@ from crewai.events.listeners.tracing.first_time_trace_handler import (
|
||||
from crewai.events.listeners.tracing.trace_batch_manager import TraceBatchManager
|
||||
from crewai.events.listeners.tracing.types import TraceEvent
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
is_tracing_enabled_in_context,
|
||||
safe_serialize_to_dict,
|
||||
should_auto_collect_first_time_traces,
|
||||
should_enable_tracing,
|
||||
)
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AAgentCardFetchedEvent,
|
||||
@@ -198,6 +201,17 @@ class TraceCollectionListener(BaseEventListener):
|
||||
if self._listeners_setup:
|
||||
return
|
||||
|
||||
# Skip registration entirely if tracing is disabled and not first-time user
|
||||
# This avoids overhead of 50+ handler registrations when tracing won't be used
|
||||
# Also check is_tracing_enabled_in_context() so per-run overrides (Crew(tracing=True)) still work
|
||||
if (
|
||||
not should_enable_tracing()
|
||||
and not is_tracing_enabled_in_context()
|
||||
and not should_auto_collect_first_time_traces()
|
||||
):
|
||||
self._listeners_setup = True
|
||||
return
|
||||
|
||||
self._register_env_event_handlers(crewai_event_bus)
|
||||
self._register_flow_event_handlers(crewai_event_bus)
|
||||
self._register_context_event_handlers(crewai_event_bus)
|
||||
@@ -235,8 +249,11 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
@event_bus.on(FlowStartedEvent)
|
||||
def on_flow_started(source: Any, event: FlowStartedEvent) -> None:
|
||||
if not self.batch_manager.is_batch_initialized():
|
||||
self._initialize_flow_batch(source, event)
|
||||
# Always call _initialize_flow_batch to claim ownership.
|
||||
# If batch was already initialized by a concurrent action event
|
||||
# (race condition), initialize_batch() returns early but
|
||||
# batch_owner_type is still correctly set to "flow".
|
||||
self._initialize_flow_batch(source, event)
|
||||
self._handle_trace_event("flow_started", source, event)
|
||||
|
||||
@event_bus.on(MethodExecutionStartedEvent)
|
||||
@@ -266,7 +283,12 @@ class TraceCollectionListener(BaseEventListener):
|
||||
|
||||
@event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
if not self.batch_manager.is_batch_initialized():
|
||||
if self.batch_manager.batch_owner_type != "flow":
|
||||
# Always call _initialize_crew_batch to claim ownership.
|
||||
# If batch was already initialized by a concurrent action event
|
||||
# (race condition with DefaultEnvEvent), initialize_batch() returns
|
||||
# early but batch_owner_type is still correctly set to "crew".
|
||||
# Skip only when a parent flow already owns the batch.
|
||||
self._initialize_crew_batch(source, event)
|
||||
self._handle_trace_event("crew_kickoff_started", source, event)
|
||||
|
||||
@@ -772,7 +794,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
"crew_name": getattr(source, "name", "Unknown Crew"),
|
||||
"crewai_version": get_crewai_version(),
|
||||
}
|
||||
self.batch_manager.initialize_batch(user_context, execution_metadata)
|
||||
self._initialize_batch(user_context, execution_metadata)
|
||||
|
||||
self.batch_manager.begin_event_processing()
|
||||
try:
|
||||
|
||||
@@ -481,6 +481,26 @@ def should_auto_collect_first_time_traces() -> bool:
|
||||
return is_first_execution()
|
||||
|
||||
|
||||
def _is_interactive_terminal() -> bool:
|
||||
"""Check if stdin is an interactive terminal.
|
||||
|
||||
Returns False in non-interactive contexts (CI, API servers, Docker, etc.)
|
||||
to avoid blocking on prompts that no one can respond to.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
stdin = getattr(sys, "stdin", None)
|
||||
if stdin is None:
|
||||
return False
|
||||
isatty = getattr(stdin, "isatty", None)
|
||||
if not callable(isatty):
|
||||
return False
|
||||
return bool(isatty())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def prompt_user_for_trace_viewing(timeout_seconds: int = 20) -> bool:
|
||||
"""
|
||||
Prompt user if they want to see their traces with timeout.
|
||||
@@ -492,6 +512,11 @@ def prompt_user_for_trace_viewing(timeout_seconds: int = 20) -> bool:
|
||||
if should_suppress_tracing_messages():
|
||||
return False
|
||||
|
||||
# Skip prompt in non-interactive contexts (CI, API servers, Docker, etc.)
|
||||
# This avoids blocking for 20 seconds when no one can respond
|
||||
if not _is_interactive_terminal():
|
||||
return False
|
||||
|
||||
try:
|
||||
import threading
|
||||
|
||||
|
||||
@@ -178,12 +178,15 @@ class HumanFeedbackRequestedEvent(FlowEvent):
|
||||
output: The method output shown to the human for review.
|
||||
message: The message displayed when requesting feedback.
|
||||
emit: Optional list of possible outcomes for routing.
|
||||
request_id: Platform-assigned identifier for this feedback request,
|
||||
used for correlating the request across system boundaries.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
output: Any
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
request_id: str | None = None
|
||||
type: str = "human_feedback_requested"
|
||||
|
||||
|
||||
@@ -198,9 +201,12 @@ class HumanFeedbackReceivedEvent(FlowEvent):
|
||||
method_name: Name of the method that received feedback.
|
||||
feedback: The raw text feedback provided by the human.
|
||||
outcome: The collapsed outcome string (if emit was specified).
|
||||
request_id: Platform-assigned identifier for this feedback request,
|
||||
used for correlating the response back to its originating request.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
feedback: str
|
||||
outcome: str | None = None
|
||||
request_id: str | None = None
|
||||
type: str = "human_feedback_received"
|
||||
|
||||
@@ -57,6 +57,7 @@ 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):
|
||||
|
||||
@@ -127,6 +127,9 @@ To update, run: uv sync --upgrade-package crewai"""
|
||||
|
||||
def _show_tracing_disabled_message_if_needed(self) -> None:
|
||||
"""Show tracing disabled message if tracing is not enabled."""
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
has_user_declined_tracing,
|
||||
is_tracing_enabled_in_context,
|
||||
@@ -136,6 +139,12 @@ To update, run: uv sync --upgrade-package crewai"""
|
||||
if should_suppress_tracing_messages():
|
||||
return
|
||||
|
||||
# Don't show "disabled" message when the first-time handler will show
|
||||
# the trace prompt after execution completes (avoids confusing mid-flow messages)
|
||||
listener = TraceCollectionListener._instance # type: ignore[misc]
|
||||
if listener and listener.first_time_handler.is_first_time:
|
||||
return
|
||||
|
||||
if not is_tracing_enabled_in_context():
|
||||
if has_user_declined_tracing():
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
@@ -11,10 +11,15 @@ import threading
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, Field, GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
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 (
|
||||
@@ -119,6 +124,7 @@ 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)
|
||||
@@ -152,6 +158,9 @@ 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)
|
||||
@@ -159,136 +168,74 @@ 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().
|
||||
"""
|
||||
|
||||
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.
|
||||
_skip_auto_memory: bool = 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()
|
||||
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
|
||||
)
|
||||
|
||||
# Error context storage for recovery
|
||||
self._last_parser_error: OutputParserError | None = None
|
||||
self._last_context_error: Exception | None = None
|
||||
_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)
|
||||
|
||||
# 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
|
||||
] = []
|
||||
@model_validator(mode="after")
|
||||
def _setup_executor(self) -> Self:
|
||||
"""Configure executor after Pydantic field initialization."""
|
||||
self._i18n = self.i18n or get_i18n()
|
||||
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", [])
|
||||
self.llm.stop = list(
|
||||
set(
|
||||
existing_stop + self.stop
|
||||
if isinstance(existing_stop, list)
|
||||
else self.stop
|
||||
)
|
||||
)
|
||||
if not isinstance(existing_stop, list):
|
||||
existing_stop = []
|
||||
self.llm.stop = list(set(existing_stop + self.stop_words))
|
||||
|
||||
self._state = AgentExecutorState()
|
||||
self.max_method_calls = self.max_iter * 10
|
||||
|
||||
# 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
|
||||
current_tracing = is_tracing_enabled_in_context()
|
||||
self.tracing = current_tracing if current_tracing else None
|
||||
self._flow_post_init()
|
||||
return self
|
||||
|
||||
def _check_native_tool_support(self) -> bool:
|
||||
"""Check if LLM supports native function calling."""
|
||||
@@ -318,19 +265,13 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
|
||||
@property
|
||||
def state(self) -> AgentExecutorState:
|
||||
"""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
|
||||
"""Get thread-safe state proxy."""
|
||||
return StateProxy(self._state, self._state_lock) # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def iterations(self) -> int:
|
||||
"""Compatibility property for mixin - returns state iterations."""
|
||||
return self._state.iterations
|
||||
return self._state.iterations # type: ignore[no-any-return]
|
||||
|
||||
@iterations.setter
|
||||
def iterations(self, value: int) -> None:
|
||||
@@ -340,7 +281,7 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
@property
|
||||
def messages(self) -> list[LLMMessage]:
|
||||
"""Compatibility property - returns state messages."""
|
||||
return self._state.messages
|
||||
return self._state.messages # type: ignore[no-any-return]
|
||||
|
||||
@messages.setter
|
||||
def messages(self, value: list[LLMMessage]) -> None:
|
||||
@@ -1966,42 +1907,10 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
"original_tool": original_tool,
|
||||
}
|
||||
|
||||
def _extract_tool_name(self, tool_call: Any) -> str:
|
||||
"""Extract tool name from various tool call formats."""
|
||||
if hasattr(tool_call, "function"):
|
||||
return sanitize_tool_name(tool_call.function.name)
|
||||
if hasattr(tool_call, "function_call") and tool_call.function_call:
|
||||
return sanitize_tool_name(tool_call.function_call.name)
|
||||
if hasattr(tool_call, "name"):
|
||||
return sanitize_tool_name(tool_call.name)
|
||||
if isinstance(tool_call, dict):
|
||||
func_info = tool_call.get("function", {})
|
||||
return sanitize_tool_name(
|
||||
func_info.get("name", "") or tool_call.get("name", "unknown")
|
||||
)
|
||||
return "unknown"
|
||||
|
||||
@router(execute_native_tool)
|
||||
def check_native_todo_completion(
|
||||
self,
|
||||
) -> Literal["todo_satisfied", "todo_not_satisfied"]:
|
||||
"""Check if the native tool execution satisfied the active todo.
|
||||
|
||||
Similar to check_todo_completion but for native tool execution path.
|
||||
"""
|
||||
current_todo = self.state.todos.current_todo
|
||||
|
||||
if not current_todo:
|
||||
return "todo_not_satisfied"
|
||||
|
||||
# For native tools, any tool execution satisfies the todo
|
||||
return "todo_satisfied"
|
||||
|
||||
@listen("initialized")
|
||||
def continue_iteration(self) -> Literal["check_iteration"]:
|
||||
"""Bridge listener that connects iteration loop back to iteration check."""
|
||||
if self._flow_initialized:
|
||||
self._discard_or_listener(FlowMethodName("continue_iteration"))
|
||||
self._discard_or_listener(FlowMethodName("continue_iteration"))
|
||||
return "check_iteration"
|
||||
|
||||
@router(or_(initialize_reasoning, continue_iteration))
|
||||
@@ -2629,8 +2538,6 @@ 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(
|
||||
@@ -2721,8 +2628,6 @@ class AgentExecutor(Flow[AgentExecutorState], CrewAgentExecutorMixin):
|
||||
Returns:
|
||||
Dictionary with agent output.
|
||||
"""
|
||||
self._ensure_flow_initialized()
|
||||
|
||||
with self._execution_lock:
|
||||
if self._is_executing:
|
||||
raise RuntimeError(
|
||||
@@ -3038,17 +2943,6 @@ 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
|
||||
|
||||
@@ -182,7 +182,7 @@ class ConsoleProvider:
|
||||
console.print(message, style="yellow")
|
||||
console.print()
|
||||
|
||||
response = input(">>> \n").strip()
|
||||
response = input(">>> ").strip()
|
||||
else:
|
||||
response = input(f"{message} ").strip()
|
||||
|
||||
|
||||
@@ -63,6 +63,32 @@ class PendingFeedbackContext:
|
||||
llm: dict[str, Any] | str | None = None
|
||||
requested_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
@staticmethod
|
||||
def _make_json_safe(value: Any) -> Any:
|
||||
"""Convert a value to a JSON-serializable form.
|
||||
|
||||
Handles Pydantic models, dataclasses, and arbitrary objects by
|
||||
progressively falling back to string representation.
|
||||
"""
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [PendingFeedbackContext._make_json_safe(v) for v in value]
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
k: PendingFeedbackContext._make_json_safe(v) for k, v in value.items()
|
||||
}
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
if isinstance(value, BaseModel):
|
||||
return value.model_dump(mode="json")
|
||||
import dataclasses
|
||||
|
||||
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
||||
return PendingFeedbackContext._make_json_safe(dataclasses.asdict(value))
|
||||
return str(value)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Serialize context to a dictionary for persistence.
|
||||
|
||||
@@ -73,11 +99,11 @@ class PendingFeedbackContext:
|
||||
"flow_id": self.flow_id,
|
||||
"flow_class": self.flow_class,
|
||||
"method_name": self.method_name,
|
||||
"method_output": self.method_output,
|
||||
"method_output": self._make_json_safe(self.method_output),
|
||||
"message": self.message,
|
||||
"emit": self.emit,
|
||||
"default_outcome": self.default_outcome,
|
||||
"metadata": self.metadata,
|
||||
"metadata": self._make_json_safe(self.metadata),
|
||||
"llm": self.llm,
|
||||
"requested_at": self.requested_at.isoformat(),
|
||||
}
|
||||
|
||||
@@ -39,7 +39,14 @@ from uuid import uuid4
|
||||
|
||||
from opentelemetry import baggage
|
||||
from opentelemetry.context import attach, detach
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
ValidationError,
|
||||
)
|
||||
from pydantic._internal._model_construction import ModelMetaclass
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
@@ -81,6 +88,7 @@ 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 (
|
||||
@@ -108,7 +116,6 @@ 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
|
||||
@@ -728,7 +735,7 @@ class StateProxy(Generic[T]):
|
||||
return result
|
||||
|
||||
|
||||
class FlowMeta(type):
|
||||
class FlowMeta(ModelMetaclass):
|
||||
def __new__(
|
||||
mcs,
|
||||
name: str,
|
||||
@@ -736,6 +743,45 @@ class FlowMeta(type):
|
||||
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 = []
|
||||
@@ -820,85 +866,90 @@ class FlowMeta(type):
|
||||
return cls
|
||||
|
||||
|
||||
class Flow(Generic[T], metaclass=FlowMeta):
|
||||
class Flow(BaseModel, 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
|
||||
|
||||
def __class_getitem__(cls: type[Flow[T]], item: type[T]) -> type[Flow[T]]:
|
||||
class _FlowGeneric(cls): # type: ignore
|
||||
_initial_state_t = item
|
||||
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
|
||||
|
||||
_FlowGeneric.__name__ = f"{cls.__name__}[{item.__name__}]"
|
||||
_FlowGeneric._initial_state_t = item
|
||||
return _FlowGeneric
|
||||
|
||||
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 __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)
|
||||
|
||||
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 model_post_init(self, __context: Any) -> None:
|
||||
self._flow_post_init()
|
||||
|
||||
# Human feedback storage
|
||||
self.human_feedback_history: list[HumanFeedbackResult] = []
|
||||
self.last_human_feedback: HumanFeedbackResult | None = None
|
||||
self._pending_feedback_context: PendingFeedbackContext | None = None
|
||||
self.suppress_flow_events: bool = suppress_flow_events
|
||||
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)
|
||||
|
||||
# User input history (for self.ask())
|
||||
self._input_history: list[InputHistoryEntry] = []
|
||||
if self._state is None:
|
||||
self._state = self._create_initial_state()
|
||||
|
||||
# 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(
|
||||
@@ -1223,9 +1274,6 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
# Mark that we're resuming execution
|
||||
instance._is_execution_resuming = True
|
||||
|
||||
# Mark the method as completed (it ran before pausing)
|
||||
instance._completed_methods.add(FlowMethodName(pending_context.method_name))
|
||||
|
||||
return instance
|
||||
|
||||
@property
|
||||
@@ -1380,12 +1428,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self.human_feedback_history.append(result)
|
||||
self.last_human_feedback = result
|
||||
|
||||
# Clear pending context after processing
|
||||
self._completed_methods.add(FlowMethodName(context.method_name))
|
||||
|
||||
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(
|
||||
@@ -1403,7 +1452,10 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
# This allows methods to re-execute in loops (e.g., implement_changes → suggest_changes → implement_changes)
|
||||
self._is_execution_resuming = False
|
||||
|
||||
final_result: Any = result
|
||||
if emit and collapsed_outcome is None:
|
||||
collapsed_outcome = default_outcome or emit[0]
|
||||
result.outcome = collapsed_outcome
|
||||
|
||||
try:
|
||||
if emit and collapsed_outcome:
|
||||
self._method_outputs.append(collapsed_outcome)
|
||||
@@ -1421,18 +1473,19 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackPending
|
||||
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
# Auto-save pending feedback (create default persistence if needed)
|
||||
if self._persistence is None:
|
||||
self._pending_feedback_context = e.context
|
||||
|
||||
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,
|
||||
@@ -1455,6 +1508,8 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
return e
|
||||
raise
|
||||
|
||||
final_result = self._method_outputs[-1] if self._method_outputs else result
|
||||
|
||||
# Emit flow finished
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
@@ -1480,39 +1535,33 @@ class Flow(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 state_class()
|
||||
return cast(T, 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:
|
||||
@@ -1520,7 +1569,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
model_instance = state_class()
|
||||
if not getattr(model_instance, "id", None):
|
||||
object.__setattr__(model_instance, "id", str(uuid4()))
|
||||
return model_instance
|
||||
return cast(T, model_instance)
|
||||
if init_state is dict:
|
||||
return cast(T, {"id": str(uuid4())})
|
||||
|
||||
@@ -1531,32 +1580,21 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
new_state["id"] = str(uuid4())
|
||||
return cast(T, new_state)
|
||||
|
||||
# Handle BaseModel instance case
|
||||
if isinstance(init_state, BaseModel):
|
||||
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
|
||||
model = init_state
|
||||
if hasattr(model, "id"):
|
||||
state_dict = model.model_dump()
|
||||
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("_")
|
||||
}
|
||||
if not state_dict.get("id"):
|
||||
state_dict["id"] = str(uuid4())
|
||||
model_class = type(model)
|
||||
return cast(T, model_class(**state_dict))
|
||||
|
||||
# Ensure id is set - generate UUID if empty
|
||||
if not state_dict.get("id"):
|
||||
state_dict["id"] = str(uuid4())
|
||||
class StateWithId(FlowState, type(model)): # type: ignore
|
||||
pass
|
||||
|
||||
# Create new instance of the same class
|
||||
model_class = type(model)
|
||||
return cast(T, model_class(**state_dict))
|
||||
state_dict = model.model_dump()
|
||||
state_dict["id"] = str(uuid4())
|
||||
return cast(T, StateWithId(**state_dict))
|
||||
raise TypeError(
|
||||
f"Initial state must be dict or BaseModel, got {type(self.initial_state)}"
|
||||
)
|
||||
@@ -1569,17 +1607,17 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
"""
|
||||
if isinstance(self._state, BaseModel):
|
||||
try:
|
||||
return self._state.model_copy(deep=True)
|
||||
return cast(T, self._state.model_copy(deep=True))
|
||||
except (TypeError, AttributeError):
|
||||
try:
|
||||
state_dict = self._state.model_dump()
|
||||
model_class = type(self._state)
|
||||
return model_class(**state_dict)
|
||||
return cast(T, model_class(**state_dict))
|
||||
except Exception:
|
||||
return self._state.model_copy(deep=False)
|
||||
return cast(T, self._state.model_copy(deep=False))
|
||||
else:
|
||||
try:
|
||||
return copy.deepcopy(self._state)
|
||||
return cast(T, copy.deepcopy(self._state))
|
||||
except (TypeError, AttributeError):
|
||||
return cast(T, self._state.copy())
|
||||
|
||||
@@ -1655,7 +1693,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
elif isinstance(self._state, BaseModel):
|
||||
# For BaseModel states, preserve existing fields unless overridden
|
||||
try:
|
||||
model = cast(BaseModel, self._state)
|
||||
model = self._state
|
||||
# Get current state as dict
|
||||
if hasattr(model, "model_dump"):
|
||||
current_state = model.model_dump()
|
||||
@@ -1706,7 +1744,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self._state.update(stored_state)
|
||||
elif isinstance(self._state, BaseModel):
|
||||
# For BaseModel states, create new instance with stored values
|
||||
model = cast(BaseModel, self._state)
|
||||
model = self._state
|
||||
if hasattr(model, "model_validate"):
|
||||
# Pydantic v2
|
||||
self._state = cast(T, type(model).model_validate(stored_state))
|
||||
@@ -1931,7 +1969,7 @@ class Flow(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()
|
||||
@@ -1957,9 +1995,9 @@ class Flow(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}"
|
||||
@@ -2029,17 +2067,17 @@ class Flow(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,
|
||||
@@ -2286,6 +2324,17 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
result = await result
|
||||
|
||||
self._method_outputs.append(result)
|
||||
|
||||
# For @human_feedback methods with emit, the result is the collapsed outcome
|
||||
# (e.g., "approved") used for routing. But we want the actual method output
|
||||
# to be the stored result (for final flow output). Replace the last entry
|
||||
# if a stashed output exists. Dict-based stash is concurrency-safe and
|
||||
# handles None return values (presence in dict = stashed, not value).
|
||||
if method_name in self._human_feedback_method_outputs:
|
||||
self._method_outputs[-1] = self._human_feedback_method_outputs.pop(
|
||||
method_name
|
||||
)
|
||||
|
||||
self._method_execution_counts[method_name] = (
|
||||
self._method_execution_counts.get(method_name, 0) + 1
|
||||
)
|
||||
@@ -2314,11 +2363,10 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
e.context.method_name = method_name
|
||||
|
||||
# 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()
|
||||
|
||||
# Emit paused event (not failed)
|
||||
if not self.suppress_flow_events:
|
||||
@@ -2679,9 +2727,9 @@ class Flow(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."
|
||||
@@ -2788,7 +2836,7 @@ class Flow(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 = (
|
||||
@@ -2796,7 +2844,7 @@ class Flow(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,
|
||||
@@ -3133,10 +3181,16 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
if outcome.lower() == response_clean.lower():
|
||||
return outcome
|
||||
|
||||
# Partial match
|
||||
# Partial match (longest wins, first on length ties)
|
||||
response_lower = response_clean.lower()
|
||||
best_outcome: str | None = None
|
||||
best_len = -1
|
||||
for outcome in outcomes:
|
||||
if outcome.lower() in response_clean.lower():
|
||||
return outcome
|
||||
if outcome.lower() in response_lower and len(outcome) > best_len:
|
||||
best_outcome = outcome
|
||||
best_len = len(outcome)
|
||||
if best_outcome is not None:
|
||||
return best_outcome
|
||||
|
||||
# Fallback to first outcome
|
||||
logger.warning(
|
||||
|
||||
@@ -116,10 +116,11 @@ def _deserialize_llm_from_context(
|
||||
return LLM(model=llm_data)
|
||||
|
||||
if isinstance(llm_data, dict):
|
||||
model = llm_data.pop("model", None)
|
||||
data = dict(llm_data)
|
||||
model = data.pop("model", None)
|
||||
if not model:
|
||||
return None
|
||||
return LLM(model=model, **llm_data)
|
||||
return LLM(model=model, **data)
|
||||
return None
|
||||
|
||||
|
||||
@@ -450,12 +451,12 @@ def human_feedback(
|
||||
|
||||
# -- Core feedback helpers ------------------------------------
|
||||
|
||||
def _request_feedback(flow_instance: Flow[Any], method_output: Any) -> str:
|
||||
"""Request feedback using provider or default console."""
|
||||
def _build_feedback_context(
|
||||
flow_instance: Flow[Any], method_output: Any
|
||||
) -> tuple[Any, Any]:
|
||||
"""Build the PendingFeedbackContext and resolve the effective provider."""
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
# Build context for provider
|
||||
# Use flow_id property which handles both dict and BaseModel states
|
||||
context = PendingFeedbackContext(
|
||||
flow_id=flow_instance.flow_id or "unknown",
|
||||
flow_class=f"{flow_instance.__class__.__module__}.{flow_instance.__class__.__name__}",
|
||||
@@ -468,15 +469,53 @@ def human_feedback(
|
||||
llm=llm if isinstance(llm, str) else _serialize_llm_for_context(llm),
|
||||
)
|
||||
|
||||
# Determine effective provider:
|
||||
effective_provider = provider
|
||||
if effective_provider is None:
|
||||
from crewai.flow.flow_config import flow_config
|
||||
|
||||
effective_provider = flow_config.hitl_provider
|
||||
|
||||
return context, effective_provider
|
||||
|
||||
def _request_feedback(flow_instance: Flow[Any], method_output: Any) -> str:
|
||||
"""Request feedback using provider or default console (sync)."""
|
||||
context, effective_provider = _build_feedback_context(
|
||||
flow_instance, method_output
|
||||
)
|
||||
|
||||
if effective_provider is not None:
|
||||
return effective_provider.request_feedback(context, flow_instance)
|
||||
feedback_result = effective_provider.request_feedback(
|
||||
context, flow_instance
|
||||
)
|
||||
if asyncio.iscoroutine(feedback_result):
|
||||
raise TypeError(
|
||||
f"Provider {type(effective_provider).__name__}.request_feedback() "
|
||||
"returned a coroutine in a sync flow method. Use an async flow "
|
||||
"method or a synchronous provider."
|
||||
)
|
||||
return str(feedback_result)
|
||||
return flow_instance._request_human_feedback(
|
||||
message=message,
|
||||
output=method_output,
|
||||
metadata=metadata,
|
||||
emit=emit,
|
||||
)
|
||||
|
||||
async def _request_feedback_async(
|
||||
flow_instance: Flow[Any], method_output: Any
|
||||
) -> str:
|
||||
"""Request feedback, awaiting the provider if it returns a coroutine."""
|
||||
context, effective_provider = _build_feedback_context(
|
||||
flow_instance, method_output
|
||||
)
|
||||
|
||||
if effective_provider is not None:
|
||||
feedback_result = effective_provider.request_feedback(
|
||||
context, flow_instance
|
||||
)
|
||||
if asyncio.iscoroutine(feedback_result):
|
||||
return str(await feedback_result)
|
||||
return str(feedback_result)
|
||||
return flow_instance._request_human_feedback(
|
||||
message=message,
|
||||
output=method_output,
|
||||
@@ -524,10 +563,11 @@ def human_feedback(
|
||||
flow_instance.human_feedback_history.append(result)
|
||||
flow_instance.last_human_feedback = result
|
||||
|
||||
# Return based on mode
|
||||
if emit:
|
||||
# Return outcome for routing
|
||||
return collapsed_outcome # type: ignore[return-value]
|
||||
if collapsed_outcome is None:
|
||||
collapsed_outcome = default_outcome or emit[0]
|
||||
result.outcome = collapsed_outcome
|
||||
return collapsed_outcome
|
||||
return result
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
@@ -540,7 +580,7 @@ def human_feedback(
|
||||
if learn and getattr(self, "memory", None) is not None:
|
||||
method_output = _pre_review_with_lessons(self, method_output)
|
||||
|
||||
raw_feedback = _request_feedback(self, method_output)
|
||||
raw_feedback = await _request_feedback_async(self, method_output)
|
||||
result = _process_feedback(self, method_output, raw_feedback)
|
||||
|
||||
# Distill: extract lessons from output + feedback, store in memory
|
||||
@@ -551,6 +591,13 @@ def human_feedback(
|
||||
):
|
||||
_distill_and_store_lessons(self, method_output, raw_feedback)
|
||||
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
|
||||
return result
|
||||
|
||||
wrapper: Any = async_wrapper
|
||||
@@ -575,6 +622,13 @@ def human_feedback(
|
||||
):
|
||||
_distill_and_store_lessons(self, method_output, raw_feedback)
|
||||
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
|
||||
return result
|
||||
|
||||
wrapper = sync_wrapper
|
||||
|
||||
@@ -3,12 +3,15 @@ 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(ABC):
|
||||
class BaseKnowledgeStorage(BaseModel, ABC):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
"""Abstract base class for knowledge storage implementations."""
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -3,6 +3,9 @@ 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
|
||||
@@ -22,31 +25,32 @@ class KnowledgeStorage(BaseKnowledgeStorage):
|
||||
search efficiency.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedder: ProviderSpec
|
||||
collection_name: str | None = None
|
||||
embedder: (
|
||||
ProviderSpec
|
||||
| BaseEmbeddingsProvider[Any]
|
||||
| type[BaseEmbeddingsProvider[Any]]
|
||||
| None = None,
|
||||
collection_name: str | None = None,
|
||||
) -> None:
|
||||
self.collection_name = collection_name
|
||||
self._client: BaseClient | None = None
|
||||
| None
|
||||
) = Field(default=None, exclude=True)
|
||||
_client: BaseClient | None = PrivateAttr(default=None)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_client(self) -> Self:
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=r".*'model_fields'.*is deprecated.*",
|
||||
module=r"^chromadb(\.|$)",
|
||||
)
|
||||
|
||||
if embedder:
|
||||
embedding_function = build_embedder(embedder) # type: ignore[arg-type]
|
||||
if self.embedder:
|
||||
embedding_function = build_embedder(self.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,7 +22,6 @@ from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
@@ -204,7 +203,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 | InstanceOf[BaseLLM] | Any | None = Field(
|
||||
llm: str | BaseLLM | Any | None = Field(
|
||||
default=None, description="Language model that will run the agent"
|
||||
)
|
||||
tools: list[BaseTool] = Field(
|
||||
|
||||
@@ -20,8 +20,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
@@ -37,7 +36,12 @@ from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.llms.base_llm import BaseLLM, get_current_call_id, llm_call_context
|
||||
from crewai.llms.base_llm import (
|
||||
BaseLLM,
|
||||
JsonResponseFormat,
|
||||
get_current_call_id,
|
||||
llm_call_context,
|
||||
)
|
||||
from crewai.llms.constants import (
|
||||
ANTHROPIC_MODELS,
|
||||
AZURE_MODELS,
|
||||
@@ -63,8 +67,6 @@ 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
|
||||
@@ -342,6 +344,27 @@ 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.
|
||||
@@ -436,10 +459,7 @@ class LLM(BaseLLM):
|
||||
logger.error(error_msg)
|
||||
raise ImportError(error_msg) from None
|
||||
|
||||
instance = object.__new__(cls)
|
||||
super(LLM, instance).__init__(model=model, is_litellm=True, **kwargs)
|
||||
instance.is_litellm = True
|
||||
return instance
|
||||
return object.__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def _matches_provider_pattern(cls, model: str, provider: str) -> bool:
|
||||
@@ -483,8 +503,8 @@ class LLM(BaseLLM):
|
||||
for prefix in ["gpt-", "gpt-35-", "o1", "o3", "o4", "azure-"]
|
||||
)
|
||||
|
||||
# OpenAI-compatible providers - accept any model name since these
|
||||
# providers host many different models with varied naming conventions
|
||||
# OpenAI-compatible providers - most accept any model name, but some
|
||||
# (DeepSeek, Dashscope) restrict to their own model prefixes
|
||||
if provider == "deepseek":
|
||||
return model_lower.startswith("deepseek")
|
||||
|
||||
@@ -624,89 +644,23 @@ class LLM(BaseLLM):
|
||||
|
||||
return None
|
||||
|
||||
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="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
|
||||
|
||||
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()
|
||||
@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
|
||||
|
||||
@staticmethod
|
||||
def _is_anthropic_model(model: str) -> bool:
|
||||
@@ -753,7 +707,7 @@ class LLM(BaseLLM):
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"n": self.n,
|
||||
"stop": self.stop or None,
|
||||
"stop": (self.stop or None) if self.supports_stop_words() else None,
|
||||
"max_tokens": self.max_tokens or self.max_completion_tokens,
|
||||
"presence_penalty": self.presence_penalty,
|
||||
"frequency_penalty": self.frequency_penalty,
|
||||
@@ -1016,21 +970,25 @@ 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
|
||||
|
||||
@@ -1040,12 +998,14 @@ 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
|
||||
|
||||
@@ -1067,6 +1027,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=self._usage_to_dict(usage_info),
|
||||
)
|
||||
return full_response
|
||||
|
||||
@@ -1218,6 +1179,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=None,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1248,6 +1210,8 @@ 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
|
||||
@@ -1259,6 +1223,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1290,6 +1255,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1313,6 +1279,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1362,6 +1329,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=None,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1388,6 +1356,8 @@ 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()
|
||||
@@ -1397,6 +1367,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -1426,6 +1397,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1448,6 +1420,7 @@ class LLM(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=response_usage,
|
||||
)
|
||||
return text_response
|
||||
|
||||
@@ -1594,12 +1567,14 @@ 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
|
||||
|
||||
@@ -1621,6 +1596,7 @@ 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
|
||||
@@ -1825,9 +1801,11 @@ class LLM(BaseLLM):
|
||||
# whether to summarize the content or abort based on the respect_context_window flag
|
||||
raise
|
||||
except Exception as e:
|
||||
unsupported_stop = "Unsupported parameter" in str(
|
||||
e
|
||||
) and "'stop'" in str(e)
|
||||
error_str = str(e)
|
||||
unsupported_stop = "'stop'" in error_str and (
|
||||
"Unsupported parameter" in error_str
|
||||
or "does not support parameters" in error_str
|
||||
)
|
||||
|
||||
if unsupported_stop:
|
||||
if (
|
||||
@@ -1961,9 +1939,11 @@ class LLM(BaseLLM):
|
||||
except LLMContextLengthExceededError:
|
||||
raise
|
||||
except Exception as e:
|
||||
unsupported_stop = "Unsupported parameter" in str(
|
||||
e
|
||||
) and "'stop'" in str(e)
|
||||
error_str = str(e)
|
||||
unsupported_stop = "'stop'" in error_str and (
|
||||
"Unsupported parameter" in error_str
|
||||
or "does not support parameters" in error_str
|
||||
)
|
||||
|
||||
if unsupported_stop:
|
||||
if (
|
||||
@@ -2003,6 +1983,19 @@ 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,
|
||||
@@ -2010,6 +2003,7 @@ 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.
|
||||
|
||||
@@ -2019,6 +2013,7 @@ 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,
|
||||
@@ -2030,6 +2025,7 @@ class LLM(BaseLLM):
|
||||
from_agent=from_agent,
|
||||
model=self.model,
|
||||
call_id=get_current_call_id(),
|
||||
usage=usage,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2263,6 +2259,10 @@ class LLM(BaseLLM):
|
||||
Note: This method is only used by the litellm fallback path.
|
||||
Native providers override this method with their own implementation.
|
||||
"""
|
||||
model_lower = self.model.lower() if self.model else ""
|
||||
if "gpt-5" in model_lower:
|
||||
return False
|
||||
|
||||
if not LITELLM_AVAILABLE or get_supported_openai_params is None:
|
||||
# When litellm is not available, assume stop words are supported
|
||||
return True
|
||||
@@ -2434,7 +2434,7 @@ class LLM(BaseLLM):
|
||||
**filtered_params,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo: dict[int, Any] | None) -> LLM:
|
||||
def __deepcopy__(self, memo: dict[int, Any] | None = None) -> LLM:
|
||||
"""Create a deep copy of the LLM instance."""
|
||||
import copy
|
||||
|
||||
@@ -2513,18 +2513,51 @@ class LLM(BaseLLM):
|
||||
True if the model likely supports images.
|
||||
"""
|
||||
vision_prefixes = (
|
||||
# OpenAI — GPT-4 vision models
|
||||
"gpt-4o",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4-vision",
|
||||
"gpt-4.1",
|
||||
# OpenAI — GPT-5 family (all variants support multimodal)
|
||||
"gpt-5",
|
||||
# OpenAI — o-series reasoning models with vision
|
||||
# o1, o3, o4, o4-mini support multimodal
|
||||
# o1-mini, o1-preview, o3-mini are text-only — handled via exclusion below
|
||||
"o1",
|
||||
"o3",
|
||||
"o4-mini",
|
||||
"o4",
|
||||
# Anthropic — Claude 3+ models support vision
|
||||
"claude-3",
|
||||
"claude-4",
|
||||
"claude-sonnet-4",
|
||||
"claude-opus-4",
|
||||
"claude-haiku-4",
|
||||
# Google — all Gemini models support multimodal
|
||||
"gemini",
|
||||
# xAI — Grok models support vision
|
||||
"grok",
|
||||
# Mistral — Pixtral vision model
|
||||
"pixtral",
|
||||
# Open-source vision models
|
||||
"llava",
|
||||
# Alibaba — Qwen vision-language models
|
||||
"qwen-vl",
|
||||
"qwen2-vl",
|
||||
"qwen3-vl",
|
||||
)
|
||||
# Text-only models that would otherwise match vision prefixes
|
||||
text_only_models = ("o3-mini", "o1-mini", "o1-preview")
|
||||
|
||||
model_lower = self.model.lower()
|
||||
|
||||
# Check exclusion first
|
||||
if any(
|
||||
model_lower.startswith(m) or f"/{m}" in model_lower
|
||||
for m in text_only_models
|
||||
):
|
||||
return False
|
||||
|
||||
return any(
|
||||
model_lower.startswith(p) or f"/{p}" in model_lower for p in vision_prefixes
|
||||
)
|
||||
|
||||
@@ -14,10 +14,18 @@ from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import (
|
||||
AliasChoices,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.llm_events import (
|
||||
@@ -51,6 +59,12 @@ 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)
|
||||
@@ -82,7 +96,7 @@ def get_current_call_id() -> str:
|
||||
return call_id
|
||||
|
||||
|
||||
class BaseLLM(ABC):
|
||||
class BaseLLM(BaseModel, ABC):
|
||||
"""Abstract base class for LLM implementations.
|
||||
|
||||
This class defines the interface that all LLM implementations must follow.
|
||||
@@ -101,56 +115,100 @@ class BaseLLM(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 __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 __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)
|
||||
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
if not model:
|
||||
raise ValueError("Model name is required and cannot be empty")
|
||||
return self.stop
|
||||
|
||||
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 = {
|
||||
_token_usage: dict[str, int] = PrivateAttr(
|
||||
default_factory=lambda: {
|
||||
"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)``.
|
||||
@@ -174,16 +232,6 @@ class BaseLLM(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,
|
||||
@@ -412,6 +460,7 @@ class BaseLLM(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
|
||||
@@ -426,6 +475,7 @@ class BaseLLM(ABC):
|
||||
from_agent=from_agent,
|
||||
model=self.model,
|
||||
call_id=get_current_call_id(),
|
||||
usage=usage,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeGuard, cast
|
||||
from typing import Any, Final, Literal, TypeGuard, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType
|
||||
from crewai.llms.base_llm import BaseLLM, llm_call_context
|
||||
from crewai.llms.base_llm import BaseLLM, JsonResponseFormat, llm_call_context
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
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 (
|
||||
@@ -17,9 +18,6 @@ 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 (
|
||||
@@ -150,60 +148,47 @@ class AnthropicCompletion(BaseLLM):
|
||||
offering native tool use, streaming support, and proper message formatting.
|
||||
"""
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_async_client: Any = PrivateAttr(default=None)
|
||||
_previous_thinking_blocks: list[Any] = PrivateAttr(default_factory=list)
|
||||
|
||||
# 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="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
|
||||
|
||||
self.client = Anthropic(**self._get_client_params())
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> AnthropicCompletion:
|
||||
self._client = Anthropic(**self._get_client_params())
|
||||
|
||||
async_client_params = self._get_client_params()
|
||||
if self.interceptor:
|
||||
@@ -211,51 +196,8 @@ 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)
|
||||
|
||||
# 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 = []
|
||||
self._async_client = AsyncAnthropic(**async_client_params)
|
||||
return self
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Anthropic-specific fields."""
|
||||
@@ -751,11 +693,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 ""},
|
||||
],
|
||||
)
|
||||
@@ -809,7 +751,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle non-streaming message completion."""
|
||||
uses_file_api = _contains_file_id_reference(params.get("messages", []))
|
||||
@@ -843,11 +785,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):
|
||||
@@ -869,6 +811,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
else:
|
||||
@@ -884,6 +827,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -906,6 +850,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(tool_uses)
|
||||
|
||||
@@ -928,7 +873,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(
|
||||
@@ -937,6 +882,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -952,7 +898,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle streaming message completion."""
|
||||
betas: list[str] = []
|
||||
@@ -991,9 +937,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
|
||||
@@ -1072,7 +1018,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)
|
||||
@@ -1086,6 +1032,7 @@ 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:
|
||||
@@ -1100,6 +1047,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -1129,6 +1077,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -1269,7 +1218,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)
|
||||
@@ -1288,7 +1237,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)
|
||||
|
||||
@@ -1299,6 +1248,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=follow_up_params["messages"],
|
||||
usage=follow_up_usage,
|
||||
)
|
||||
|
||||
# Log combined token usage
|
||||
@@ -1330,7 +1280,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle non-streaming async message completion."""
|
||||
uses_file_api = _contains_file_id_reference(params.get("messages", []))
|
||||
@@ -1364,11 +1314,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):
|
||||
@@ -1390,6 +1340,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
else:
|
||||
@@ -1405,6 +1356,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -1425,6 +1377,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(tool_uses)
|
||||
|
||||
@@ -1448,6 +1401,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -1461,7 +1415,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
response_model: JsonResponseFormat | type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle async streaming message completion."""
|
||||
betas: list[str] = []
|
||||
@@ -1498,11 +1452,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
|
||||
@@ -1585,6 +1539,7 @@ 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:
|
||||
@@ -1599,6 +1554,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_data
|
||||
|
||||
@@ -1627,6 +1583,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1664,7 +1621,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
|
||||
)
|
||||
|
||||
@@ -1685,6 +1642,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=follow_up_params["messages"],
|
||||
usage=follow_up_usage,
|
||||
)
|
||||
|
||||
total_usage = {
|
||||
@@ -1786,8 +1744,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,11 +3,13 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
from typing import Any, TypedDict
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
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,
|
||||
@@ -16,10 +18,6 @@ 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,
|
||||
@@ -76,109 +74,84 @@ class AzureCompletion(BaseLLM):
|
||||
offering native function calling, streaming support, and proper Azure authentication.
|
||||
"""
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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:
|
||||
_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:
|
||||
raise NotImplementedError(
|
||||
"HTTP interceptors are not yet supported for Azure AI Inference provider. "
|
||||
"Interceptors are currently supported for OpenAI and Anthropic providers only."
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
model=model, temperature=temperature, stop=stop or [], **kwargs
|
||||
)
|
||||
|
||||
self.api_key = api_key or os.getenv("AZURE_API_KEY")
|
||||
self.endpoint = (
|
||||
endpoint
|
||||
# Resolve env vars
|
||||
data["api_key"] = data.get("api_key") or os.getenv("AZURE_API_KEY")
|
||||
data["endpoint"] = (
|
||||
data.get("endpoint")
|
||||
or os.getenv("AZURE_ENDPOINT")
|
||||
or os.getenv("AZURE_OPENAI_ENDPOINT")
|
||||
or os.getenv("AZURE_API_BASE")
|
||||
)
|
||||
self.api_version = api_version or os.getenv("AZURE_API_VERSION") or "2024-06-01"
|
||||
self.timeout = timeout
|
||||
self.max_retries = max_retries
|
||||
data["api_version"] = (
|
||||
data.get("api_version") or os.getenv("AZURE_API_VERSION") or "2024-06-01"
|
||||
)
|
||||
|
||||
if not self.api_key:
|
||||
if not data["api_key"]:
|
||||
raise ValueError(
|
||||
"Azure API key is required. Set AZURE_API_KEY environment variable or pass api_key parameter."
|
||||
)
|
||||
if not self.endpoint:
|
||||
if not data["endpoint"]:
|
||||
raise ValueError(
|
||||
"Azure endpoint is required. Set AZURE_ENDPOINT environment variable or pass endpoint parameter."
|
||||
)
|
||||
|
||||
# Validate and potentially fix Azure OpenAI endpoint URL
|
||||
self.endpoint = self._validate_and_fix_endpoint(self.endpoint, model)
|
||||
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
|
||||
|
||||
# Build client kwargs
|
||||
client_kwargs = {
|
||||
@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] = {
|
||||
"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) # 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
|
||||
)
|
||||
self._client = ChatCompletionsClient(**client_kwargs)
|
||||
self._async_client = AsyncChatCompletionsClient(**client_kwargs)
|
||||
return self
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Azure-specific fields."""
|
||||
@@ -215,7 +188,11 @@ class AzureCompletion(BaseLLM):
|
||||
Returns:
|
||||
Validated and potentially corrected endpoint URL
|
||||
"""
|
||||
if "openai.azure.com" in endpoint and "/openai/deployments/" not in endpoint:
|
||||
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:
|
||||
endpoint = endpoint.rstrip("/")
|
||||
|
||||
if not endpoint.endswith("/openai/deployments"):
|
||||
@@ -592,6 +569,7 @@ 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.
|
||||
|
||||
@@ -617,6 +595,7 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return structured_data
|
||||
@@ -666,6 +645,7 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(message.tool_calls)
|
||||
|
||||
@@ -703,6 +683,7 @@ class AzureCompletion(BaseLLM):
|
||||
params=params,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
content = self._apply_stop_words(content)
|
||||
@@ -714,6 +695,7 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -731,7 +713,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) # type: ignore[assignment,arg-type]
|
||||
response: ChatCompletions = self._client.complete(**params)
|
||||
return self._process_completion_response(
|
||||
response=response,
|
||||
params=params,
|
||||
@@ -817,7 +799,7 @@ class AzureCompletion(BaseLLM):
|
||||
self,
|
||||
full_response: str,
|
||||
tool_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
usage_data: dict[str, Any] | None,
|
||||
params: AzureCompletionParams,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -829,7 +811,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
|
||||
usage_data: Token usage data from the stream, or None if unavailable
|
||||
params: Completion parameters containing messages
|
||||
available_functions: Available functions for tool calling
|
||||
from_task: Task that initiated the call
|
||||
@@ -839,7 +821,8 @@ class AzureCompletion(BaseLLM):
|
||||
Returns:
|
||||
Final response content after processing, or structured output
|
||||
"""
|
||||
self._track_token_usage_internal(usage_data)
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
# Handle structured output validation
|
||||
if response_model and self.is_openai_model:
|
||||
@@ -849,6 +832,7 @@ 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
|
||||
@@ -871,6 +855,7 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
return formatted_tool_calls
|
||||
|
||||
@@ -907,6 +892,7 @@ class AzureCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -925,8 +911,8 @@ class AzureCompletion(BaseLLM):
|
||||
full_response = ""
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
usage_data = {"total_tokens": 0}
|
||||
for update in self.client.complete(**params): # type: ignore[arg-type]
|
||||
usage_data: dict[str, Any] | None = None
|
||||
for update in self._client.complete(**params):
|
||||
if isinstance(update, StreamingChatCompletionsUpdate):
|
||||
if update.usage:
|
||||
usage = update.usage
|
||||
@@ -967,7 +953,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) # type: ignore[assignment,arg-type]
|
||||
response: ChatCompletions = await self._async_client.complete(**params)
|
||||
return self._process_completion_response(
|
||||
response=response,
|
||||
params=params,
|
||||
@@ -991,10 +977,10 @@ class AzureCompletion(BaseLLM):
|
||||
full_response = ""
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
usage_data = {"total_tokens": 0}
|
||||
usage_data: dict[str, Any] | None = None
|
||||
|
||||
stream = await self.async_client.complete(**params) # type: ignore[arg-type]
|
||||
async for update in stream: # type: ignore[union-attr]
|
||||
stream = await self._async_client.complete(**params)
|
||||
async for update in stream:
|
||||
if isinstance(update, StreamingChatCompletionsUpdate):
|
||||
if hasattr(update, "usage") and update.usage:
|
||||
usage = update.usage
|
||||
@@ -1110,8 +1096,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
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
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,129 +228,97 @@ class BedrockCompletion(BaseLLM):
|
||||
- Model-specific conversation format handling (e.g., Cohere requirements)
|
||||
"""
|
||||
|
||||
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.
|
||||
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 = ""
|
||||
|
||||
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:
|
||||
_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:
|
||||
raise NotImplementedError(
|
||||
"HTTP interceptors are not yet supported for AWS Bedrock provider. "
|
||||
"Interceptors are currently supported for OpenAI and Anthropic providers only."
|
||||
)
|
||||
|
||||
# Extract provider from kwargs to avoid duplicate argument
|
||||
kwargs.pop("provider", None)
|
||||
# Force provider to bedrock
|
||||
data.pop("provider", None)
|
||||
data["provider"] = "bedrock"
|
||||
|
||||
super().__init__(
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
stop=stop_sequences or [],
|
||||
provider="bedrock",
|
||||
**kwargs,
|
||||
# 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
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
self.region_name = (
|
||||
region_name
|
||||
data["aws_session_token"] = data.get("aws_session_token") or os.getenv(
|
||||
"AWS_SESSION_TOKEN"
|
||||
)
|
||||
data["region_name"] = (
|
||||
data.get("region_name")
|
||||
or os.getenv("AWS_DEFAULT_REGION")
|
||||
or os.getenv("AWS_REGION_NAME")
|
||||
or "us-east-1"
|
||||
)
|
||||
|
||||
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")
|
||||
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
|
||||
|
||||
# Initialize Bedrock client with proper configuration
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> BedrockCompletion:
|
||||
config = Config(
|
||||
read_timeout=300,
|
||||
retries={"max_attempts": 3, "mode": "adaptive"},
|
||||
tcp_keepalive=True,
|
||||
)
|
||||
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
|
||||
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
|
||||
return self
|
||||
|
||||
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:
|
||||
@@ -363,30 +331,6 @@ 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],
|
||||
@@ -710,7 +654,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]",
|
||||
@@ -720,8 +664,9 @@ class BedrockCompletion(BaseLLM):
|
||||
)
|
||||
|
||||
# Track token usage according to AWS response format
|
||||
if "usage" in response:
|
||||
self._track_token_usage_internal(response["usage"])
|
||||
usage = response.get("usage")
|
||||
if usage:
|
||||
self._track_token_usage_internal(usage)
|
||||
|
||||
stop_reason = response.get("stopReason")
|
||||
if stop_reason:
|
||||
@@ -761,6 +706,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -783,6 +729,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return non_structured_output_tool_uses
|
||||
|
||||
@@ -862,6 +809,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -992,15 +940,16 @@ 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, # type: ignore[arg-type]
|
||||
**body,
|
||||
)
|
||||
|
||||
stream = response.get("stream")
|
||||
@@ -1101,6 +1050,7 @@ 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:
|
||||
@@ -1168,6 +1118,7 @@ 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:
|
||||
@@ -1197,6 +1148,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1308,8 +1260,9 @@ class BedrockCompletion(BaseLLM):
|
||||
**body,
|
||||
)
|
||||
|
||||
if "usage" in response:
|
||||
self._track_token_usage_internal(response["usage"])
|
||||
usage = response.get("usage")
|
||||
if usage:
|
||||
self._track_token_usage_internal(usage)
|
||||
|
||||
stop_reason = response.get("stopReason")
|
||||
if stop_reason:
|
||||
@@ -1348,6 +1301,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -1370,6 +1324,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
return non_structured_output_tool_uses
|
||||
|
||||
@@ -1444,6 +1399,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return text_content
|
||||
@@ -1564,6 +1520,7 @@ 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()
|
||||
@@ -1675,6 +1632,7 @@ 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:
|
||||
@@ -1747,6 +1705,7 @@ 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:
|
||||
@@ -1776,6 +1735,7 @@ class BedrockCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
|
||||
@@ -5,12 +5,13 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
||||
|
||||
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,
|
||||
@@ -19,10 +20,6 @@ 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
|
||||
@@ -44,137 +41,84 @@ class GeminiCompletion(BaseLLM):
|
||||
offering native function calling, streaming support, and proper Gemini formatting.
|
||||
"""
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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:
|
||||
_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:
|
||||
raise NotImplementedError(
|
||||
"HTTP interceptors are not yet supported for Google Gemini provider. "
|
||||
"Interceptors are currently supported for OpenAI and Anthropic providers only."
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
model=model, temperature=temperature, stop=stop_sequences or [], **kwargs
|
||||
# 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"
|
||||
)
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Model-specific settings
|
||||
model = data.get("model", "gemini-2.0-flash-001")
|
||||
version_match = re.search(r"gemini-(\d+(?:\.\d+)?)", model.lower())
|
||||
self.supports_tools = bool(
|
||||
data["supports_tools"] = bool(
|
||||
version_match and float(version_match.group(1)) >= 1.5
|
||||
)
|
||||
self.is_gemini_2_0 = bool(
|
||||
data["is_gemini_2_0"] = bool(
|
||||
version_match and float(version_match.group(1)) >= 2.0
|
||||
)
|
||||
|
||||
self.thinking_config = thinking_config
|
||||
# Auto-enable thinking for gemini-2.5+
|
||||
if (
|
||||
self.thinking_config is None
|
||||
data.get("thinking_config") is None
|
||||
and version_match
|
||||
and float(version_match.group(1)) >= 2.5
|
||||
):
|
||||
self.thinking_config = types.ThinkingConfig(include_thoughts=True)
|
||||
data["thinking_config"] = types.ThinkingConfig(include_thoughts=True)
|
||||
|
||||
@property
|
||||
def stop(self) -> list[str]:
|
||||
"""Get stop sequences sent to the API."""
|
||||
return self.stop_sequences
|
||||
return data
|
||||
|
||||
@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 = []
|
||||
@model_validator(mode="after")
|
||||
def _init_client(self) -> GeminiCompletion:
|
||||
self._client = self._initialize_client(self.use_vertexai)
|
||||
return self
|
||||
|
||||
def to_config_dict(self) -> dict[str, Any]:
|
||||
"""Extend base config with Gemini/Vertex-specific fields."""
|
||||
@@ -283,8 +227,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(
|
||||
@@ -721,6 +665,7 @@ 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.
|
||||
|
||||
@@ -746,6 +691,7 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages_for_event,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return structured_data
|
||||
@@ -761,6 +707,7 @@ 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.
|
||||
|
||||
@@ -784,6 +731,7 @@ class GeminiCompletion(BaseLLM):
|
||||
messages_for_event=messages_for_event,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
self._emit_call_completed_event(
|
||||
@@ -792,6 +740,7 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages_for_event,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -805,6 +754,7 @@ 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.
|
||||
|
||||
@@ -829,6 +779,7 @@ 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:
|
||||
@@ -847,6 +798,7 @@ 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.
|
||||
|
||||
@@ -887,6 +839,7 @@ 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
|
||||
@@ -908,6 +861,7 @@ 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
|
||||
|
||||
@@ -949,6 +903,7 @@ class GeminiCompletion(BaseLLM):
|
||||
response_model=effective_response_model,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
def _process_stream_chunk(
|
||||
@@ -956,10 +911,10 @@ class GeminiCompletion(BaseLLM):
|
||||
chunk: GenerateContentResponse,
|
||||
full_response: str,
|
||||
function_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
usage_data: dict[str, int] | None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
) -> tuple[str, dict[int, dict[str, Any]], dict[str, int]]:
|
||||
) -> tuple[str, dict[int, dict[str, Any]], dict[str, int] | None]:
|
||||
"""Process a single streaming chunk.
|
||||
|
||||
Args:
|
||||
@@ -1035,7 +990,7 @@ class GeminiCompletion(BaseLLM):
|
||||
self,
|
||||
full_response: str,
|
||||
function_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
usage_data: dict[str, int] | None,
|
||||
contents: list[types.Content],
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1047,7 +1002,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
|
||||
usage_data: Token usage data from the stream, or None if unavailable
|
||||
contents: Original contents for event conversion
|
||||
available_functions: Available functions for function calling
|
||||
from_task: Task that initiated the call
|
||||
@@ -1057,7 +1012,8 @@ class GeminiCompletion(BaseLLM):
|
||||
Returns:
|
||||
Final response content after processing
|
||||
"""
|
||||
self._track_token_usage_internal(usage_data)
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
if response_model and function_calls:
|
||||
for call_data in function_calls.values():
|
||||
@@ -1069,6 +1025,7 @@ class GeminiCompletion(BaseLLM):
|
||||
contents=contents,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
non_structured_output_calls = {
|
||||
@@ -1097,6 +1054,7 @@ 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
|
||||
|
||||
@@ -1137,6 +1095,7 @@ class GeminiCompletion(BaseLLM):
|
||||
response_model=effective_response_model,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
def _handle_completion(
|
||||
@@ -1152,7 +1111,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,
|
||||
@@ -1174,6 +1133,7 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
def _handle_streaming_completion(
|
||||
@@ -1188,11 +1148,11 @@ class GeminiCompletion(BaseLLM):
|
||||
"""Handle streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[int, dict[str, Any]] = {}
|
||||
usage_data = {"total_tokens": 0}
|
||||
usage_data: dict[str, int] | None = None
|
||||
|
||||
# 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,
|
||||
@@ -1230,7 +1190,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,
|
||||
@@ -1252,6 +1212,7 @@ class GeminiCompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
async def _ahandle_streaming_completion(
|
||||
@@ -1266,11 +1227,11 @@ class GeminiCompletion(BaseLLM):
|
||||
"""Handle async streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[int, dict[str, Any]] = {}
|
||||
usage_data = {"total_tokens": 0}
|
||||
usage_data: dict[str, int] | None = None
|
||||
|
||||
# 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,
|
||||
@@ -1474,6 +1435,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,10 +14,11 @@ 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
|
||||
from pydantic import BaseModel, PrivateAttr, model_validator
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType
|
||||
from crewai.llms.base_llm import BaseLLM, llm_call_context
|
||||
from crewai.llms.base_llm import BaseLLM, JsonResponseFormat, llm_call_context
|
||||
from crewai.llms.hooks.base import BaseInterceptor
|
||||
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 (
|
||||
@@ -29,7 +30,6 @@ 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,77 +183,69 @@ class OpenAICompletion(BaseLLM):
|
||||
"computer_use": "computer_use_preview",
|
||||
}
|
||||
|
||||
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."""
|
||||
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
|
||||
|
||||
if provider is None:
|
||||
provider = kwargs.pop("provider", "openai")
|
||||
_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)
|
||||
|
||||
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="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
|
||||
|
||||
@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:
|
||||
@@ -261,35 +253,8 @@ 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)
|
||||
|
||||
# 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
|
||||
self._async_client = AsyncOpenAI(**async_client_config)
|
||||
return self
|
||||
|
||||
@property
|
||||
def last_response_id(self) -> str | None:
|
||||
@@ -818,7 +783,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:
|
||||
@@ -844,6 +809,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -856,6 +822,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return function_calls
|
||||
|
||||
@@ -893,6 +860,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -906,6 +874,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
content = self._invoke_after_llm_call_hooks(
|
||||
@@ -950,7 +919,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:
|
||||
@@ -976,6 +945,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -988,6 +958,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return function_calls
|
||||
|
||||
@@ -1025,6 +996,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1038,6 +1010,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
except NotFoundError as e:
|
||||
@@ -1080,8 +1053,9 @@ 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:
|
||||
@@ -1137,6 +1111,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -1173,6 +1148,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1186,6 +1162,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
@@ -1204,8 +1181,9 @@ 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:
|
||||
@@ -1261,6 +1239,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return parsed_result
|
||||
@@ -1297,6 +1276,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1310,6 +1290,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params.get("input", []),
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1595,7 +1576,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,
|
||||
)
|
||||
@@ -1615,10 +1596,11 @@ 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)
|
||||
|
||||
@@ -1636,6 +1618,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(message.tool_calls)
|
||||
|
||||
@@ -1674,6 +1657,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -1687,6 +1671,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -1728,7 +1713,7 @@ class OpenAICompletion(BaseLLM):
|
||||
self,
|
||||
full_response: str,
|
||||
tool_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
usage_data: dict[str, Any] | None,
|
||||
params: dict[str, Any],
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1739,7 +1724,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.
|
||||
usage_data: Token usage data from the stream, or None if unavailable.
|
||||
params: The completion parameters containing messages.
|
||||
available_functions: Available functions for tool calling.
|
||||
from_task: Task that initiated the call.
|
||||
@@ -1750,7 +1735,8 @@ class OpenAICompletion(BaseLLM):
|
||||
tool execution result when available_functions is provided,
|
||||
or the text response string.
|
||||
"""
|
||||
self._track_token_usage_internal(usage_data)
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
if tool_calls and not available_functions:
|
||||
tool_calls_list = [
|
||||
@@ -1771,6 +1757,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
return tool_calls_list
|
||||
|
||||
@@ -1813,6 +1800,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return full_response
|
||||
@@ -1837,7 +1825,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:
|
||||
@@ -1866,6 +1854,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return parsed_result
|
||||
|
||||
@@ -1873,10 +1862,10 @@ class OpenAICompletion(BaseLLM):
|
||||
return ""
|
||||
|
||||
completion_stream: Stream[ChatCompletionChunk] = (
|
||||
self.client.chat.completions.create(**params)
|
||||
self._client.chat.completions.create(**params)
|
||||
)
|
||||
|
||||
usage_data = {"total_tokens": 0}
|
||||
usage_data: dict[str, Any] | None = None
|
||||
|
||||
for completion_chunk in completion_stream:
|
||||
response_id_stream = (
|
||||
@@ -1970,7 +1959,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,
|
||||
)
|
||||
@@ -1990,10 +1979,11 @@ 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
|
||||
)
|
||||
|
||||
@@ -2013,6 +2003,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return list(message.tool_calls)
|
||||
|
||||
@@ -2051,6 +2042,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
return structured_result
|
||||
except ValueError as e:
|
||||
@@ -2064,6 +2056,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
@@ -2111,10 +2104,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 = {"total_tokens": 0}
|
||||
usage_data: dict[str, Any] | None = None
|
||||
async for chunk in completion_stream:
|
||||
response_id_stream = chunk.id if hasattr(chunk, "id") else None
|
||||
|
||||
@@ -2137,7 +2130,8 @@ class OpenAICompletion(BaseLLM):
|
||||
response_id=response_id_stream,
|
||||
)
|
||||
|
||||
self._track_token_usage_internal(usage_data)
|
||||
if usage_data:
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
try:
|
||||
parsed_object = response_model.model_validate_json(accumulated_content)
|
||||
@@ -2148,6 +2142,7 @@ class OpenAICompletion(BaseLLM):
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
usage=usage_data,
|
||||
)
|
||||
|
||||
return parsed_object
|
||||
@@ -2159,14 +2154,15 @@ 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 = {"total_tokens": 0}
|
||||
usage_data = None
|
||||
|
||||
async for chunk in stream:
|
||||
response_id_stream = chunk.id if hasattr(chunk, "id") else None
|
||||
@@ -2245,6 +2241,9 @@ class OpenAICompletion(BaseLLM):
|
||||
|
||||
def supports_stop_words(self) -> bool:
|
||||
"""Check if the model supports stop words."""
|
||||
model_lower = self.model.lower() if self.model else ""
|
||||
if "gpt-5" in model_lower:
|
||||
return False
|
||||
return not self.is_o1_model
|
||||
|
||||
def get_context_window_size(self) -> int:
|
||||
@@ -2353,8 +2352,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,6 +16,8 @@ from dataclasses import dataclass, field
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from crewai.llms.providers.openai.completion import OpenAICompletion
|
||||
|
||||
|
||||
@@ -140,31 +142,13 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
)
|
||||
"""
|
||||
|
||||
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.
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _resolve_provider_config(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
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.
|
||||
"""
|
||||
provider = data.get("provider", "")
|
||||
config = OPENAI_COMPATIBLE_PROVIDERS.get(provider)
|
||||
if config is None:
|
||||
supported = ", ".join(sorted(OPENAI_COMPATIBLE_PROVIDERS.keys()))
|
||||
@@ -173,21 +157,15 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
f"Supported providers: {supported}"
|
||||
)
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _resolve_api_key(
|
||||
self,
|
||||
api_key: str | None,
|
||||
config: ProviderConfig,
|
||||
provider: str,
|
||||
@@ -220,8 +198,8 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
|
||||
return config.default_api_key
|
||||
|
||||
@staticmethod
|
||||
def _resolve_base_url(
|
||||
self,
|
||||
base_url: str | None,
|
||||
config: ProviderConfig,
|
||||
provider: str,
|
||||
@@ -239,7 +217,8 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
if base_url:
|
||||
resolved = base_url
|
||||
elif config.base_url_env:
|
||||
resolved = os.getenv(config.base_url_env, config.base_url)
|
||||
env_value = os.getenv(config.base_url_env)
|
||||
resolved = env_value if env_value else config.base_url
|
||||
else:
|
||||
resolved = config.base_url
|
||||
|
||||
@@ -248,8 +227,8 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
|
||||
return resolved
|
||||
|
||||
@staticmethod
|
||||
def _resolve_headers(
|
||||
self,
|
||||
headers: dict[str, str] | None,
|
||||
config: ProviderConfig,
|
||||
) -> dict[str, str] | None:
|
||||
@@ -274,9 +253,11 @@ class OpenAICompatibleCompletion(OpenAICompletion):
|
||||
def supports_function_calling(self) -> bool:
|
||||
"""Check if the provider supports function calling.
|
||||
|
||||
All modern OpenAI-compatible providers support function calling.
|
||||
Delegates to the parent OpenAI implementation which handles
|
||||
edge cases like o1 models (which may be routed through
|
||||
OpenRouter or other compatible providers).
|
||||
|
||||
Returns:
|
||||
True, as all supported providers have function calling support.
|
||||
Whether the model supports function calling.
|
||||
"""
|
||||
return True
|
||||
return super().supports_function_calling()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Third-party LLM implementations for crewAI."""
|
||||
@@ -98,7 +98,7 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
|
||||
_skip_auto_memory: bool = True
|
||||
|
||||
initial_state = EncodingState
|
||||
initial_state: type[EncodingState] = EncodingState
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -65,7 +65,7 @@ class RecallFlow(Flow[RecallState]):
|
||||
|
||||
_skip_auto_memory: bool = True
|
||||
|
||||
initial_state = RecallState
|
||||
initial_state: type[RecallState] = RecallState
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -148,6 +148,36 @@ 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, InstanceOf
|
||||
from pydantic import BaseModel, Field
|
||||
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: InstanceOf[BaseLLM] | str | None = None,
|
||||
eval_llm: BaseLLM | str | None = None,
|
||||
openai_model_name: str | None = None,
|
||||
llm: InstanceOf[BaseLLM] | str | None = None,
|
||||
llm: BaseLLM | str | None = None,
|
||||
) -> None:
|
||||
self.crew = crew
|
||||
self.llm = eval_llm
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any, Literal, TypedDict
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
|
||||
|
||||
319
lib/crewai/tests/a2a/extensions/test_a2ui_schema_conformance.py
Normal file
319
lib/crewai/tests/a2a/extensions/test_a2ui_schema_conformance.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""Cross-validate A2UI Pydantic models against vendored JSON schemas.
|
||||
|
||||
Ensures the two validation sources stay in sync: representative payloads
|
||||
must be accepted or rejected consistently by both the Pydantic models and
|
||||
the JSON schemas.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
|
||||
from crewai.a2a.extensions.a2ui import catalog
|
||||
from crewai.a2a.extensions.a2ui.models import A2UIEvent, A2UIMessage
|
||||
from crewai.a2a.extensions.a2ui.schema import load_schema
|
||||
|
||||
|
||||
SERVER_SCHEMA = load_schema("server_to_client")
|
||||
CLIENT_SCHEMA = load_schema("client_to_server")
|
||||
CATALOG_SCHEMA = load_schema("standard_catalog_definition")
|
||||
|
||||
|
||||
def _json_schema_valid(schema: dict[str, Any], instance: dict[str, Any]) -> bool:
|
||||
"""Return True if *instance* validates against *schema*."""
|
||||
try:
|
||||
jsonschema.validate(instance, schema)
|
||||
return True
|
||||
except jsonschema.ValidationError:
|
||||
return False
|
||||
|
||||
|
||||
def _pydantic_valid_message(data: dict[str, Any]) -> bool:
|
||||
"""Return True if *data* validates as an A2UIMessage."""
|
||||
try:
|
||||
A2UIMessage.model_validate(data)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _pydantic_valid_event(data: dict[str, Any]) -> bool:
|
||||
"""Return True if *data* validates as an A2UIEvent."""
|
||||
try:
|
||||
A2UIEvent.model_validate(data)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Valid server-to-client payloads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_SERVER_MESSAGES: list[dict[str, Any]] = [
|
||||
{
|
||||
"beginRendering": {
|
||||
"surfaceId": "s1",
|
||||
"root": "root-col",
|
||||
},
|
||||
},
|
||||
{
|
||||
"beginRendering": {
|
||||
"surfaceId": "s2",
|
||||
"root": "root-col",
|
||||
"catalogId": "standard (v0.8)",
|
||||
"styles": {"primaryColor": "#FF0000", "font": "Roboto"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "s1",
|
||||
"components": [
|
||||
{
|
||||
"id": "title",
|
||||
"component": {
|
||||
"Text": {"text": {"literalString": "Hello"}},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "s1",
|
||||
"components": [
|
||||
{
|
||||
"id": "weighted",
|
||||
"weight": 2.0,
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {"explicitList": ["a", "b"]},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"dataModelUpdate": {
|
||||
"surfaceId": "s1",
|
||||
"contents": [
|
||||
{"key": "name", "valueString": "Alice"},
|
||||
{"key": "score", "valueNumber": 42},
|
||||
{"key": "active", "valueBoolean": True},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"dataModelUpdate": {
|
||||
"surfaceId": "s1",
|
||||
"path": "/user",
|
||||
"contents": [
|
||||
{
|
||||
"key": "prefs",
|
||||
"valueMap": [
|
||||
{"key": "theme", "valueString": "dark"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"deleteSurface": {"surfaceId": "s1"},
|
||||
},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid server-to-client payloads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
INVALID_SERVER_MESSAGES: list[dict[str, Any]] = [
|
||||
{},
|
||||
{"beginRendering": {"surfaceId": "s1"}},
|
||||
{"surfaceUpdate": {"surfaceId": "s1", "components": []}},
|
||||
{
|
||||
"beginRendering": {"surfaceId": "s1", "root": "r"},
|
||||
"deleteSurface": {"surfaceId": "s1"},
|
||||
},
|
||||
{"unknownType": {"surfaceId": "s1"}},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Valid client-to-server payloads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_CLIENT_EVENTS: list[dict[str, Any]] = [
|
||||
{
|
||||
"userAction": {
|
||||
"name": "click",
|
||||
"surfaceId": "s1",
|
||||
"sourceComponentId": "btn-1",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
"context": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
"userAction": {
|
||||
"name": "submit",
|
||||
"surfaceId": "s1",
|
||||
"sourceComponentId": "btn-2",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
"context": {"field": "value"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"error": {"message": "render failed", "code": 500},
|
||||
},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid client-to-server payloads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
INVALID_CLIENT_EVENTS: list[dict[str, Any]] = [
|
||||
{},
|
||||
{"userAction": {"name": "click"}},
|
||||
{
|
||||
"userAction": {
|
||||
"name": "click",
|
||||
"surfaceId": "s1",
|
||||
"sourceComponentId": "btn-1",
|
||||
"timestamp": "2026-03-12T10:00:00Z",
|
||||
"context": {},
|
||||
},
|
||||
"error": {"message": "oops"},
|
||||
},
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Catalog component payloads (validated structurally)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_COMPONENTS: dict[str, dict[str, Any]] = {
|
||||
"Text": {"text": {"literalString": "hello"}, "usageHint": "h1"},
|
||||
"Image": {"url": {"path": "/img/url"}, "fit": "cover", "usageHint": "avatar"},
|
||||
"Icon": {"name": {"literalString": "home"}},
|
||||
"Video": {"url": {"literalString": "https://example.com/video.mp4"}},
|
||||
"AudioPlayer": {"url": {"literalString": "https://example.com/audio.mp3"}},
|
||||
"Row": {"children": {"explicitList": ["a", "b"]}, "distribution": "center"},
|
||||
"Column": {"children": {"template": {"componentId": "c1", "dataBinding": "/list"}}},
|
||||
"List": {"children": {"explicitList": ["x"]}, "direction": "horizontal"},
|
||||
"Card": {"child": "inner"},
|
||||
"Tabs": {"tabItems": [{"title": {"literalString": "Tab 1"}, "child": "content"}]},
|
||||
"Divider": {"axis": "horizontal"},
|
||||
"Modal": {"entryPointChild": "trigger", "contentChild": "body"},
|
||||
"Button": {"child": "label", "action": {"name": "go"}},
|
||||
"CheckBox": {"label": {"literalString": "Accept"}, "value": {"literalBoolean": False}},
|
||||
"TextField": {"label": {"literalString": "Name"}},
|
||||
"DateTimeInput": {"value": {"path": "/date"}},
|
||||
"MultipleChoice": {
|
||||
"selections": {"literalArray": ["a"]},
|
||||
"options": [{"label": {"literalString": "A"}, "value": "a"}],
|
||||
},
|
||||
"Slider": {"value": {"literalNumber": 50}, "minValue": 0, "maxValue": 100},
|
||||
}
|
||||
|
||||
|
||||
class TestServerToClientConformance:
|
||||
"""Pydantic models and JSON schema must agree on server-to-client messages."""
|
||||
|
||||
@pytest.mark.parametrize("payload", VALID_SERVER_MESSAGES)
|
||||
def test_valid_accepted_by_both(self, payload: dict[str, Any]) -> None:
|
||||
assert _json_schema_valid(SERVER_SCHEMA, payload), (
|
||||
f"JSON schema rejected valid payload: {payload}"
|
||||
)
|
||||
assert _pydantic_valid_message(payload), (
|
||||
f"Pydantic rejected valid payload: {payload}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("payload", INVALID_SERVER_MESSAGES)
|
||||
def test_invalid_rejected_by_pydantic(self, payload: dict[str, Any]) -> None:
|
||||
assert not _pydantic_valid_message(payload), (
|
||||
f"Pydantic accepted invalid payload: {payload}"
|
||||
)
|
||||
|
||||
|
||||
class TestClientToServerConformance:
|
||||
"""Pydantic models and JSON schema must agree on client-to-server events."""
|
||||
|
||||
@pytest.mark.parametrize("payload", VALID_CLIENT_EVENTS)
|
||||
def test_valid_accepted_by_both(self, payload: dict[str, Any]) -> None:
|
||||
assert _json_schema_valid(CLIENT_SCHEMA, payload), (
|
||||
f"JSON schema rejected valid payload: {payload}"
|
||||
)
|
||||
assert _pydantic_valid_event(payload), (
|
||||
f"Pydantic rejected valid payload: {payload}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("payload", INVALID_CLIENT_EVENTS)
|
||||
def test_invalid_rejected_by_pydantic(self, payload: dict[str, Any]) -> None:
|
||||
assert not _pydantic_valid_event(payload), (
|
||||
f"Pydantic accepted invalid payload: {payload}"
|
||||
)
|
||||
|
||||
|
||||
class TestCatalogConformance:
|
||||
"""Catalog component schemas and Pydantic models must define the same components."""
|
||||
|
||||
def test_catalog_component_names_match(self) -> None:
|
||||
from crewai.a2a.extensions.a2ui.catalog import STANDARD_CATALOG_COMPONENTS
|
||||
|
||||
schema_components = set(CATALOG_SCHEMA["components"].keys())
|
||||
assert schema_components == STANDARD_CATALOG_COMPONENTS
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,props",
|
||||
list(VALID_COMPONENTS.items()),
|
||||
)
|
||||
def test_valid_component_accepted_by_catalog_schema(
|
||||
self, name: str, props: dict[str, Any]
|
||||
) -> None:
|
||||
component_schema = CATALOG_SCHEMA["components"][name]
|
||||
assert _json_schema_valid(component_schema, props), (
|
||||
f"Catalog schema rejected valid {name}: {props}"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,props",
|
||||
list(VALID_COMPONENTS.items()),
|
||||
)
|
||||
def test_valid_component_accepted_by_pydantic(
|
||||
self, name: str, props: dict[str, Any]
|
||||
) -> None:
|
||||
model_cls = getattr(catalog, name)
|
||||
try:
|
||||
model_cls.model_validate(props)
|
||||
except Exception as exc:
|
||||
pytest.fail(f"Pydantic {name} rejected valid props: {exc}")
|
||||
|
||||
def test_catalog_required_fields_match(self) -> None:
|
||||
"""Required fields in the JSON schema match non-optional Pydantic fields."""
|
||||
for comp_name, comp_schema in CATALOG_SCHEMA["components"].items():
|
||||
schema_required = set(comp_schema.get("required", []))
|
||||
model_cls = getattr(catalog, comp_name)
|
||||
pydantic_required = {
|
||||
info.alias or field_name
|
||||
for field_name, info in model_cls.model_fields.items()
|
||||
if info.is_required()
|
||||
}
|
||||
assert schema_required == pydantic_required, (
|
||||
f"{comp_name}: schema requires {schema_required}, "
|
||||
f"Pydantic requires {pydantic_required}"
|
||||
)
|
||||
|
||||
def test_catalog_fields_match(self) -> None:
|
||||
"""Field names in JSON schema match Pydantic model aliases."""
|
||||
for comp_name, comp_schema in CATALOG_SCHEMA["components"].items():
|
||||
schema_fields = set(comp_schema.get("properties", {}).keys())
|
||||
model_cls = getattr(catalog, comp_name)
|
||||
pydantic_fields = {
|
||||
info.alias or field_name
|
||||
for field_name, info in model_cls.model_fields.items()
|
||||
}
|
||||
assert schema_fields == pydantic_fields, (
|
||||
f"{comp_name}: schema has {schema_fields}, "
|
||||
f"Pydantic has {pydantic_fields}"
|
||||
)
|
||||
@@ -1692,9 +1692,27 @@ def test_agent_with_knowledge_sources_works_with_copy():
|
||||
) as mock_knowledge_storage:
|
||||
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
|
||||
|
||||
mock_knowledge_storage_instance = mock_knowledge_storage.return_value
|
||||
mock_knowledge_storage_instance.__class__ = BaseKnowledgeStorage
|
||||
agent.knowledge_storage = mock_knowledge_storage_instance
|
||||
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()
|
||||
|
||||
agent_copy = agent.copy()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user