mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-06 19:18:16 +00:00
Compare commits
20 Commits
devin/1775
...
docs/conve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f789cf854b | ||
|
|
575bf87f07 | ||
|
|
9c4fb28956 | ||
|
|
4fe0cc348f | ||
|
|
3bc168f223 | ||
|
|
b690ef69ae | ||
|
|
804c26bd01 | ||
|
|
4e46913045 | ||
|
|
335130cb15 | ||
|
|
186ea77c63 | ||
|
|
9e51229e6c | ||
|
|
247d623499 | ||
|
|
c260f3e19f | ||
|
|
d9cf7dda31 | ||
|
|
c14abf1758 | ||
|
|
f10d320ddb | ||
|
|
258f31d44c | ||
|
|
68720fd4e5 | ||
|
|
3132910084 | ||
|
|
c8f3a96779 |
13
.github/workflows/docs-broken-links.yml
vendored
13
.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:
|
||||
@@ -25,11 +25,12 @@ jobs:
|
||||
with:
|
||||
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,48 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2 أبريل 2026">
|
||||
## v1.13.0a7
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة امتداد A2UI مع دعم v0.8/v0.9، والمخططات، والوثائق
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح بادئات الرؤية متعددة الأنماط عن طريق إضافة GPT-5 وسلسلة o
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0a6
|
||||
|
||||
## المساهمون
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: wrench
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### شاهد: بناء Agents و Flows في CrewAI باستخدام Coding Agent Skills
|
||||
|
||||
قم بتثبيت مهارات وكيل البرمجة الخاصة بنا (Claude Code، Codex، ...) لتشغيل وكلاء البرمجة بسرعة مع CrewAI.
|
||||
|
||||
يمكنك تثبيتها باستخدام `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## فيديو تعليمي
|
||||
|
||||
شاهد هذا الفيديو التعليمي لعرض تفصيلي لعملية التثبيت:
|
||||
|
||||
@@ -16,6 +16,14 @@ mode: "wide"
|
||||
|
||||
مع أكثر من 100,000 مطور معتمد عبر دوراتنا المجتمعية، يُعد CrewAI المعيار لأتمتة الذكاء الاصطناعي الجاهزة للمؤسسات.
|
||||
|
||||
### شاهد: بناء Agents و Flows في CrewAI باستخدام Coding Agent Skills
|
||||
|
||||
قم بتثبيت مهارات وكيل البرمجة الخاصة بنا (Claude Code، Codex، ...) لتشغيل وكلاء البرمجة بسرعة مع CrewAI.
|
||||
|
||||
يمكنك تثبيتها باستخدام `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## بنية CrewAI المعمارية
|
||||
|
||||
صُممت بنية CrewAI لتحقيق التوازن بين الاستقلالية والتحكم.
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: rocket
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### شاهد: بناء Agents و Flows في CrewAI باستخدام Coding Agent Skills
|
||||
|
||||
قم بتثبيت مهارات وكيل البرمجة الخاصة بنا (Claude Code، Codex، ...) لتشغيل وكلاء البرمجة بسرعة مع CrewAI.
|
||||
|
||||
يمكنك تثبيتها باستخدام `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## ابنِ أول وكيل CrewAI
|
||||
|
||||
لننشئ طاقماً بسيطاً يساعدنا في `البحث` و`إعداد التقارير` عن `أحدث تطورات الذكاء الاصطناعي` لموضوع أو مجال معين.
|
||||
|
||||
@@ -2237,7 +2237,9 @@
|
||||
"en/learn/using-annotations",
|
||||
"en/learn/execution-hooks",
|
||||
"en/learn/llm-hooks",
|
||||
"en/learn/tool-hooks"
|
||||
"en/learn/tool-hooks",
|
||||
"en/learn/a2a-agent-delegation",
|
||||
"en/learn/a2ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3174,7 +3176,9 @@
|
||||
"en/learn/using-annotations",
|
||||
"en/learn/execution-hooks",
|
||||
"en/learn/llm-hooks",
|
||||
"en/learn/tool-hooks"
|
||||
"en/learn/tool-hooks",
|
||||
"en/learn/a2a-agent-delegation",
|
||||
"en/learn/a2ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,48 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Apr 02, 2026">
|
||||
## v1.13.0a7
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add A2UI extension with v0.8/v0.9 support, schemas, and docs
|
||||
|
||||
### Bug Fixes
|
||||
- Fix multimodal vision prefixes by adding GPT-5 and o-series
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a6
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -46,7 +46,7 @@ You can configure users and roles in Settings → Roles.
|
||||
| Role | Description |
|
||||
| :--------- | :-------------------------------------------------------------------------- |
|
||||
| **Owner** | Full access to all features and settings. Cannot be restricted. |
|
||||
| **Member** | Read access to most features, manage access to Studio projects. Cannot modify organization or default settings. |
|
||||
| **Member** | Read access to most features, manage access to environment variables, LLM connections, and Studio projects. Cannot modify organization or default settings. |
|
||||
|
||||
### Configuration summary
|
||||
|
||||
@@ -65,22 +65,22 @@ Every role has a permission level for each feature area. The three levels are:
|
||||
- **Read** — view-only access
|
||||
- **No access** — feature is hidden/inaccessible
|
||||
|
||||
| Feature | Owner | Member (default) | Description |
|
||||
| :------------------------ | :------ | :--------------- | :-------------------------------------------------------------- |
|
||||
| `usage_dashboards` | Manage | Read | View usage metrics and analytics |
|
||||
| `crews_dashboards` | Manage | Read | View deployment dashboards, access automation details |
|
||||
| `invitations` | Manage | Read | Invite new members to the organization |
|
||||
| `training_ui` | Manage | Read | Access training/fine-tuning interfaces |
|
||||
| `tools` | Manage | Read | Create and manage tools |
|
||||
| `agents` | Manage | Read | Create and manage agents |
|
||||
| `environment_variables` | Manage | Read | Create and manage environment variables |
|
||||
| `llm_connections` | Manage | Read | Configure LLM provider connections |
|
||||
| `default_settings` | Manage | No access | Modify organization-wide default settings |
|
||||
| `organization_settings` | Manage | No access | Manage billing, plans, and organization configuration |
|
||||
| `studio_projects` | Manage | Manage | Create and edit projects in Studio |
|
||||
| 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, you can set each feature independently to **Manage**, **Read**, or **No access** to match your team's needs.
|
||||
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>
|
||||
|
||||
---
|
||||
@@ -208,7 +208,7 @@ A role for team members who build and deploy automations but don't manage organi
|
||||
| `tools` | Manage |
|
||||
| `agents` | Manage |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Read |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Manage |
|
||||
@@ -229,7 +229,7 @@ A role for non-technical stakeholders who need to monitor automations and view r
|
||||
| `llm_connections` | No access |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Read |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
### Ops / Platform Admin Role
|
||||
|
||||
@@ -247,7 +247,7 @@ A role for platform operators who manage infrastructure settings but may not bui
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | Manage |
|
||||
| `organization_settings` | Read |
|
||||
| `studio_projects` | Read |
|
||||
| `studio_projects` | No access |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: wrench
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
|
||||
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
|
||||
|
||||
You can install it with `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
Watch this video tutorial for a step-by-step demonstration of the installation process:
|
||||
|
||||
@@ -16,6 +16,14 @@ It empowers developers to build production-ready multi-agent systems by combinin
|
||||
|
||||
With over 100,000 developers certified through our community courses, CrewAI is the standard for enterprise-ready AI automation.
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
|
||||
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
|
||||
|
||||
You can install it with `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## The CrewAI Architecture
|
||||
|
||||
CrewAI's architecture is designed to balance autonomy with control.
|
||||
|
||||
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
|
||||
@@ -5,6 +5,14 @@ icon: rocket
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
|
||||
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
|
||||
|
||||
You can install it with `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## Build your first CrewAI Agent
|
||||
|
||||
Let's create a simple crew that will help us `research` and `report` on the `latest AI developments` for a given topic or subject.
|
||||
|
||||
@@ -4,6 +4,48 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 4월 2일">
|
||||
## v1.13.0a7
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- v0.8/v0.9 지원, 스키마 및 문서가 포함된 A2UI 확장 추가
|
||||
|
||||
### 버그 수정
|
||||
- GPT-5 및 o-series를 추가하여 다중 모드 비전 접두사 수정
|
||||
|
||||
### 문서
|
||||
- v1.13.0a6에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: wrench
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### 영상: 코딩 에이전트 스킬을 활용한 CrewAI Agents & Flows 구축
|
||||
|
||||
코딩 에이전트 스킬(Claude Code, Codex 등)을 설치하여 CrewAI로 코딩 에이전트를 빠르게 시작하세요.
|
||||
|
||||
`npx skills add crewaiinc/skills` 명령어로 설치할 수 있습니다
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## 비디오 튜토리얼
|
||||
|
||||
설치 과정을 단계별로 시연하는 비디오 튜토리얼을 시청하세요:
|
||||
|
||||
@@ -16,6 +16,14 @@ mode: "wide"
|
||||
|
||||
10만 명이 넘는 개발자가 커뮤니티 과정을 통해 인증을 받았으며, CrewAI는 기업용 AI 자동화의 표준입니다.
|
||||
|
||||
### 영상: 코딩 에이전트 스킬을 활용한 CrewAI Agents & Flows 구축
|
||||
|
||||
코딩 에이전트 스킬(Claude Code, Codex 등)을 설치하여 CrewAI로 코딩 에이전트를 빠르게 시작하세요.
|
||||
|
||||
`npx skills add crewaiinc/skills` 명령어로 설치할 수 있습니다
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## CrewAI 아키텍처
|
||||
|
||||
CrewAI의 아키텍처는 자율성과 제어의 균형을 맞추도록 설계되었습니다.
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: rocket
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### 영상: 코딩 에이전트 스킬을 활용한 CrewAI Agents & Flows 구축
|
||||
|
||||
코딩 에이전트 스킬(Claude Code, Codex 등)을 설치하여 CrewAI로 코딩 에이전트를 빠르게 시작하세요.
|
||||
|
||||
`npx skills add crewaiinc/skills` 명령어로 설치할 수 있습니다
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## 첫 번째 CrewAI Agent 만들기
|
||||
|
||||
이제 주어진 주제나 항목에 대해 `최신 AI 개발 동향`을 `연구`하고 `보고`하는 간단한 crew를 만들어보겠습니다.
|
||||
|
||||
@@ -4,6 +4,48 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="02 abr 2026">
|
||||
## v1.13.0a7
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar a extensão A2UI com suporte a v0.8/v0.9, esquemas e documentação
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir prefixos de visão multimodal adicionando GPT-5 e o-series
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a6
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: wrench
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### Assista: Construindo Agents e Flows CrewAI com Coding Agent Skills
|
||||
|
||||
Instale nossas coding agent skills (Claude Code, Codex, ...) para colocar seus agentes de código para funcionar rapidamente com o CrewAI.
|
||||
|
||||
Você pode instalar com `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## Tutorial em Vídeo
|
||||
|
||||
Assista a este tutorial em vídeo para uma demonstração passo a passo do processo de instalação:
|
||||
|
||||
@@ -16,6 +16,14 @@ Ele capacita desenvolvedores a construir sistemas multi-agente prontos para prod
|
||||
|
||||
Com mais de 100.000 desenvolvedores certificados em nossos cursos comunitários, o CrewAI é o padrão para automação de IA pronta para empresas.
|
||||
|
||||
### Assista: Construindo Agents e Flows CrewAI com Coding Agent Skills
|
||||
|
||||
Instale nossas coding agent skills (Claude Code, Codex, ...) para colocar seus agentes de código para funcionar rapidamente com o CrewAI.
|
||||
|
||||
Você pode instalar com `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## A Arquitetura do CrewAI
|
||||
|
||||
A arquitetura do CrewAI foi projetada para equilibrar autonomia com controle.
|
||||
|
||||
@@ -5,6 +5,14 @@ icon: rocket
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### Assista: Construindo Agents e Flows CrewAI com Coding Agent Skills
|
||||
|
||||
Instale nossas coding agent skills (Claude Code, Codex, ...) para colocar seus agentes de código para funcionar rapidamente com o CrewAI.
|
||||
|
||||
Você pode instalar com `npx skills add crewaiinc/skills`
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
## Construa seu primeiro Agente CrewAI
|
||||
|
||||
Vamos criar uma tripulação simples que nos ajudará a `pesquisar` e `relatar` sobre os `últimos avanços em IA` para um determinado tópico ou assunto.
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0a7"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.13.0a5",
|
||||
"crewai==1.13.0a7",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0a7"
|
||||
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.13.0a5",
|
||||
"crewai-tools==1.13.0a7",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import PydanticUserError
|
||||
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
from crewai.context import ExecutionContext
|
||||
from crewai.crew import Crew
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.flow.flow import Flow
|
||||
@@ -15,6 +16,7 @@ from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.process import Process
|
||||
from crewai.runtime_state import _entity_discriminator
|
||||
from crewai.task import Task
|
||||
from crewai.tasks.llm_guardrail import LLMGuardrail
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
@@ -44,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0a7"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
@@ -96,6 +98,10 @@ def __getattr__(name: str) -> Any:
|
||||
|
||||
|
||||
try:
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent as _BaseAgent
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import (
|
||||
CrewAgentExecutorMixin as _CrewAgentExecutorMixin,
|
||||
)
|
||||
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
|
||||
@@ -105,27 +111,93 @@ try:
|
||||
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,
|
||||
},
|
||||
)
|
||||
_base_namespace: dict[str, type] = {
|
||||
"Agent": Agent,
|
||||
"BaseAgent": _BaseAgent,
|
||||
"Crew": Crew,
|
||||
"Flow": Flow,
|
||||
"BaseLLM": BaseLLM,
|
||||
"Task": Task,
|
||||
"CrewAgentExecutorMixin": _CrewAgentExecutorMixin,
|
||||
"ExecutionContext": ExecutionContext,
|
||||
}
|
||||
|
||||
try:
|
||||
from crewai.a2a.config import (
|
||||
A2AClientConfig as _A2AClientConfig,
|
||||
A2AConfig as _A2AConfig,
|
||||
A2AServerConfig as _A2AServerConfig,
|
||||
)
|
||||
|
||||
_base_namespace.update(
|
||||
{
|
||||
"A2AConfig": _A2AConfig,
|
||||
"A2AClientConfig": _A2AClientConfig,
|
||||
"A2AServerConfig": _A2AServerConfig,
|
||||
}
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import sys
|
||||
|
||||
_full_namespace = {
|
||||
**_base_namespace,
|
||||
"ToolsHandler": _ToolsHandler,
|
||||
"StandardPromptResult": _StandardPromptResult,
|
||||
"SystemPromptResult": _SystemPromptResult,
|
||||
"LLMCallHookContext": _LLMCallHookContext,
|
||||
"ToolResult": _ToolResult,
|
||||
}
|
||||
|
||||
_resolve_namespace = {
|
||||
**_full_namespace,
|
||||
**sys.modules[_BaseAgent.__module__].__dict__,
|
||||
}
|
||||
|
||||
for _mod_name in (
|
||||
_BaseAgent.__module__,
|
||||
Agent.__module__,
|
||||
Crew.__module__,
|
||||
Flow.__module__,
|
||||
Task.__module__,
|
||||
_AgentExecutor.__module__,
|
||||
):
|
||||
sys.modules[_mod_name].__dict__.update(_resolve_namespace)
|
||||
|
||||
from crewai.tasks.conditional_task import ConditionalTask as _ConditionalTask
|
||||
|
||||
_BaseAgent.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
Task.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
_ConditionalTask.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
Crew.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
Flow.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
_AgentExecutor.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import Discriminator, RootModel, Tag
|
||||
|
||||
Entity = Annotated[
|
||||
Annotated[Flow, Tag("flow")] # type: ignore[type-arg]
|
||||
| Annotated[Crew, Tag("crew")]
|
||||
| Annotated[Agent, Tag("agent")],
|
||||
Discriminator(_entity_discriminator),
|
||||
]
|
||||
RuntimeState = RootModel[list[Entity]]
|
||||
|
||||
try:
|
||||
Agent.model_rebuild(force=True, _types_namespace=_full_namespace)
|
||||
except PydanticUserError:
|
||||
pass
|
||||
except (ImportError, PydanticUserError):
|
||||
import logging as _logging
|
||||
|
||||
_logging.getLogger(__name__).warning(
|
||||
"AgentExecutor.model_rebuild() failed; forward refs may be unresolved.",
|
||||
"model_rebuild() failed; forward refs may be unresolved.",
|
||||
exc_info=True,
|
||||
)
|
||||
RuntimeState = None # type: ignore[assignment,misc]
|
||||
|
||||
__all__ = [
|
||||
"LLM",
|
||||
@@ -133,12 +205,14 @@ __all__ = [
|
||||
"BaseLLM",
|
||||
"Crew",
|
||||
"CrewOutput",
|
||||
"ExecutionContext",
|
||||
"Flow",
|
||||
"Knowledge",
|
||||
"LLMGuardrail",
|
||||
"Memory",
|
||||
"PlanningConfig",
|
||||
"Process",
|
||||
"RuntimeState",
|
||||
"Task",
|
||||
"TaskOutput",
|
||||
"__version__",
|
||||
|
||||
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,
|
||||
|
||||
@@ -14,6 +14,7 @@ import subprocess
|
||||
import time
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Literal,
|
||||
NoReturn,
|
||||
@@ -23,11 +24,14 @@ import warnings
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
ConfigDict,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic.functional_serializers import PlainSerializer
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
@@ -45,7 +49,11 @@ from crewai.agent.utils import (
|
||||
save_last_messages,
|
||||
validate_max_execution_time,
|
||||
)
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.base_agent import (
|
||||
BaseAgent,
|
||||
_serialize_llm_ref,
|
||||
_validate_llm_ref,
|
||||
)
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
@@ -121,6 +129,24 @@ if TYPE_CHECKING:
|
||||
|
||||
_passthrough_exceptions: tuple[type[Exception], ...] = ()
|
||||
|
||||
_EXECUTOR_CLASS_MAP: dict[str, type] = {
|
||||
"CrewAgentExecutor": CrewAgentExecutor,
|
||||
"AgentExecutor": AgentExecutor,
|
||||
}
|
||||
|
||||
|
||||
def _validate_executor_class(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
cls = _EXECUTOR_CLASS_MAP.get(value)
|
||||
if cls is None:
|
||||
raise ValueError(f"Unknown executor class: {value}")
|
||||
return cls
|
||||
return value
|
||||
|
||||
|
||||
def _serialize_executor_class(value: Any) -> str:
|
||||
return value.__name__ if isinstance(value, type) else str(value)
|
||||
|
||||
|
||||
class Agent(BaseAgent):
|
||||
"""Represents an agent in a system.
|
||||
@@ -166,12 +192,16 @@ class Agent(BaseAgent):
|
||||
default=True,
|
||||
description="Use system prompt for the agent.",
|
||||
)
|
||||
llm: str | BaseLLM | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
function_calling_llm: str | BaseLLM | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
llm: Annotated[
|
||||
str | BaseLLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(description="Language model that will run the agent.", default=None)
|
||||
function_calling_llm: Annotated[
|
||||
str | BaseLLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(description="Language model that will run the agent.", default=None)
|
||||
system_template: str | None = Field(
|
||||
default=None, description="System format for the agent."
|
||||
)
|
||||
@@ -267,7 +297,14 @@ class Agent(BaseAgent):
|
||||
Can be a single A2AConfig/A2AClientConfig/A2AServerConfig, or a list of any number of A2AConfig/A2AClientConfig with a single A2AServerConfig.
|
||||
""",
|
||||
)
|
||||
executor_class: type[CrewAgentExecutor] | type[AgentExecutor] = Field(
|
||||
agent_executor: InstanceOf[CrewAgentExecutor] | InstanceOf[AgentExecutor] | None = (
|
||||
Field(default=None, description="An instance of the CrewAgentExecutor class.")
|
||||
)
|
||||
executor_class: Annotated[
|
||||
type[CrewAgentExecutor] | type[AgentExecutor],
|
||||
BeforeValidator(_validate_executor_class),
|
||||
PlainSerializer(_serialize_executor_class, return_type=str, when_used="json"),
|
||||
] = Field(
|
||||
default=CrewAgentExecutor,
|
||||
description="Class to use for the agent executor. Defaults to CrewAgentExecutor, can optionally use AgentExecutor.",
|
||||
)
|
||||
@@ -690,7 +727,9 @@ class Agent(BaseAgent):
|
||||
task_prompt,
|
||||
knowledge_config,
|
||||
self.knowledge.query if self.knowledge else lambda *a, **k: None,
|
||||
self.crew.query_knowledge if self.crew else lambda *a, **k: None,
|
||||
self.crew.query_knowledge
|
||||
if self.crew and not isinstance(self.crew, str)
|
||||
else lambda *a, **k: None,
|
||||
)
|
||||
|
||||
task_prompt = self._finalize_task_prompt(task_prompt, tools, task)
|
||||
@@ -777,14 +816,18 @@ class Agent(BaseAgent):
|
||||
if not self.agent_executor:
|
||||
raise RuntimeError("Agent executor is not initialized.")
|
||||
|
||||
return self.agent_executor.invoke(
|
||||
{
|
||||
"input": task_prompt,
|
||||
"tool_names": self.agent_executor.tools_names,
|
||||
"tools": self.agent_executor.tools_description,
|
||||
"ask_for_human_input": task.human_input,
|
||||
}
|
||||
)["output"]
|
||||
result = cast(
|
||||
dict[str, Any],
|
||||
self.agent_executor.invoke(
|
||||
{
|
||||
"input": task_prompt,
|
||||
"tool_names": self.agent_executor.tools_names,
|
||||
"tools": self.agent_executor.tools_description,
|
||||
"ask_for_human_input": task.human_input,
|
||||
}
|
||||
),
|
||||
)
|
||||
return result["output"]
|
||||
|
||||
async def aexecute_task(
|
||||
self,
|
||||
@@ -955,19 +998,23 @@ class Agent(BaseAgent):
|
||||
if self.agent_executor is not None:
|
||||
self._update_executor_parameters(
|
||||
task=task,
|
||||
tools=parsed_tools, # type: ignore[arg-type]
|
||||
tools=parsed_tools,
|
||||
raw_tools=raw_tools,
|
||||
prompt=prompt,
|
||||
stop_words=stop_words,
|
||||
rpm_limit_fn=rpm_limit_fn,
|
||||
)
|
||||
else:
|
||||
if not isinstance(self.llm, BaseLLM):
|
||||
raise RuntimeError(
|
||||
"LLM must be resolved before creating agent executor."
|
||||
)
|
||||
self.agent_executor = self.executor_class(
|
||||
llm=cast(BaseLLM, self.llm),
|
||||
llm=self.llm,
|
||||
task=task, # type: ignore[arg-type]
|
||||
i18n=self.i18n,
|
||||
agent=self,
|
||||
crew=self.crew,
|
||||
crew=self.crew, # type: ignore[arg-type]
|
||||
tools=parsed_tools,
|
||||
prompt=prompt,
|
||||
original_tools=raw_tools,
|
||||
@@ -991,7 +1038,7 @@ class Agent(BaseAgent):
|
||||
def _update_executor_parameters(
|
||||
self,
|
||||
task: Task | None,
|
||||
tools: list[BaseTool],
|
||||
tools: list[CrewStructuredTool],
|
||||
raw_tools: list[BaseTool],
|
||||
prompt: SystemPromptResult | StandardPromptResult,
|
||||
stop_words: list[str],
|
||||
@@ -1007,11 +1054,17 @@ class Agent(BaseAgent):
|
||||
stop_words: Stop words list.
|
||||
rpm_limit_fn: RPM limit callback function.
|
||||
"""
|
||||
if self.agent_executor is None:
|
||||
raise RuntimeError("Agent executor is not initialized.")
|
||||
|
||||
self.agent_executor.task = task
|
||||
self.agent_executor.tools = tools
|
||||
self.agent_executor.original_tools = raw_tools
|
||||
self.agent_executor.prompt = prompt
|
||||
self.agent_executor.stop_words = stop_words
|
||||
if isinstance(self.agent_executor, AgentExecutor):
|
||||
self.agent_executor.stop_words = stop_words
|
||||
else:
|
||||
self.agent_executor.stop = stop_words
|
||||
self.agent_executor.tools_names = get_tool_names(tools)
|
||||
self.agent_executor.tools_description = render_text_description_and_args(tools)
|
||||
self.agent_executor.response_model = (
|
||||
@@ -1033,7 +1086,7 @@ class Agent(BaseAgent):
|
||||
)
|
||||
)
|
||||
|
||||
def get_delegation_tools(self, agents: list[BaseAgent]) -> list[BaseTool]:
|
||||
def get_delegation_tools(self, agents: Sequence[BaseAgent]) -> list[BaseTool]:
|
||||
agent_tools = AgentTools(agents=agents)
|
||||
return agent_tools.tools()
|
||||
|
||||
@@ -1787,21 +1840,3 @@ class Agent(BaseAgent):
|
||||
LiteAgentOutput: The result of the agent execution.
|
||||
"""
|
||||
return await self.kickoff_async(messages, response_format, input_files)
|
||||
|
||||
|
||||
try:
|
||||
from crewai.a2a.config import (
|
||||
A2AClientConfig as _A2AClientConfig,
|
||||
A2AConfig as _A2AConfig,
|
||||
A2AServerConfig as _A2AServerConfig,
|
||||
)
|
||||
|
||||
Agent.model_rebuild(
|
||||
_types_namespace={
|
||||
"A2AConfig": _A2AConfig,
|
||||
"A2AClientConfig": _A2AClientConfig,
|
||||
"A2AServerConfig": _A2AServerConfig,
|
||||
}
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -137,7 +137,8 @@ def handle_knowledge_retrieval(
|
||||
Returns:
|
||||
The task prompt potentially augmented with knowledge context.
|
||||
"""
|
||||
if not (agent.knowledge or (agent.crew and agent.crew.knowledge)):
|
||||
_crew = agent.crew if not isinstance(agent.crew, str) else None
|
||||
if not (agent.knowledge or (_crew and _crew.knowledge)):
|
||||
return task_prompt
|
||||
|
||||
crewai_event_bus.emit(
|
||||
@@ -244,7 +245,7 @@ def apply_training_data(agent: Agent, task_prompt: str) -> str:
|
||||
Returns:
|
||||
The task prompt with training data applied.
|
||||
"""
|
||||
if agent.crew and agent.crew._train:
|
||||
if agent.crew and not isinstance(agent.crew, str) and agent.crew._train:
|
||||
return agent._training_handler(task_prompt=task_prompt)
|
||||
return agent._use_trained_data(task_prompt=task_prompt)
|
||||
|
||||
@@ -355,7 +356,8 @@ async def ahandle_knowledge_retrieval(
|
||||
Returns:
|
||||
The task prompt potentially augmented with knowledge context.
|
||||
"""
|
||||
if not (agent.knowledge or (agent.crew and agent.crew.knowledge)):
|
||||
_crew = agent.crew if not isinstance(agent.crew, str) else None
|
||||
if not (agent.knowledge or (_crew and _crew.knowledge)):
|
||||
return task_prompt
|
||||
|
||||
crewai_event_bus.emit(
|
||||
@@ -381,15 +383,16 @@ async def ahandle_knowledge_retrieval(
|
||||
if agent.agent_knowledge_context:
|
||||
task_prompt += agent.agent_knowledge_context
|
||||
|
||||
knowledge_snippets = await agent.crew.aquery_knowledge(
|
||||
[agent.knowledge_search_query], **knowledge_config
|
||||
)
|
||||
if knowledge_snippets:
|
||||
agent.crew_knowledge_context = extract_knowledge_context(
|
||||
knowledge_snippets
|
||||
if _crew:
|
||||
knowledge_snippets = await _crew.aquery_knowledge(
|
||||
[agent.knowledge_search_query], **knowledge_config
|
||||
)
|
||||
if agent.crew_knowledge_context:
|
||||
task_prompt += agent.crew_knowledge_context
|
||||
if knowledge_snippets:
|
||||
agent.crew_knowledge_context = extract_knowledge_context(
|
||||
knowledge_snippets
|
||||
)
|
||||
if agent.crew_knowledge_context:
|
||||
task_prompt += agent.crew_knowledge_context
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent,
|
||||
|
||||
@@ -5,7 +5,7 @@ with CrewAI's agent system. Provides memory persistence, tool integration, and s
|
||||
output functionality.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import ConfigDict, Field, PrivateAttr
|
||||
@@ -30,6 +30,7 @@ from crewai.events.types.agent_events import (
|
||||
)
|
||||
from crewai.tools.agent_tools.agent_tools import AgentTools
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.types.callback import SerializableCallable
|
||||
from crewai.utilities import Logger
|
||||
from crewai.utilities.converter import Converter
|
||||
from crewai.utilities.import_utils import require
|
||||
@@ -50,7 +51,7 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
|
||||
_memory: Any = PrivateAttr(default=None)
|
||||
_max_iterations: int = PrivateAttr(default=10)
|
||||
function_calling_llm: Any = Field(default=None)
|
||||
step_callback: Callable[..., Any] | None = Field(default=None)
|
||||
step_callback: SerializableCallable | None = Field(default=None)
|
||||
|
||||
model: str = Field(default="gpt-4o")
|
||||
verbose: bool = Field(default=False)
|
||||
@@ -272,7 +273,7 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
|
||||
available_tools: list[Any] = self._tool_adapter.tools()
|
||||
self._graph.tools = available_tools
|
||||
|
||||
def get_delegation_tools(self, agents: list[BaseAgent]) -> list[BaseTool]:
|
||||
def get_delegation_tools(self, agents: Sequence[BaseAgent]) -> list[BaseTool]:
|
||||
"""Implement delegation tools support for LangGraph.
|
||||
|
||||
Creates delegation tools that allow this agent to delegate tasks to other agents.
|
||||
|
||||
@@ -4,6 +4,7 @@ This module contains the OpenAIAgentAdapter class that integrates OpenAI Assista
|
||||
with CrewAI's agent system, providing tool integration and structured output support.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import ConfigDict, Field, PrivateAttr
|
||||
@@ -188,14 +189,14 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
|
||||
self._openai_agent = OpenAIAgent(
|
||||
name=self.role,
|
||||
instructions=instructions,
|
||||
model=self.llm,
|
||||
model=str(self.llm),
|
||||
**self._agent_config or {},
|
||||
)
|
||||
|
||||
if all_tools:
|
||||
self.configure_tools(all_tools)
|
||||
|
||||
self.agent_executor = Runner
|
||||
self.agent_executor = Runner # type: ignore[assignment]
|
||||
|
||||
def configure_tools(self, tools: list[BaseTool] | None = None) -> None:
|
||||
"""Configure tools for the OpenAI Assistant.
|
||||
@@ -221,7 +222,7 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
|
||||
"""
|
||||
return self._converter_adapter.post_process_result(result.final_output)
|
||||
|
||||
def get_delegation_tools(self, agents: list[BaseAgent]) -> list[BaseTool]:
|
||||
def get_delegation_tools(self, agents: Sequence[BaseAgent]) -> list[BaseTool]:
|
||||
"""Implement delegation tools support.
|
||||
|
||||
Creates delegation tools that allow this agent to delegate tasks to other agents.
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from copy import copy as shallow_copy
|
||||
from hashlib import md5
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Final, Literal
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal
|
||||
import uuid
|
||||
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
Field,
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic.functional_serializers import PlainSerializer
|
||||
from pydantic_core import PydanticCustomError
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.agent.internal.meta import AgentMeta
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
@@ -27,6 +32,7 @@ from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.knowledge.knowledge_config import KnowledgeConfig
|
||||
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.mcp.config import MCPServerConfig
|
||||
from crewai.memory.memory_scope import MemoryScope, MemorySlice
|
||||
from crewai.memory.unified_memory import Memory
|
||||
@@ -42,6 +48,41 @@ from crewai.utilities.rpm_controller import RPMController
|
||||
from crewai.utilities.string_utils import interpolate_only
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.context import ExecutionContext
|
||||
from crewai.crew import Crew
|
||||
|
||||
|
||||
def _validate_crew_ref(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _serialize_crew_ref(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return str(value.id) if hasattr(value, "id") else str(value)
|
||||
|
||||
|
||||
def _validate_llm_ref(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_agent(value: Any, info: Any) -> Any:
|
||||
if isinstance(value, BaseAgent) or value is None or not isinstance(value, dict):
|
||||
return value
|
||||
from crewai.agent.core import Agent
|
||||
|
||||
return Agent.model_validate(value, context=getattr(info, "context", None))
|
||||
|
||||
|
||||
def _serialize_llm_ref(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return getattr(value, "model", str(value))
|
||||
|
||||
|
||||
_SLUG_RE: Final[re.Pattern[str]] = re.compile(
|
||||
r"^(?:crewai-amp:)?[a-zA-Z0-9][a-zA-Z0-9_-]*(?:#[\w-]+)?$"
|
||||
)
|
||||
@@ -119,10 +160,12 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
Set private attributes.
|
||||
"""
|
||||
|
||||
entity_type: Literal["agent"] = "agent"
|
||||
|
||||
__hash__ = object.__hash__
|
||||
_logger: Logger = PrivateAttr(default_factory=lambda: Logger(verbose=False))
|
||||
_rpm_controller: RPMController | None = PrivateAttr(default=None)
|
||||
_request_within_rpm_limit: Any = PrivateAttr(default=None)
|
||||
_request_within_rpm_limit: SerializableCallable | None = PrivateAttr(default=None)
|
||||
_original_role: str | None = PrivateAttr(default=None)
|
||||
_original_goal: str | None = PrivateAttr(default=None)
|
||||
_original_backstory: str | None = PrivateAttr(default=None)
|
||||
@@ -154,13 +197,21 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
max_iter: int = Field(
|
||||
default=25, description="Maximum iterations for an agent to execute a task"
|
||||
)
|
||||
agent_executor: Any = Field(
|
||||
agent_executor: InstanceOf[CrewAgentExecutorMixin] | None = Field(
|
||||
default=None, description="An instance of the CrewAgentExecutor class."
|
||||
)
|
||||
llm: Any = Field(
|
||||
default=None, description="Language model that will run the agent."
|
||||
)
|
||||
crew: Any = Field(default=None, description="Crew to which the agent belongs.")
|
||||
llm: Annotated[
|
||||
str | BaseLLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(default=None, description="Language model that will run the agent.")
|
||||
crew: Annotated[
|
||||
Crew | str | None,
|
||||
BeforeValidator(_validate_crew_ref),
|
||||
PlainSerializer(
|
||||
_serialize_crew_ref, return_type=str | None, when_used="always"
|
||||
),
|
||||
] = Field(default=None, description="Crew to which the agent belongs.")
|
||||
i18n: I18N = Field(
|
||||
default_factory=get_i18n, description="Internationalization settings."
|
||||
)
|
||||
@@ -172,7 +223,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
description="An instance of the ToolsHandler class.",
|
||||
)
|
||||
tools_results: list[dict[str, Any]] = Field(
|
||||
default=[], description="Results of the tools used by the agent."
|
||||
default_factory=list, description="Results of the tools used by the agent."
|
||||
)
|
||||
max_tokens: int | None = Field(
|
||||
default=None, description="Maximum number of tokens for the agent's execution."
|
||||
@@ -223,6 +274,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
description="Agent Skills. Accepts paths for discovery or pre-loaded Skill objects.",
|
||||
min_length=1,
|
||||
)
|
||||
execution_context: ExecutionContext | None = Field(default=None)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -337,11 +389,12 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
def _deny_user_set_id(cls, v: UUID4 | None) -> None:
|
||||
if v:
|
||||
def _deny_user_set_id(cls, v: UUID4 | None, info: Any) -> UUID4 | None:
|
||||
if v and not (info.context or {}).get("from_checkpoint"):
|
||||
raise PydanticCustomError(
|
||||
"may_not_set_field", "This field is not to be set by the user.", {}
|
||||
)
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_private_attrs(self) -> Self:
|
||||
@@ -398,7 +451,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_delegation_tools(self, agents: list[BaseAgent]) -> list[BaseTool]:
|
||||
def get_delegation_tools(self, agents: Sequence[BaseAgent]) -> list[BaseTool]:
|
||||
"""Set the task tools that init BaseAgenTools class."""
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -3,20 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.tools.cache_tools.cache_tools import CacheTools
|
||||
from crewai.tools.tool_calling import InstructorToolCalling, ToolCalling
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.tools.tool_calling import InstructorToolCalling, ToolCalling
|
||||
|
||||
|
||||
class ToolsHandler:
|
||||
class ToolsHandler(BaseModel):
|
||||
"""Callback handler for tool usage.
|
||||
|
||||
Attributes:
|
||||
@@ -24,14 +19,8 @@ class ToolsHandler:
|
||||
cache: Optional cache handler for storing tool outputs.
|
||||
"""
|
||||
|
||||
def __init__(self, cache: CacheHandler | None = None) -> None:
|
||||
"""Initialize the callback handler.
|
||||
|
||||
Args:
|
||||
cache: Optional cache handler for storing tool outputs.
|
||||
"""
|
||||
self.cache: CacheHandler | None = cache
|
||||
self.last_used_tool: ToolCalling | InstructorToolCalling | None = None
|
||||
cache: CacheHandler | None = Field(default=None)
|
||||
last_used_tool: ToolCalling | InstructorToolCalling | None = Field(default=None)
|
||||
|
||||
def on_tool_use(
|
||||
self,
|
||||
@@ -48,7 +37,6 @@ class ToolsHandler:
|
||||
"""
|
||||
self.last_used_tool = calling
|
||||
if self.cache and should_cache and calling.tool_name != CacheTools().name:
|
||||
# Convert arguments to string for cache
|
||||
input_str = ""
|
||||
if calling.arguments:
|
||||
if isinstance(calling.arguments, dict):
|
||||
@@ -61,14 +49,3 @@ class ToolsHandler:
|
||||
input=input_str,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, _source_type: Any, _handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
"""Generate Pydantic core schema for BaseClient Protocol.
|
||||
|
||||
This allows the Protocol to be used in Pydantic models without
|
||||
requiring arbitrary_types_allowed=True.
|
||||
"""
|
||||
return core_schema.any_schema()
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.13.0a5"
|
||||
"crewai[tools]==1.13.0a7"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.13.0a5"
|
||||
"crewai[tools]==1.13.0a7"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.13.0a5"
|
||||
"crewai[tools]==1.13.0a7"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -4,6 +4,23 @@ import contextvars
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.events.base_events import (
|
||||
get_emission_sequence,
|
||||
set_emission_counter,
|
||||
)
|
||||
from crewai.events.event_context import (
|
||||
_event_id_stack,
|
||||
_last_event_id,
|
||||
_triggering_event_id,
|
||||
)
|
||||
from crewai.flow.flow_context import (
|
||||
current_flow_id,
|
||||
current_flow_method_name,
|
||||
current_flow_request_id,
|
||||
)
|
||||
|
||||
|
||||
_platform_integration_token: contextvars.ContextVar[str | None] = (
|
||||
contextvars.ContextVar("platform_integration_token", default=None)
|
||||
@@ -63,3 +80,53 @@ def reset_current_task_id(token: contextvars.Token[str | None]) -> None:
|
||||
def get_current_task_id() -> str | None:
|
||||
"""Get the current task ID from the context."""
|
||||
return _current_task_id.get()
|
||||
|
||||
|
||||
class ExecutionContext(BaseModel):
|
||||
"""Snapshot of ContextVar execution state."""
|
||||
|
||||
current_task_id: str | None = Field(default=None)
|
||||
flow_request_id: str | None = Field(default=None)
|
||||
flow_id: str | None = Field(default=None)
|
||||
flow_method_name: str = Field(default="unknown")
|
||||
|
||||
event_id_stack: tuple[tuple[str, str], ...] = Field(default=())
|
||||
last_event_id: str | None = Field(default=None)
|
||||
triggering_event_id: str | None = Field(default=None)
|
||||
emission_sequence: int = Field(default=0)
|
||||
|
||||
feedback_callback_info: dict[str, Any] | None = Field(default=None)
|
||||
platform_token: str | None = Field(default=None)
|
||||
|
||||
|
||||
def capture_execution_context(
|
||||
feedback_callback_info: dict[str, Any] | None = None,
|
||||
) -> ExecutionContext:
|
||||
"""Read current ContextVars into an ExecutionContext."""
|
||||
return ExecutionContext(
|
||||
current_task_id=_current_task_id.get(),
|
||||
flow_request_id=current_flow_request_id.get(),
|
||||
flow_id=current_flow_id.get(),
|
||||
flow_method_name=current_flow_method_name.get(),
|
||||
event_id_stack=_event_id_stack.get(),
|
||||
last_event_id=_last_event_id.get(),
|
||||
triggering_event_id=_triggering_event_id.get(),
|
||||
emission_sequence=get_emission_sequence(),
|
||||
feedback_callback_info=feedback_callback_info,
|
||||
platform_token=_platform_integration_token.get(),
|
||||
)
|
||||
|
||||
|
||||
def apply_execution_context(ctx: ExecutionContext) -> None:
|
||||
"""Write an ExecutionContext back into the ContextVars."""
|
||||
_current_task_id.set(ctx.current_task_id)
|
||||
current_flow_request_id.set(ctx.flow_request_id)
|
||||
current_flow_id.set(ctx.flow_id)
|
||||
current_flow_method_name.set(ctx.flow_method_name)
|
||||
|
||||
_event_id_stack.set(ctx.event_id_stack)
|
||||
_last_event_id.set(ctx.last_event_id)
|
||||
_triggering_event_id.set(ctx.triggering_event_id)
|
||||
set_emission_counter(ctx.emission_sequence)
|
||||
|
||||
_platform_integration_token.set(ctx.platform_token)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from concurrent.futures import Future
|
||||
from copy import copy as shallow_copy
|
||||
from hashlib import md5
|
||||
@@ -10,7 +10,9 @@ from pathlib import Path
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
Literal,
|
||||
cast,
|
||||
)
|
||||
import uuid
|
||||
@@ -21,12 +23,14 @@ from opentelemetry.context import attach, detach
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
Field,
|
||||
Json,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic.functional_serializers import PlainSerializer
|
||||
from pydantic_core import PydanticCustomError
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -37,6 +41,8 @@ if TYPE_CHECKING:
|
||||
from crewai_files import FileInput
|
||||
from opentelemetry.trace import Span
|
||||
|
||||
from crewai.context import ExecutionContext
|
||||
|
||||
try:
|
||||
from crewai_files import get_supported_content_types
|
||||
|
||||
@@ -49,7 +55,12 @@ except ImportError:
|
||||
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.base_agent import (
|
||||
BaseAgent,
|
||||
_resolve_agent,
|
||||
_serialize_llm_ref,
|
||||
_validate_llm_ref,
|
||||
)
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.crews.utils import (
|
||||
@@ -132,6 +143,12 @@ from crewai.utilities.training_handler import CrewTrainingHandler
|
||||
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
|
||||
|
||||
|
||||
def _resolve_agents(value: Any, info: Any) -> Any:
|
||||
if not isinstance(value, list):
|
||||
return value
|
||||
return [_resolve_agent(a, info) for a in value]
|
||||
|
||||
|
||||
class Crew(FlowTrackable, BaseModel):
|
||||
"""
|
||||
Represents a group of agents, defining how they should collaborate and the
|
||||
@@ -170,6 +187,8 @@ class Crew(FlowTrackable, BaseModel):
|
||||
fingerprinting.
|
||||
"""
|
||||
|
||||
entity_type: Literal["crew"] = "crew"
|
||||
|
||||
__hash__ = object.__hash__
|
||||
_execution_span: Span | None = PrivateAttr()
|
||||
_rpm_controller: RPMController = PrivateAttr()
|
||||
@@ -191,7 +210,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
name: str | None = Field(default="crew")
|
||||
cache: bool = Field(default=True)
|
||||
tasks: list[Task] = Field(default_factory=list)
|
||||
agents: list[BaseAgent] = Field(default_factory=list)
|
||||
agents: Annotated[
|
||||
list[BaseAgent],
|
||||
BeforeValidator(_resolve_agents),
|
||||
] = Field(default_factory=list)
|
||||
process: Process = Field(default=Process.sequential)
|
||||
verbose: bool = Field(default=False)
|
||||
memory: bool | Memory | MemoryScope | MemorySlice | None = Field(
|
||||
@@ -209,15 +231,20 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=None,
|
||||
description="Metrics for the LLM usage during all tasks execution.",
|
||||
)
|
||||
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 | LLM | None = Field(
|
||||
description="Language model that will run the agent.", default=None
|
||||
)
|
||||
manager_llm: Annotated[
|
||||
str | BaseLLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(description="Language model that will run the agent.", default=None)
|
||||
manager_agent: Annotated[
|
||||
BaseAgent | None,
|
||||
BeforeValidator(_resolve_agent),
|
||||
] = Field(description="Custom agent that will be used as manager.", default=None)
|
||||
function_calling_llm: Annotated[
|
||||
str | LLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(description="Language model that will run the agent.", default=None)
|
||||
config: Json[dict[str, Any]] | dict[str, Any] | None = Field(default=None)
|
||||
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True)
|
||||
share_crew: bool | None = Field(default=False)
|
||||
@@ -266,7 +293,11 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=False,
|
||||
description="Plan the crew execution and add the plan to the crew.",
|
||||
)
|
||||
planning_llm: str | BaseLLM | Any | None = Field(
|
||||
planning_llm: Annotated[
|
||||
str | BaseLLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Language model that will run the AgentPlanner if planning is True."
|
||||
@@ -287,7 +318,11 @@ class Crew(FlowTrackable, BaseModel):
|
||||
"knowledge object."
|
||||
),
|
||||
)
|
||||
chat_llm: str | BaseLLM | Any | None = Field(
|
||||
chat_llm: Annotated[
|
||||
str | BaseLLM | None,
|
||||
BeforeValidator(_validate_llm_ref),
|
||||
PlainSerializer(_serialize_llm_ref, return_type=str | None, when_used="json"),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="LLM used to handle chatting with the crew.",
|
||||
)
|
||||
@@ -313,14 +348,20 @@ class Crew(FlowTrackable, BaseModel):
|
||||
description="Whether to enable tracing for the crew. True=always enable, False=always disable, None=check environment/user settings.",
|
||||
)
|
||||
|
||||
execution_context: ExecutionContext | None = Field(default=None)
|
||||
checkpoint_inputs: dict[str, Any] | None = Field(default=None)
|
||||
checkpoint_train: bool | None = Field(default=None)
|
||||
checkpoint_kickoff_event_id: str | None = Field(default=None)
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
def _deny_user_set_id(cls, v: UUID4 | None) -> None:
|
||||
def _deny_user_set_id(cls, v: UUID4 | None, info: Any) -> UUID4 | None:
|
||||
"""Prevent manual setting of the 'id' field by users."""
|
||||
if v:
|
||||
if v and not (info.context or {}).get("from_checkpoint"):
|
||||
raise PydanticCustomError(
|
||||
"may_not_set_field", "The 'id' field cannot be set by the user.", {}
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("config", mode="before")
|
||||
@classmethod
|
||||
@@ -1311,7 +1352,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
and hasattr(agent, "multimodal")
|
||||
and getattr(agent, "multimodal", False)
|
||||
):
|
||||
if not (agent.llm and agent.llm.supports_multimodal()):
|
||||
if not (isinstance(agent.llm, BaseLLM) and agent.llm.supports_multimodal()):
|
||||
tools = self._add_multimodal_tools(agent, tools)
|
||||
|
||||
if agent and (hasattr(agent, "apps") and getattr(agent, "apps", None)):
|
||||
@@ -1328,7 +1369,11 @@ class Crew(FlowTrackable, BaseModel):
|
||||
files = get_all_files(self.id, task.id)
|
||||
if files:
|
||||
supported_types: list[str] = []
|
||||
if agent and agent.llm and agent.llm.supports_multimodal():
|
||||
if (
|
||||
agent
|
||||
and isinstance(agent.llm, BaseLLM)
|
||||
and agent.llm.supports_multimodal()
|
||||
):
|
||||
provider = (
|
||||
getattr(agent.llm, "provider", None)
|
||||
or getattr(agent.llm, "model", None)
|
||||
@@ -1384,7 +1429,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
self,
|
||||
tools: list[BaseTool],
|
||||
task_agent: BaseAgent,
|
||||
agents: list[BaseAgent],
|
||||
agents: Sequence[BaseAgent],
|
||||
) -> list[BaseTool]:
|
||||
if hasattr(task_agent, "get_delegation_tools"):
|
||||
delegation_tools = task_agent.get_delegation_tools(agents)
|
||||
@@ -1781,17 +1826,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
token_sum = self.manager_agent._token_process.get_summary()
|
||||
total_usage_metrics.add_usage_metrics(token_sum)
|
||||
|
||||
if (
|
||||
self.manager_agent
|
||||
and hasattr(self.manager_agent, "llm")
|
||||
and hasattr(self.manager_agent.llm, "get_token_usage_summary")
|
||||
):
|
||||
if self.manager_agent:
|
||||
if isinstance(self.manager_agent.llm, BaseLLM):
|
||||
llm_usage = self.manager_agent.llm.get_token_usage_summary()
|
||||
else:
|
||||
llm_usage = self.manager_agent.llm._token_process.get_summary()
|
||||
|
||||
total_usage_metrics.add_usage_metrics(llm_usage)
|
||||
total_usage_metrics.add_usage_metrics(llm_usage)
|
||||
|
||||
self.usage_metrics = total_usage_metrics
|
||||
return total_usage_metrics
|
||||
|
||||
@@ -21,7 +21,7 @@ class CrewOutput(BaseModel):
|
||||
description="JSON dict output of Crew", default=None
|
||||
)
|
||||
tasks_output: list[TaskOutput] = Field(
|
||||
description="Output of each task", default=[]
|
||||
description="Output of each task", default_factory=list
|
||||
)
|
||||
token_usage: UsageMetrics = Field(
|
||||
description="Processed token summary", default_factory=UsageMetrics
|
||||
|
||||
@@ -11,6 +11,7 @@ from opentelemetry import baggage
|
||||
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.skills.loader import activate_skill, discover_skills
|
||||
from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel
|
||||
@@ -50,7 +51,7 @@ def enable_agent_streaming(agents: Iterable[BaseAgent]) -> None:
|
||||
agents: Iterable of agents to enable streaming on.
|
||||
"""
|
||||
for agent in agents:
|
||||
if agent.llm is not None:
|
||||
if isinstance(agent.llm, BaseLLM):
|
||||
agent.llm.stream = True
|
||||
|
||||
|
||||
|
||||
@@ -25,13 +25,25 @@ def _get_or_create_counter() -> Iterator[int]:
|
||||
return counter
|
||||
|
||||
|
||||
_last_emitted: contextvars.ContextVar[int] = contextvars.ContextVar(
|
||||
"_last_emitted", default=0
|
||||
)
|
||||
|
||||
|
||||
def get_next_emission_sequence() -> int:
|
||||
"""Get the next emission sequence number.
|
||||
|
||||
Returns:
|
||||
The next sequence number.
|
||||
"""
|
||||
return next(_get_or_create_counter())
|
||||
seq = next(_get_or_create_counter())
|
||||
_last_emitted.set(seq)
|
||||
return seq
|
||||
|
||||
|
||||
def get_emission_sequence() -> int:
|
||||
"""Get the current emission sequence value without incrementing."""
|
||||
return _last_emitted.get()
|
||||
|
||||
|
||||
def reset_emission_counter() -> None:
|
||||
@@ -41,6 +53,14 @@ def reset_emission_counter() -> None:
|
||||
"""
|
||||
counter: Iterator[int] = itertools.count(start=1)
|
||||
_emission_counter.set(counter)
|
||||
_last_emitted.set(0)
|
||||
|
||||
|
||||
def set_emission_counter(start: int) -> None:
|
||||
"""Set the emission counter to resume from a given value."""
|
||||
counter: Iterator[int] = itertools.count(start=start + 1)
|
||||
_emission_counter.set(counter)
|
||||
_last_emitted.set(start)
|
||||
|
||||
|
||||
class BaseEvent(BaseModel):
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -78,9 +78,15 @@ from crewai.events.types.mcp_events import (
|
||||
MCPConnectionCompletedEvent,
|
||||
MCPConnectionFailedEvent,
|
||||
MCPConnectionStartedEvent,
|
||||
MCPToolExecutionCompletedEvent,
|
||||
MCPToolExecutionFailedEvent,
|
||||
MCPToolExecutionStartedEvent,
|
||||
)
|
||||
from crewai.events.types.memory_events import (
|
||||
MemoryQueryCompletedEvent,
|
||||
MemoryRetrievalCompletedEvent,
|
||||
MemorySaveCompletedEvent,
|
||||
)
|
||||
from crewai.events.types.observation_events import (
|
||||
GoalAchievedEarlyEvent,
|
||||
PlanRefinementEvent,
|
||||
@@ -94,6 +100,12 @@ from crewai.events.types.reasoning_events import (
|
||||
AgentReasoningFailedEvent,
|
||||
AgentReasoningStartedEvent,
|
||||
)
|
||||
from crewai.events.types.skill_events import (
|
||||
SkillActivatedEvent,
|
||||
SkillDiscoveryCompletedEvent,
|
||||
SkillLoadFailedEvent,
|
||||
SkillLoadedEvent,
|
||||
)
|
||||
from crewai.events.types.task_events import (
|
||||
TaskCompletedEvent,
|
||||
TaskFailedEvent,
|
||||
@@ -478,6 +490,7 @@ class EventListener(BaseEventListener):
|
||||
self.formatter.handle_guardrail_completed(
|
||||
event.success, event.error, event.retry_count
|
||||
)
|
||||
self._telemetry.feature_usage_span("guardrail:execution")
|
||||
|
||||
@crewai_event_bus.on(CrewTestStartedEvent)
|
||||
def on_crew_test_started(source: Any, event: CrewTestStartedEvent) -> None:
|
||||
@@ -559,6 +572,7 @@ class EventListener(BaseEventListener):
|
||||
event.plan,
|
||||
event.ready,
|
||||
)
|
||||
self._telemetry.feature_usage_span("planning:creation")
|
||||
|
||||
@crewai_event_bus.on(AgentReasoningFailedEvent)
|
||||
def on_agent_reasoning_failed(_: Any, event: AgentReasoningFailedEvent) -> None:
|
||||
@@ -616,6 +630,7 @@ class EventListener(BaseEventListener):
|
||||
event.replan_count,
|
||||
event.completed_steps_preserved,
|
||||
)
|
||||
self._telemetry.feature_usage_span("planning:replan")
|
||||
|
||||
@crewai_event_bus.on(GoalAchievedEarlyEvent)
|
||||
def on_goal_achieved_early(_: Any, event: GoalAchievedEarlyEvent) -> None:
|
||||
@@ -623,6 +638,25 @@ class EventListener(BaseEventListener):
|
||||
event.steps_completed,
|
||||
event.steps_remaining,
|
||||
)
|
||||
self._telemetry.feature_usage_span("planning:goal_achieved_early")
|
||||
|
||||
# ----------- SKILL EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(SkillDiscoveryCompletedEvent)
|
||||
def on_skill_discovery(_: Any, event: SkillDiscoveryCompletedEvent) -> None:
|
||||
self._telemetry.feature_usage_span("skill:discovery")
|
||||
|
||||
@crewai_event_bus.on(SkillLoadedEvent)
|
||||
def on_skill_loaded(_: Any, event: SkillLoadedEvent) -> None:
|
||||
self._telemetry.feature_usage_span("skill:loaded")
|
||||
|
||||
@crewai_event_bus.on(SkillLoadFailedEvent)
|
||||
def on_skill_load_failed(_: Any, event: SkillLoadFailedEvent) -> None:
|
||||
self._telemetry.feature_usage_span("skill:load_failed")
|
||||
|
||||
@crewai_event_bus.on(SkillActivatedEvent)
|
||||
def on_skill_activated(_: Any, event: SkillActivatedEvent) -> None:
|
||||
self._telemetry.feature_usage_span("skill:activated")
|
||||
|
||||
# ----------- AGENT LOGGING EVENTS -----------
|
||||
|
||||
@@ -662,6 +696,7 @@ class EventListener(BaseEventListener):
|
||||
event.error,
|
||||
event.is_multiturn,
|
||||
)
|
||||
self._telemetry.feature_usage_span("a2a:delegation")
|
||||
|
||||
@crewai_event_bus.on(A2AConversationStartedEvent)
|
||||
def on_a2a_conversation_started(
|
||||
@@ -703,6 +738,7 @@ class EventListener(BaseEventListener):
|
||||
event.error,
|
||||
event.total_turns,
|
||||
)
|
||||
self._telemetry.feature_usage_span("a2a:conversation")
|
||||
|
||||
@crewai_event_bus.on(A2APollingStartedEvent)
|
||||
def on_a2a_polling_started(_: Any, event: A2APollingStartedEvent) -> None:
|
||||
@@ -744,6 +780,7 @@ class EventListener(BaseEventListener):
|
||||
event.connection_duration_ms,
|
||||
event.is_reconnect,
|
||||
)
|
||||
self._telemetry.feature_usage_span("mcp:connection")
|
||||
|
||||
@crewai_event_bus.on(MCPConnectionFailedEvent)
|
||||
def on_mcp_connection_failed(_: Any, event: MCPConnectionFailedEvent) -> None:
|
||||
@@ -754,6 +791,7 @@ class EventListener(BaseEventListener):
|
||||
event.error,
|
||||
event.error_type,
|
||||
)
|
||||
self._telemetry.feature_usage_span("mcp:connection_failed")
|
||||
|
||||
@crewai_event_bus.on(MCPConfigFetchFailedEvent)
|
||||
def on_mcp_config_fetch_failed(
|
||||
@@ -764,6 +802,7 @@ class EventListener(BaseEventListener):
|
||||
event.error,
|
||||
event.error_type,
|
||||
)
|
||||
self._telemetry.feature_usage_span("mcp:config_fetch_failed")
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionStartedEvent)
|
||||
def on_mcp_tool_execution_started(
|
||||
@@ -775,6 +814,12 @@ class EventListener(BaseEventListener):
|
||||
event.tool_args,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionCompletedEvent)
|
||||
def on_mcp_tool_execution_completed(
|
||||
_: Any, event: MCPToolExecutionCompletedEvent
|
||||
) -> None:
|
||||
self._telemetry.feature_usage_span("mcp:tool_execution")
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionFailedEvent)
|
||||
def on_mcp_tool_execution_failed(
|
||||
_: Any, event: MCPToolExecutionFailedEvent
|
||||
@@ -786,6 +831,45 @@ class EventListener(BaseEventListener):
|
||||
event.error,
|
||||
event.error_type,
|
||||
)
|
||||
self._telemetry.feature_usage_span("mcp:tool_execution_failed")
|
||||
|
||||
# ----------- MEMORY TELEMETRY -----------
|
||||
|
||||
@crewai_event_bus.on(MemorySaveCompletedEvent)
|
||||
def on_memory_save_completed(_: Any, event: MemorySaveCompletedEvent) -> None:
|
||||
self._telemetry.feature_usage_span("memory:save")
|
||||
|
||||
@crewai_event_bus.on(MemoryQueryCompletedEvent)
|
||||
def on_memory_query_completed(_: Any, event: MemoryQueryCompletedEvent) -> None:
|
||||
self._telemetry.feature_usage_span("memory:query")
|
||||
|
||||
@crewai_event_bus.on(MemoryRetrievalCompletedEvent)
|
||||
def on_memory_retrieval_completed_telemetry(
|
||||
_: Any, event: MemoryRetrievalCompletedEvent
|
||||
) -> None:
|
||||
self._telemetry.feature_usage_span("memory:retrieval")
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_kickoff_hooks(_: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
from crewai.hooks.llm_hooks import (
|
||||
get_after_llm_call_hooks,
|
||||
get_before_llm_call_hooks,
|
||||
)
|
||||
from crewai.hooks.tool_hooks import (
|
||||
get_after_tool_call_hooks,
|
||||
get_before_tool_call_hooks,
|
||||
)
|
||||
|
||||
has_hooks = any(
|
||||
[
|
||||
get_before_llm_call_hooks(),
|
||||
get_after_llm_call_hooks(),
|
||||
get_before_tool_call_hooks(),
|
||||
get_after_tool_call_hooks(),
|
||||
]
|
||||
)
|
||||
if has_hooks:
|
||||
self._telemetry.feature_usage_span("hooks:registered")
|
||||
|
||||
|
||||
event_listener = EventListener()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import logging
|
||||
import threading
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
Any,
|
||||
ClassVar,
|
||||
Generic,
|
||||
@@ -41,9 +42,11 @@ from opentelemetry import baggage
|
||||
from opentelemetry.context import attach, detach
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
ConfigDict,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
SerializeAsAny,
|
||||
ValidationError,
|
||||
)
|
||||
from pydantic._internal._model_construction import ModelMetaclass
|
||||
@@ -115,6 +118,7 @@ from crewai.memory.unified_memory import Memory
|
||||
if TYPE_CHECKING:
|
||||
from crewai_files import FileInput
|
||||
|
||||
from crewai.context import ExecutionContext
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
|
||||
@@ -134,6 +138,19 @@ from crewai.utilities.streaming import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_persistence(value: Any) -> Any:
|
||||
if value is None or isinstance(value, FlowPersistence):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
from crewai.flow.persistence.base import _persistence_registry
|
||||
|
||||
type_name = value.get("persistence_type", "SQLiteFlowPersistence")
|
||||
cls = _persistence_registry.get(type_name)
|
||||
if cls is not None:
|
||||
return cls.model_validate(value)
|
||||
return value
|
||||
|
||||
|
||||
class FlowState(BaseModel):
|
||||
"""Base model for all flow states, ensuring each state has a unique ID."""
|
||||
|
||||
@@ -883,6 +900,8 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
_routers: ClassVar[set[FlowMethodName]] = set()
|
||||
_router_paths: ClassVar[dict[FlowMethodName, list[FlowMethodName]]] = {}
|
||||
|
||||
entity_type: Literal["flow"] = "flow"
|
||||
|
||||
initial_state: Any = Field(default=None)
|
||||
name: str | None = Field(default=None)
|
||||
tracing: bool | None = Field(default=None)
|
||||
@@ -893,8 +912,17 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
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)
|
||||
persistence: Annotated[
|
||||
SerializeAsAny[FlowPersistence] | Any,
|
||||
BeforeValidator(lambda v, _: _resolve_persistence(v)),
|
||||
] = Field(default=None)
|
||||
max_method_calls: int = Field(default=100)
|
||||
|
||||
execution_context: ExecutionContext | None = Field(default=None)
|
||||
checkpoint_completed_methods: set[str] | None = Field(default=None)
|
||||
checkpoint_method_outputs: list[Any] | None = Field(default=None)
|
||||
checkpoint_method_counts: dict[str, int] | None = Field(default=None)
|
||||
checkpoint_state: dict[str, Any] | None = Field(default=None)
|
||||
|
||||
_methods: dict[FlowMethodName, FlowMethod[Any, Any]] = PrivateAttr(
|
||||
default_factory=dict
|
||||
|
||||
@@ -5,14 +5,17 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
|
||||
class FlowPersistence(ABC):
|
||||
_persistence_registry: dict[str, type[FlowPersistence]] = {}
|
||||
|
||||
|
||||
class FlowPersistence(BaseModel, ABC):
|
||||
"""Abstract base class for flow state persistence.
|
||||
|
||||
This class defines the interface that all persistence implementations must follow.
|
||||
@@ -24,6 +27,13 @@ class FlowPersistence(ABC):
|
||||
- clear_pending_feedback(): Clears pending feedback after resume
|
||||
"""
|
||||
|
||||
persistence_type: str = Field(default="base")
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
if not getattr(cls, "__abstractmethods__", set()):
|
||||
_persistence_registry[cls.__name__] = cls
|
||||
|
||||
@abstractmethod
|
||||
def init_db(self) -> None:
|
||||
"""Initialize the persistence backend.
|
||||
@@ -95,7 +105,7 @@ class FlowPersistence(ABC):
|
||||
"""
|
||||
return None
|
||||
|
||||
def clear_pending_feedback(self, flow_uuid: str) -> None: # noqa: B027
|
||||
def clear_pending_feedback(self, flow_uuid: str) -> None:
|
||||
"""Clear the pending feedback marker after successful resume.
|
||||
|
||||
This is called after feedback is received and the flow resumes.
|
||||
|
||||
@@ -9,7 +9,8 @@ from pathlib import Path
|
||||
import sqlite3
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.flow.persistence.base import FlowPersistence
|
||||
from crewai.utilities.lock_store import lock as store_lock
|
||||
@@ -50,26 +51,22 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str | None = None) -> None:
|
||||
"""Initialize SQLite persistence.
|
||||
persistence_type: str = Field(default="SQLiteFlowPersistence")
|
||||
db_path: str = Field(
|
||||
default_factory=lambda: str(Path(db_storage_path()) / "flow_states.db")
|
||||
)
|
||||
_lock_name: str = PrivateAttr()
|
||||
|
||||
Args:
|
||||
db_path: Path to the SQLite database file. If not provided, uses
|
||||
db_storage_path() from utilities.paths.
|
||||
def __init__(self, db_path: str | None = None, /, **kwargs: Any) -> None:
|
||||
if db_path is not None:
|
||||
kwargs["db_path"] = db_path
|
||||
super().__init__(**kwargs)
|
||||
|
||||
Raises:
|
||||
ValueError: If db_path is invalid
|
||||
"""
|
||||
|
||||
# Get path from argument or default location
|
||||
path = db_path or str(Path(db_storage_path()) / "flow_states.db")
|
||||
|
||||
if not path:
|
||||
raise ValueError("Database path must be provided")
|
||||
|
||||
self.db_path = path # Now mypy knows this is str
|
||||
@model_validator(mode="after")
|
||||
def _setup(self) -> Self:
|
||||
self._lock_name = f"sqlite:{os.path.realpath(self.db_path)}"
|
||||
self.init_db()
|
||||
return self
|
||||
|
||||
def init_db(self) -> None:
|
||||
"""Create the necessary tables if they don't exist."""
|
||||
|
||||
@@ -40,7 +40,9 @@ class LiteAgentOutput(BaseModel):
|
||||
usage_metrics: dict[str, Any] | None = Field(
|
||||
description="Token usage metrics for this execution", default=None
|
||||
)
|
||||
messages: list[LLMMessage] = Field(description="Messages of the agent", default=[])
|
||||
messages: list[LLMMessage] = Field(
|
||||
description="Messages of the agent", default_factory=list
|
||||
)
|
||||
|
||||
plan: str | None = Field(
|
||||
default=None, description="The execution plan that was generated, if any"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -32,6 +32,10 @@ class MemoryScope(BaseModel):
|
||||
"""Extract memory dependency and normalize root path before validation."""
|
||||
if isinstance(data, MemoryScope):
|
||||
return data
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Expected dict or MemoryScope, got {type(data).__name__}")
|
||||
if "memory" not in data:
|
||||
raise ValueError("MemoryScope requires a 'memory' key")
|
||||
memory = data.pop("memory")
|
||||
instance: MemoryScope = handler(data)
|
||||
instance._memory = memory
|
||||
@@ -199,6 +203,10 @@ class MemorySlice(BaseModel):
|
||||
"""Extract memory dependency and normalize scopes before validation."""
|
||||
if isinstance(data, MemorySlice):
|
||||
return data
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Expected dict or MemorySlice, got {type(data).__name__}")
|
||||
if "memory" not in data:
|
||||
raise ValueError("MemorySlice requires a 'memory' key")
|
||||
memory = data.pop("memory")
|
||||
data["scopes"] = [s.rstrip("/") or "/" for s in data.get("scopes", [])]
|
||||
instance: MemorySlice = handler(data)
|
||||
|
||||
18
lib/crewai/src/crewai/runtime_state.py
Normal file
18
lib/crewai/src/crewai/runtime_state.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Unified runtime state for crewAI.
|
||||
|
||||
``RuntimeState`` is a ``RootModel`` whose ``model_dump_json()`` produces a
|
||||
complete, self-contained snapshot of every active entity in the program.
|
||||
|
||||
The ``Entity`` type alias and ``RuntimeState`` model are built at import time
|
||||
in ``crewai/__init__.py`` after all forward references are resolved.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _entity_discriminator(v: dict[str, Any] | object) -> str:
|
||||
if isinstance(v, dict):
|
||||
raw = v.get("entity_type", "agent")
|
||||
else:
|
||||
raw = getattr(v, "entity_type", "agent")
|
||||
return str(raw)
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Sequence
|
||||
from concurrent.futures import Future
|
||||
import contextvars
|
||||
from copy import copy as shallow_copy
|
||||
@@ -12,6 +13,7 @@ import logging
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
ClassVar,
|
||||
cast,
|
||||
@@ -24,6 +26,7 @@ import warnings
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
Field,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
@@ -32,7 +35,7 @@ from pydantic import (
|
||||
from pydantic_core import PydanticCustomError
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent, _resolve_agent
|
||||
from crewai.context import reset_current_task_id, set_current_task_id
|
||||
from crewai.core.providers.content_processor import process_content
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
@@ -41,6 +44,7 @@ from crewai.events.types.task_events import (
|
||||
TaskFailedEvent,
|
||||
TaskStartedEvent,
|
||||
)
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.security import Fingerprint, SecurityConfig
|
||||
from crewai.tasks.output_format import OutputFormat
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
@@ -128,9 +132,10 @@ class Task(BaseModel):
|
||||
callback: SerializableCallable | None = Field(
|
||||
description="Callback to be executed after the task is completed.", default=None
|
||||
)
|
||||
agent: BaseAgent | None = Field(
|
||||
description="Agent responsible for execution the task.", default=None
|
||||
)
|
||||
agent: Annotated[
|
||||
BaseAgent | None,
|
||||
BeforeValidator(_resolve_agent),
|
||||
] = Field(description="Agent responsible for execution the task.", default=None)
|
||||
context: list[Task] | None | _NotSpecified = Field(
|
||||
description="Other tasks that will have their output used as context for this task.",
|
||||
default=NOT_SPECIFIED,
|
||||
@@ -316,6 +321,10 @@ class Task(BaseModel):
|
||||
if self.agent is None:
|
||||
raise ValueError("Agent is required to use LLMGuardrail")
|
||||
|
||||
if not isinstance(self.agent.llm, BaseLLM):
|
||||
raise ValueError(
|
||||
"Agent must have a BaseLLM instance to use LLMGuardrail"
|
||||
)
|
||||
self._guardrail = cast(
|
||||
GuardrailCallable,
|
||||
LLMGuardrail(description=self.guardrail, llm=self.agent.llm),
|
||||
@@ -339,6 +348,10 @@ class Task(BaseModel):
|
||||
)
|
||||
from crewai.tasks.llm_guardrail import LLMGuardrail
|
||||
|
||||
if not isinstance(self.agent.llm, BaseLLM):
|
||||
raise ValueError(
|
||||
"Agent must have a BaseLLM instance to use LLMGuardrail"
|
||||
)
|
||||
guardrails.append(
|
||||
cast(
|
||||
GuardrailCallable,
|
||||
@@ -359,6 +372,10 @@ class Task(BaseModel):
|
||||
)
|
||||
from crewai.tasks.llm_guardrail import LLMGuardrail
|
||||
|
||||
if not isinstance(self.agent.llm, BaseLLM):
|
||||
raise ValueError(
|
||||
"Agent must have a BaseLLM instance to use LLMGuardrail"
|
||||
)
|
||||
guardrails.append(
|
||||
cast(
|
||||
GuardrailCallable,
|
||||
@@ -379,11 +396,12 @@ class Task(BaseModel):
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
def _deny_user_set_id(cls, v: UUID4 | None) -> None:
|
||||
if v:
|
||||
def _deny_user_set_id(cls, v: UUID4 | None, info: Any) -> UUID4 | None:
|
||||
if v and not (info.context or {}).get("from_checkpoint"):
|
||||
raise PydanticCustomError(
|
||||
"may_not_set_field", "This field is not to be set by the user.", {}
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("input_files", mode="before")
|
||||
@classmethod
|
||||
@@ -646,7 +664,12 @@ class Task(BaseModel):
|
||||
await cb_result
|
||||
|
||||
crew = self.agent.crew # type: ignore[union-attr]
|
||||
if crew and crew.task_callback and crew.task_callback != self.callback:
|
||||
if (
|
||||
crew
|
||||
and not isinstance(crew, str)
|
||||
and crew.task_callback
|
||||
and crew.task_callback != self.callback
|
||||
):
|
||||
cb_result = crew.task_callback(self.output)
|
||||
if inspect.isawaitable(cb_result):
|
||||
await cb_result
|
||||
@@ -761,7 +784,12 @@ class Task(BaseModel):
|
||||
asyncio.run(cb_result)
|
||||
|
||||
crew = self.agent.crew # type: ignore[union-attr]
|
||||
if crew and crew.task_callback and crew.task_callback != self.callback:
|
||||
if (
|
||||
crew
|
||||
and not isinstance(crew, str)
|
||||
and crew.task_callback
|
||||
and crew.task_callback != self.callback
|
||||
):
|
||||
cb_result = crew.task_callback(self.output)
|
||||
if inspect.iscoroutine(cb_result):
|
||||
asyncio.run(cb_result)
|
||||
@@ -812,11 +840,14 @@ class Task(BaseModel):
|
||||
if trigger_payload is not None:
|
||||
description += f"\n\nTrigger Payload: {trigger_payload}"
|
||||
|
||||
if self.agent and self.agent.crew:
|
||||
if self.agent and self.agent.crew and not isinstance(self.agent.crew, str):
|
||||
files = get_all_files(self.agent.crew.id, self.id)
|
||||
if files:
|
||||
supported_types: list[str] = []
|
||||
if self.agent.llm and self.agent.llm.supports_multimodal():
|
||||
if (
|
||||
isinstance(self.agent.llm, BaseLLM)
|
||||
and self.agent.llm.supports_multimodal()
|
||||
):
|
||||
provider: str = str(
|
||||
getattr(self.agent.llm, "provider", None)
|
||||
or getattr(self.agent.llm, "model", "openai")
|
||||
@@ -971,7 +1002,7 @@ Follow these guidelines:
|
||||
self.delegations += 1
|
||||
|
||||
def copy( # type: ignore
|
||||
self, agents: list[BaseAgent], task_mapping: dict[str, Task]
|
||||
self, agents: Sequence[BaseAgent], task_mapping: dict[str, Task]
|
||||
) -> Task:
|
||||
"""Creates a deep copy of the Task while preserving its original class type.
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import Field
|
||||
from crewai.task import Task
|
||||
from crewai.tasks.output_format import OutputFormat
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
from crewai.types.callback import SerializableCallable
|
||||
|
||||
|
||||
class ConditionalTask(Task):
|
||||
@@ -24,7 +25,7 @@ class ConditionalTask(Task):
|
||||
- Cannot be the first task since it needs context from the previous task
|
||||
"""
|
||||
|
||||
condition: Callable[[TaskOutput], bool] | None = Field(
|
||||
condition: SerializableCallable | None = Field(
|
||||
default=None,
|
||||
description="Function that determines whether the task should be executed based on previous task output.",
|
||||
)
|
||||
@@ -51,7 +52,7 @@ class ConditionalTask(Task):
|
||||
"""
|
||||
if self.condition is None:
|
||||
raise ValueError("No condition function set for conditional task")
|
||||
return self.condition(context)
|
||||
return bool(self.condition(context))
|
||||
|
||||
def get_skipped_task_output(self) -> TaskOutput:
|
||||
"""Generate a TaskOutput for when the conditional task is skipped.
|
||||
|
||||
@@ -43,7 +43,9 @@ class TaskOutput(BaseModel):
|
||||
output_format: OutputFormat = Field(
|
||||
description="Output format of the task", default=OutputFormat.RAW
|
||||
)
|
||||
messages: list[LLMMessage] = Field(description="Messages of the task", default=[])
|
||||
messages: list[LLMMessage] = Field(
|
||||
description="Messages of the task", default_factory=list
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_summary(self) -> TaskOutput:
|
||||
|
||||
@@ -41,6 +41,7 @@ from crewai.events.types.system_events import (
|
||||
SigTStpEvent,
|
||||
SigTermEvent,
|
||||
)
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.telemetry.constants import (
|
||||
CREWAI_TELEMETRY_BASE_URL,
|
||||
CREWAI_TELEMETRY_SERVICE_NAME,
|
||||
@@ -323,7 +324,9 @@ class Telemetry:
|
||||
if getattr(agent, "function_calling_llm", None)
|
||||
else ""
|
||||
),
|
||||
"llm": agent.llm.model,
|
||||
"llm": agent.llm.model
|
||||
if isinstance(agent.llm, BaseLLM)
|
||||
else str(agent.llm),
|
||||
"delegation_enabled?": agent.allow_delegation,
|
||||
"allow_code_execution?": getattr(
|
||||
agent, "allow_code_execution", False
|
||||
@@ -427,7 +430,9 @@ class Telemetry:
|
||||
if getattr(agent, "function_calling_llm", None)
|
||||
else ""
|
||||
),
|
||||
"llm": agent.llm.model,
|
||||
"llm": agent.llm.model
|
||||
if isinstance(agent.llm, BaseLLM)
|
||||
else str(agent.llm),
|
||||
"delegation_enabled?": agent.allow_delegation,
|
||||
"allow_code_execution?": getattr(
|
||||
agent, "allow_code_execution", False
|
||||
@@ -840,7 +845,9 @@ class Telemetry:
|
||||
"max_iter": agent.max_iter,
|
||||
"max_rpm": agent.max_rpm,
|
||||
"i18n": agent.i18n.prompt_file,
|
||||
"llm": agent.llm.model,
|
||||
"llm": agent.llm.model
|
||||
if isinstance(agent.llm, BaseLLM)
|
||||
else str(agent.llm),
|
||||
"delegation_enabled?": agent.allow_delegation,
|
||||
"tools_names": [
|
||||
sanitize_tool_name(tool.name)
|
||||
@@ -1033,3 +1040,20 @@ class Telemetry:
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def feature_usage_span(self, feature: str) -> None:
|
||||
"""Records that a feature was used. One span = one count.
|
||||
|
||||
Args:
|
||||
feature: Feature identifier, e.g. "planning:creation",
|
||||
"mcp:connection", "a2a:delegation".
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Feature Usage")
|
||||
self._add_attribute(span, "crewai_version", version("crewai"))
|
||||
self._add_attribute(span, "feature", feature)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool
|
||||
@@ -16,7 +17,7 @@ if TYPE_CHECKING:
|
||||
class AgentTools:
|
||||
"""Manager class for agent-related tools"""
|
||||
|
||||
def __init__(self, agents: list[BaseAgent], i18n: I18N | None = None) -> None:
|
||||
def __init__(self, agents: Sequence[BaseAgent], i18n: I18N | None = None) -> None:
|
||||
self.agents = agents
|
||||
self.i18n = i18n if i18n is not None else get_i18n()
|
||||
|
||||
|
||||
@@ -318,6 +318,8 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_delegations(coworker)
|
||||
|
||||
fingerprint_config = self._build_fingerprint_config()
|
||||
|
||||
if calling.arguments:
|
||||
try:
|
||||
acceptable_args = tool.args_schema.model_json_schema()[
|
||||
@@ -328,15 +330,16 @@ class ToolUsage:
|
||||
for k, v in calling.arguments.items()
|
||||
if k in acceptable_args
|
||||
}
|
||||
arguments = self._add_fingerprint_metadata(arguments)
|
||||
result = await tool.ainvoke(input=arguments)
|
||||
result = await tool.ainvoke(
|
||||
input=arguments, config=fingerprint_config
|
||||
)
|
||||
except Exception:
|
||||
arguments = calling.arguments
|
||||
arguments = self._add_fingerprint_metadata(arguments)
|
||||
result = await tool.ainvoke(input=arguments)
|
||||
result = await tool.ainvoke(
|
||||
input=arguments, config=fingerprint_config
|
||||
)
|
||||
else:
|
||||
arguments = self._add_fingerprint_metadata({})
|
||||
result = await tool.ainvoke(input=arguments)
|
||||
result = await tool.ainvoke(input={}, config=fingerprint_config)
|
||||
|
||||
if self.tools_handler:
|
||||
should_cache = True
|
||||
@@ -550,6 +553,8 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_delegations(coworker)
|
||||
|
||||
fingerprint_config = self._build_fingerprint_config()
|
||||
|
||||
if calling.arguments:
|
||||
try:
|
||||
acceptable_args = tool.args_schema.model_json_schema()[
|
||||
@@ -560,15 +565,16 @@ class ToolUsage:
|
||||
for k, v in calling.arguments.items()
|
||||
if k in acceptable_args
|
||||
}
|
||||
arguments = self._add_fingerprint_metadata(arguments)
|
||||
result = tool.invoke(input=arguments)
|
||||
result = tool.invoke(
|
||||
input=arguments, config=fingerprint_config
|
||||
)
|
||||
except Exception:
|
||||
arguments = calling.arguments
|
||||
arguments = self._add_fingerprint_metadata(arguments)
|
||||
result = tool.invoke(input=arguments)
|
||||
result = tool.invoke(
|
||||
input=arguments, config=fingerprint_config
|
||||
)
|
||||
else:
|
||||
arguments = self._add_fingerprint_metadata({})
|
||||
result = tool.invoke(input=arguments)
|
||||
result = tool.invoke(input={}, config=fingerprint_config)
|
||||
|
||||
if self.tools_handler:
|
||||
should_cache = True
|
||||
@@ -1008,23 +1014,16 @@ class ToolUsage:
|
||||
|
||||
return event_data
|
||||
|
||||
def _add_fingerprint_metadata(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Add fingerprint metadata to tool arguments if available.
|
||||
def _build_fingerprint_config(self) -> dict[str, Any]:
|
||||
"""Build fingerprint metadata as a config dict for tool invocation.
|
||||
|
||||
Args:
|
||||
arguments: The original tool arguments
|
||||
Returns the fingerprint data in a config dict rather than injecting it
|
||||
into tool arguments, so it doesn't conflict with strict tool schemas.
|
||||
|
||||
Returns:
|
||||
Updated arguments dictionary with fingerprint metadata
|
||||
Config dictionary with security_context metadata.
|
||||
"""
|
||||
# Create a shallow copy to avoid modifying the original
|
||||
arguments = arguments.copy()
|
||||
|
||||
# Add security metadata under a designated key
|
||||
if "security_context" not in arguments:
|
||||
arguments["security_context"] = {}
|
||||
|
||||
security_context = arguments["security_context"]
|
||||
security_context: dict[str, Any] = {}
|
||||
|
||||
# Add agent fingerprint if available
|
||||
if self.agent and hasattr(self.agent, "security_config"):
|
||||
@@ -1048,4 +1047,4 @@ class ToolUsage:
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return arguments
|
||||
return {"security_context": security_context} if security_context else {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Annotated, Final
|
||||
|
||||
from pydantic_core import CoreSchema
|
||||
|
||||
from crewai.utilities.printer import PrinterColor
|
||||
|
||||
|
||||
@@ -36,6 +38,25 @@ class _NotSpecified:
|
||||
def __repr__(self) -> str:
|
||||
return "NOT_SPECIFIED"
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, _source_type: object, _handler: object
|
||||
) -> CoreSchema:
|
||||
from pydantic_core import core_schema
|
||||
|
||||
def _validate(v: object) -> _NotSpecified:
|
||||
if isinstance(v, _NotSpecified) or v == "NOT_SPECIFIED":
|
||||
return NOT_SPECIFIED
|
||||
raise ValueError(f"Expected NOT_SPECIFIED sentinel, got {type(v).__name__}")
|
||||
|
||||
return core_schema.no_info_plain_validator_function(
|
||||
_validate,
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
lambda v: "NOT_SPECIFIED",
|
||||
info_arg=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
NOT_SPECIFIED: Final[
|
||||
Annotated[
|
||||
|
||||
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}"
|
||||
)
|
||||
@@ -793,6 +793,10 @@ class TestTraceListenerSetup:
|
||||
"crewai.events.listeners.tracing.utils._is_test_environment",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"crewai.events.listeners.tracing.utils._is_interactive_terminal",
|
||||
return_value=True,
|
||||
),
|
||||
patch("threading.Thread") as mock_thread,
|
||||
):
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
|
||||
@@ -32,8 +32,10 @@ def wait_for_event_handlers(timeout: float = 5.0) -> None:
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
crewai_event_bus._sync_executor.shutdown(wait=True)
|
||||
crewai_event_bus._sync_executor = ThreadPoolExecutor(
|
||||
max_workers=10,
|
||||
thread_name_prefix="CrewAISyncHandler",
|
||||
)
|
||||
# Guard against lazy-initialized executor (may not exist if no events were emitted)
|
||||
if getattr(crewai_event_bus, "_executor_initialized", False):
|
||||
crewai_event_bus._sync_executor.shutdown(wait=True)
|
||||
crewai_event_bus._sync_executor = ThreadPoolExecutor(
|
||||
max_workers=10,
|
||||
thread_name_prefix="CrewAISyncHandler",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.13.0a5"
|
||||
__version__ = "1.13.0a7"
|
||||
|
||||
Reference in New Issue
Block a user