mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-15 07:39:28 +00:00
Compare commits
17 Commits
cursor/fil
...
cursor/sim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2062c2d63d | ||
|
|
5d7ae87177 | ||
|
|
27083f4131 | ||
|
|
77a929ecf5 | ||
|
|
93d7a07422 | ||
|
|
8b646620be | ||
|
|
75aeb7bdf5 | ||
|
|
9366702275 | ||
|
|
78da56f332 | ||
|
|
a68a038cd6 | ||
|
|
967af3f96d | ||
|
|
6fd0afdb03 | ||
|
|
f0c453a187 | ||
|
|
d8ae9bac64 | ||
|
|
558fe5f026 | ||
|
|
fe66a567f0 | ||
|
|
4a651677a2 |
@@ -118,6 +118,24 @@
|
||||
"edge/en/guides/flows/inputs-id-deprecation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Frontend",
|
||||
"icon": "browser",
|
||||
"pages": [
|
||||
"edge/en/guides/frontend/overview",
|
||||
"edge/en/guides/frontend/conversational-flows",
|
||||
"edge/en/guides/frontend/generative-ui",
|
||||
"edge/en/guides/frontend/tool-based-generative-ui",
|
||||
"edge/en/guides/frontend/agentic-generative-ui",
|
||||
"edge/en/guides/frontend/reasoning",
|
||||
"edge/en/guides/frontend/a2ui",
|
||||
"edge/en/guides/frontend/human-in-the-loop",
|
||||
"edge/en/guides/frontend/shared-state",
|
||||
"edge/en/guides/frontend/frontend-actions",
|
||||
"edge/en/guides/frontend/predictive-state-updates",
|
||||
"edge/en/guides/frontend/channels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Tools",
|
||||
"icon": "wrench",
|
||||
|
||||
@@ -59,9 +59,10 @@ os.environ['OTEL_SDK_DISABLED'] = 'true'
|
||||
| نعم | بيانات تنفيذ الاختبار | تشمل: مفتاح ومعرّف الطاقم المُولّد عشوائياً، عدد التكرارات، اسم النموذج المستخدم، درجة الجودة (عدد عشري)، وقت التنفيذ (بالثواني). كلها غير شخصية. |
|
||||
| نعم | بيانات دورة حياة المهمة | تشمل: أوقات الإنشاء وبدء/انتهاء التنفيذ، معرّفات الطاقم والمهمة. مخزنة كنطاقات مع طوابع زمنية. لا بيانات شخصية. |
|
||||
| نعم | سمات LLM | تشمل: الاسم، model_name، model، top_k، temperature، واسم فئة LLM. كلها بيانات تقنية غير شخصية. |
|
||||
| نعم | محاولة نشر الطاقم باستخدام CLI الخاص بـ CrewAI | تشمل: حقيقة إجراء النشر ومعرّف الطاقم، وما إذا كان يحاول سحب السجلات، لا بيانات أخرى. |
|
||||
| نعم | محاولة نشر الطاقم باستخدام CLI الخاص بـ CrewAI | تشمل: حقيقة إجراء النشر ومعرّف الطاقم، وما إذا كان يحاول سحب السجلات، وما إذا بدأ النشر من أمر CLI أو من واجهة التشغيل TUI. لا تُسجَّل محتويات المشروع أو الطاقم. لا توجد بيانات شخصية. |
|
||||
| نعم | بيئة التنفيذ | تشمل: مساعد البرمجة بالذكاء الاصطناعي الذي يشغّل العملية إن وُجد (واحد من قائمة ثابتة مثل `claude_code` أو `codex` أو `cursor` أو `unknown`)، ومكان تشغيل العملية (واحد من قائمة ثابتة مثل `ci` أو `container` أو `serverless` أو `interactive`)، و`project_id` من ملف `pyproject.toml` عند ضبطه. يتحقق الاكتشاف فقط مما إذا كانت متغيرات البيئة المعروفة مضبوطة، ولا يقرأ قيمها أبدًا. لا بيانات شخصية. |
|
||||
| نعم | إشارات دورة حياة التدفق | تشمل: بدء التدفق، وما إذا اكتمل أو فشل، وما إذا فشلت إحدى دواله، وما إذا توقّف مؤقتًا لطلب إدخال أو ملاحظات بشرية، وما إذا كان البدء استئنافًا لتشغيل سابق، وما إذا فشلت دورة محادثة، ومدة تشغيل التدفق، وما إذا كان التدفق من التدفقات التي يشغّلها CrewAI داخليًا أم من كتابتك. يُسجَّل اسم التدفق، كما هو الحال بالفعل عند إنشاء التدفق وتنفيذه. لا تُسجَّل أبدًا أسماء الدوال أو رسائل الأخطاء أو حالة التدفق. لا بيانات شخصية. |
|
||||
| نعم | إشارات دورة حياة التدفق | تشمل: بدء التدفق، وما إذا اكتمل أو فشل، وما إذا فشلت إحدى دواله، وما إذا توقّف مؤقتًا لطلب إدخال أو ملاحظات بشرية، وما إذا كان البدء استئنافًا لتشغيل سابق، وما إذا فشلت دورة محادثة، ومدة تشغيل التدفق، وما إذا كان التدفق من التدفقات التي يشغّلها CrewAI داخليًا أم من كتابتك. يُسجَّل اسم التدفق (يجب ألا يتضمن معلومات شخصية)، كما هو الحال بالفعل عند إنشاء التدفق وتنفيذه. لا تُسجَّل أبدًا أسماء الدوال أو رسائل الأخطاء أو حالة التدفق. لا بيانات شخصية. |
|
||||
| نعم | إشارة مشاركة التتبع | تشمل: نجاح مشاركة دفعة من عمليات التتبع مع CrewAI AMP، وما إذا تمت المشاركة بشكل مجهول (قبل إنشاء حساب) أو مرتبطة بحسابك. ومثل كل span، تحمل أيضًا سمات بيئة التنفيذ الموضحة أعلاه (`project_id` عند تكوينه، ومساعد البرمجة، وبيئة التشغيل). يصف هذا الصف بيانات القياس عن بُعد الخاصة بالمشاركة فقط — وليس محتويات التتبع أو الوصول الذي تمنحه روابط التتبع المشتركة. لا تُسجَّل محتويات التتبع أو المدخلات أو المخرجات في هذه الإشارة. قبل مشاركة التتبعات، راجع الأسرار والبيانات الشخصية وإعدادات التنقيح والاحتفاظ في AMP. |
|
||||
| لا | بيانات الوكيل الموسّعة | تشمل: وصف الهدف، نص الخلفية، معرّف ملف موجهات i18n. يجب على المستخدمين التأكد من عدم تضمين معلومات شخصية في حقول النص. |
|
||||
| لا | معلومات المهمة التفصيلية | تشمل: وصف المهمة، وصف المخرجات المتوقعة، مراجع السياق. يجب على المستخدمين التأكد من عدم تضمين معلومات شخصية في هذه الحقول. |
|
||||
| لا | معلومات البيئة | تشمل: المنصة، الإصدار، النظام، الإصدار، وعدد وحدات المعالجة المركزية. مثال: 'Windows 10'، 'x86_64'. لا بيانات شخصية. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: قراءة الملفات
|
||||
description: أداة `FileReadTool` مصممة لقراءة الملفات من نظام الملفات المحلي.
|
||||
description: أداة `FileReadTool` تقرأ الملفات من نظام الملفات المحلي.
|
||||
icon: folders
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -8,35 +8,63 @@ mode: "wide"
|
||||
## نظرة عامة
|
||||
|
||||
<Note>
|
||||
لا نزال نعمل على تحسين الأدوات، لذا قد يحدث سلوك غير متوقع أو تغييرات في المستقبل.
|
||||
لا نزال نعمل على تحسين الأدوات، لذا قد يتغير السلوك.
|
||||
</Note>
|
||||
|
||||
تمثل أداة FileReadTool مفهوميًا مجموعة من الوظائف ضمن حزمة crewai_tools تهدف إلى تسهيل قراءة الملفات واسترجاع المحتوى. تتضمن هذه المجموعة أدوات لمعالجة ملفات نصية دفعية، وقراءة ملفات التكوين أثناء التشغيل، واستيراد البيانات للتحليلات. تدعم مجموعة متنوعة من صيغ الملفات النصية مثل `.txt` و `.csv` و `.json` وغيرها. يُعاد المحتوى دائمًا نصًا عاديًا.
|
||||
تقرأ أداة `FileReadTool` ملفًا محليًا وتعيد محتواه كنص.
|
||||
استخدمها لمعالجة الملفات النصية، أو قراءة ملفات التكوين، أو تحميل البيانات للتحليل.
|
||||
تعمل مع أي صيغة نصية، مثل `.txt` و `.csv` و `.json` و `.md`.
|
||||
تعيد الأداة دائمًا نصًا عاديًا. إذا احتجت بيانات منظمة (مثل JSON)، فقم بتحليلها في الـ Agent أو في التعليمات البرمجية الخاصة بك.
|
||||
|
||||
للملفات الكبيرة، يمكن للـ Agent تمرير `start_line` و `line_count` لقراءة نطاق من الأسطر فقط.
|
||||
تتوقف الأداة بمجرد الحصول على تلك الأسطر، لذا لا تمسح بقية الملف.
|
||||
|
||||
## التثبيت
|
||||
|
||||
لاستخدام الوظائف المنسوبة سابقاً لأداة FileReadTool، قم بتثبيت حزمة crewai_tools:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
uv add 'crewai[tools]'
|
||||
```
|
||||
|
||||
## مثال على الاستخدام
|
||||
|
||||
للبدء مع FileReadTool:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileReadTool
|
||||
|
||||
# Initialize the tool to read any files the agents knows or lean the path for
|
||||
file_read_tool = FileReadTool()
|
||||
# Agent chooses the file path at runtime
|
||||
tool = FileReadTool()
|
||||
|
||||
# OR
|
||||
# OR set a default file the agent can read with no path argument
|
||||
tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
|
||||
# Initialize the tool with a specific file path, so the agent can only read the content of the specified file
|
||||
file_read_tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
# OR let the agent read any file under a directory
|
||||
tool = FileReadTool(base_dir='/data')
|
||||
```
|
||||
|
||||
امنح الأداة لـ Agent. في وقت التشغيل يمرر الـ LLM قيمة `file_path`، واختياريًا `start_line` و `line_count`.
|
||||
|
||||
## المعاملات
|
||||
|
||||
- `file_path`: مسار الملف المراد قراءته. يقبل كلاً من المسارات المطلقة والنسبية. تأكد من وجود الملف وأن لديك الصلاحيات اللازمة للوصول إليه.
|
||||
يمكن للـ Agent تمرير هذه المعاملات في وقت التشغيل:
|
||||
|
||||
- `file_path`: (اختياري) مسار الملف المراد قراءته. المسارات المطلقة والنسبية صالحة فقط عندما تُحل داخل بيئة الحماية `base_dir`. يُحل المسار النسبي بالنسبة إلى `base_dir` عند تعيينه، وإلا بالنسبة إلى مجلد العمل الحالي (بيئة الحماية الافتراضية). احذفه لقراءة الملف الافتراضي المحدد عند الإنشاء. إذا لم يكن هناك افتراضي، تعيد الأداة خطأ يفيد بعدم توفير مسار.
|
||||
- `start_line`: (اختياري) أول سطر للقراءة. تبدأ أرقام الأسطر من `1`. الافتراضي هو `1`.
|
||||
- `line_count`: (اختياري) عدد الأسطر المراد قراءتها. إذا حُذف، تقرأ الأداة من `start_line` حتى نهاية الملف.
|
||||
|
||||
يمكنك تعيين هذه المعاملات عند إنشاء الأداة:
|
||||
|
||||
- `file_path`: (اختياري) الملف الافتراضي للقراءة عندما يستدعي الـ Agent الأداة بدون مسار. يُحل المسار النسبي بالنسبة إلى `base_dir` عند توفير `base_dir`، وإلا بالنسبة إلى مجلد العمل الحالي.
|
||||
- `base_dir`: (اختياري) المجلد الذي يجب أن تبقى داخله مسارات وقت التشغيل. الافتراضي هو مجلد العمل الحالي. تحل الأداة هذا المسار عند إنشائها، لذا لا يؤدي تغيير لاحق لمجلد العمل إلى نقل بيئة الحماية.
|
||||
- `encoding`: (اختياري) ترميز النص المستخدم لفك تشفير الملف. الافتراضي هو `utf-8`. إذا فشل فك التشفير، تعيد الأداة خطأ وتقترح تمرير `encoding` مختلف.
|
||||
|
||||
تعيد حالات الفشل الشائعة (ملف مفقود، رفض الإذن، ترميز خاطئ، أو مسار خارج بيئة الحماية) سلسلة خطأ. ولا تُثير استثناءً.
|
||||
|
||||
## المسارات المسموح بها
|
||||
|
||||
عادةً ما يختار الـ LLM مسار الملف في وقت التشغيل، لذا تقتصر عمليات القراءة على بيئة حماية:
|
||||
|
||||
- يجب أن تُحل مسارات وقت التشغيل داخل `base_dir` (الافتراضي: مجلد العمل الحالي). تحل الأداة مقاطع `..` والروابط الرمزية قبل فحص المسار، لذا لا يمكنها الخروج من بيئة الحماية.
|
||||
- مسار `file_path` الذي تمرره إلى المُنشئ مسموح به دائمًا، حتى لو كان خارج `base_dir`. قد تفشل القراءة نفسها إذا كان الملف مفقودًا أو مجلدًا أو لا يمكن الوصول إليه. يُثبَّت هذا المسار عند إنشاء الأداة، لذا لا يغيّر تغيير لاحق لمجلد العمل الملف الذي يشير إليه. يمكن للـ Agent قراءته بحذف `file_path`، أو باستخدام الاسم الظاهر في وصف الأداة. إعلان ملف واحد لا يتيح الوصول إلى ملفات أخرى في نفس المجلد.
|
||||
|
||||
للسماح لـ Agent بقراءة ملفات خارج مجلد العمل، عيّن `base_dir` عند إنشاء الأداة (انظر المثال أعلاه).
|
||||
|
||||
كحل أخير، عيّن `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` لإيقاف فحوصات المسار. ينطبق هذا الإعداد على كل أداة من crewai-tools في العملية، بما في ذلك حماية SSRF على أدوات جلب عناوين URL. فضّل `base_dir` بدلاً من ذلك.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: كتابة الملفات
|
||||
description: تكتب محتوى نصيًا إلى ملف.
|
||||
description: أداة `FileWriterTool` مصممة لكتابة المحتوى في الملفات.
|
||||
icon: file-pen
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -9,25 +9,27 @@ mode: "wide"
|
||||
|
||||
## الوصف
|
||||
|
||||
تكتب `FileWriterTool` محتوى نصيًا إلى ملف. تُنشأ المجلدات المفقودة تلقائيًا.
|
||||
ترميز النص الافتراضي هو UTF-8.
|
||||
|
||||
تقتصر عمليات الكتابة على مجلد sandbox. بشكل افتراضي يكون هذا المجلد هو مجلد
|
||||
العمل الحالي. مرّر `base_dir` لتغييره.
|
||||
أداة `FileWriterTool` هي مكوّن من حزمة crewai_tools، مصممة لتبسيط عملية كتابة المحتوى في الملفات مع توافق عبر المنصات (Windows و Linux و macOS). تكون مفيدة بشكل خاص في سيناريوهات مثل توليد التقارير وحفظ السجلات وإنشاء ملفات التكوين والمزيد. تتعامل هذه الأداة مع اختلافات المسارات عبر أنظمة التشغيل، وتدعم ترميز UTF-8، وتنشئ المجلدات تلقائياً إذا لم تكن موجودة، مما يسهل تنظيم المخرجات بشكل موثوق عبر المنصات المختلفة.
|
||||
|
||||
## التثبيت
|
||||
|
||||
قم بتثبيت حزمة crewai_tools لاستخدام `FileWriterTool` في مشاريعك:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## مثال
|
||||
|
||||
للبدء مع `FileWriterTool`:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileWriterTool
|
||||
|
||||
# Initialize the tool
|
||||
file_writer_tool = FileWriterTool()
|
||||
|
||||
# Write content to a file in a specified directory
|
||||
result = file_writer_tool.run(
|
||||
filename='example.txt',
|
||||
content='This is a test content.',
|
||||
@@ -38,46 +40,12 @@ print(result)
|
||||
|
||||
## المعاملات
|
||||
|
||||
يوفّرها الوكيل في وقت التشغيل:
|
||||
- `filename`: اسم الملف الذي تريد إنشاءه أو الكتابة فوقه.
|
||||
- `content`: المحتوى المراد كتابته في الملف.
|
||||
- `directory` (اختياري): مسار المجلد حيث سيتم إنشاء الملف. الافتراضي هو المجلد الحالي (`.`). إذا لم يكن المجلد موجوداً، سيتم إنشاؤه.
|
||||
|
||||
- `filename`: اسم الملف المراد كتابته، نسبةً إلى `directory`. يمكن أن يتضمن
|
||||
مجلدات فرعية. تُنشأ تلك المجلدات الفرعية إذا لم تكن موجودة.
|
||||
- `content`: المحتوى النصي المراد كتابته.
|
||||
- `directory` (اختياري): المجلد الذي تتم الكتابة إليه. يُحل المسار النسبي
|
||||
داخل المجلد المسموح للأداة (`base_dir` عند تعيينه، وإلا مجلد العمل الحالي)
|
||||
ويُعيَّن افتراضيًا إلى جذر ذلك المجلد (`./`). يُنشأ إذا لم يكن موجودًا.
|
||||
- `overwrite` (اختياري): ما إذا كان يجب استبدال ملف موجود. يقبل قيمة منطقية
|
||||
أو السلاسل `y`/`yes`/`t`/`true`/`on`/`1` و
|
||||
`n`/`no`/`f`/`false`/`off`/`0`. الافتراضي هو `false`. عندما يكون `false`
|
||||
والملف موجودًا مسبقًا، تُرجع الأداة خطأً ولا تعدّل الملف.
|
||||
## الخلاصة
|
||||
|
||||
تُعيَّن هذه عند إنشاء الأداة:
|
||||
من خلال دمج `FileWriterTool` في أطقمك، يمكن للوكلاء كتابة المحتوى في الملفات بشكل موثوق عبر أنظمة التشغيل المختلفة. هذه الأداة ضرورية للمهام التي تتطلب حفظ بيانات المخرجات وإنشاء أنظمة ملفات منظمة والتعامل مع عمليات الملفات عبر المنصات. يُوصى بها بشكل خاص لمستخدمي Windows الذين قد يواجهون مشكلات في كتابة الملفات مع عمليات ملفات Python القياسية.
|
||||
|
||||
- `base_dir` (اختياري): المجلد الذي يجب أن تبقى عمليات الكتابة داخله.
|
||||
الافتراضي هو مجلد العمل الحالي. يُحل المسار النسبي عند إنشاء الأداة، لذا
|
||||
لا ينقل تغيير لاحق لمجلد العمل حدود الـ sandbox.
|
||||
- `encoding` (اختياري): ترميز النص المستخدم لكتابة الملف. الافتراضي هو
|
||||
`utf-8`.
|
||||
|
||||
## المسارات المسموح بها
|
||||
|
||||
عادةً ما يختار نموذج لغوي قيم `directory` و`filename` في وقت التشغيل. ترفض
|
||||
الأداة المسارات التي تخرج من الـ sandbox:
|
||||
|
||||
- يجب أن يكون `directory` المحلول داخل `base_dir` (مجلد العمل الحالي
|
||||
افتراضيًا).
|
||||
- يجب أن يكون مسار الملف المحلول داخل ذلك `directory`.
|
||||
- تُحل مقاطع `..` والمسارات المطلقة والروابط الرمزية قبل كلا الفحصين.
|
||||
|
||||
للسماح بالكتابة تحت شجرة مختلفة، عيّن `base_dir`:
|
||||
|
||||
```python Code
|
||||
file_writer_tool = FileWriterTool(base_dir='/var/output')
|
||||
```
|
||||
|
||||
<Note>
|
||||
سابقًا، كان بإمكان `directory` المطلق الكتابة في أي مكان يملك فيه العملية
|
||||
صلاحية. إذا اعتمدت على ذلك، عيّن `base_dir` إلى الشجرة التي تريد السماح بها.
|
||||
تعيين `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` يعيد السلوك القديم، لكنه يعطّل
|
||||
فحوصات المسار وURL على مستوى العملية لكل أدوات crewai-tools. فضّل `base_dir`.
|
||||
</Note>
|
||||
من خلال الالتزام بإرشادات الإعداد والاستخدام المقدمة، فإن دمج هذه الأداة في المشاريع أمر مباشر ويضمن سلوكاً متسقاً لكتابة الملفات عبر جميع المنصات.
|
||||
|
||||
127
docs/edge/en/guides/frontend/a2ui.mdx
Normal file
127
docs/edge/en/guides/frontend/a2ui.mdx
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: A2UI
|
||||
description: The declarative tier of generative UI — the agent assembles a surface from a catalog of components you own.
|
||||
icon: table-cells
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## The agent assembles the UI
|
||||
|
||||
[Tool-based rendering](/en/guides/frontend/tool-based-generative-ui) maps one tool to one component: the agent picks a component, you draw it. A2UI is the **declarative** tier of the [generative-UI spectrum](/en/guides/frontend/generative-ui#declarative) — instead of picking a single component, the agent **assembles a surface** by combining building blocks from a catalog you define.
|
||||
|
||||
You still own the components. The agent can only use what is in your catalog, so it can never render something you did not ship. What the agent decides is the **layout and the data** — how those building blocks come together into a panel, and what goes in them.
|
||||
|
||||
<Note>
|
||||
A2UI works with [Flows](/en/concepts/flows). Both modes below — dynamic and fixed-schema — run as Flows served over AG-UI, exactly like the rest of this section.
|
||||
</Note>
|
||||
|
||||
## The catalog (same for every mode)
|
||||
|
||||
The frontend wiring is identical no matter which backend mode you use: you register a **catalog** on the `<CopilotKit>` provider with the `a2ui` prop.
|
||||
|
||||
```tsx
|
||||
import { CopilotKit } from "@copilotkit/react-core";
|
||||
import { catalog } from "@/a2ui-catalog";
|
||||
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" agent="assistant" a2ui={{ catalog }}>
|
||||
{/* ... */}
|
||||
</CopilotKit>
|
||||
```
|
||||
|
||||
The catalog is your set of React components keyed by a catalog id — a `FlightCard`, a `HotelCard`, a `Chart`, whatever your app needs. The agent references catalog ids; CopilotKit paints your components with the data the agent supplies.
|
||||
|
||||
<Note>
|
||||
Authoring the catalog itself — the id schema, prop mapping, and composition rules — is deeper than this page covers. See the [CopilotKit A2UI docs](https://docs.copilotkit.ai) for the full authoring reference. Here we focus on the two backend modes and when to reach for each.
|
||||
</Note>
|
||||
|
||||
## Two backend modes
|
||||
|
||||
A2UI backends come in two shapes. In **dynamic** mode the agent designs the surface; in **fixed-schema** mode you pre-author the layout and the agent only fills in data.
|
||||
|
||||
| Mode | Who designs the layout | Backend | Predictability |
|
||||
| --- | --- | --- | --- |
|
||||
| **[Dynamic](#dynamic)** | The agent, from the conversation | No A2UI tool — auto-injected | Novel layouts, LLM layout step |
|
||||
| **[Fixed-schema](#fixed-schema)** | You, up front | Backend tools return an envelope | Deterministic, no layout step |
|
||||
|
||||
### Dynamic
|
||||
|
||||
The Flow wires **no** A2UI tool. Enable A2UI on the runtime for this agent and it gains a `generate_a2ui` tool automatically. A sub-agent designs a surface from the conversation against your catalog, streams it to the frontend progressively, and self-heals invalid output through a validate-then-retry recovery pass. You write a normal agentic-chat Flow; the tool is injected for you.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Register the catalog on the provider">
|
||||
|
||||
Same as above — pass your catalog through the `a2ui` prop:
|
||||
|
||||
```tsx
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" agent="assistant" a2ui={{ catalog }}>
|
||||
{/* ... */}
|
||||
</CopilotKit>
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Serve a normal Flow">
|
||||
|
||||
Your backend is a plain agentic-chat Flow. You do not define an A2UI tool — the runtime injects `generate_a2ui` when A2UI is enabled for the agent, and the sub-agent invents the layout from the conversation.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Let the agent compose">
|
||||
|
||||
When a turn calls for UI, the agent assembles a surface from your catalog, streams the components in as it designs them, and repairs any invalid output before it reaches the screen. Your registered components render in the layout the agent chose.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
### Fixed-schema
|
||||
|
||||
When you already know the layout and only the data changes per call, pre-author the surface and let the agent fill it. The Flow wires backend tools (for example `search_flights`, `search_hotels`). Each tool returns an **A2UI operations envelope** as its result — `createSurface` -> `updateComponents` -> `updateDataModel` — which the frontend paints. There is no sub-agent, no generation, and no recovery pass: the layout JSON is authored by you, and only the data varies.
|
||||
|
||||
Install the toolkit that provides the envelope helpers:
|
||||
|
||||
```bash
|
||||
pip install ag-ui-a2ui-toolkit
|
||||
```
|
||||
|
||||
Build the envelope with the toolkit helpers and emit it as the tool result:
|
||||
|
||||
```python
|
||||
from ag_ui_a2ui_toolkit import (
|
||||
A2UI_OPERATIONS_KEY,
|
||||
create_surface,
|
||||
update_components,
|
||||
update_data_model,
|
||||
)
|
||||
from ag_ui_crewai.sdk import copilotkit_emit_tool_result, copilotkit_stream
|
||||
```
|
||||
|
||||
The tool assembles the `createSurface` -> `updateComponents` -> `updateDataModel` operations into an envelope keyed by `A2UI_OPERATIONS_KEY`, then hands it back with `copilotkit_emit_tool_result(...)`. Because the layout is fixed, the same tool always produces the same shape — only the values differ from call to call.
|
||||
|
||||
## When to use which
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Dynamic" icon="wand-magic-sparkles">
|
||||
The layout is not known ahead of time and you want the agent to compose novel surfaces from your primitives. You gain flexibility and pay for an LLM layout step.
|
||||
</Card>
|
||||
<Card title="Fixed-schema" icon="table-cells">
|
||||
The layout is known and only the data varies. More predictable and deterministic — no generation, no recovery, no LLM in the layout path.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
Both modes share the same frontend: one catalog, registered once on the provider. Start with fixed-schema when your surfaces are stable, and reach for dynamic when you want the agent to design layouts you did not anticipate.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Generative UI" icon="wand-magic-sparkles" href="/en/guides/frontend/generative-ui">
|
||||
The full spectrum — A2UI is its declarative tier.
|
||||
</Card>
|
||||
<Card title="Tool-Based Generative UI" icon="puzzle-piece" href="/en/guides/frontend/tool-based-generative-ui">
|
||||
Map one tool to one component (controlled).
|
||||
</Card>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state (controlled).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
208
docs/edge/en/guides/frontend/agentic-generative-ui.mdx
Normal file
208
docs/edge/en/guides/frontend/agentic-generative-ui.mdx
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Agentic Generative UI
|
||||
description: Render your CrewAI Flow's live state as UI that updates as the agent works through multi-step tasks.
|
||||
icon: list-check
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Render the agent's live state
|
||||
|
||||
Some work does not fit into a single tool call. A research task, a multi-step plan, a long-running job: the interesting thing to show the user is not one result, but *progress*. Agentic generative UI renders the agent's **state** and re-renders it every time that state changes.
|
||||
|
||||
The pattern has two halves:
|
||||
|
||||
1. Your Flow writes progress into its own state as it works.
|
||||
2. Your frontend reads that state with `useAgent` and paints it, re-rendering as the state streams in.
|
||||
|
||||
The Flow's state reaches the frontend over AG-UI without you wiring up any transport. A state snapshot is emitted automatically at each step (method) boundary of the Flow, and you can push intermediate updates during a long-running step by calling `copilotkit_emit_state` explicitly. You subclass the state to add your own fields, update them in the Flow, and read them in React.
|
||||
|
||||
<Note>
|
||||
State-driven rendering requires a **Flow** with custom state (`Flow[AgentState]`). Crews are chat-oriented and do not expose custom state this way, so with a Crew use [tool rendering](/en/guides/frontend/tool-based-generative-ui) instead.
|
||||
</Note>
|
||||
|
||||
## Build a live task planner
|
||||
|
||||
This example builds a planner that breaks a request into about ten steps and streams them to the UI as a checklist. It assumes you already have a CrewAI server and a CopilotKit frontend wired up. If you do not, start with the [Frontend Overview](/en/guides/frontend/overview).
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Add your own fields to the agent state">
|
||||
|
||||
Subclass `CopilotKitState` to declare the state your UI needs. `CopilotKitState` already carries the conversation (`messages`); you add whatever else you want to render, here a list of task steps.
|
||||
|
||||
```python
|
||||
from typing import List, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
from ag_ui_crewai.sdk import CopilotKitState
|
||||
|
||||
|
||||
class TaskStep(BaseModel):
|
||||
description: str
|
||||
status: Literal["enabled", "disabled"]
|
||||
|
||||
|
||||
class AgentState(CopilotKitState):
|
||||
steps: List[TaskStep] = Field(default_factory=list)
|
||||
```
|
||||
|
||||
Everything on `AgentState` is included in the state snapshot the frontend receives. A snapshot is emitted automatically at each step boundary, so writing to `self.state` is enough for the UI to pick it up between steps. To update the UI *during* a long step, emit explicitly (shown below).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Write progress into state from the Flow">
|
||||
|
||||
Type your Flow with the custom state (`Flow[AgentState]`) and let the model fill it in. Here the LLM calls a `generate_task_steps` tool; the streamed tool call lands in the conversation and the steps become visible in state.
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, start
|
||||
from litellm import acompletion
|
||||
from ag_ui_crewai.sdk import copilotkit_stream
|
||||
|
||||
GENERATE_TASK_STEPS_TOOL = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "generate_task_steps",
|
||||
"description": "Break a task into about 10 short imperative steps.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {"type": "string"},
|
||||
"status": {"type": "string", "enum": ["enabled"]},
|
||||
},
|
||||
"required": ["description", "status"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["steps"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TaskPlannerFlow(Flow[AgentState]):
|
||||
@start()
|
||||
async def chat(self):
|
||||
response = await copilotkit_stream(
|
||||
await acompletion(
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": "Plan the task the user asks for."},
|
||||
*self.state.messages,
|
||||
],
|
||||
tools=[GENERATE_TASK_STEPS_TOOL],
|
||||
parallel_tool_calls=False,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
```
|
||||
|
||||
Wrapping the LLM call in `copilotkit_stream` streams the assistant's tokens and tool call to the frontend as they are produced. The `steps` you write to `self.state` are sent in the state snapshot emitted at the end of this step.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Stream progress during a long step (optional)">
|
||||
|
||||
The automatic snapshot fires at step boundaries. If a single step does substantial work and you want the checklist to fill in *as it happens*, emit intermediate state yourself with `copilotkit_emit_state`. Each call pushes the current state to the frontend immediately.
|
||||
|
||||
```python
|
||||
from ag_ui_crewai.sdk import copilotkit_emit_state
|
||||
|
||||
class TaskPlannerFlow(Flow[AgentState]):
|
||||
@start()
|
||||
async def execute(self):
|
||||
for step in self.state.steps:
|
||||
step.status = "disabled" # mark done as you go
|
||||
await copilotkit_emit_state(self.state) # push update now
|
||||
await do_work(step)
|
||||
```
|
||||
|
||||
Import `copilotkit_emit_state` from `ag_ui_crewai.sdk`. It requires the CopilotKit SDK (`pip install "copilotkit[crewai]"`). Reach for it only when a step is long enough that waiting for its boundary snapshot would feel unresponsive.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Serve the Flow over AG-UI">
|
||||
|
||||
Register the Flow exactly as any other, on its own path:
|
||||
|
||||
```python
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
|
||||
from my_agents.task_planner import TaskPlannerFlow
|
||||
|
||||
app = FastAPI(title="CrewAI Agent Server")
|
||||
|
||||
add_crewai_flow_fastapi_endpoint(
|
||||
app=app,
|
||||
flow=TaskPlannerFlow(),
|
||||
path="/task_planner",
|
||||
)
|
||||
```
|
||||
|
||||
See the [Frontend Overview](/en/guides/frontend/overview) for the full server, runtime, and provider setup, and remember to register the agent (here `task_planner`) in your CopilotKit runtime route.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Read the live state in React">
|
||||
|
||||
On the frontend, `useAgent` gives you the agent's live state. Subscribe to state changes so your component re-renders every time the Flow writes an update.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";
|
||||
|
||||
function TaskPlan() {
|
||||
const { agent } = useAgent({
|
||||
agentId: "task_planner",
|
||||
updates: [UseAgentUpdate.OnStateChanged],
|
||||
});
|
||||
|
||||
const steps = agent?.state?.steps ?? [];
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{steps.map((s, i) => (
|
||||
<li key={i}>{s.description}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useAgent` returns `{ agent }`. A few things to know:
|
||||
|
||||
- `agent.state` is the live Flow state. Its shape matches the fields you added to `AgentState`, so `agent.state.steps` is your list of task steps.
|
||||
- `agent.isRunning` tells you when the agent is actively working, useful for showing a spinner or disabling input.
|
||||
- `updates: [UseAgentUpdate.OnStateChanged]` re-renders the component whenever state changes, so the checklist fills in as the Flow streams its steps.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Where this goes next
|
||||
|
||||
Reading state is the foundation. Two guides build directly on it:
|
||||
|
||||
- [Shared State](/en/guides/frontend/shared-state) adds the other direction: editing the agent's state from the UI and having the Flow pick up the change.
|
||||
- [Predictive State](/en/guides/frontend/predictive-state-updates) streams a tool's in-progress arguments into state so the UI reflects work before it is committed.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Shared State" icon="arrows-rotate" href="/en/guides/frontend/shared-state">
|
||||
Sync agent state and app UI in both directions.
|
||||
</Card>
|
||||
<Card title="Predictive State" icon="gauge-high" href="/en/guides/frontend/predictive-state-updates">
|
||||
Stream in-progress tool arguments into state.
|
||||
</Card>
|
||||
<Card title="Tool-Based Generative UI" icon="puzzle-piece" href="/en/guides/frontend/tool-based-generative-ui">
|
||||
Map agent tool calls to components.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
125
docs/edge/en/guides/frontend/channels.mdx
Normal file
125
docs/edge/en/guides/frontend/channels.mdx
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Channels
|
||||
description: Run the same CrewAI agent as a chat bot on Slack and Discord with the CopilotKit Channels SDK.
|
||||
icon: slack
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Meet your users where they already are
|
||||
|
||||
The CrewAI agent you built in the [Overview](/en/guides/frontend/overview) does not have to live behind a web app. The same Crew or Flow can run as a bot inside a messaging platform. No rebuild, no second copy of your agent logic: the agent stays exposed over the [AG-UI protocol](https://docs.ag-ui.com), and a bot process drives it.
|
||||
|
||||
CopilotKit's [Channels SDK](https://docs.copilotkit.ai/reference/channels) provides that bot process. It ships a platform-agnostic engine plus per-platform adapters.
|
||||
|
||||
## How it fits together
|
||||
|
||||
Nothing about your agent server changes. It keeps serving your Crew or Flow over AG-UI exactly as in the Overview. What you add is a separate **bot process**: it connects to a platform adapter, listens for messages, and runs your agent when it is messaged. The reply streams back into the channel.
|
||||
|
||||
```
|
||||
Slack / Discord ──► Channels bot process ──► CrewAI server (AG-UI) ──► Crew / Flow
|
||||
```
|
||||
|
||||
Your agent server can keep serving the web frontend from the Overview at the same time. The web app and the bot are just two clients of one AG-UI endpoint.
|
||||
|
||||
## Slack
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Install the Channels packages">
|
||||
|
||||
```bash
|
||||
npm install @copilotkit/channels @copilotkit/channels-slack @ag-ui/crewai
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create a Slack app and get tokens">
|
||||
|
||||
Create an app in the Slack API dashboard for your workspace, enable Socket Mode, and grant it the message and event scopes it needs to read and post in channels. Then expose its tokens to the bot process:
|
||||
|
||||
```bash
|
||||
export SLACK_BOT_TOKEN=xoxb-... # bot user token
|
||||
export SLACK_APP_TOKEN=xapp-... # app-level token (Socket Mode)
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Point the bot at your CrewAI agent">
|
||||
|
||||
`createBot` wires a Slack adapter to your agent. The `agent` factory returns a `CrewAIAgent` pointed at the AG-UI path your server exposes (the same URL you registered in the runtime in the Overview).
|
||||
|
||||
```ts
|
||||
// bot.ts
|
||||
import { createBot } from "@copilotkit/channels";
|
||||
import { slack, defaultSlackTools, defaultSlackContext } from "@copilotkit/channels-slack";
|
||||
import { CrewAIAgent } from "@ag-ui/crewai";
|
||||
|
||||
const bot = createBot({
|
||||
adapters: [
|
||||
slack({
|
||||
botToken: process.env.SLACK_BOT_TOKEN!, // xoxb-…
|
||||
appToken: process.env.SLACK_APP_TOKEN!, // xapp-… (Socket Mode)
|
||||
}),
|
||||
],
|
||||
agent: (threadId) => new CrewAIAgent({ url: "http://localhost:8000/recipe" }),
|
||||
tools: [...defaultSlackTools],
|
||||
context: [...defaultSlackContext],
|
||||
});
|
||||
|
||||
bot.start();
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run the bot">
|
||||
|
||||
Start the bot process alongside your agent server:
|
||||
|
||||
```bash
|
||||
uvicorn server:app --port 8000 # terminal 1 — CrewAI agent server
|
||||
node bot.ts # terminal 2 — Slack bot
|
||||
```
|
||||
|
||||
Message the bot in Slack and it runs your Crew or Flow, streaming the reply back into the thread.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
Slack app scopes, Socket Mode setup, and the full adapter options are maintained by CopilotKit. Follow the [Slack channel reference](https://docs.copilotkit.ai/reference/channels/slack) together with Slack's own app setup guide for the authoritative steps.
|
||||
</Note>
|
||||
|
||||
## Discord
|
||||
|
||||
Discord uses the same `createBot` engine with the Discord adapter from `@copilotkit/channels-discord`:
|
||||
|
||||
```ts
|
||||
import { createBot } from "@copilotkit/channels";
|
||||
import { discord } from "@copilotkit/channels-discord";
|
||||
import { CrewAIAgent } from "@ag-ui/crewai";
|
||||
|
||||
const bot = createBot({
|
||||
adapters: [discord({ token: process.env.DISCORD_BOT_TOKEN! })],
|
||||
agent: (threadId) => new CrewAIAgent({ url: "http://localhost:8000/recipe" }),
|
||||
});
|
||||
|
||||
bot.start();
|
||||
```
|
||||
|
||||
See the [Discord channel reference](https://docs.copilotkit.ai/reference/channels/discord) for the exact adapter options and bot setup.
|
||||
|
||||
## Platform support
|
||||
|
||||
Slack and Discord have official Channels adapters (`@copilotkit/channels-slack`, `@copilotkit/channels-discord`). Microsoft Teams is available through CopilotKit's managed offering (currently waitlisted). Check the [Channels reference](https://docs.copilotkit.ai/reference/channels) for the current list before promising a platform.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Frontend Overview" icon="browser" href="/en/guides/frontend/overview">
|
||||
Serve your Crew or Flow over AG-UI — the foundation every channel builds on.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Pause the agent to collect user approval or input mid-run.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
106
docs/edge/en/guides/frontend/conversational-flows.mdx
Normal file
106
docs/edge/en/guides/frontend/conversational-flows.mdx
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Conversational Flows
|
||||
description: Serve native, session-aware CrewAI Flows over AG-UI with managed conversation state and full frontend parity.
|
||||
icon: comments
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Three execution shapes, one bridge
|
||||
|
||||
Behind the AG-UI bridge, a CrewAI backend can take one of three shapes. Knowing which one you are serving decides how you author the backend, not how you build the frontend.
|
||||
|
||||
| Shape | What it is | How it is entered |
|
||||
| --- | --- | --- |
|
||||
| **Regular Flows** | Author-controlled `@start`/`@listen`/`@router` graphs. The default used throughout these guides. | `kickoff` / `astream` |
|
||||
| **Conversational Flows** | Native, session-aware, turn-based Flows with managed conversation state. | `stream_turn(message, session_id=...)` |
|
||||
| **Crews** | Closed autonomous task/agent loops. Basic chat only, a separate compatibility path. | Not the focus here. |
|
||||
|
||||
Conversational Flows are a newer CrewAI capability, and an important thing to be clear about up front: **they are Flows, not Crews.** They now run at full regular-Flow feature parity. This page introduces them and shows how they fit the rest of the frontend guides.
|
||||
|
||||
<Note>
|
||||
Reach for a Conversational Flow when you want native multi-turn conversation with CrewAI managing session state and history for you, rather than wiring turn and state handling into a regular Flow yourself. If you are new here, start with the [Frontend Overview](/en/guides/frontend/overview) for the base server, runtime, and provider setup.
|
||||
</Note>
|
||||
|
||||
## Register a Conversational Flow
|
||||
|
||||
You register a Conversational Flow through the same endpoint helper as any other Flow, with one extra argument: `conversational=True`.
|
||||
|
||||
```python
|
||||
# server.py
|
||||
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
|
||||
|
||||
add_crewai_flow_fastapi_endpoint(
|
||||
app,
|
||||
flow,
|
||||
"/conversation",
|
||||
conversational=True,
|
||||
)
|
||||
```
|
||||
|
||||
Two requirements must hold for this to work:
|
||||
|
||||
- The Flow instance declares `conversational = True`.
|
||||
- The Flow exposes CrewAI's public, callable `stream_turn(message, session_id=...)`.
|
||||
|
||||
Detection is capability-based, not version-gated: the bridge checks that the Flow actually offers turn-based conversation, rather than keying off a version number.
|
||||
|
||||
<Warning>
|
||||
If those requirements are not met, the request fails loudly with a `RUN_ERROR` (code `AGUI_CREWAI_CONVERSATIONAL_FLOW_UNSUPPORTED`). It never silently falls back to regular kickoff semantics, so you always know exactly which path you are on.
|
||||
</Warning>
|
||||
|
||||
Authoring the Flow itself, including how you implement `stream_turn`, belongs to CrewAI's Conversational Flows documentation. This page stays at the registration and integration boundary.
|
||||
|
||||
## Session and state
|
||||
|
||||
Conversational Flows manage session state and history for you across turns. You do not re-thread history manually.
|
||||
|
||||
- The AG-UI `threadId` **is** the CrewAI conversation `session_id`. The same thread is the same conversation.
|
||||
- Before each turn the bridge hydrates the Flow's state and conversation history, then calls `stream_turn`. CrewAI restores the stored session state, and a per-request overlay reapplies the incoming AG-UI state and history so the browser's latest edits win over stale storage.
|
||||
|
||||
The result: from the backend author's side, each turn arrives already carrying the conversation's state, and CrewAI persists what you write for the next turn.
|
||||
|
||||
## Frontend parity
|
||||
|
||||
This is the point to hold onto: **Conversational Flows run through the same event pipeline as regular Flows, so the frontend code is identical.**
|
||||
|
||||
There is no Conversational-Flow-specific frontend API. Every feature in these guides works exactly the same way with a Conversational Flow as it does with a regular Flow, using the same hooks and components:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Tool-Based Generative UI" icon="puzzle-piece" href="/en/guides/frontend/tool-based-generative-ui">
|
||||
Map agent tool calls to your React components.
|
||||
</Card>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render the Flow's live state as it works.
|
||||
</Card>
|
||||
<Card title="Shared State" icon="arrows-rotate" href="/en/guides/frontend/shared-state">
|
||||
Keep agent state and app UI in two-way sync.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Pause the agent for user approval or input mid-turn.
|
||||
</Card>
|
||||
<Card title="Predictive State" icon="gauge-high" href="/en/guides/frontend/predictive-state-updates">
|
||||
Stream in-progress tool arguments into state.
|
||||
</Card>
|
||||
<Card title="Reasoning" icon="brain" href="/en/guides/frontend/reasoning">
|
||||
Show the model's thinking in the chat.
|
||||
</Card>
|
||||
<Card title="A2UI" icon="table-cells" href="/en/guides/frontend/a2ui">
|
||||
Render agent-authored UI from a component catalog.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
The only difference is on the backend: how you author the Flow (turn-based `stream_turn` with managed session state) and the `conversational=True` registration. Once the endpoint is up, everything you already know about building the frontend applies unchanged.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Frontend Overview" icon="browser" href="/en/guides/frontend/overview">
|
||||
Wire a Crew or Flow to a Next.js frontend end to end.
|
||||
</Card>
|
||||
<Card title="Generative UI" icon="wand-magic-sparkles" href="/en/guides/frontend/generative-ui">
|
||||
Render tool calls and agent state as custom components.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Gate agent actions behind user approval.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
121
docs/edge/en/guides/frontend/frontend-actions.mdx
Normal file
121
docs/edge/en/guides/frontend/frontend-actions.mdx
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: Frontend Actions
|
||||
description: Let your CrewAI agent call functions that run in the user's browser, from switching themes to navigating your app.
|
||||
icon: bolt
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Let the agent act on the app
|
||||
|
||||
A frontend action is a tool the agent calls that runs code in the browser instead of on the server. The model decides to invoke it; your handler switches the theme, navigates, highlights an element, or updates your app data; and the result flows back to the agent.
|
||||
|
||||
It uses the same hook as tool-based generative UI, `useFrontendTool`. The difference is what you give it: a `handler` that runs code, instead of (or alongside) a `render` that draws UI.
|
||||
|
||||
<Note>
|
||||
Frontend actions work with both Crews and Flows. Any agent that binds `copilotkit.actions` into its LLM call can invoke them.
|
||||
</Note>
|
||||
|
||||
## Build a frontend action
|
||||
|
||||
The example below lets the agent switch the app into dark mode on request.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Register the action on the frontend">
|
||||
|
||||
Call `useFrontendTool` with a `handler`. The handler runs in the browser when the agent invokes the tool, and the string it returns is fed back to the agent.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useFrontendTool } from "@copilotkit/react-core/v2";
|
||||
import { z } from "zod";
|
||||
|
||||
useFrontendTool({
|
||||
agentId: "assistant",
|
||||
name: "set_theme",
|
||||
description: "Switch the app between light and dark mode.",
|
||||
parameters: z.object({
|
||||
theme: z.enum(["light", "dark"]),
|
||||
}),
|
||||
followUp: false,
|
||||
handler: async ({ theme }) => {
|
||||
document.documentElement.dataset.theme = theme; // runs in the browser
|
||||
return `Theme set to ${theme}.`;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The arguments:
|
||||
|
||||
- **`name`** — the tool name the model calls (`set_theme`).
|
||||
- **`description`** — a short explanation of what the tool does. The model reads it to decide *when* to call the tool, so make it specific. Omitting it leaves the model guessing from the name alone.
|
||||
- **`parameters`** — a [zod](https://zod.dev) schema describing the arguments the model must supply. CopilotKit turns this into the tool's JSON schema and validates the incoming call.
|
||||
- **`handler(args)`** — runs in the browser with the parsed arguments. Do your side effect here (set the theme, navigate, update state). The string you return is handed back to the agent as the tool result.
|
||||
- **`followUp: false`** — stops the agent from taking another turn after the action runs. Leave it out (or set `true`) when you want the agent to respond after acting.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Bind the frontend tools on the backend">
|
||||
|
||||
The agent can only call a tool it has been given. In your Flow, pass the frontend-registered tools into the LLM `tools` list with `*self.state.copilotkit.actions`.
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, start
|
||||
from litellm import acompletion
|
||||
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
|
||||
|
||||
class AssistantFlow(Flow[CopilotKitState]):
|
||||
@start()
|
||||
async def chat(self):
|
||||
response = await copilotkit_stream(
|
||||
await acompletion(
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": "Help the user. Use the tools available to control the app."},
|
||||
*self.state.messages,
|
||||
],
|
||||
tools=[*self.state.copilotkit.actions], # tools the frontend registered
|
||||
parallel_tool_calls=False,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
```
|
||||
|
||||
`self.state.copilotkit.actions` holds the tool definitions for every frontend action registered with `useFrontendTool`. Spreading them into the LLM `tools` list is what makes the agent able to invoke browser-side actions. `copilotkit_stream` streams the response, including the tool call, back to the frontend, where CopilotKit runs the matching handler.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Serve the Flow">
|
||||
|
||||
Expose the Flow over AG-UI with `add_crewai_flow_fastapi_endpoint(...)` and register it in the CopilotKit runtime, exactly as in the [Frontend Overview](/en/guides/frontend/overview). Once both are running, asking the assistant to "switch to dark mode" triggers `set_theme`, and the page flips.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Actions vs. generative UI
|
||||
|
||||
`useFrontendTool` covers both ends of a spectrum, and you pick per tool:
|
||||
|
||||
| You provide | What it does |
|
||||
| --- | --- |
|
||||
| **`handler`** | Runs code in the browser (a frontend action) |
|
||||
| **`render`** | Draws UI for the tool call (generative UI) |
|
||||
|
||||
You can supply either one, or both. A `handler` with a `render` alongside it performs the action and draws UI while it runs. For render-only tools that just display the result of an agent action, see [Tool-Based Generative UI](/en/guides/frontend/tool-based-generative-ui).
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Tool-Based Generative UI" icon="puzzle-piece" href="/en/guides/frontend/tool-based-generative-ui">
|
||||
Map agent tool calls to React components.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Gate agent actions behind user approval.
|
||||
</Card>
|
||||
<Card title="Shared State" icon="arrows-rotate" href="/en/guides/frontend/shared-state">
|
||||
Keep agent state and your app UI in two-way sync.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
107
docs/edge/en/guides/frontend/generative-ui.mdx
Normal file
107
docs/edge/en/guides/frontend/generative-ui.mdx
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Generative UI
|
||||
description: Render your CrewAI agent's work as live React components, across the full spectrum from author-controlled to agent-invented UI.
|
||||
icon: wand-magic-sparkles
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Beyond the chat bubble
|
||||
|
||||
Generative UI means the agent's work shows up as real interface, not just text. When your Crew or Flow calls a tool, updates its state, or reasons about a problem, you decide what the user sees: a progress checklist, a recipe card, a chart, a whole assembled panel.
|
||||
|
||||
CopilotKit renders generative UI along a **spectrum**, from fully author-controlled (you decide every pixel) to agent-invented (the agent assembles the surface):
|
||||
|
||||
| Tier | Who decides the UI | CrewAI mechanism |
|
||||
| --- | --- | --- |
|
||||
| **[Controlled](#controlled)** | You — a fixed set of components the agent picks from | `useRenderTool`, `useAgent`, reasoning |
|
||||
| **[Declarative](#declarative)** | The agent — assembles a surface from *your* component catalog | [A2UI](/en/guides/frontend/a2ui) |
|
||||
| **[Open-ended](#open-ended)** | An external tool/server invents the surface | MCP tools |
|
||||
|
||||
The tiers compose freely; a single app usually mixes them.
|
||||
|
||||
## Controlled
|
||||
|
||||
You own the components. The agent chooses which to show and with what data. This is the most predictable tier and where most apps start.
|
||||
|
||||
### Tool rendering
|
||||
|
||||
The agent calls a tool on the backend. You register a matching component on the frontend with `useRenderTool`, and CopilotKit renders it, streaming the arguments in as they arrive.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useRenderTool } from "@copilotkit/react-core/v2";
|
||||
import { z } from "zod";
|
||||
|
||||
useRenderTool({
|
||||
name: "generate_recipe",
|
||||
parameters: z.object({
|
||||
title: z.string(),
|
||||
ingredients: z.array(z.string()),
|
||||
}),
|
||||
render: ({ args }) => <RecipeCard title={args.title} ingredients={args.ingredients} />,
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`useRenderTool` renders a tool call. When a tool also needs to *run* code in the browser, use [`useFrontendTool`](/en/guides/frontend/frontend-actions) (a `handler`, with optional `render`).
|
||||
</Note>
|
||||
|
||||
See [Tool-Based Generative UI](/en/guides/frontend/tool-based-generative-ui) for the full walkthrough, including progressive rendering as arguments stream, and [Backend Tool Rendering](/en/guides/frontend/tool-based-generative-ui#backend-tools) for tools your Crew or Flow executes server-side.
|
||||
|
||||
### State rendering
|
||||
|
||||
Instead of reacting to a single tool call, render the agent's **state** as it changes. This is the right pattern for multi-step work: read the agent's working state with `useAgent` and paint it however you like.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useAgent } from "@copilotkit/react-core/v2";
|
||||
|
||||
function TaskProgress() {
|
||||
const { agent } = useAgent({ agentId: "task_runner" });
|
||||
const steps = agent?.state?.steps ?? [];
|
||||
return <StepList steps={steps} />;
|
||||
}
|
||||
```
|
||||
|
||||
See [Agentic Generative UI](/en/guides/frontend/agentic-generative-ui) for streaming state from a Flow, and [Shared State](/en/guides/frontend/shared-state) for editing that state from the UI.
|
||||
|
||||
### Reasoning
|
||||
|
||||
When the model reasons before answering, that thinking renders in the chat automatically. No component to write. See [Reasoning](/en/guides/frontend/reasoning).
|
||||
|
||||
## Declarative
|
||||
|
||||
The agent goes beyond picking a component: it **assembles a surface** by combining building blocks from a catalog *you* define. You still own the components (the agent can only use what is in your catalog), but the layout is the agent's.
|
||||
|
||||
This is [A2UI](/en/guides/frontend/a2ui). You register a catalog on the provider:
|
||||
|
||||
```tsx
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" agent="assistant" a2ui={{ catalog }}>
|
||||
{/* ... */}
|
||||
</CopilotKit>
|
||||
```
|
||||
|
||||
The agent then builds surfaces from that catalog — either dynamically (it designs the layout from the conversation) or from a fixed schema your backend fills with data. See [A2UI](/en/guides/frontend/a2ui) for both modes and error recovery.
|
||||
|
||||
## Open-ended
|
||||
|
||||
At the far end, the surface is invented outside your app entirely. For CrewAI this comes through **MCP**: tools served by an MCP server the agent connects to render as tool calls in the chat, the same way backend tools do. This is the least constrained and the least predictable tier.
|
||||
|
||||
MCP tool calls surface as standard tool-call UI — render them with `useRenderTool` like any other tool. Full agent-invented "MCP App" surfaces are an emerging capability; see the [CopilotKit docs](https://docs.copilotkit.ai) for the current state.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Tool-Based Generative UI" icon="puzzle-piece" href="/en/guides/frontend/tool-based-generative-ui">
|
||||
Map agent tool calls to components (controlled).
|
||||
</Card>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state (controlled).
|
||||
</Card>
|
||||
<Card title="A2UI" icon="table-cells" href="/en/guides/frontend/a2ui">
|
||||
Let the agent assemble surfaces from your catalog (declarative).
|
||||
</Card>
|
||||
<Card title="Reasoning" icon="brain" href="/en/guides/frontend/reasoning">
|
||||
Render the agent's thinking.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
191
docs/edge/en/guides/frontend/human-in-the-loop.mdx
Normal file
191
docs/edge/en/guides/frontend/human-in-the-loop.mdx
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Human-in-the-Loop
|
||||
description: Pause your CrewAI agent mid-run to collect a user decision, then resume the agent with their answer.
|
||||
icon: user-check
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Put the user in the loop
|
||||
|
||||
Some steps should not happen without a human saying yes. Human-in-the-loop pauses the agent mid-run, renders an interactive component in the frontend, and waits. The user makes a choice; the agent resumes with that choice and continues.
|
||||
|
||||
The mechanism is a tool the frontend registers. When the model calls it, the run halts at that tool call until the user responds. Nothing happens automatically: the agent stays parked until `respond()` hands control back.
|
||||
|
||||
In the example below, the agent proposes a list of task steps. The user enables or disables each step and confirms. The agent then continues, respecting exactly what the user approved.
|
||||
|
||||
<Note>
|
||||
This pattern works with Flows. It relies on the Flow's chat loop re-entering after `respond()`: the returned value comes back as a tool result, and the agent's next turn acts on it.
|
||||
</Note>
|
||||
|
||||
## Build it
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Bind the frontend actions into the model's tools">
|
||||
|
||||
In your Flow, add the frontend-registered actions to the model's tool list with `*self.state.copilotkit.actions`. Those actions are the tools your frontend registered (via `useHumanInTheLoop`). Binding them lets the model call them; the run pauses at that tool call until the user responds.
|
||||
|
||||
```python
|
||||
# human_in_the_loop_flow.py
|
||||
from crewai.flow.flow import Flow, start, router, listen
|
||||
from litellm import acompletion
|
||||
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
|
||||
|
||||
|
||||
class HumanInTheLoopFlow(Flow[CopilotKitState]):
|
||||
@start()
|
||||
@listen("route_follow_up")
|
||||
async def start_flow(self):
|
||||
pass
|
||||
|
||||
@router(start_flow)
|
||||
async def chat(self):
|
||||
system_prompt = (
|
||||
"You perform tasks for the user. When asked to do a task, call the "
|
||||
"tool the frontend provides so the user can approve or adjust the steps "
|
||||
"before you continue."
|
||||
)
|
||||
|
||||
response = await copilotkit_stream(
|
||||
await acompletion(
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
*self.state.messages,
|
||||
],
|
||||
tools=[*self.state.copilotkit.actions], # tools registered by the frontend
|
||||
parallel_tool_calls=False,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
return "route_end"
|
||||
|
||||
@listen("route_end")
|
||||
async def end(self):
|
||||
pass
|
||||
```
|
||||
|
||||
`CopilotKitState` carries the frontend-registered actions on `self.state.copilotkit.actions`. When the model calls one, the run pauses there. After the user responds, the returned value lands in `self.state.messages` as the tool result, and the Flow loops back through `chat` so the model can act on the decision.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Serve the Flow over AG-UI">
|
||||
|
||||
Expose the Flow from your FastAPI server with `add_crewai_flow_fastapi_endpoint`, the same way as every other agent. See [Frontend Overview](/en/guides/frontend/overview) for the full server, runtime, and provider setup.
|
||||
|
||||
```python
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
|
||||
from my_agents.human_in_the_loop_flow import HumanInTheLoopFlow
|
||||
|
||||
app = FastAPI(title="CrewAI Agent Server")
|
||||
|
||||
add_crewai_flow_fastapi_endpoint(
|
||||
app=app,
|
||||
flow=HumanInTheLoopFlow(),
|
||||
path="/human_in_the_loop",
|
||||
)
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Register the interactive tool on the frontend">
|
||||
|
||||
`useHumanInTheLoop` registers the tool the agent pauses on and gives you a `render` function to draw the interactive UI. When the agent calls the tool, your component appears; when the user acts, you call `respond()` to resume the agent.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
|
||||
import { z } from "zod";
|
||||
|
||||
useHumanInTheLoop({
|
||||
agentId: "human_in_the_loop",
|
||||
name: "generate_task_steps",
|
||||
parameters: z.object({
|
||||
steps: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
status: z.enum(["enabled", "disabled", "executing"]),
|
||||
})
|
||||
),
|
||||
}),
|
||||
render: ({ args, respond, status }) => (
|
||||
<StepReview
|
||||
steps={args.steps ?? []}
|
||||
// `status === "executing"` means the agent is waiting for the user
|
||||
waiting={status === "executing"}
|
||||
onConfirm={(chosen) => respond?.(chosen)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
The `render` function receives:
|
||||
|
||||
- **`args`** — the tool arguments the model produced (here, the proposed `steps`). These stream in as the model generates them.
|
||||
- **`status`** — the tool call's lifecycle. While it is `"executing"`, the agent is paused and waiting on the human.
|
||||
- **`respond(value)`** — resumes the agent with the user's decision. The agent's next turn sees the returned value and acts on it.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Let the user decide, then respond">
|
||||
|
||||
Your component reads `args.steps`, lets the user toggle each one, and calls `respond()` with the final selection. That value is what the agent continues with.
|
||||
|
||||
```tsx
|
||||
function StepReview({ steps, waiting, onConfirm }) {
|
||||
const [choices, setChoices] = useState(steps);
|
||||
|
||||
const toggle = (i) =>
|
||||
setChoices((prev) =>
|
||||
prev.map((s, idx) =>
|
||||
idx === i
|
||||
? { ...s, status: s.status === "enabled" ? "disabled" : "enabled" }
|
||||
: s
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{choices.map((step, i) => (
|
||||
<label key={i}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={step.status === "enabled"}
|
||||
disabled={!waiting}
|
||||
onChange={() => toggle(i)}
|
||||
/>
|
||||
{step.description}
|
||||
</label>
|
||||
))}
|
||||
<button disabled={!waiting} onClick={() => onConfirm(choices)}>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Once the user clicks Confirm, `respond()` fires, the run resumes, and the Flow's `chat` step runs again with the user's choices in the message history.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Frontend Actions" icon="bolt" href="/en/guides/frontend/frontend-actions">
|
||||
Let the agent call functions that run in the browser.
|
||||
</Card>
|
||||
<Card title="Shared State" icon="arrows-rotate" href="/en/guides/frontend/shared-state">
|
||||
Keep agent state and your app UI in two-way sync.
|
||||
</Card>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state as custom components.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
238
docs/edge/en/guides/frontend/overview.mdx
Normal file
238
docs/edge/en/guides/frontend/overview.mdx
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: Frontend Overview
|
||||
description: Build interactive user interfaces for your CrewAI agents with CopilotKit and the AG-UI protocol.
|
||||
icon: browser
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Give your agents a user interface
|
||||
|
||||
CrewAI runs your agents. [CopilotKit](https://copilotkit.ai) gives them a frontend. Together they let you build applications where users chat with a Crew or Flow, watch it work in real time, approve its decisions, and see its output rendered as live UI instead of walls of text.
|
||||
|
||||
The two connect through the [AG-UI protocol](https://docs.ag-ui.com). The `ag-ui-crewai` package exposes any Crew or Flow as an AG-UI endpoint. CopilotKit's React hooks and components consume that endpoint. This unlocks experiences that go well beyond a chat box:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Generative UI" icon="wand-magic-sparkles" href="/en/guides/frontend/generative-ui">
|
||||
Render agent tool calls and state as your own React components.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Pause the agent to collect user approval or input mid-run.
|
||||
</Card>
|
||||
<Card title="Shared State" icon="arrows-rotate" href="/en/guides/frontend/shared-state">
|
||||
Keep agent state and your app UI in two-way sync.
|
||||
</Card>
|
||||
<Card title="Channels" icon="slack" href="/en/guides/frontend/channels">
|
||||
Run the same agent as a Slack, Discord, or Teams bot.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
This guide gets a Crew or Flow talking to a Next.js frontend end to end. The rest of the section builds on the app you set up here.
|
||||
|
||||
## Architecture
|
||||
|
||||
There are three pieces:
|
||||
|
||||
1. **CrewAI agent server** — a Python process that serves your Crew or Flow over AG-UI (FastAPI + `ag-ui-crewai`).
|
||||
2. **CopilotKit runtime** — a Next.js route that registers your agent and proxies requests to it.
|
||||
3. **React frontend** — the `<CopilotKit>` provider plus chat and generative-UI components.
|
||||
|
||||
```
|
||||
React app ──► CopilotKit runtime (/api/copilotkit) ──► CrewAI server (AG-UI) ──► Crew / Flow
|
||||
```
|
||||
|
||||
<Note>
|
||||
This guide covers the **self-hosted** path: you run the CrewAI agent server yourself with `ag-ui-crewai`, and it works locally with no managed service. CopilotKit also offers a **managed** path (CopilotKit Cloud / Enterprise Intelligence) with hosted threads and an inspector — see the [CopilotKit CrewAI quickstart](https://docs.copilotkit.ai/crewai-crews/quickstart) if you want that instead. The frontend code in this section is the same either way; only how the agent is hosted and registered differs.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
CrewAI runs behind AG-UI in three shapes: regular **Flows** (used throughout these guides), **[Conversational Flows](/en/guides/frontend/conversational-flows)** (native, session-aware, turn-based, at full feature parity), and **Crews** (basic chat). The frontend in this section is identical across them — only the backend authoring and registration differ.
|
||||
</Note>
|
||||
|
||||
## Integration guide
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Serve your agent over AG-UI">
|
||||
|
||||
Install the integration package into your CrewAI project:
|
||||
|
||||
```bash
|
||||
pip install ag-ui-crewai
|
||||
```
|
||||
|
||||
Expose your agent from a FastAPI app. Flows use `add_crewai_flow_fastapi_endpoint`; Crews use `add_crewai_crew_fastapi_endpoint`. You can register as many as you want, each on its own path.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Flow
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
|
||||
from my_agents.recipe_flow import RecipeFlow
|
||||
|
||||
app = FastAPI(title="CrewAI Agent Server")
|
||||
|
||||
add_crewai_flow_fastapi_endpoint(
|
||||
app=app,
|
||||
flow=RecipeFlow(),
|
||||
path="/recipe",
|
||||
)
|
||||
```
|
||||
|
||||
```python Crew
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_crewai.endpoint import add_crewai_crew_fastapi_endpoint
|
||||
from my_agents.research_crew import ResearchCrew
|
||||
|
||||
app = FastAPI(title="CrewAI Agent Server")
|
||||
|
||||
add_crewai_crew_fastapi_endpoint(
|
||||
app=app,
|
||||
crew=ResearchCrew().crew(),
|
||||
path="/research",
|
||||
)
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
<Note>
|
||||
Set the environment variables for your LLM provider (for example `OPENAI_API_KEY`) before starting the server.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Create a Next.js app">
|
||||
|
||||
If you do not have a frontend yet, scaffold one:
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest my-app
|
||||
cd my-app
|
||||
```
|
||||
|
||||
Install CopilotKit and the CrewAI AG-UI client:
|
||||
|
||||
```bash
|
||||
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime @ag-ui/crewai
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add the CopilotKit runtime">
|
||||
|
||||
Create a route that registers your CrewAI agent(s) with the CopilotKit runtime. Each agent points at a path on your Python server via `CrewAIAgent`.
|
||||
|
||||
```ts
|
||||
// app/api/copilotkit/route.ts
|
||||
import {
|
||||
CopilotRuntime,
|
||||
InMemoryAgentRunner,
|
||||
createCopilotEndpoint,
|
||||
} from "@copilotkit/runtime/v2";
|
||||
import { CrewAIAgent } from "@ag-ui/crewai";
|
||||
import { handle } from "hono/vercel";
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
agents: {
|
||||
recipe: new CrewAIAgent({ url: "http://localhost:8000/recipe" }),
|
||||
},
|
||||
runner: new InMemoryAgentRunner(),
|
||||
});
|
||||
|
||||
const app = createCopilotEndpoint({
|
||||
runtime,
|
||||
basePath: "/api/copilotkit",
|
||||
});
|
||||
|
||||
const handler = handle(app);
|
||||
export const GET = handler;
|
||||
export const POST = handler;
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Wrap your app with the provider">
|
||||
|
||||
Point `<CopilotKit>` at the runtime route and name the agent you registered.
|
||||
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
"use client";
|
||||
import { CopilotKit } from "@copilotkit/react-core";
|
||||
import { CopilotSidebar } from "@copilotkit/react-core/v2";
|
||||
import "@copilotkit/react-core/v2/styles.css";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<CopilotKit runtimeUrl="/api/copilotkit" agent="recipe">
|
||||
<YourApp />
|
||||
<CopilotSidebar agentId="recipe" labels={{ modalHeaderTitle: "Assistant" }} />
|
||||
</CopilotKit>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run it">
|
||||
|
||||
Start both processes and open the app. Chatting in the sidebar now runs your Crew or Flow.
|
||||
|
||||
```bash
|
||||
uvicorn server:app --port 8000 # terminal 1
|
||||
npm run dev # terminal 2
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Chat UI options
|
||||
|
||||
CopilotKit ships three interchangeable chat surfaces. Swap the component; the wiring is identical.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```tsx Sidebar
|
||||
import { CopilotSidebar } from "@copilotkit/react-core/v2";
|
||||
|
||||
<CopilotSidebar agentId="recipe" />
|
||||
```
|
||||
|
||||
```tsx Popup
|
||||
import { CopilotPopup } from "@copilotkit/react-core/v2";
|
||||
|
||||
<CopilotPopup agentId="recipe" />
|
||||
```
|
||||
|
||||
```tsx Inline
|
||||
import { CopilotChat } from "@copilotkit/react-core/v2";
|
||||
|
||||
<CopilotChat agentId="recipe" />
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Where to go next
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Generative UI" icon="wand-magic-sparkles" href="/en/guides/frontend/generative-ui">
|
||||
Render tool calls and agent state as custom components.
|
||||
</Card>
|
||||
<Card title="Frontend Actions" icon="bolt" href="/en/guides/frontend/frontend-actions">
|
||||
Let the agent call functions that run in the browser.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Gate agent actions behind user approval.
|
||||
</Card>
|
||||
<Card title="Predictive State" icon="gauge-high" href="/en/guides/frontend/predictive-state-updates">
|
||||
Stream in-progress state to the UI as the agent works.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
142
docs/edge/en/guides/frontend/predictive-state-updates.mdx
Normal file
142
docs/edge/en/guides/frontend/predictive-state-updates.mdx
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Predictive State Updates
|
||||
description: Stream an in-progress tool call's arguments into agent state so the UI updates optimistically while the agent is still generating.
|
||||
icon: gauge-high
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Show the work as it happens
|
||||
|
||||
Normally a tool call is atomic from the UI's point of view: the agent decides what to write, and your interface only sees the result once the call finishes. For a tool that produces a large document that means a long pause followed by everything snapping into place at once.
|
||||
|
||||
Predictive state updates remove the wait. You project a streaming tool argument onto a field of the agent's state, so as the model generates the argument token by token, that state field fills in live. A document the agent is writing appears in the editor as it is typed, not after.
|
||||
|
||||
<Note>
|
||||
Predictive state relies on a Flow with custom state (`Flow[AgentState]`). It projects a streaming tool argument onto a state field, so there is no equivalent for a bare Crew.
|
||||
</Note>
|
||||
|
||||
## How it compares to Shared State
|
||||
|
||||
Both patterns read the agent's state from the frontend, but they solve different problems:
|
||||
|
||||
| Pattern | What it does |
|
||||
| --- | --- |
|
||||
| **Predictive state** | One-way. Streams an in-progress tool argument into a state field so the UI updates *during* generation, before the call completes. |
|
||||
| **[Shared State](/en/guides/frontend/shared-state)** | Two-way. The UI reads *and writes* the agent's committed state, keeping app and agent in sync across turns. |
|
||||
|
||||
Reach for predictive state when you want an optimistic, in-flight preview of what the agent is producing. Reach for [Shared State](/en/guides/frontend/shared-state) when the user needs to edit that state back.
|
||||
|
||||
## Walkthrough
|
||||
|
||||
This assumes you already have a Crew or Flow served over AG-UI and a CopilotKit frontend wired up. If not, start with the [Frontend Overview](/en/guides/frontend/overview).
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Define a Flow with custom state">
|
||||
|
||||
Predictive state projects a tool argument onto a state field, so your Flow needs a typed state field to receive it. Add the field you want to stream into to your `CopilotKitState` subclass.
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
from crewai.flow.flow import Flow, start, router, listen
|
||||
from litellm import acompletion
|
||||
from ag_ui_crewai.sdk import copilotkit_stream, copilotkit_predict_state, CopilotKitState
|
||||
|
||||
WRITE_DOCUMENT_TOOL = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_document",
|
||||
"description": "Write the full document in markdown.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"document": {"type": "string", "description": "The document to write"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
class AgentState(CopilotKitState):
|
||||
document: Optional[str] = None
|
||||
|
||||
class DocumentFlow(Flow[AgentState]):
|
||||
@start()
|
||||
@listen("route_follow_up")
|
||||
async def start_flow(self):
|
||||
pass
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Map a state field to a tool argument">
|
||||
|
||||
Call `copilotkit_predict_state` **before** you start streaming the completion. It tells the runtime to project the named tool argument onto the named state field: as the `write_document` call streams its `document` argument, the `document` state field updates live.
|
||||
|
||||
```python
|
||||
@router(start_flow)
|
||||
async def chat(self):
|
||||
# Map the `document` state field to the `document` argument of write_document.
|
||||
# As the tool call streams, the state field updates live.
|
||||
await copilotkit_predict_state({
|
||||
"document": {"tool_name": "write_document", "tool_argument": "document"},
|
||||
})
|
||||
|
||||
response = await copilotkit_stream(
|
||||
await acompletion(
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": "Write and edit the document with write_document."},
|
||||
*self.state.messages,
|
||||
],
|
||||
tools=[*self.state.copilotkit.actions, WRITE_DOCUMENT_TOOL],
|
||||
parallel_tool_calls=False,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
```
|
||||
|
||||
The key is `copilotkit_predict_state({ "<state_field>": {"tool_name": ..., "tool_argument": ...} })`. Without it, the frontend would only see `document` once the tool call completed. With it, the partial argument streams onto the field while the agent is still generating.
|
||||
|
||||
Serve the Flow with `add_crewai_flow_fastapi_endpoint(...)` as shown in the [Frontend Overview](/en/guides/frontend/overview).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Read the predicted state on the frontend">
|
||||
|
||||
On the frontend, read the field with `useAgent` and subscribe to state changes. Because the backend is projecting the streaming argument onto `document`, this component re-renders as the agent types.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";
|
||||
|
||||
function DocumentView() {
|
||||
const { agent } = useAgent({
|
||||
agentId: "document",
|
||||
updates: [UseAgentUpdate.OnStateChanged],
|
||||
});
|
||||
const document = (agent?.state as { document?: string })?.document ?? "";
|
||||
return <article>{document}</article>; // updates as the agent types
|
||||
}
|
||||
```
|
||||
|
||||
The `document` field fills in progressively as the agent generates the `write_document` call, so the editor updates in real time rather than snapping in at the end.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Shared State" icon="arrows-rotate" href="/en/guides/frontend/shared-state">
|
||||
Read and write the agent's state two-way.
|
||||
</Card>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state as it changes.
|
||||
</Card>
|
||||
<Card title="Tool-Based Generative UI" icon="puzzle-piece" href="/en/guides/frontend/tool-based-generative-ui">
|
||||
Map agent tool calls to components.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
68
docs/edge/en/guides/frontend/reasoning.mdx
Normal file
68
docs/edge/en/guides/frontend/reasoning.mdx
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Reasoning
|
||||
description: Show the model's thinking in the chat automatically, with no component to build.
|
||||
icon: brain
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Thinking, rendered for free
|
||||
|
||||
When a reasoning-capable model thinks before it answers, CopilotKit renders that thinking right in the chat. This is the simplest generative-UI pattern in the whole section: there is nothing to build. No hook, no component, no props. Use a reasoning-capable model, keep the streaming wrapper your Flows already have, and the chat surface from the [Overview](/en/guides/frontend/overview) does the rest.
|
||||
|
||||
## Use a reasoning-capable model
|
||||
|
||||
Reasoning is surfaced automatically by `copilotkit_stream`, which every Flow example already wraps the model call in. The bridge reads the model's reasoning deltas and emits them to the frontend. It is provider-agnostic and works over both of CrewAI's streaming transports, so the only thing you change is the model.
|
||||
|
||||
```python
|
||||
# recipe_flow.py
|
||||
from crewai.flow.flow import Flow, start
|
||||
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
|
||||
from litellm import acompletion
|
||||
|
||||
|
||||
class RecipeFlow(Flow[CopilotKitState]):
|
||||
@start()
|
||||
async def chat(self):
|
||||
response = await copilotkit_stream(
|
||||
acompletion(
|
||||
# any reasoning-capable model, e.g. deepseek-reasoner
|
||||
model="deepseek/deepseek-reasoner",
|
||||
messages=self.state.messages,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
```
|
||||
|
||||
Models that emit reasoning over the standard channel include DeepSeek `deepseek-reasoner`, Anthropic extended thinking (Claude), and Gemini thinking, among others. Swap the `model` for one of these and its thinking starts streaming through.
|
||||
|
||||
This works the same for both Crews and Flows, since both run their model calls through `copilotkit_stream`.
|
||||
|
||||
## Render it
|
||||
|
||||
There is no frontend step. The `CopilotChat`, `CopilotSidebar`, or `CopilotPopup` surface you already mounted shows the reasoning as it streams, above the answer it produced.
|
||||
|
||||
```tsx
|
||||
import { CopilotChat } from "@copilotkit/react-core/v2";
|
||||
|
||||
<CopilotChat agentId="recipe" />
|
||||
```
|
||||
|
||||
<Note>
|
||||
There is no `useReasoning` hook and no reasoning component to write. Reasoning is not something you wire up on the frontend; it renders automatically as long as the model emits it.
|
||||
</Note>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Generative UI" icon="wand-magic-sparkles" href="/en/guides/frontend/generative-ui">
|
||||
The full spectrum, from author-controlled to agent-invented UI.
|
||||
</Card>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state as the Flow works.
|
||||
</Card>
|
||||
<Card title="Frontend Overview" icon="browser" href="/en/guides/frontend/overview">
|
||||
Set up the chat surface and runtime.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
210
docs/edge/en/guides/frontend/shared-state.mdx
Normal file
210
docs/edge/en/guides/frontend/shared-state.mdx
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: Shared State
|
||||
description: Keep your CrewAI agent's state and your app's UI in two-way sync, so edits on either side flow to the other.
|
||||
icon: arrows-rotate
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## One state, both directions
|
||||
|
||||
Shared state is a single state object that the agent and the UI both read and write. The agent updates it as it works and your React components render it live. When the user edits that same state in the UI, the change flows back so the agent sees it on its next turn.
|
||||
|
||||
The classic example is a recipe: the agent drafts it, the user tweaks an ingredient or an instruction, and the agent picks up from the edited version. Neither side owns the state; they share it.
|
||||
|
||||
<Note>
|
||||
Shared state relies on a Flow with custom state. Define an `AgentState` that subclasses `CopilotKitState` and type your Flow as `Flow[AgentState]`. Crews do not carry custom state, so this pattern is Flow-only.
|
||||
</Note>
|
||||
|
||||
## How it works
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Define the shared state on your Flow">
|
||||
|
||||
Subclass `CopilotKitState` so the agent keeps CopilotKit's message plumbing, then add your own fields. Here the shared field is `recipe`.
|
||||
|
||||
```python
|
||||
# recipe_flow.py
|
||||
import json
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from crewai.flow.flow import Flow, start, router, listen
|
||||
from litellm import acompletion
|
||||
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
|
||||
|
||||
|
||||
class Ingredient(BaseModel):
|
||||
name: str
|
||||
amount: str
|
||||
|
||||
|
||||
class Recipe(BaseModel):
|
||||
title: str
|
||||
ingredients: List[Ingredient] = Field(default_factory=list)
|
||||
instructions: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AgentState(CopilotKitState):
|
||||
recipe: Optional[Recipe] = None
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Read and write the state from the agent">
|
||||
|
||||
The agent reads the current state by dumping it into the system prompt, and writes it back by assigning to `self.state.recipe`. A `generate_recipe` tool lets the model return the updated recipe as structured arguments.
|
||||
|
||||
```python
|
||||
GENERATE_RECIPE_TOOL = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "generate_recipe",
|
||||
"description": "Generate or modify the recipe.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"recipe": {"type": "object"}},
|
||||
"required": ["recipe"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class SharedStateFlow(Flow[AgentState]):
|
||||
@start()
|
||||
@listen("route_follow_up")
|
||||
async def start_flow(self):
|
||||
pass
|
||||
|
||||
@router(start_flow)
|
||||
async def chat(self):
|
||||
# The current shared state is visible to the model.
|
||||
system_prompt = f"""You help the user build a recipe.
|
||||
Current recipe: {self.state.model_dump_json(indent=2)}
|
||||
Modify it by calling generate_recipe."""
|
||||
|
||||
response = await copilotkit_stream(
|
||||
await acompletion(
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
*self.state.messages,
|
||||
],
|
||||
tools=[*self.state.copilotkit.actions, GENERATE_RECIPE_TOOL],
|
||||
parallel_tool_calls=False,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
call = message.tool_calls[0]
|
||||
if call.function.name == "generate_recipe":
|
||||
args = json.loads(call.function.arguments)
|
||||
self.state.recipe = Recipe(**args["recipe"]) # write to shared state
|
||||
self.state.messages.append({
|
||||
"role": "tool",
|
||||
"content": "Recipe updated.",
|
||||
"tool_call_id": call.id,
|
||||
})
|
||||
return "route_follow_up"
|
||||
return "route_end"
|
||||
|
||||
@listen("route_end")
|
||||
async def end(self):
|
||||
pass
|
||||
```
|
||||
|
||||
Two things make this shared rather than one-way: dumping `self.state` into the prompt means the agent always works from the latest recipe (including edits the user made in the UI), and assigning `self.state.recipe` puts the new value into the state snapshot sent to connected clients at the end of the step. For updates during a long step, emit explicitly with `copilotkit_emit_state` (see [Agentic Generative UI](/en/guides/frontend/agentic-generative-ui)).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Serve the Flow over AG-UI">
|
||||
|
||||
Expose the Flow from your FastAPI app with `add_crewai_flow_fastapi_endpoint`, then register it in the CopilotKit runtime. See the [Frontend Overview](/en/guides/frontend/overview) for the full server and runtime setup.
|
||||
|
||||
```python
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
|
||||
from recipe_flow import SharedStateFlow
|
||||
|
||||
app = FastAPI(title="CrewAI Agent Server")
|
||||
|
||||
add_crewai_flow_fastapi_endpoint(
|
||||
app=app,
|
||||
flow=SharedStateFlow(),
|
||||
path="/shared_state",
|
||||
)
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Read and write the state from the UI">
|
||||
|
||||
`useAgent` gives you both directions in one hook. Read the shared state off `agent.state`, and write it back with `agent.setState(...)`. Subscribe to `OnStateChanged` so your component re-renders whenever the agent updates the state.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";
|
||||
|
||||
function RecipeEditor() {
|
||||
const { agent } = useAgent({
|
||||
agentId: "shared_state",
|
||||
updates: [UseAgentUpdate.OnStateChanged],
|
||||
});
|
||||
|
||||
const state = agent?.state as { recipe?: Recipe } | undefined;
|
||||
const isLoading = agent?.isRunning;
|
||||
|
||||
const recipe = state?.recipe;
|
||||
|
||||
// setState replaces the whole state object, so spread the current
|
||||
// state and override only the field you changed. Passing just
|
||||
// `{ recipe }` would drop messages and other runtime fields.
|
||||
const updateRecipe = (patch: Partial<Recipe>) =>
|
||||
agent?.setState({ ...(agent.state ?? {}), recipe: { ...(recipe ?? {}), ...patch } });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={recipe?.title ?? ""}
|
||||
disabled={isLoading}
|
||||
onChange={(e) => updateRecipe({ title: e.target.value })}
|
||||
/>
|
||||
{/* render inputs for ingredients and instructions the same way */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`agent.state` reads the shared state, `agent.setState(...)` writes it back so the agent sees the change on its next turn, and `agent.isRunning` reflects whether the agent is currently working.
|
||||
|
||||
<Note>
|
||||
`setState` **replaces** the entire state object rather than merging. Always spread the current state (`{ ...agent.state, ... }`) and override only the fields you are changing, or you will drop the conversation and other runtime fields the agent depends on.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## The two-way loop
|
||||
|
||||
Putting the pieces together, a single recipe object is kept in sync in both directions:
|
||||
|
||||
- **Agent edits, UI updates.** The Flow assigns `self.state.recipe`, the new value ships in the step's state snapshot, and `OnStateChanged` re-renders your inputs.
|
||||
- **User edits, agent sees it.** A change in the UI calls `agent.setState(...)`, and because the Flow dumps `self.state` into its prompt, the agent works from the edited recipe on its next turn.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state as it changes.
|
||||
</Card>
|
||||
<Card title="Predictive State" icon="gauge-high" href="/en/guides/frontend/predictive-state-updates">
|
||||
Stream in-progress state to the UI as the agent works.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Pause the agent to collect user approval or input mid-run.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
235
docs/edge/en/guides/frontend/tool-based-generative-ui.mdx
Normal file
235
docs/edge/en/guides/frontend/tool-based-generative-ui.mdx
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
title: Tool-Based Generative UI
|
||||
description: Map a CrewAI agent's tool calls to React components and stream the arguments in as they arrive.
|
||||
icon: puzzle-piece
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Render tool calls as components
|
||||
|
||||
When your Crew or Flow calls a tool, you rarely want the raw arguments dumped into the chat. Tool-based generative UI maps each tool the agent calls to a React component you own. The agent decides *when* to call the tool; you decide what the user sees.
|
||||
|
||||
Because CopilotKit streams the tool call to the frontend as the model generates it, the arguments fill in progressively. Your component can paint the moment the first field arrives and update as the rest stream in.
|
||||
|
||||
This guide builds a haiku generator: the agent calls a `generate_haiku` tool, and the frontend renders each haiku as a card. It assumes you already have a Crew or Flow talking to a Next.js app. If not, start with the [Frontend Overview](/en/guides/frontend/overview) for the full server, runtime, and provider setup.
|
||||
|
||||
<Note>
|
||||
Tool rendering works with both Crews and Flows. The example below uses a Flow, but the frontend wiring is identical either way.
|
||||
</Note>
|
||||
|
||||
## Walkthrough
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Define the tool on the backend">
|
||||
|
||||
Declare the tool with a JSON schema and pass it to the model. The `copilotkit_stream` wrapper together with `stream=True` is what streams the tool call to the frontend as it is generated, one argument chunk at a time.
|
||||
|
||||
```python
|
||||
# haiku_flow.py
|
||||
from crewai.flow.flow import Flow, start
|
||||
from litellm import acompletion
|
||||
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
|
||||
|
||||
GENERATE_HAIKU_TOOL = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "generate_haiku",
|
||||
"description": "Generate a haiku in Japanese and its English translation",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"japanese": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Three lines in Japanese",
|
||||
},
|
||||
"english": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Three lines in English",
|
||||
},
|
||||
},
|
||||
"required": ["japanese", "english"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class HaikuFlow(Flow[CopilotKitState]):
|
||||
@start()
|
||||
async def chat(self):
|
||||
system_prompt = "You help the user write haikus. Use the generate_haiku tool."
|
||||
|
||||
response = await copilotkit_stream(
|
||||
await acompletion(
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
*self.state.messages,
|
||||
],
|
||||
tools=[GENERATE_HAIKU_TOOL],
|
||||
parallel_tool_calls=False,
|
||||
stream=True,
|
||||
)
|
||||
)
|
||||
|
||||
message = response.choices[0].message
|
||||
self.state.messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
self.state.messages.append({
|
||||
"tool_call_id": message.tool_calls[0].id,
|
||||
"role": "tool",
|
||||
"content": "Haiku generated.",
|
||||
})
|
||||
```
|
||||
|
||||
The tool has no Python implementation. It exists only so the model emits a structured call the frontend can render. After the call, append a short tool result so the conversation stays well-formed for the next turn.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Serve the Flow over AG-UI">
|
||||
|
||||
Expose the Flow from your FastAPI app on its own path:
|
||||
|
||||
```python
|
||||
# server.py
|
||||
from fastapi import FastAPI
|
||||
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
|
||||
from haiku_flow import HaikuFlow
|
||||
|
||||
app = FastAPI(title="CrewAI Agent Server")
|
||||
|
||||
add_crewai_flow_fastapi_endpoint(
|
||||
app=app,
|
||||
flow=HaikuFlow(),
|
||||
path="/haiku",
|
||||
)
|
||||
```
|
||||
|
||||
Register the agent with the CopilotKit runtime and point `<CopilotKit>` at it exactly as shown in the [Frontend Overview](/en/guides/frontend/overview). The rest of this guide assumes the agent is registered under the id `haiku`.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Register the rendering component">
|
||||
|
||||
On the frontend, call `useRenderTool` with the same `name` the backend declared. `useRenderTool` is the hook for *rendering* a tool call: it takes a `render` function and nothing to execute, because this tool is pure display.
|
||||
|
||||
<Note>
|
||||
Use `useRenderTool` when the tool only draws UI. If the tool also needs to *run* something in the browser, use [`useFrontendTool`](/en/guides/frontend/frontend-actions) instead, which pairs a `handler` with an optional `render`.
|
||||
</Note>
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useRenderTool } from "@copilotkit/react-core/v2";
|
||||
import { z } from "zod";
|
||||
|
||||
useRenderTool({
|
||||
name: "generate_haiku",
|
||||
parameters: z.object({
|
||||
japanese: z.array(z.string()),
|
||||
english: z.array(z.string()),
|
||||
}),
|
||||
render: ({ args, status }) => {
|
||||
if (!args.japanese) return <></>; // still streaming
|
||||
return <HaikuCard japanese={args.japanese} english={args.english} />;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The tool is scoped to the active agent by the `<CopilotKit agent="haiku">` provider, so no `agentId` is needed here. A few things to note:
|
||||
|
||||
- **`name` must match the backend tool name** exactly (`generate_haiku`). That match is how CopilotKit routes the call to this component.
|
||||
- **`render` receives `{ args, status }`.** `args` fills in progressively as the model streams the call; early on it may be empty or partial. `status` moves through `"inProgress"` / `"executing"` to `"complete"` if you want to show a loading state while arguments stream.
|
||||
- **Guard against partial args.** Return an empty fragment until the fields you need exist. Here we wait for `args.japanese` before rendering the card.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Render the haiku">
|
||||
|
||||
The `render` function delegates to an ordinary React component. Nothing about it is CopilotKit-specific: it takes props and returns markup.
|
||||
|
||||
```tsx
|
||||
function HaikuCard({
|
||||
japanese,
|
||||
english,
|
||||
}: {
|
||||
japanese: string[];
|
||||
english: string[];
|
||||
}) {
|
||||
return (
|
||||
<div className="haiku-card">
|
||||
{japanese.map((line, i) => (
|
||||
<div key={i} className="haiku-line">
|
||||
<span className="jp">{line}</span>
|
||||
<span className="en">{english?.[i]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Because `english` streams in alongside `japanese`, use optional access (`english?.[i]`) so the card renders cleanly while the translation is still arriving.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Run it">
|
||||
|
||||
Start both processes and ask the assistant for a haiku. The card renders as the arguments stream in, filling out line by line.
|
||||
|
||||
```bash
|
||||
uvicorn server:app --port 8000 # terminal 1
|
||||
npm run dev # terminal 2
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## How progressive rendering works
|
||||
|
||||
The model does not emit the tool call all at once. It streams tokens, and CopilotKit re-invokes your `render` function every time a new chunk of arguments arrives:
|
||||
|
||||
1. The call begins. `args` is empty, so your guard returns an empty fragment.
|
||||
2. `args.japanese` fills in line by line. The card appears and grows.
|
||||
3. `args.english` fills in. Translations slot into place.
|
||||
4. The call completes. `args` holds the final, fully-validated object.
|
||||
|
||||
This is why the partial-args guard matters: `render` runs against incomplete data by design. Read only the fields you have, and let the rest paint as they arrive.
|
||||
|
||||
## Backend tools
|
||||
|
||||
The `generate_haiku` tool above has no Python implementation — it exists only so the model emits a structured call the frontend renders. But a **real tool your Crew or Flow runs server-side** renders the same way.
|
||||
|
||||
When an Agent or Crew executes a tool during its run, the bridge surfaces that tool call along with its **result**. Register a `useRenderTool` for the tool's name and read `result` in the render:
|
||||
|
||||
```tsx
|
||||
useRenderTool({
|
||||
name: "get_weather",
|
||||
parameters: z.object({ location: z.string() }),
|
||||
render: ({ args, result, status }) => {
|
||||
if (status !== "complete") return <WeatherSkeleton location={args.location} />;
|
||||
return <WeatherCard data={JSON.parse(result)} />;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
A backend tool must return a **JSON string**, not a Python dict. The bridge stringifies tool output, so a raw dict arrives as a Python repr the browser cannot `JSON.parse`. Return `json.dumps(...)` from the tool.
|
||||
</Note>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Agentic Generative UI" icon="list-check" href="/en/guides/frontend/agentic-generative-ui">
|
||||
Render live agent state as it changes across a multi-step run.
|
||||
</Card>
|
||||
<Card title="Human-in-the-Loop" icon="user-check" href="/en/guides/frontend/human-in-the-loop">
|
||||
Pause the agent to collect user approval or input mid-run.
|
||||
</Card>
|
||||
<Card title="Frontend Actions" icon="bolt" href="/en/guides/frontend/frontend-actions">
|
||||
Let the agent call functions that run in the browser.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -59,9 +59,10 @@ own tracer provider, which is independent of the one described here.
|
||||
| Yes | Test Execution Data | Includes: crew's randomly generated key and ID, number of iterations, model name used, quality score (float), execution time (in seconds). All non-personal. |
|
||||
| Yes | Task Lifecycle Data | Includes: creation and execution start/end times, crew and task identifiers. Stored as spans with timestamps. No personal data. |
|
||||
| Yes | LLM Attributes | Includes: name, model_name, model, top_k, temperature, and class name of the LLM. All technical, non-personal data. |
|
||||
| Yes | Crew Deployment attempt using crewAI CLI | Includes: The fact a deploy is being made and crew id, and if it's trying to pull logs, no other data. |
|
||||
| Yes | Crew Deployment attempt using crewAI CLI | Includes: The fact a deploy is being made and crew id, whether it's trying to pull logs, and whether the deploy was started from a CLI command or from the run TUI. No project or crew contents. No personal data. |
|
||||
| Yes | Execution Environment | Includes: which AI coding assistant is running the process, if any (one of a fixed list such as `claude_code`, `codex`, `cursor`, or `unknown`), where the process runs (one of a fixed list such as `ci`, `container`, `serverless`, `interactive`), and the `project_id` from your `pyproject.toml` when one is configured. Detection reads only whether known environment variables are set, never their values. No personal data. |
|
||||
| Yes | Flow Lifecycle Signals | Includes: that a flow started, whether it completed or failed, whether one of its methods failed, whether it paused for human input or feedback, whether the start was a resumed run, whether a conversation turn failed, how long the flow ran, and whether the flow is one CrewAI runs internally or one you wrote. The flow name is recorded, as it already is for flow creation and execution. Method names, error messages and flow state are never recorded. No personal data. |
|
||||
| Yes | Flow Lifecycle Signals | Includes: that a flow started, whether it completed or failed, whether one of its methods failed, whether it paused for human input or feedback, whether the start was a resumed run, whether a conversation turn failed, how long the flow ran, and whether the flow is one CrewAI runs internally or one you wrote. The flow name is recorded (should not include personal info), as it already is for flow creation and execution. Method names, error messages and flow state are never recorded. No personal data. |
|
||||
| Yes | Trace Sharing Signal | Includes: that a batch of traces was successfully shared with CrewAI AMP, and whether it was shared anonymously (before you have an account) or linked to your account. Like every span, it also carries the Execution Environment attributes described above (`project_id` when configured, the coding assistant, and the runtime). This row describes sharing telemetry only — not the trace contents or access granted by shared trace links. Trace contents, inputs, and outputs are never recorded on this signal. Before sharing traces, review secrets, personal data, and AMP redaction and retention settings. |
|
||||
| No | Agent's Expanded Data | Includes: goal description, backstory text, i18n prompt file identifier. Users should ensure no personal info is included in text fields. |
|
||||
| No | Detailed Task Information | Includes: task description, expected output description, context references. Users should ensure no personal info is included in these fields. |
|
||||
| No | Environment Information | Includes: platform, release, system, version, and CPU count. Example: 'Windows 10', 'x86_64'. No personal data. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: File Read
|
||||
description: The `FileReadTool` is designed to read files from the local file system.
|
||||
description: The `FileReadTool` reads files from the local file system.
|
||||
icon: folders
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -8,73 +8,63 @@ mode: "wide"
|
||||
## Overview
|
||||
|
||||
<Note>
|
||||
We are still working on improving tools, so there might be unexpected behavior or changes in the future.
|
||||
We are still improving tools, so behavior may change.
|
||||
</Note>
|
||||
|
||||
The `FileReadTool` reads the contents of a file from the local file system and returns it as text.
|
||||
It is useful for batch text file processing, reading runtime configuration files, and importing data for analytics.
|
||||
It supports any text-based file format, such as `.txt`, `.csv`, `.json`, and `.md`.
|
||||
Content is always returned as plain text — parsing it (for example, `json.loads` on a `.json` file) is up to the agent or your own code.
|
||||
The `FileReadTool` reads a local file and returns its content as text.
|
||||
Use it to process text files, read config files, or load data for analysis.
|
||||
It works with any text format, such as `.txt`, `.csv`, `.json`, and `.md`.
|
||||
The tool always returns plain text. If you need structured data (for example, JSON), parse it in your agent or your own code.
|
||||
|
||||
For large files, `start_line` and `line_count` read just a window of lines instead of loading the whole file.
|
||||
For large files, the agent can pass `start_line` and `line_count` to read only a range of lines.
|
||||
The tool stops once it has those lines, so it does not scan the rest of the file.
|
||||
|
||||
## Installation
|
||||
|
||||
To utilize the functionalities previously attributed to the FileReadTool, install the crewai_tools package:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
uv add 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
To get started with the FileReadTool:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileReadTool
|
||||
|
||||
# Initialize the tool to read any file the agent knows or learns the path for
|
||||
file_read_tool = FileReadTool()
|
||||
# Agent chooses the file path at runtime
|
||||
tool = FileReadTool()
|
||||
|
||||
# OR
|
||||
# OR set a default file the agent can read with no path argument
|
||||
tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
|
||||
# Initialize with a specific file path, so the agent reads that file by default
|
||||
file_read_tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
|
||||
# Read a window of lines (lines 100-149) instead of the whole file
|
||||
partial_content = file_read_tool.run(
|
||||
file_path='path/to/your/file.txt',
|
||||
start_line=100,
|
||||
line_count=50,
|
||||
)
|
||||
# OR let the agent read any file under a directory
|
||||
tool = FileReadTool(base_dir='/data')
|
||||
```
|
||||
|
||||
Give the tool to an agent. At runtime the LLM passes `file_path`, and optionally `start_line` and `line_count`.
|
||||
|
||||
## Arguments
|
||||
|
||||
The agent supplies these at runtime:
|
||||
The agent can pass these at runtime:
|
||||
|
||||
- `file_path`: (Optional) The path to the file you want to read. Accepts absolute and relative paths. Ensure the file exists and you have the necessary permissions to access it. Omit it to read the default file configured at construction; if there is no default, the tool reports that no path was provided.
|
||||
- `start_line`: (Optional) The line number to start reading from (1-indexed). Defaults to `1`.
|
||||
- `line_count`: (Optional) The number of lines to read. If omitted, reads from `start_line` to the end of the file.
|
||||
- `file_path`: (Optional) Path to the file to read. Absolute and relative paths are both valid only when they resolve inside the `base_dir` sandbox. A relative path resolves against `base_dir` when set, otherwise against the current working directory (the default sandbox). Omit it to read the default file set at construction. If there is no default, the tool returns an error saying no path was provided.
|
||||
- `start_line`: (Optional) First line to read. Line numbers start at `1`. Default is `1`.
|
||||
- `line_count`: (Optional) How many lines to read. If omitted, the tool reads from `start_line` to the end of the file.
|
||||
|
||||
You set these when constructing the tool:
|
||||
You can set these when you create the tool:
|
||||
|
||||
- `file_path`: (Optional) A default file to read when the agent calls the tool with no arguments.
|
||||
- `base_dir`: (Optional) The directory that runtime paths must stay inside. Defaults to the current working directory.
|
||||
- `encoding`: (Optional) Text encoding used to decode the file. Defaults to `utf-8`.
|
||||
- `file_path`: (Optional) Default file to read when the agent calls the tool with no path. A relative path resolves against `base_dir` when `base_dir` is provided, otherwise against the current working directory.
|
||||
- `base_dir`: (Optional) Directory that runtime paths must stay inside. Default is the current working directory. The tool resolves this path when the tool is created, so a later change of working directory does not move the sandbox.
|
||||
- `encoding`: (Optional) Text encoding used to decode the file. Default is `utf-8`. If decoding fails, the tool returns an error and suggests passing a different `encoding`.
|
||||
|
||||
Common failures (missing file, permission denied, wrong encoding, or a path outside the sandbox) return an error string. They do not raise an exception.
|
||||
|
||||
## Allowed paths
|
||||
|
||||
Because the file path is usually chosen by an LLM at runtime, reads are confined to a sandbox:
|
||||
An LLM usually chooses the file path at runtime, so reads are limited to a sandbox:
|
||||
|
||||
- Paths supplied at runtime must resolve inside `base_dir`, which defaults to the current working directory. `..` segments and symlinks are resolved before the check, so they cannot be used to escape.
|
||||
- A `file_path` passed to the constructor is developer-declared intent, so it is always allowed past the containment check — even outside `base_dir`. The read itself can still fail if the file is missing, is a directory, or is not permitted. It is pinned when the tool is built, so a later change of working directory cannot repoint it, and the agent can address it either by omitting `file_path` or by using the name shown in the tool's description. Declaring one file does not expose its siblings.
|
||||
- Runtime paths must resolve inside `base_dir` (default: the current working directory). The tool resolves `..` segments and symlinks before it checks the path, so they cannot escape the sandbox.
|
||||
- A `file_path` you pass to the constructor is always allowed, even if it is outside `base_dir`. The read can still fail if the file is missing, is a directory, or cannot be accessed. That path is fixed when the tool is created, so a later change of working directory does not change which file it points to. The agent can read it by omitting `file_path`, or by using the name shown in the tool description. Declaring one file does not allow access to other files in the same folder.
|
||||
|
||||
To let an agent read a directory tree outside the working directory, point `base_dir` at it:
|
||||
To let an agent read files outside the working directory, set `base_dir` when you create the tool (see the example above).
|
||||
|
||||
```python Code
|
||||
# The agent may read anything under /data, and nothing outside it
|
||||
file_read_tool = FileReadTool(base_dir='/data')
|
||||
```
|
||||
|
||||
As a last resort, setting `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` disables path validation. This applies process-wide to every crewai-tools tool, including the SSRF protections on URL-fetching tools, so prefer `base_dir`.
|
||||
As a last resort, set `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` to turn off path checks. This setting applies to every crewai-tools tool in the process, including SSRF protections on URL-fetching tools. Prefer `base_dir` instead.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: File Write
|
||||
description: Writes text content to a file.
|
||||
description: The `FileWriterTool` is designed to write content to files.
|
||||
icon: file-pen
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -9,25 +9,29 @@ mode: "wide"
|
||||
|
||||
## Description
|
||||
|
||||
`FileWriterTool` writes text content to a file. Missing directories are created
|
||||
automatically. The default text encoding is UTF-8.
|
||||
|
||||
Writes are confined to a sandbox directory. By default that directory is the
|
||||
current working directory. Pass `base_dir` to change it.
|
||||
The `FileWriterTool` is a component of the crewai_tools package, designed to simplify the process of writing content to files with cross-platform compatibility (Windows, Linux, macOS).
|
||||
It is particularly useful in scenarios such as generating reports, saving logs, creating configuration files, and more.
|
||||
This tool handles path differences across operating systems, writes UTF-8 by default rather than the platform's locale encoding, and automatically creates directories if they don't exist, making it easier to organize your output reliably across different platforms.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the crewai_tools package to use the `FileWriterTool` in your projects:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
To get started with the `FileWriterTool`:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileWriterTool
|
||||
|
||||
# Initialize the tool
|
||||
file_writer_tool = FileWriterTool()
|
||||
|
||||
# Write content to a file in a specified directory
|
||||
result = file_writer_tool.run(
|
||||
filename='example.txt',
|
||||
content='This is a test content.',
|
||||
@@ -40,47 +44,38 @@ print(result)
|
||||
|
||||
The agent supplies these at runtime:
|
||||
|
||||
- `filename`: Name of the file to write, relative to `directory`. May include
|
||||
subdirectories. Those subdirectories are created if they do not exist.
|
||||
- `content`: Text content to write.
|
||||
- `directory` (optional): Directory to write into. A relative path resolves
|
||||
inside the tool's allowed directory (`base_dir` when set, otherwise the
|
||||
current working directory) and defaults to that directory's root (`./`).
|
||||
Created if it does not exist.
|
||||
- `overwrite` (optional): Whether to replace an existing file. Accepts a
|
||||
boolean or the strings `y`/`yes`/`t`/`true`/`on`/`1` and
|
||||
`n`/`no`/`f`/`false`/`off`/`0`. Defaults to `false`. When `false` and the
|
||||
file already exists, the tool returns an error and does not modify the file.
|
||||
- `filename`: The name of the file to write, relative to `directory`. May include subdirectories, which are created if they don't exist.
|
||||
- `content`: The text content to write into the file.
|
||||
- `directory` (optional): The path to the directory where the file will be created. A relative path resolves inside the tool's allowed directory — `base_dir` when set, the current working directory otherwise — and defaults to its root. If the directory does not exist, it will be created.
|
||||
- `overwrite` (optional): Whether to replace the file when it already exists. Accepts `true`/`false` (also `yes`/`no`, `on`/`off`, `1`/`0`). Defaults to `false`, which reports an error instead of replacing existing content.
|
||||
|
||||
You set these when constructing the tool:
|
||||
|
||||
- `base_dir` (optional): Directory that writes must stay inside. Defaults to
|
||||
the current working directory. A relative path is resolved when the tool is
|
||||
constructed, so a later change of working directory does not move the
|
||||
sandbox.
|
||||
- `encoding` (optional): Text encoding used to write the file. Defaults to
|
||||
`utf-8`.
|
||||
- `base_dir` (optional): The directory that writes must stay inside. Defaults to the current working directory.
|
||||
- `encoding` (optional): Text encoding used to write the file. Defaults to `utf-8`.
|
||||
|
||||
## Allowed paths
|
||||
|
||||
Runtime `directory` and `filename` values are usually chosen by an LLM. The
|
||||
tool rejects paths that escape the sandbox:
|
||||
Because both the directory and the filename are usually chosen by an LLM at runtime, writes are confined to a sandbox:
|
||||
|
||||
- The resolved `directory` must be inside `base_dir` (the current working
|
||||
directory by default).
|
||||
- The resolved file path must be inside that `directory`.
|
||||
- `..` segments, absolute paths, and symlinks are resolved before both checks.
|
||||
- The resolved `directory` must be inside `base_dir`, which defaults to the current working directory.
|
||||
- The resolved file must then be inside that `directory`. `..` segments, absolute paths, and symlinks are resolved before both checks, so they cannot be used to escape.
|
||||
|
||||
To allow writes under a different tree, set `base_dir`:
|
||||
To let an agent write outside the working directory, point `base_dir` at the target tree:
|
||||
|
||||
```python Code
|
||||
# The agent may write anywhere under /var/output, and nowhere outside it
|
||||
file_writer_tool = FileWriterTool(base_dir='/var/output')
|
||||
```
|
||||
|
||||
<Note>
|
||||
Previously, an absolute `directory` could write anywhere the process had
|
||||
permission. If you relied on that, set `base_dir` to the tree you want to
|
||||
allow. Setting `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` restores the old
|
||||
behavior, but it disables path and URL checks process-wide for every
|
||||
crewai-tools tool. Prefer `base_dir`.
|
||||
Previously an absolute `directory` could write anywhere the process had permission to. If you relied on that, set `base_dir` to the tree you want to allow. Setting `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` restores the old behavior, but it applies process-wide to every crewai-tools tool, including the SSRF protections on URL-fetching tools, so prefer `base_dir`.
|
||||
</Note>
|
||||
|
||||
## Conclusion
|
||||
|
||||
By integrating the `FileWriterTool` into your crews, the agents can reliably write content to files across different operating systems.
|
||||
This tool is essential for tasks that require saving output data, creating structured file systems, and handling cross-platform file operations.
|
||||
It's particularly recommended for Windows users who may encounter file writing issues with standard Python file operations.
|
||||
|
||||
By adhering to the setup and usage guidelines provided, incorporating this tool into projects is straightforward and ensures consistent file writing behavior across all platforms.
|
||||
|
||||
@@ -57,9 +57,10 @@ provider로 등록하지 않습니다. 이를 통해 양방향이 분리됩니
|
||||
| 예 | 테스트 실행 데이터 | crew의 랜덤 생성 키와 ID, 반복 횟수, 사용된 모델명, 품질 점수(실수), 실행 시간(초 단위)이 포함됩니다. 모두 비개인 정보입니다. |
|
||||
| 예 | 작업 라이프사이클 데이터 | 생성 및 실행 시작/종료 시각, crew 및 작업 식별자가 포함됩니다. 타임스탬프를 포함한 span으로 저장됩니다. 개인 정보 없음. |
|
||||
| 예 | LLM 속성 | LLM의 이름, model_name, 모델, top_k, temperature 및 클래스명이 포함됩니다. 모두 기술적이고 비개인 정보입니다. |
|
||||
| 예 | crewAI CLI를 통한 Crew 배포 시도 | 배포가 시도되고 있고 crew id가 포함되며, 로그를 가져오려고 하는 경우에만 해당. 다른 데이터 없음. |
|
||||
| 예 | crewAI CLI를 통한 Crew 배포 시도 | 포함 항목: 배포가 시도되고 있다는 사실과 crew id, 로그를 가져오려고 하는지 여부, 그리고 배포가 CLI 명령에서 시작되었는지 실행 TUI에서 시작되었는지 여부. 프로젝트나 crew의 내용은 기록되지 않습니다. 개인 정보 없음. |
|
||||
| 예 | 실행 환경 | 포함: 프로세스를 실행 중인 AI 코딩 어시스턴트(있는 경우, `claude_code`, `codex`, `cursor`, `unknown` 등 고정 목록 중 하나), 프로세스가 실행되는 위치(`ci`, `container`, `serverless`, `interactive` 등 고정 목록 중 하나), 그리고 `pyproject.toml`에 설정된 경우 `project_id`. 감지는 알려진 환경 변수의 설정 여부만 확인하며 값은 읽지 않음. 개인 데이터 없음. |
|
||||
| 예 | Flow 라이프사이클 신호 | 포함 항목: flow의 시작, 완료 또는 실패 여부, 해당 메서드의 실패 여부, 사람의 입력이나 피드백을 위해 일시 중지되었는지 여부, 해당 시작이 재개된 실행인지 여부, 대화 턴의 실패 여부, flow 실행 시간, 그리고 해당 flow가 CrewAI가 내부적으로 실행하는 것인지 사용자가 작성한 것인지 여부. flow 이름은 기록되며, 이는 flow 생성 및 실행에서 이미 그러합니다. 메서드 이름, 오류 메시지, flow 상태는 절대 기록하지 않습니다. 개인 정보 없음. |
|
||||
| 예 | Flow 라이프사이클 신호 | 포함 항목: flow의 시작, 완료 또는 실패 여부, 해당 메서드의 실패 여부, 사람의 입력이나 피드백을 위해 일시 중지되었는지 여부, 해당 시작이 재개된 실행인지 여부, 대화 턴의 실패 여부, flow 실행 시간, 그리고 해당 flow가 CrewAI가 내부적으로 실행하는 것인지 사용자가 작성한 것인지 여부. flow 이름은 기록되며(개인 정보를 포함해서는 안 됨), 이는 flow 생성 및 실행에서 이미 그러합니다. 메서드 이름, 오류 메시지, flow 상태는 절대 기록하지 않습니다. 개인 정보 없음. |
|
||||
| 예 | 트레이스 공유 신호 | 포함 항목: 트레이스 배치가 CrewAI AMP에 성공적으로 공유되었는지 여부와, 익명으로(계정 생성 전) 공유되었는지 또는 계정에 연결되어 공유되었는지 여부. 모든 span과 마찬가지로 위에서 설명한 실행 환경 속성(구성된 경우 `project_id`, 코딩 어시스턴트, 런타임)도 함께 기록됩니다. 이 행은 공유 텔레메트리만 설명하며 — 트레이스 내용이나 공유된 트레이스 링크로 부여되는 접근 권한은 설명하지 않습니다. 트레이스 내용, 입력, 출력은 이 신호에는 기록되지 않습니다. 트레이스를 공유하기 전에 비밀 정보, 개인 데이터, AMP 편집 및 보존 설정을 검토하세요. |
|
||||
| 아니오 | 에이전트 확장 데이터 | 목표 설명, 배경 이야기 텍스트, i18n 프롬프트 파일 식별자가 포함됩니다. 사용자들은 텍스트 필드에 개인 정보가 포함되지 않도록 해야 합니다. |
|
||||
| 아니오 | 상세 작업 정보 | 작업 설명, 예상 출력 설명, 컨텍스트 참조가 포함됩니다. 사용자들은 이러한 필드에 개인 정보가 포함되지 않도록 해야 합니다. |
|
||||
| 아니오 | 환경 정보 | 플랫폼, 릴리즈, 시스템, 버전, CPU 개수가 포함됩니다. 예: 'Windows 10', 'x86_64'. 개인 정보 없음. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 파일 읽기
|
||||
description: FileReadTool은 로컬 파일 시스템에서 파일을 읽도록 설계되었습니다.
|
||||
description: FileReadTool은 로컬 파일 시스템에서 파일을 읽습니다.
|
||||
icon: folders
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -8,37 +8,63 @@ mode: "wide"
|
||||
## 개요
|
||||
|
||||
<Note>
|
||||
우리는 도구를 계속 개선하고 있으므로, 향후 예기치 않은 동작이나 변경 사항이 발생할 수 있습니다.
|
||||
도구를 계속 개선하고 있으므로 동작이 변경될 수 있습니다.
|
||||
</Note>
|
||||
|
||||
FileReadTool은 crewai_tools 패키지 내에서 파일 읽기와 콘텐츠 검색을 용이하게 하는 기능 모음입니다.
|
||||
이 모음에는 배치 텍스트 파일 처리, 런타임 구성 파일 읽기, 분석을 위한 데이터 가져오기 등 다양한 도구가 포함되어 있습니다.
|
||||
`.txt`, `.csv`, `.json` 등 다양한 텍스트 기반 파일 형식을 지원합니다. 콘텐츠는 항상 일반 텍스트로 반환됩니다.
|
||||
`FileReadTool`은 로컬 파일을 읽고 내용을 텍스트로 반환합니다.
|
||||
텍스트 파일 처리, 구성 파일 읽기, 분석용 데이터 로드에 사용하세요.
|
||||
`.txt`, `.csv`, `.json`, `.md`와 같은 모든 텍스트 형식에서 동작합니다.
|
||||
도구는 항상 일반 텍스트를 반환합니다. 구조화된 데이터(예: JSON)가 필요하면 Agent 또는 사용자 코드에서 파싱하세요.
|
||||
|
||||
큰 파일의 경우 Agent가 `start_line`과 `line_count`를 전달해 일부 줄만 읽을 수 있습니다.
|
||||
요청한 줄을 모으면 읽기를 멈추므로 파일의 나머지를 스캔하지 않습니다.
|
||||
|
||||
## 설치
|
||||
|
||||
이전에 FileReadTool에 할당된 기능을 사용하려면 crewai_tools 패키지를 설치하세요:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
uv add 'crewai[tools]'
|
||||
```
|
||||
|
||||
## 사용 예시
|
||||
|
||||
FileReadTool을 시작하려면:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileReadTool
|
||||
|
||||
# 에이전트가 알고 있거나 경로를 학습한 파일을 읽기 위해 도구를 초기화합니다.
|
||||
file_read_tool = FileReadTool()
|
||||
# Agent chooses the file path at runtime
|
||||
tool = FileReadTool()
|
||||
|
||||
# 또는
|
||||
# OR set a default file the agent can read with no path argument
|
||||
tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
|
||||
# 특정 파일 경로로 도구를 초기화하여 에이전트가 지정된 파일의 내용만 읽을 수 있도록 합니다.
|
||||
file_read_tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
# OR let the agent read any file under a directory
|
||||
tool = FileReadTool(base_dir='/data')
|
||||
```
|
||||
|
||||
도구를 Agent에 전달하세요. 런타임에 LLM이 `file_path`를 전달하며, 선택적으로 `start_line`과 `line_count`도 전달합니다.
|
||||
|
||||
## 인수
|
||||
|
||||
- `file_path`: 읽고자 하는 파일의 경로입니다. 절대 경로와 상대 경로 모두 허용됩니다. 파일이 존재하는지와 필요한 접근 권한이 있는지 반드시 확인하세요.
|
||||
Agent가 런타임에 전달할 수 있는 인수:
|
||||
|
||||
- `file_path`: (선택) 읽을 파일 경로입니다. 절대 경로와 상대 경로는 `base_dir` 샌드박스 안에서 해석될 때만 유효합니다. 상대 경로는 `base_dir`이 설정된 경우 그 기준이고, 그렇지 않으면 현재 작업 디렉터리(기본 샌드박스)를 기준으로 해석됩니다. 생략하면 생성 시 설정한 기본 파일을 읽습니다. 기본값이 없으면 경로가 제공되지 않았다는 오류를 반환합니다.
|
||||
- `start_line`: (선택) 읽기를 시작할 첫 줄입니다. 줄 번호는 `1`부터 시작합니다. 기본값은 `1`입니다.
|
||||
- `line_count`: (선택) 읽을 줄 수입니다. 생략하면 `start_line`부터 파일 끝까지 읽습니다.
|
||||
|
||||
도구를 만들 때 설정할 수 있는 인수:
|
||||
|
||||
- `file_path`: (선택) Agent가 경로 없이 도구를 호출할 때 읽을 기본 파일입니다. 상대 경로는 `base_dir`이 제공되면 `base_dir`을 기준으로, 그렇지 않으면 현재 작업 디렉터리를 기준으로 해석됩니다.
|
||||
- `base_dir`: (선택) 런타임 경로가 머물러야 하는 디렉터리입니다. 기본값은 현재 작업 디렉터리입니다. 도구 생성 시 이 경로를 해석하므로, 이후 작업 디렉터리가 바뀌어도 샌드박스는 이동하지 않습니다.
|
||||
- `encoding`: (선택) 파일을 디코딩할 때 사용하는 텍스트 인코딩입니다. 기본값은 `utf-8`입니다. 디코딩에 실패하면 오류를 반환하고 다른 `encoding`을 전달하도록 안내합니다.
|
||||
|
||||
일반적인 실패(파일 없음, 권한 거부, 잘못된 인코딩, 샌드박스 밖 경로)는 예외를 발생시키지 않고 오류 문자열을 반환합니다.
|
||||
|
||||
## 허용 경로
|
||||
|
||||
LLM이 보통 런타임에 파일 경로를 선택하므로, 읽기는 샌드박스로 제한됩니다.
|
||||
|
||||
- 런타임 경로는 `base_dir`(기본값: 현재 작업 디렉터리) 안에서 해석되어야 합니다. 도구는 경로를 검사하기 전에 `..` 세그먼트와 심볼릭 링크를 해석하므로 샌드박스를 벗어날 수 없습니다.
|
||||
- 생성자에 전달한 `file_path`는 `base_dir` 밖이어도 항상 허용됩니다. 파일이 없거나, 디렉터리이거나, 접근할 수 없으면 읽기 자체는 실패할 수 있습니다. 이 경로는 도구 생성 시 고정되므로, 이후 작업 디렉터리가 바뀌어도 가리키는 파일이 바뀌지 않습니다. Agent는 `file_path`를 생략하거나 도구 설명에 표시된 이름으로 읽을 수 있습니다. 파일 하나를 선언해도 같은 폴더의 다른 파일에는 접근할 수 없습니다.
|
||||
|
||||
Agent가 작업 디렉터리 밖의 파일을 읽게 하려면 도구를 만들 때 `base_dir`을 설정하세요(위 예시 참고).
|
||||
|
||||
최후의 수단으로 `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true`를 설정하면 경로 검사가 꺼집니다. 이 설정은 프로세스의 모든 crewai-tools 도구에 적용되며, URL을 가져오는 도구의 SSRF 보호도 포함됩니다. 가능하면 `base_dir`을 사용하세요.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 파일 쓰기
|
||||
description: 텍스트 내용을 파일에 씁니다.
|
||||
description: FileWriterTool은 파일에 내용을 쓰도록 설계되었습니다.
|
||||
icon: file-pen
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -9,25 +9,29 @@ mode: "wide"
|
||||
|
||||
## 설명
|
||||
|
||||
`FileWriterTool`은 텍스트 내용을 파일에 씁니다. 없는 디렉터리는 자동으로
|
||||
만들어집니다. 기본 텍스트 인코딩은 UTF-8입니다.
|
||||
|
||||
쓰기는 샌드박스 디렉터리로 제한됩니다. 기본적으로 이 디렉터리는 현재 작업
|
||||
디렉터리입니다. 변경하려면 `base_dir`을 전달하세요.
|
||||
`FileWriterTool`은 crewai_tools 패키지의 구성 요소로, 다양한 운영 체제(Windows, Linux, macOS)에서 파일에 내용을 작성하는 과정을 간소화하도록 설계되었습니다.
|
||||
이 도구는 보고서 생성, 로그 저장, 설정 파일 작성 등과 같은 시나리오에서 특히 유용합니다.
|
||||
운영 체제마다 다른 경로 처리를 지원하며, UTF-8 인코딩을 지원하고, 디렉토리가 존재하지 않으면 자동으로 생성하여, 다양한 플랫폼에서 출력 결과를 안정적으로 정리할 수 있도록 도와줍니다.
|
||||
|
||||
## 설치
|
||||
|
||||
프로젝트에서 `FileWriterTool`을 사용하려면 crewai_tools 패키지를 설치하세요:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## 예시
|
||||
|
||||
`FileWriterTool`을(를) 시작하려면:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileWriterTool
|
||||
|
||||
# Initialize the tool
|
||||
file_writer_tool = FileWriterTool()
|
||||
|
||||
# Write content to a file in a specified directory
|
||||
result = file_writer_tool.run(
|
||||
filename='example.txt',
|
||||
content='This is a test content.',
|
||||
@@ -38,49 +42,14 @@ print(result)
|
||||
|
||||
## 인자
|
||||
|
||||
에이전트가 런타임에 제공하는 값:
|
||||
- `filename`: 생성하거나 덮어쓸 파일의 이름입니다.
|
||||
- `content`: 파일에 쓸 내용입니다.
|
||||
- `directory` (선택 사항): 파일이 생성될 디렉터리의 경로입니다. 기본값은 현재 디렉터리(`.`)입니다. 디렉터리가 존재하지 않으면 생성됩니다.
|
||||
|
||||
- `filename`: `directory`를 기준으로 쓸 파일 이름입니다. 하위 디렉터리를
|
||||
포함할 수 있으며, 없는 하위 디렉터리는 생성됩니다.
|
||||
- `content`: 쓸 텍스트 내용입니다.
|
||||
- `directory` (선택 사항): 파일을 쓸 디렉터리입니다. 상대 경로는 도구의
|
||||
허용 디렉터리(`base_dir`이 설정된 경우 그 값, 그렇지 않으면 현재 작업
|
||||
디렉터리) 안에서 해석되며, 기본값은 해당 디렉터리의 루트(`./`)입니다.
|
||||
없으면 생성됩니다.
|
||||
- `overwrite` (선택 사항): 기존 파일을 덮어쓸지 여부입니다. 불리언 또는
|
||||
문자열 `y`/`yes`/`t`/`true`/`on`/`1` 및
|
||||
`n`/`no`/`f`/`false`/`off`/`0`을 받습니다. 기본값은 `false`입니다.
|
||||
`false`이고 파일이 이미 있으면 도구는 오류를 반환하고 파일을 수정하지
|
||||
않습니다.
|
||||
## 결론
|
||||
|
||||
도구를 생성할 때 설정하는 값:
|
||||
`FileWriterTool`을 crew에 통합함으로써, 에이전트는 다양한 운영 체제에서 파일에 내용을 안정적으로 쓸 수 있습니다.
|
||||
이 도구는 출력 데이터를 저장하거나, 구조화된 파일 시스템을 생성하거나, 크로스 플랫폼 파일 작업을 처리해야 하는 작업에 필수적입니다.
|
||||
특히 표준 Python 파일 작업에서 파일 쓰기 이슈가 발생할 수 있는 Windows 사용자에게 권장됩니다.
|
||||
|
||||
- `base_dir` (선택 사항): 쓰기가 머물 수 있는 디렉터리입니다. 기본값은
|
||||
현재 작업 디렉터리입니다. 상대 경로는 도구가 생성될 때 해석되므로, 이후
|
||||
작업 디렉터리를 바꿔도 샌드박스는 이동하지 않습니다.
|
||||
- `encoding` (선택 사항): 파일을 쓸 때 사용하는 텍스트 인코딩입니다.
|
||||
기본값은 `utf-8`입니다.
|
||||
|
||||
## 허용 경로
|
||||
|
||||
런타임의 `directory`와 `filename` 값은 보통 LLM이 선택합니다. 도구는
|
||||
샌드박스를 벗어나는 경로를 거부합니다:
|
||||
|
||||
- 해석된 `directory`는 `base_dir`(기본값은 현재 작업 디렉터리) 안에 있어야
|
||||
합니다.
|
||||
- 해석된 파일 경로는 그 `directory` 안에 있어야 합니다.
|
||||
- 두 검사 전에 `..` 세그먼트, 절대 경로, 심볼릭 링크가 해석됩니다.
|
||||
|
||||
다른 트리 아래에 쓰기를 허용하려면 `base_dir`을 설정하세요:
|
||||
|
||||
```python Code
|
||||
file_writer_tool = FileWriterTool(base_dir='/var/output')
|
||||
```
|
||||
|
||||
<Note>
|
||||
이전에는 절대 `directory`가 프로세스 권한이 있는 임의의 위치에 쓸 수
|
||||
있었습니다. 그 동작에 의존했다면 허용할 트리로 `base_dir`을 설정하세요.
|
||||
`CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true`를 설정하면 이전 동작이 복원되지만,
|
||||
모든 crewai-tools 도구에 대해 프로세스 전역으로 경로 및 URL 검사가
|
||||
비활성화됩니다. `base_dir`을 선호하세요.
|
||||
</Note>
|
||||
제공된 설정 및 사용 가이드라인을 준수하면, 이 도구를 프로젝트에 통합하는 과정이 간단하며 모든 플랫폼에서 일관된 파일 쓰기 동작을 보장합니다.
|
||||
|
||||
@@ -59,9 +59,10 @@ por meio do próprio tracer provider, que é independente do descrito aqui.
|
||||
| Sim | Dados de Execução de Testes | Inclui: chave e ID aleatórias do crew, número de iterações, nome do modelo usado, score de qualidade (float), tempo de execução (em segundos). Tudo não pessoal. |
|
||||
| Sim | Dados do Ciclo de Vida da Tarefa | Inclui: horários de criação, início/fim de execução, identificadores de crew e tarefa. Armazenado como spans com timestamps. Sem dados pessoais. |
|
||||
| Sim | Atributos do LLM | Inclui: nome, model_name, model, top_k, temperatura e nome da classe do LLM. Todos técnicos, sem dados pessoais. |
|
||||
| Sim | Tentativa de Deploy do Crew pelo CLI do crewAI | Inclui: O fato de um deploy estar sendo realizado e o crew id, e se está tentando buscar logs, sem mais dados. |
|
||||
| Sim | Tentativa de Deploy do Crew pelo CLI do crewAI | Inclui: O fato de um deploy estar sendo realizado e o crew id, se está tentando buscar logs, e se o deploy foi iniciado por um comando do CLI ou pela TUI de execução. Não inclui conteúdo do projeto ou do crew nem dados pessoais. |
|
||||
| Sim | Ambiente de Execução | Inclui: qual assistente de código com IA está executando o processo, se houver (um de uma lista fixa como `claude_code`, `codex`, `cursor` ou `unknown`), onde o processo é executado (um de uma lista fixa como `ci`, `container`, `serverless`, `interactive`) e o `project_id` do seu `pyproject.toml` quando houver um configurado. A detecção lê apenas se variáveis de ambiente conhecidas estão definidas, nunca seus valores. Sem dados pessoais. |
|
||||
| Sim | Sinais de Ciclo de Vida do Flow | Inclui: que um flow iniciou, se foi concluído ou falhou, se um de seus métodos falhou, se pausou para entrada ou feedback humano, se o início foi uma execução retomada, se um turno de conversa falhou, por quanto tempo o flow executou e se o flow é um que o CrewAI executa internamente ou um que você escreveu. O nome do flow é registrado, como já ocorre na criação e execução do flow. Nomes de métodos, mensagens de erro e estado do flow nunca são registrados. Sem dados pessoais. |
|
||||
| Sim | Sinais de Ciclo de Vida do Flow | Inclui: que um flow iniciou, se foi concluído ou falhou, se um de seus métodos falhou, se pausou para entrada ou feedback humano, se o início foi uma execução retomada, se um turno de conversa falhou, por quanto tempo o flow executou e se o flow é um que o CrewAI executa internamente ou um que você escreveu. O nome do flow é registrado (não deve incluir informações pessoais), como já ocorre na criação e execução do flow. Nomes de métodos, mensagens de erro e estado do flow nunca são registrados. Sem dados pessoais. |
|
||||
| Sim | Sinal de Compartilhamento de Trace | Inclui: que um lote de traces foi compartilhado com sucesso com o CrewAI AMP, e se foi compartilhado anonimamente (antes de você ter uma conta) ou vinculado à sua conta. Como todo span, também carrega os atributos de Ambiente de Execução descritos acima (`project_id` quando configurado, o assistente de programação e o runtime). Esta linha descreve apenas a telemetria do compartilhamento — não o conteúdo dos traces nem o acesso concedido por links de traces compartilhados. O conteúdo dos traces, entradas e saídas nunca são registrados neste sinal. Antes de compartilhar traces, revise segredos, dados pessoais e as configurações de redação e retenção do AMP. |
|
||||
| Não | Dados Expandidos do Agente | Inclui: descrição do objetivo, texto da história, identificador de arquivo i18n prompt. Usuários devem garantir que não haja info pessoal nesses campos de texto. |
|
||||
| Não | Informações Detalhadas da Tarefa | Inclui: descrição da tarefa, descrição do resultado esperado, referências de contexto. Usuários devem garantir que não haja info pessoal nessas áreas. |
|
||||
| Não | Informações de Ambiente | Inclui: plataforma, release, sistema, versão e quantidade de CPUs. Exemplo: 'Windows 10', 'x86_64'. Sem dados pessoais. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Leitura de Arquivo
|
||||
description: O `FileReadTool` foi desenvolvido para ler arquivos do sistema de arquivos local.
|
||||
description: O `FileReadTool` lê arquivos do sistema de arquivos local.
|
||||
icon: folders
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -8,37 +8,63 @@ mode: "wide"
|
||||
## Visão Geral
|
||||
|
||||
<Note>
|
||||
Ainda estamos trabalhando para melhorar as ferramentas, portanto pode haver comportamentos inesperados ou alterações no futuro.
|
||||
Ainda estamos melhorando as ferramentas, então o comportamento pode mudar.
|
||||
</Note>
|
||||
|
||||
O FileReadTool representa conceitualmente um conjunto de funcionalidades dentro do pacote crewai_tools voltadas para facilitar a leitura e a recuperação de conteúdo de arquivos.
|
||||
Esse conjunto inclui ferramentas para processar arquivos de texto em lote, ler arquivos de configuração em tempo de execução e importar dados para análise.
|
||||
Ele suporta uma variedade de formatos de arquivo baseados em texto, como `.txt`, `.csv`, `.json` e outros. O conteúdo é sempre retornado como texto simples.
|
||||
O `FileReadTool` lê um arquivo local e retorna o conteúdo como texto.
|
||||
Use-o para processar arquivos de texto, ler arquivos de configuração ou carregar dados para análise.
|
||||
Ele funciona com qualquer formato de texto, como `.txt`, `.csv`, `.json` e `.md`.
|
||||
A ferramenta sempre retorna texto simples. Se você precisar de dados estruturados (por exemplo, JSON), faça o parse no Agent ou no seu próprio código.
|
||||
|
||||
Para arquivos grandes, o Agent pode passar `start_line` e `line_count` para ler apenas um intervalo de linhas.
|
||||
A ferramenta para assim que obtém essas linhas, então não percorre o restante do arquivo.
|
||||
|
||||
## Instalação
|
||||
|
||||
Para utilizar as funcionalidades anteriormente atribuídas ao FileReadTool, instale o pacote crewai_tools:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
uv add 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Exemplo de Uso
|
||||
|
||||
Para começar a usar o FileReadTool:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileReadTool
|
||||
|
||||
# Inicialize a ferramenta para ler quaisquer arquivos que os agentes conhecem ou informe o caminho para
|
||||
file_read_tool = FileReadTool()
|
||||
# Agent chooses the file path at runtime
|
||||
tool = FileReadTool()
|
||||
|
||||
# OU
|
||||
# OR set a default file the agent can read with no path argument
|
||||
tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
|
||||
# Inicialize a ferramenta com um caminho de arquivo específico, assim o agente poderá ler apenas o conteúdo do arquivo especificado
|
||||
file_read_tool = FileReadTool(file_path='path/to/your/file.txt')
|
||||
# OR let the agent read any file under a directory
|
||||
tool = FileReadTool(base_dir='/data')
|
||||
```
|
||||
|
||||
Passe a ferramenta para um Agent. Em tempo de execução, o LLM envia `file_path` e, opcionalmente, `start_line` e `line_count`.
|
||||
|
||||
## Argumentos
|
||||
|
||||
- `file_path`: O caminho para o arquivo que você deseja ler. Aceita caminhos absolutos e relativos. Certifique-se de que o arquivo exista e de que você tenha as permissões necessárias para acessá-lo.
|
||||
O Agent pode passar estes argumentos em tempo de execução:
|
||||
|
||||
- `file_path`: (Opcional) Caminho do arquivo a ler. Caminhos absolutos e relativos só são válidos quando resolvem dentro do sandbox de `base_dir`. Um caminho relativo resolve em relação a `base_dir` quando definido; caso contrário, em relação ao diretório de trabalho atual (o sandbox padrão). Omita-o para ler o arquivo padrão definido na construção. Se não houver padrão, a ferramenta retorna um erro dizendo que nenhum caminho foi fornecido.
|
||||
- `start_line`: (Opcional) Primeira linha a ler. A numeração começa em `1`. O padrão é `1`.
|
||||
- `line_count`: (Opcional) Quantidade de linhas a ler. Se omitido, a ferramenta lê de `start_line` até o fim do arquivo.
|
||||
|
||||
Você pode definir estes argumentos ao criar a ferramenta:
|
||||
|
||||
- `file_path`: (Opcional) Arquivo padrão a ler quando o Agent chama a ferramenta sem caminho. Um caminho relativo resolve em relação a `base_dir` quando `base_dir` é fornecido; caso contrário, em relação ao diretório de trabalho atual.
|
||||
- `base_dir`: (Opcional) Diretório dentro do qual os caminhos em tempo de execução devem permanecer. O padrão é o diretório de trabalho atual. A ferramenta resolve este caminho na criação, então uma mudança posterior do diretório de trabalho não move o sandbox.
|
||||
- `encoding`: (Opcional) Codificação de texto usada para decodificar o arquivo. O padrão é `utf-8`. Se a decodificação falhar, a ferramenta retorna um erro e sugere passar um `encoding` diferente.
|
||||
|
||||
Falhas comuns (arquivo ausente, permissão negada, encoding incorreto ou caminho fora do sandbox) retornam uma string de erro. Elas não levantam uma exceção.
|
||||
|
||||
## Caminhos permitidos
|
||||
|
||||
Um LLM geralmente escolhe o caminho do arquivo em tempo de execução, então as leituras ficam limitadas a um sandbox:
|
||||
|
||||
- Os caminhos em tempo de execução devem resolver dentro de `base_dir` (padrão: o diretório de trabalho atual). A ferramenta resolve segmentos `..` e links simbólicos antes de verificar o caminho, então eles não podem escapar do sandbox.
|
||||
- Um `file_path` passado ao construtor é sempre permitido, mesmo fora de `base_dir`. A leitura ainda pode falhar se o arquivo estiver ausente, for um diretório ou não puder ser acessado. Esse caminho fica fixo quando a ferramenta é criada, então uma mudança posterior do diretório de trabalho não altera o arquivo apontado. O Agent pode lê-lo omitindo `file_path` ou usando o nome mostrado na descrição da ferramenta. Declarar um arquivo não permite acesso a outros arquivos na mesma pasta.
|
||||
|
||||
Para permitir que um Agent leia arquivos fora do diretório de trabalho, defina `base_dir` ao criar a ferramenta (veja o exemplo acima).
|
||||
|
||||
Como último recurso, defina `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true` para desativar as verificações de caminho. Essa configuração se aplica a todas as ferramentas crewai-tools no processo, incluindo proteções SSRF em ferramentas que buscam URLs. Prefira `base_dir`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Escrita de Arquivo
|
||||
description: Escreve conteúdo de texto em um arquivo.
|
||||
description: O `FileWriterTool` foi projetado para escrever conteúdo em arquivos.
|
||||
icon: file-pen
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -9,25 +9,29 @@ mode: "wide"
|
||||
|
||||
## Descrição
|
||||
|
||||
O `FileWriterTool` escreve conteúdo de texto em um arquivo. Diretórios
|
||||
ausentes são criados automaticamente. A codificação de texto padrão é UTF-8.
|
||||
|
||||
As escritas ficam confinadas a um diretório sandbox. Por padrão, esse
|
||||
diretório é o diretório de trabalho atual. Passe `base_dir` para alterá-lo.
|
||||
O `FileWriterTool` é um componente do pacote crewai_tools, projetado para simplificar o processo de escrita de conteúdo em arquivos com compatibilidade multiplataforma (Windows, Linux, macOS).
|
||||
É particularmente útil em cenários como geração de relatórios, salvamento de logs, criação de arquivos de configuração e mais.
|
||||
Essa ferramenta lida com diferenças de caminhos entre sistemas operacionais, suporta codificação UTF-8 e cria diretórios automaticamente caso eles não existam, facilitando a organização da sua saída de forma confiável em diferentes plataformas.
|
||||
|
||||
## Instalação
|
||||
|
||||
Instale o pacote crewai_tools para utilizar o `FileWriterTool` em seus projetos:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Exemplo
|
||||
|
||||
Para começar a usar o `FileWriterTool`:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import FileWriterTool
|
||||
|
||||
# Inicialize a ferramenta
|
||||
file_writer_tool = FileWriterTool()
|
||||
|
||||
# Escreva conteúdo em um arquivo em um diretório especificado
|
||||
result = file_writer_tool.run(
|
||||
filename='example.txt',
|
||||
content='This is a test content.',
|
||||
@@ -38,50 +42,14 @@ print(result)
|
||||
|
||||
## Argumentos
|
||||
|
||||
O agente fornece estes em tempo de execução:
|
||||
- `filename`: O nome do arquivo que você deseja criar ou sobrescrever.
|
||||
- `content`: O conteúdo a ser escrito no arquivo.
|
||||
- `directory` (opcional): O caminho para o diretório onde o arquivo será criado. Por padrão, utiliza o diretório atual (`.`). Se o diretório não existir, ele será criado.
|
||||
|
||||
- `filename`: Nome do arquivo a escrever, relativo a `directory`. Pode incluir
|
||||
subdiretórios. Esses subdiretórios são criados se não existirem.
|
||||
- `content`: Conteúdo de texto a escrever.
|
||||
- `directory` (opcional): Diretório no qual escrever. Um caminho relativo é
|
||||
resolvido dentro do diretório permitido da ferramenta (`base_dir` quando
|
||||
definido; caso contrário, o diretório de trabalho atual) e tem como padrão a
|
||||
raiz desse diretório (`./`). É criado se não existir.
|
||||
- `overwrite` (opcional): Se um arquivo existente deve ser substituído. Aceita
|
||||
um booleano ou as strings `y`/`yes`/`t`/`true`/`on`/`1` e
|
||||
`n`/`no`/`f`/`false`/`off`/`0`. O padrão é `false`. Quando é `false` e o
|
||||
arquivo já existe, a ferramenta retorna um erro e não modifica o arquivo.
|
||||
## Conclusão
|
||||
|
||||
Você define estes ao construir a ferramenta:
|
||||
Ao integrar o `FileWriterTool` aos seus crews, os agentes podem escrever conteúdo em arquivos de forma confiável em diferentes sistemas operacionais.
|
||||
Esta ferramenta é essencial para tarefas que exigem salvamento de dados de saída, criação de sistemas de arquivos estruturados e manipulação de operações de arquivos multiplataforma.
|
||||
É especialmente recomendada para usuários do Windows que possam enfrentar problemas ao escrever arquivos com as operações padrão do Python.
|
||||
|
||||
- `base_dir` (opcional): Diretório dentro do qual as escritas devem
|
||||
permanecer. O padrão é o diretório de trabalho atual. Um caminho relativo é
|
||||
resolvido quando a ferramenta é construída, então uma mudança posterior do
|
||||
diretório de trabalho não move o sandbox.
|
||||
- `encoding` (opcional): Codificação de texto usada para escrever o arquivo.
|
||||
O padrão é `utf-8`.
|
||||
|
||||
## Caminhos permitidos
|
||||
|
||||
Os valores de `directory` e `filename` em tempo de execução costumam ser
|
||||
escolhidos por um LLM. A ferramenta rejeita caminhos que escapam do sandbox:
|
||||
|
||||
- O `directory` resolvido deve estar dentro de `base_dir` (o diretório de
|
||||
trabalho atual por padrão).
|
||||
- O caminho do arquivo resolvido deve estar dentro desse `directory`.
|
||||
- Segmentos `..`, caminhos absolutos e symlinks são resolvidos antes das duas
|
||||
verificações.
|
||||
|
||||
Para permitir escritas em outra árvore, defina `base_dir`:
|
||||
|
||||
```python Code
|
||||
file_writer_tool = FileWriterTool(base_dir='/var/output')
|
||||
```
|
||||
|
||||
<Note>
|
||||
Anteriormente, um `directory` absoluto podia escrever em qualquer lugar em que
|
||||
o processo tivesse permissão. Se você dependia disso, defina `base_dir` para a
|
||||
árvore que deseja permitir. Definir `CREWAI_TOOLS_ALLOW_UNSAFE_PATHS=true`
|
||||
restaura o comportamento antigo, mas desativa verificações de caminho e URL em
|
||||
todo o processo para todas as ferramentas do crewai-tools. Prefira `base_dir`.
|
||||
</Note>
|
||||
Seguindo as orientações de configuração e uso fornecidas, incorporar essa ferramenta em projetos é simples e garante um comportamento consistente de escrita de arquivos em todas as plataformas.
|
||||
@@ -1027,6 +1027,9 @@ FooterKey .footer-key--key {
|
||||
def action_view_traces(self) -> None:
|
||||
if self._status != "completed":
|
||||
return
|
||||
# Recorded here rather than in on_button_pressed so the `t` key binding
|
||||
# is counted too, and only once the action can actually do something.
|
||||
self._record_tui_button_click("view_traces")
|
||||
if self._trace_url:
|
||||
import webbrowser
|
||||
|
||||
@@ -1115,6 +1118,9 @@ FooterKey .footer-key--key {
|
||||
def action_deploy_crew(self) -> None:
|
||||
if self._status != "completed":
|
||||
return
|
||||
# Recorded here rather than in on_button_pressed so the `d` key binding
|
||||
# is counted too, and only once the action can actually do something.
|
||||
self._record_tui_button_click("deploy")
|
||||
self._want_deploy = True
|
||||
self._unsubscribe()
|
||||
self.exit(self._crew_result)
|
||||
@@ -1130,10 +1136,8 @@ FooterKey .footer-key--key {
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id in ("btn-traces", "btn-traces-done"):
|
||||
self._record_tui_button_click("view_traces")
|
||||
self.action_view_traces()
|
||||
elif event.button.id == "btn-deploy":
|
||||
self._record_tui_button_click("deploy")
|
||||
self.action_deploy_crew()
|
||||
|
||||
def _scroll_to_result(self) -> None:
|
||||
|
||||
@@ -5,6 +5,7 @@ from urllib.parse import quote
|
||||
import webbrowser
|
||||
|
||||
from crewai_core.plus_api import CreateCrewPayload
|
||||
from crewai_core.telemetry import DeploySource
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli import git
|
||||
@@ -285,17 +286,23 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
|
||||
return _deployment_identifier(status_response)
|
||||
|
||||
def deploy(self, uuid: str | None = None, skip_validate: bool = False) -> None:
|
||||
def deploy(
|
||||
self,
|
||||
uuid: str | None = None,
|
||||
skip_validate: bool = False,
|
||||
source: DeploySource = "cli",
|
||||
) -> None:
|
||||
"""
|
||||
Deploy a crew using either UUID or project name.
|
||||
|
||||
Args:
|
||||
uuid (Optional[str]): The UUID of the crew to deploy.
|
||||
skip_validate (bool): Skip pre-deploy validation checks.
|
||||
source (DeploySource): Where the deployment was initiated from.
|
||||
"""
|
||||
if not _prepare_project_for_deploy(skip_validate):
|
||||
return
|
||||
self._telemetry.start_deployment_span(uuid)
|
||||
self._telemetry.start_deployment_span(uuid, source=source)
|
||||
console.print("Starting deployment...", style="bold blue")
|
||||
repository = self._prepare_git_repository()
|
||||
remote_repo_url = repository.origin_url() if repository else None
|
||||
@@ -337,17 +344,23 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
raise ValueError("Deployment status response did not include a uuid")
|
||||
return str(uuid)
|
||||
|
||||
def create_crew(self, confirm: bool = False, skip_validate: bool = False) -> None:
|
||||
def create_crew(
|
||||
self,
|
||||
confirm: bool = False,
|
||||
skip_validate: bool = False,
|
||||
source: DeploySource = "cli",
|
||||
) -> None:
|
||||
"""
|
||||
Create a new crew deployment.
|
||||
|
||||
Args:
|
||||
confirm (bool): Whether to skip the interactive confirmation prompt.
|
||||
skip_validate (bool): Skip pre-deploy validation checks.
|
||||
source (DeploySource): Where the deployment was initiated from.
|
||||
"""
|
||||
if not _prepare_project_for_deploy(skip_validate):
|
||||
return
|
||||
self._telemetry.create_crew_deployment_span()
|
||||
self._telemetry.create_crew_deployment_span(source=source)
|
||||
console.print("Creating deployment...", style="bold blue")
|
||||
env_vars = fetch_and_json_env_file()
|
||||
repository = self._prepare_git_repository()
|
||||
|
||||
@@ -512,14 +512,14 @@ def _chain_deploy() -> None:
|
||||
from crewai_cli.deploy.main import DeployCommand
|
||||
|
||||
console.print("\nStarting deployment…\n", style="bold #FF5A50")
|
||||
DeployCommand().create_crew(confirm=True, skip_validate=True)
|
||||
DeployCommand().create_crew(confirm=True, skip_validate=True, source="tui")
|
||||
except AuthenticationRequiredError:
|
||||
from crewai_cli.authentication.main import AuthenticationCommand
|
||||
|
||||
console.print()
|
||||
AuthenticationCommand().login()
|
||||
try:
|
||||
DeployCommand().create_crew(confirm=True, skip_validate=True)
|
||||
DeployCommand().create_crew(confirm=True, skip_validate=True, source="tui")
|
||||
except AuthenticationRequiredError:
|
||||
console.print(
|
||||
"\nDeploy failed: authentication is still required.\n",
|
||||
|
||||
@@ -104,8 +104,8 @@ def test_chain_deploy_skips_validation_after_auth_retry(monkeypatch) -> None:
|
||||
run_crew._chain_deploy()
|
||||
|
||||
assert create_calls == [
|
||||
{"confirm": True, "skip_validate": True},
|
||||
{"confirm": True, "skip_validate": True},
|
||||
{"confirm": True, "skip_validate": True, "source": "tui"},
|
||||
{"confirm": True, "skip_validate": True, "source": "tui"},
|
||||
]
|
||||
assert login_calls == [True]
|
||||
|
||||
@@ -131,7 +131,7 @@ def test_chain_deploy_does_not_login_for_deploy_exit(monkeypatch, capsys) -> Non
|
||||
|
||||
run_crew._chain_deploy()
|
||||
|
||||
assert create_calls == [{"confirm": True, "skip_validate": True}]
|
||||
assert create_calls == [{"confirm": True, "skip_validate": True, "source": "tui"}]
|
||||
assert login_calls == []
|
||||
assert "Deploy failed with exit code 42" in capsys.readouterr().out
|
||||
|
||||
@@ -1721,3 +1721,86 @@ async def test_declarative_flow_runs_on_tui() -> None:
|
||||
assert app._final_output == "flow result"
|
||||
assert app._crew_result == "flow result"
|
||||
assert app._flow_steps[0]["status"] == "done"
|
||||
|
||||
|
||||
def test_view_traces_keybinding_records_telemetry(monkeypatch) -> None:
|
||||
"""The `t` binding reaches the action directly, never on_button_pressed."""
|
||||
app = CrewRunApp()
|
||||
app._status = "completed"
|
||||
app._trace_url = "https://app.crewai.com/traces/test"
|
||||
app._telemetry = Mock()
|
||||
opened_urls: list[str] = []
|
||||
|
||||
monkeypatch.setattr("webbrowser.open", lambda url: opened_urls.append(url))
|
||||
|
||||
app.action_view_traces()
|
||||
|
||||
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces")
|
||||
assert opened_urls == ["https://app.crewai.com/traces/test"]
|
||||
|
||||
|
||||
def test_deploy_keybinding_records_telemetry() -> None:
|
||||
"""The `d` binding reaches the action directly, never on_button_pressed."""
|
||||
app = CrewRunApp()
|
||||
app._status = "completed"
|
||||
app._crew_result = object()
|
||||
app._telemetry = Mock()
|
||||
app._unsubscribe = lambda: None # type: ignore[method-assign]
|
||||
exits: list[object] = []
|
||||
app.exit = lambda result: exits.append(result) # type: ignore[method-assign]
|
||||
|
||||
app.action_deploy_crew()
|
||||
|
||||
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:deploy")
|
||||
assert app._want_deploy is True
|
||||
assert exits == [app._crew_result]
|
||||
|
||||
|
||||
def test_view_traces_before_completion_records_nothing() -> None:
|
||||
"""A keypress mid-run is a no-op, so it must not be counted as usage."""
|
||||
app = CrewRunApp()
|
||||
app._status = "running"
|
||||
app._telemetry = Mock()
|
||||
|
||||
app.action_view_traces()
|
||||
|
||||
app._telemetry.feature_usage_span.assert_not_called()
|
||||
|
||||
|
||||
def test_deploy_before_completion_records_nothing() -> None:
|
||||
app = CrewRunApp()
|
||||
app._status = "running"
|
||||
app._telemetry = Mock()
|
||||
|
||||
app.action_deploy_crew()
|
||||
|
||||
app._telemetry.feature_usage_span.assert_not_called()
|
||||
assert app._want_deploy is False
|
||||
|
||||
|
||||
def test_button_press_records_exactly_once(monkeypatch) -> None:
|
||||
"""Recording moved into the action; the button must not double-count."""
|
||||
app = CrewRunApp()
|
||||
app._status = "completed"
|
||||
app._trace_url = "https://app.crewai.com/traces/test"
|
||||
app._telemetry = Mock()
|
||||
|
||||
monkeypatch.setattr("webbrowser.open", lambda url: None)
|
||||
|
||||
app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces")))
|
||||
|
||||
assert app._telemetry.feature_usage_span.call_count == 1
|
||||
|
||||
|
||||
def test_finished_traces_button_still_records(monkeypatch) -> None:
|
||||
"""The button's id is swapped to btn-traces-done once a trace URL exists."""
|
||||
app = CrewRunApp()
|
||||
app._status = "completed"
|
||||
app._trace_url = "https://app.crewai.com/traces/test"
|
||||
app._telemetry = Mock()
|
||||
|
||||
monkeypatch.setattr("webbrowser.open", lambda url: None)
|
||||
|
||||
app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces-done")))
|
||||
|
||||
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces")
|
||||
|
||||
@@ -18,7 +18,7 @@ from functools import cache
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, ClassVar, Final
|
||||
from typing import Any, ClassVar, Final, Literal
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||
@@ -43,6 +43,9 @@ CREWAI_TELEMETRY_SERVICE_NAME: Final[str] = "crewAI-telemetry"
|
||||
|
||||
TRACER_NAME: Final[str] = "crewai.telemetry"
|
||||
|
||||
DeploySource = Literal["cli", "tui"]
|
||||
"""Where a deployment was initiated from: a direct CLI command, or the run TUI."""
|
||||
|
||||
|
||||
def close_span(span: Span) -> None:
|
||||
"""Set span status to OK and end it."""
|
||||
@@ -297,27 +300,48 @@ class Telemetry:
|
||||
|
||||
self._safe_telemetry_procedure(_operation)
|
||||
|
||||
def start_deployment_span(self, uuid: str | None = None) -> None:
|
||||
"""Records the start of a deployment process."""
|
||||
def start_deployment_span(
|
||||
self, uuid: str | None = None, source: DeploySource = "cli"
|
||||
) -> None:
|
||||
"""Records redeploying an existing crew (``crewai deploy push``).
|
||||
|
||||
Also emits ``deploy:pushed`` so that deployments are countable from the
|
||||
feature-usage aggregation regardless of where they were started from.
|
||||
|
||||
Args:
|
||||
uuid: The deployment being pushed to.
|
||||
source: Where the deployment was initiated from.
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = self.provider.get_tracer(TRACER_NAME)
|
||||
span = tracer.start_span("Start Deployment")
|
||||
if uuid:
|
||||
self._add_attribute(span, "uuid", uuid)
|
||||
self._add_attribute(span, "source", source)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_procedure(_operation)
|
||||
self.feature_usage_span("deploy:pushed")
|
||||
|
||||
def create_crew_deployment_span(self) -> None:
|
||||
"""Records the creation of a new crew deployment."""
|
||||
def create_crew_deployment_span(self, source: DeploySource = "cli") -> None:
|
||||
"""Records creating a new crew deployment (``crewai deploy create``).
|
||||
|
||||
Also emits ``deploy:created`` so that deployments are countable from the
|
||||
feature-usage aggregation regardless of where they were started from.
|
||||
|
||||
Args:
|
||||
source: Where the deployment was initiated from.
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = self.provider.get_tracer(TRACER_NAME)
|
||||
span = tracer.start_span("Create Crew Deployment")
|
||||
self._add_attribute(span, "source", source)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_procedure(_operation)
|
||||
self.feature_usage_span("deploy:created")
|
||||
|
||||
def get_crew_logs_span(
|
||||
self, uuid: str | None, log_type: str = "deployment"
|
||||
|
||||
138
lib/crewai-core/tests/test_telemetry_deploy.py
Normal file
138
lib/crewai-core/tests/test_telemetry_deploy.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Deployment telemetry: attribution by origin, and an origin-independent count.
|
||||
|
||||
``Create Crew Deployment`` and ``Start Deployment`` answer "which deployment,
|
||||
from where"; ``deploy:created`` / ``deploy:pushed`` answer "how many
|
||||
deployments", from the feature-usage aggregation, regardless of origin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai_core.telemetry import Telemetry
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def telemetry() -> Iterator[tuple[Telemetry, MagicMock]]:
|
||||
"""A Telemetry whose spans are captured instead of exported.
|
||||
|
||||
The singleton is disabled in tests and never builds a provider, so both the
|
||||
gate and the provider are supplied here.
|
||||
"""
|
||||
instance = Telemetry()
|
||||
span = MagicMock()
|
||||
provider = MagicMock()
|
||||
provider.get_tracer.return_value.start_span.return_value = span
|
||||
|
||||
with (
|
||||
patch.object(instance, "provider", provider, create=True),
|
||||
patch.object(instance, "_should_execute_telemetry", return_value=True),
|
||||
patch("crewai_core.telemetry.close_span"),
|
||||
):
|
||||
yield instance, span
|
||||
|
||||
|
||||
def _attributes(span: MagicMock) -> dict[str, Any]:
|
||||
return {call.args[0]: call.args[1] for call in span.set_attribute.call_args_list}
|
||||
|
||||
|
||||
def _span_names(provider: MagicMock) -> list[str]:
|
||||
tracer = provider.get_tracer.return_value
|
||||
return [call.args[0] for call in tracer.start_span.call_args_list]
|
||||
|
||||
|
||||
class TestCreateDeployment:
|
||||
def test_defaults_to_cli(self, telemetry: tuple[Telemetry, MagicMock]) -> None:
|
||||
instance, span = telemetry
|
||||
instance.create_crew_deployment_span()
|
||||
assert _attributes(span)["source"] == "cli"
|
||||
|
||||
def test_records_tui_when_started_from_the_run_ui(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
instance, span = telemetry
|
||||
instance.create_crew_deployment_span(source="tui")
|
||||
assert _attributes(span)["source"] == "tui"
|
||||
|
||||
def test_also_counts_the_deployment_as_a_feature(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
instance, _ = telemetry
|
||||
with patch.object(instance, "feature_usage_span") as feature:
|
||||
instance.create_crew_deployment_span()
|
||||
feature.assert_called_once_with("deploy:created")
|
||||
|
||||
def test_feature_count_is_the_same_from_either_origin(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
"""The whole point: one number for deployments, whatever started them."""
|
||||
instance, _ = telemetry
|
||||
with patch.object(instance, "feature_usage_span") as feature:
|
||||
instance.create_crew_deployment_span(source="cli")
|
||||
instance.create_crew_deployment_span(source="tui")
|
||||
assert [call.args[0] for call in feature.call_args_list] == [
|
||||
"deploy:created",
|
||||
"deploy:created",
|
||||
]
|
||||
|
||||
|
||||
class TestStartDeployment:
|
||||
def test_defaults_to_cli_and_keeps_the_uuid(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
instance, span = telemetry
|
||||
instance.start_deployment_span("dep-123")
|
||||
attributes = _attributes(span)
|
||||
assert attributes["source"] == "cli"
|
||||
assert attributes["uuid"] == "dep-123"
|
||||
|
||||
def test_records_tui_when_started_from_the_run_ui(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
instance, span = telemetry
|
||||
instance.start_deployment_span("dep-123", source="tui")
|
||||
assert _attributes(span)["source"] == "tui"
|
||||
|
||||
def test_source_is_recorded_even_without_a_uuid(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
"""uuid is optional; source must not be conditional on it."""
|
||||
instance, span = telemetry
|
||||
instance.start_deployment_span(None, source="tui")
|
||||
attributes = _attributes(span)
|
||||
assert attributes["source"] == "tui"
|
||||
assert "uuid" not in attributes
|
||||
|
||||
def test_also_counts_the_deployment_as_a_feature(
|
||||
self, telemetry: tuple[Telemetry, MagicMock]
|
||||
) -> None:
|
||||
instance, _ = telemetry
|
||||
with patch.object(instance, "feature_usage_span") as feature:
|
||||
instance.start_deployment_span("dep-123")
|
||||
feature.assert_called_once_with("deploy:pushed")
|
||||
|
||||
|
||||
class TestDisabledTelemetry:
|
||||
def test_opted_out_users_emit_nothing(self) -> None:
|
||||
"""No span and no feature count when telemetry is off."""
|
||||
instance = Telemetry()
|
||||
provider = MagicMock()
|
||||
|
||||
with (
|
||||
patch.object(instance, "provider", provider, create=True),
|
||||
patch.object(instance, "_should_execute_telemetry", return_value=False),
|
||||
patch.object(instance, "feature_usage_span") as feature,
|
||||
):
|
||||
instance.create_crew_deployment_span(source="tui")
|
||||
instance.start_deployment_span("dep-123", source="tui")
|
||||
|
||||
assert _span_names(provider) == []
|
||||
# feature_usage_span is itself gated, so it is still called; it is the
|
||||
# export that must not happen. Assert it was not bypassed some other way.
|
||||
assert [call.args[0] for call in feature.call_args_list] == [
|
||||
"deploy:created",
|
||||
"deploy:pushed",
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -6,6 +7,26 @@ from crewai_tools.rag.data_types import DataType
|
||||
from crewai_tools.tools.rag.rag_tool import RagTool
|
||||
|
||||
|
||||
_MYSQL_IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$")
|
||||
|
||||
|
||||
def _quote_mysql_table_name(table_name: str) -> str:
|
||||
identifier_parts = table_name.split(".")
|
||||
if (
|
||||
not identifier_parts
|
||||
or len(identifier_parts) > 2
|
||||
or any(
|
||||
not _MYSQL_IDENTIFIER_PATTERN.fullmatch(part) for part in identifier_parts
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"MySQL table_name must be a valid table identifier or schema.table "
|
||||
"identifier"
|
||||
)
|
||||
|
||||
return ".".join(f"`{part}`" for part in identifier_parts)
|
||||
|
||||
|
||||
class MySQLSearchToolSchema(BaseModel):
|
||||
"""Input for MySQLSearchTool."""
|
||||
|
||||
@@ -32,7 +53,8 @@ class MySQLSearchTool(RagTool):
|
||||
table_name: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().add(f"SELECT * FROM {table_name};", **kwargs) # noqa: S608
|
||||
quoted_table_name = _quote_mysql_table_name(table_name)
|
||||
super().add(f"SELECT * FROM {quoted_table_name};", **kwargs) # noqa: S608
|
||||
|
||||
def _run( # type: ignore[override]
|
||||
self,
|
||||
|
||||
110
lib/crewai-tools/tests/tools/test_mysql_search_tool.py
Normal file
110
lib/crewai-tools/tests/tools/test_mysql_search_tool.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_tools.rag.data_types import DataType
|
||||
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
|
||||
from crewai_tools.tools.rag.rag_tool import RagTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_rag_client() -> MagicMock:
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_or_create_collection = MagicMock(return_value=None)
|
||||
mock_client.add_documents = MagicMock(return_value=None)
|
||||
mock_client.search = MagicMock(return_value=[])
|
||||
return mock_client
|
||||
|
||||
|
||||
def create_mysql_search_tool(
|
||||
mock_rag_client: MagicMock, table_name: str
|
||||
) -> MySQLSearchTool:
|
||||
with (
|
||||
patch(
|
||||
"crewai_tools.adapters.crewai_rag_adapter.get_rag_client",
|
||||
return_value=mock_rag_client,
|
||||
),
|
||||
patch(
|
||||
"crewai_tools.adapters.crewai_rag_adapter.create_client",
|
||||
return_value=mock_rag_client,
|
||||
),
|
||||
):
|
||||
return MySQLSearchTool(
|
||||
db_uri="mysql://user:password@localhost:3306/test_database",
|
||||
table_name=table_name,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("table_name", "expected_query"),
|
||||
[
|
||||
("users", "SELECT * FROM `users`;"),
|
||||
("user_profiles_2026", "SELECT * FROM `user_profiles_2026`;"),
|
||||
("schema_name.users", "SELECT * FROM `schema_name`.`users`;"),
|
||||
("information_schema.tables", "SELECT * FROM `information_schema`.`tables`;"),
|
||||
],
|
||||
)
|
||||
def test_mysql_search_tool_quotes_valid_table_identifiers(
|
||||
mock_rag_client: MagicMock, table_name: str, expected_query: str
|
||||
) -> None:
|
||||
with patch.object(RagTool, "add", return_value=None) as mock_add:
|
||||
create_mysql_search_tool(mock_rag_client, table_name)
|
||||
|
||||
mock_add.assert_called_once_with(
|
||||
expected_query,
|
||||
data_type=DataType.MYSQL,
|
||||
metadata={"db_uri": "mysql://user:password@localhost:3306/test_database"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"table_name",
|
||||
[
|
||||
"users where 1=1",
|
||||
"users; drop table users;--",
|
||||
"users -- comment",
|
||||
"users/*comment*/",
|
||||
"`users`",
|
||||
"schema.users.extra",
|
||||
"schema.",
|
||||
".users",
|
||||
"123users",
|
||||
],
|
||||
)
|
||||
def test_mysql_search_tool_rejects_invalid_table_identifiers(
|
||||
mock_rag_client: MagicMock, table_name: str
|
||||
) -> None:
|
||||
with (
|
||||
patch.object(RagTool, "add", return_value=None) as mock_add,
|
||||
pytest.raises(ValueError, match="MySQL table_name must be a valid"),
|
||||
):
|
||||
create_mysql_search_tool(mock_rag_client, table_name)
|
||||
|
||||
mock_add.assert_not_called()
|
||||
|
||||
|
||||
def test_mysql_search_tool_still_runs_search_queries(
|
||||
mock_rag_client: MagicMock,
|
||||
) -> None:
|
||||
with patch.object(RagTool, "add", return_value=None):
|
||||
tool = create_mysql_search_tool(mock_rag_client, "users")
|
||||
|
||||
with patch.object(RagTool, "_run", return_value="Alice") as mock_run:
|
||||
result = tool._run("alice")
|
||||
|
||||
assert "Alice" in result
|
||||
mock_run.assert_called_once_with(
|
||||
query="alice", similarity_threshold=None, limit=None
|
||||
)
|
||||
|
||||
|
||||
def test_mysql_search_tool_uses_mysql_data_type_metadata(
|
||||
mock_rag_client: MagicMock,
|
||||
) -> None:
|
||||
with patch.object(RagTool, "add", return_value=None) as mock_add:
|
||||
create_mysql_search_tool(mock_rag_client, "users")
|
||||
|
||||
assert mock_add.call_args.kwargs == {
|
||||
"data_type": DataType.MYSQL,
|
||||
"metadata": {"db_uri": "mysql://user:password@localhost:3306/test_database"},
|
||||
}
|
||||
@@ -308,6 +308,17 @@ class EventListener(BaseEventListener):
|
||||
def on_flow_created(_: Any, event: FlowCreatedEvent) -> None:
|
||||
self._telemetry.flow_creation_span(event.flow_name)
|
||||
|
||||
def _is_conversational(source: Any) -> bool:
|
||||
"""Whether this run is a turn of a conversational session.
|
||||
|
||||
Reads the flow's own accessor, which covers both the
|
||||
``conversational = True`` class attribute and a conversational
|
||||
definition. Each turn is its own kickoff while the session reports
|
||||
a single completion, so these spans run many-to-one.
|
||||
"""
|
||||
checker = getattr(source, "_is_conversational_enabled", None)
|
||||
return bool(checker()) if callable(checker) else False
|
||||
|
||||
def _flow_origin(source: Any) -> str:
|
||||
"""Separate flows CrewAI runs itself from the ones a caller wrote.
|
||||
|
||||
@@ -335,10 +346,8 @@ class EventListener(BaseEventListener):
|
||||
conversational turn re-emits completion for a restored run - so a
|
||||
missing stamp means "no duration to report", not an error.
|
||||
"""
|
||||
# Cleared on every terminal path, not only on finish: a turn that
|
||||
# fails without deferred finalization ends via FlowFailedEvent, and
|
||||
# a flag left set there would mark the next run on this instance
|
||||
# failed.
|
||||
# Reset point for the flag a deferred session accumulates across
|
||||
# turns, so a later session on this instance starts clean.
|
||||
source._telemetry_turn_failed = False
|
||||
started_at = getattr(source, "_telemetry_started_at", None)
|
||||
if started_at is None:
|
||||
@@ -349,6 +358,7 @@ class EventListener(BaseEventListener):
|
||||
(time.monotonic() - started_at) * 1000,
|
||||
outcome,
|
||||
_flow_origin(source),
|
||||
_is_conversational(source),
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(FlowStartedEvent)
|
||||
@@ -363,6 +373,7 @@ class EventListener(BaseEventListener):
|
||||
list(source._methods.keys()),
|
||||
_flow_origin(source),
|
||||
resumed,
|
||||
_is_conversational(source),
|
||||
)
|
||||
source._telemetry_started_at = time.monotonic()
|
||||
if not getattr(source, "suppress_flow_events", False):
|
||||
@@ -406,9 +417,13 @@ class EventListener(BaseEventListener):
|
||||
source: Any, event: ConversationTurnFailedEvent
|
||||
) -> None:
|
||||
self._telemetry.feature_usage_span("flow:conversation_turn_failed")
|
||||
# A conversational session closes with FlowFinishedEvent whatever
|
||||
# happened, so record the failure for on_flow_finished to read.
|
||||
source._telemetry_turn_failed = True
|
||||
# A deferred session closes with FlowFinishedEvent whatever happened,
|
||||
# so record the failure for on_flow_finished to read. Without
|
||||
# deferral the run already emitted FlowFailedEvent before
|
||||
# handle_turn emits this one - it cleared the stamp, and flagging
|
||||
# now would mark the next turn on this instance failed.
|
||||
if getattr(source, "_telemetry_started_at", None) is not None:
|
||||
source._telemetry_turn_failed = True
|
||||
|
||||
@crewai_event_bus.on(FlowInputRequestedEvent)
|
||||
def on_flow_input_requested(_: Any, event: FlowInputRequestedEvent) -> None:
|
||||
|
||||
@@ -29,6 +29,7 @@ from crewai.events.listeners.tracing.utils import (
|
||||
should_auto_collect_first_time_traces,
|
||||
)
|
||||
from crewai.plus_api import PlusAPI
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
from crewai.version import get_crewai_version
|
||||
|
||||
|
||||
@@ -78,6 +79,7 @@ class TraceBatchManager:
|
||||
self.backend_initialized: bool = False
|
||||
self.trace_url: str | None = None
|
||||
self.ephemeral_trace_url: str | None = None
|
||||
self._telemetry: Telemetry = Telemetry()
|
||||
try:
|
||||
self.plus_api = PlusAPI(
|
||||
api_key=get_auth_token(),
|
||||
@@ -408,6 +410,14 @@ class TraceBatchManager:
|
||||
|
||||
if response.status_code == 200:
|
||||
self._batch_finalized = True
|
||||
# Emitted on finalize, not init: a batch that initializes but
|
||||
# fails to send never lands in AMP. Records only that a batch
|
||||
# arrived, never its contents.
|
||||
self._telemetry.feature_usage_span(
|
||||
"tracing:ephemeral_sent"
|
||||
if is_ephemeral
|
||||
else "tracing:authenticated_sent"
|
||||
)
|
||||
access_code = response.json().get("access_code", None)
|
||||
console = Console()
|
||||
settings = Settings()
|
||||
|
||||
@@ -997,6 +997,7 @@ class Telemetry:
|
||||
node_names: list[str],
|
||||
origin: str = "user",
|
||||
resumed: bool = False,
|
||||
conversational: bool = False,
|
||||
) -> None:
|
||||
"""Records the execution of a flow.
|
||||
|
||||
@@ -1011,6 +1012,10 @@ class Telemetry:
|
||||
Resuming re-enters ``kickoff()``, so the same event fires again;
|
||||
without this the second leg is indistinguishable from a fresh
|
||||
run and a paused flow looks like two separate executions.
|
||||
conversational: True for a turn of a conversational flow. Each turn
|
||||
is its own kickoff but a session reports one completion, so
|
||||
these spans run many-to-one and would otherwise drag any
|
||||
completion rate computed across all flows.
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
@@ -1030,12 +1035,20 @@ class Telemetry:
|
||||
# extract wrongly. crew_memory reads 1 for 99.8% of crews for exactly
|
||||
# that reason, against a field that defaults to False.
|
||||
self._add_attribute(span, "resumed", "true" if resumed else "false")
|
||||
self._add_attribute(
|
||||
span, "conversational", "true" if conversational else "false"
|
||||
)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def flow_completed_span(
|
||||
self, flow_name: str, duration_ms: float, outcome: str, origin: str = "user"
|
||||
self,
|
||||
flow_name: str,
|
||||
duration_ms: float,
|
||||
outcome: str,
|
||||
origin: str = "user",
|
||||
conversational: bool = False,
|
||||
) -> None:
|
||||
"""Records how long a flow ran and how it ended.
|
||||
|
||||
@@ -1056,6 +1069,8 @@ class Telemetry:
|
||||
outcome: Either ``"completed"`` or ``"failed"``.
|
||||
origin: ``"internal"`` for flows CrewAI itself runs (the agent
|
||||
executor), ``"user"`` for flows the caller authored.
|
||||
conversational: True when this closes a conversational session. One
|
||||
of these answers many ``Flow Execution`` spans, one per turn.
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
@@ -1066,6 +1081,9 @@ class Telemetry:
|
||||
self._add_attribute(span, "duration_ms", duration_ms)
|
||||
self._add_attribute(span, "outcome", outcome)
|
||||
self._add_attribute(span, "origin", origin)
|
||||
self._add_attribute(
|
||||
span, "conversational", "true" if conversational else "false"
|
||||
)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
@@ -17,6 +17,7 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.experimental.conversational import ConversationConfig
|
||||
from crewai.flow.async_feedback import HumanFeedbackPending, PendingFeedbackContext
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.human_feedback import human_feedback
|
||||
@@ -55,7 +56,7 @@ def flow_spans(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, str]]:
|
||||
monkeypatch.setattr(
|
||||
listener_module.event_listener._telemetry,
|
||||
"flow_execution_span",
|
||||
lambda flow_name, node_names, origin="user", resumed=False: recorded.append(
|
||||
lambda flow_name, node_names, origin="user", resumed=False, conversational=False: recorded.append(
|
||||
(flow_name, origin)
|
||||
),
|
||||
)
|
||||
@@ -73,13 +74,33 @@ def starts(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, bool]]:
|
||||
monkeypatch.setattr(
|
||||
listener_module.event_listener._telemetry,
|
||||
"flow_execution_span",
|
||||
lambda flow_name, node_names, origin="user", resumed=False: recorded.append(
|
||||
lambda flow_name, node_names, origin="user", resumed=False, conversational=False: recorded.append(
|
||||
(flow_name, resumed)
|
||||
),
|
||||
)
|
||||
return recorded
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conversational_marks(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, bool]]:
|
||||
"""Record (flow_name, conversational) for every Flow Execution span."""
|
||||
from crewai.events import event_listener as listener_module
|
||||
|
||||
_reregister_listener()
|
||||
|
||||
recorded: list[tuple[str, bool]] = []
|
||||
monkeypatch.setattr(
|
||||
listener_module.event_listener._telemetry,
|
||||
"flow_execution_span",
|
||||
lambda flow_name,
|
||||
node_names,
|
||||
origin="user",
|
||||
resumed=False,
|
||||
conversational=False: recorded.append((flow_name, conversational)),
|
||||
)
|
||||
return recorded
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pauses(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, str]]:
|
||||
"""Record (flow_name, origin) for every Flow Paused span."""
|
||||
@@ -123,7 +144,7 @@ def durations(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, float, str]]:
|
||||
monkeypatch.setattr(
|
||||
listener_module.event_listener._telemetry,
|
||||
"flow_completed_span",
|
||||
lambda flow_name, duration_ms, outcome, origin="user": recorded.append(
|
||||
lambda flow_name, duration_ms, outcome, origin="user", conversational=False: recorded.append(
|
||||
(flow_name, duration_ms, outcome)
|
||||
),
|
||||
)
|
||||
@@ -201,7 +222,7 @@ def test_a_failed_flow_is_still_counted_as_an_execution(
|
||||
monkeypatch.setattr(
|
||||
listener_module.event_listener._telemetry,
|
||||
"flow_execution_span",
|
||||
lambda flow_name, node_names, origin="user", resumed=False: started.append(
|
||||
lambda flow_name, node_names, origin="user", resumed=False, conversational=False: started.append(
|
||||
flow_name
|
||||
),
|
||||
)
|
||||
@@ -220,14 +241,17 @@ def test_a_failed_flow_is_still_counted_as_an_execution(
|
||||
def test_requesting_input_reports_both_sides(features: list[str]) -> None:
|
||||
class StubProvider:
|
||||
def request_input(self, message: str, flow: Flow, metadata=None):
|
||||
return InputResponse(value="typed answer")
|
||||
return InputResponse(text="typed answer")
|
||||
|
||||
class AskFlow(Flow):
|
||||
@start()
|
||||
def go(self) -> str:
|
||||
return self.ask("What topic?")
|
||||
|
||||
AskFlow(input_provider=StubProvider()).kickoff()
|
||||
# ask() swallows provider errors and returns None, so the answer is
|
||||
# asserted too: a provider that raises would otherwise still emit both
|
||||
# signals and pass this test.
|
||||
assert AskFlow(input_provider=StubProvider()).kickoff() == "typed answer"
|
||||
|
||||
emitted = features
|
||||
assert "flow:input_requested" in emitted
|
||||
@@ -549,6 +573,94 @@ def test_a_failed_conversation_session_is_not_reported_completed(
|
||||
assert all(outcome != "completed" for _n, _d, outcome in durations)
|
||||
|
||||
|
||||
def test_a_deferred_session_still_reports_a_failed_turn(
|
||||
durations: list[tuple[str, float, str]],
|
||||
) -> None:
|
||||
"""A deferring session has no per-turn terminal event to carry the failure.
|
||||
|
||||
Its only outcome span is the one ``finalize_session_traces()`` triggers, so
|
||||
the turn-failure flag is what makes that span say ``failed``. Deferral is
|
||||
the default for a conversational flow, so this is the common path.
|
||||
"""
|
||||
|
||||
class DeferringChat(Flow):
|
||||
conversational = True
|
||||
|
||||
@start()
|
||||
def begin(self) -> str:
|
||||
raise RuntimeError("turn exploded")
|
||||
|
||||
chat = DeferringChat()
|
||||
with pytest.raises(RuntimeError, match="turn exploded"):
|
||||
chat.handle_turn("hello")
|
||||
chat.finalize_session_traces()
|
||||
# finalize_session_traces() emits without awaiting its handlers.
|
||||
wait_for_event_handlers()
|
||||
|
||||
assert [outcome for _n, _d, outcome in durations] == ["failed"]
|
||||
|
||||
|
||||
def test_a_failed_turn_does_not_mark_the_next_turn_failed(
|
||||
durations: list[tuple[str, float, str]],
|
||||
) -> None:
|
||||
"""A session that opts out of deferral ends each turn with its own event.
|
||||
|
||||
That terminal event fires inside ``kickoff()``, before ``handle_turn()``
|
||||
emits the turn-failure event, so the flag was set after the run that owned
|
||||
it had already cleared it - and the next healthy turn read it as failed.
|
||||
"""
|
||||
|
||||
turns: list[str] = []
|
||||
|
||||
@ConversationConfig(defer_trace_finalization=False)
|
||||
class FlakyChat(Flow):
|
||||
conversational = True
|
||||
|
||||
@start()
|
||||
def begin(self) -> str:
|
||||
turns.append("turn")
|
||||
if len(turns) == 1:
|
||||
raise RuntimeError("turn exploded")
|
||||
return "second turn is fine"
|
||||
|
||||
chat = FlakyChat()
|
||||
with pytest.raises(RuntimeError, match="turn exploded"):
|
||||
chat.handle_turn("hello")
|
||||
chat.handle_turn("again")
|
||||
|
||||
assert [outcome for _n, _d, outcome in durations] == ["failed", "completed"]
|
||||
|
||||
|
||||
def test_a_failed_streamed_turn_does_not_mark_the_next_turn_failed(
|
||||
durations: list[tuple[str, float, str]],
|
||||
) -> None:
|
||||
"""``stream_turn`` is the other emitter of the turn-failure event.
|
||||
|
||||
It emits from its own ``except`` block, after ``kickoff()`` has closed the
|
||||
run out, so it leaks the same flag as the non-streamed path.
|
||||
"""
|
||||
|
||||
turns: list[str] = []
|
||||
|
||||
@ConversationConfig(defer_trace_finalization=False)
|
||||
class FlakyStreamingChat(Flow):
|
||||
conversational = True
|
||||
|
||||
@start()
|
||||
def begin(self) -> str:
|
||||
turns.append("turn")
|
||||
if len(turns) == 1:
|
||||
raise RuntimeError("turn exploded")
|
||||
return "second turn is fine"
|
||||
|
||||
chat = FlakyStreamingChat()
|
||||
with pytest.raises(RuntimeError, match="turn exploded"):
|
||||
list(chat.stream_turn("hello").events)
|
||||
list(chat.stream_turn("again").events)
|
||||
|
||||
assert [outcome for _n, _d, outcome in durations] == ["failed", "completed"]
|
||||
|
||||
|
||||
def test_infrastructure_flows_do_not_pollute_outcome_signals(
|
||||
features: list[str], durations: list[tuple[str, float, str]]
|
||||
) -> None:
|
||||
@@ -615,3 +727,37 @@ def test_a_checkpoint_restore_is_not_counted_as_a_resume(
|
||||
wait_for_event_handlers()
|
||||
|
||||
assert starts == [("RestoredFlow", False)]
|
||||
|
||||
|
||||
def test_a_conversational_turn_is_marked(
|
||||
conversational_marks: list[tuple[str, bool]],
|
||||
) -> None:
|
||||
"""Each turn is its own kickoff, but a session reports one completion.
|
||||
|
||||
Without the marker those spans run many-to-one against Flow Completed and
|
||||
silently drag any completion rate computed across all flows.
|
||||
"""
|
||||
|
||||
class Chatty(Flow):
|
||||
conversational = True
|
||||
|
||||
@start()
|
||||
def begin(self) -> str:
|
||||
return "hi"
|
||||
|
||||
Chatty().handle_turn("hello")
|
||||
|
||||
assert ("Chatty", True) in conversational_marks
|
||||
|
||||
|
||||
def test_an_ordinary_flow_is_not_marked_conversational(
|
||||
conversational_marks: list[tuple[str, bool]],
|
||||
) -> None:
|
||||
class PlainFlow(Flow):
|
||||
@start()
|
||||
def go(self) -> str:
|
||||
return "ok"
|
||||
|
||||
PlainFlow().kickoff()
|
||||
|
||||
assert ("PlainFlow", False) in conversational_marks
|
||||
|
||||
@@ -364,6 +364,17 @@ def test_flow_completed_records_duration_outcome_and_origin() -> None:
|
||||
span.set_attribute.assert_any_call("duration_ms", 12.5)
|
||||
span.set_attribute.assert_any_call("outcome", "failed")
|
||||
span.set_attribute.assert_any_call("origin", "user")
|
||||
span.set_attribute.assert_any_call("conversational", "false")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("flag", "expected"), [(True, "true"), (False, "false")])
|
||||
def test_conversational_is_recorded_as_a_string(flag: bool, expected: str) -> None:
|
||||
"""Same reason as resumed: a bool arrives as key presence, not a value."""
|
||||
_tracer, span = _emit(
|
||||
"flow_execution_span", "ResearchFlow", ["start"], "user", False, flag
|
||||
)
|
||||
|
||||
span.set_attribute.assert_any_call("conversational", expected)
|
||||
|
||||
|
||||
def test_paused_and_method_failed_record_flow_and_origin() -> None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from crewai.events.listeners.tracing.trace_listener import (
|
||||
)
|
||||
from crewai.events.listeners.tracing.types import TraceEvent
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
from tests.utils import wait_for_event_handlers
|
||||
|
||||
|
||||
@@ -1781,3 +1782,175 @@ class TestTraceBatchManagerDuplicateInitMerge:
|
||||
assert meta.get("execution_type") == "flow"
|
||||
assert meta.get("flow_name") == "ResearchFlow"
|
||||
assert meta.get("crew_name") == "Unknown Crew"
|
||||
|
||||
|
||||
class TestTraceBatchSentTelemetry:
|
||||
"""A finalized trace batch reports that it reached AMP.
|
||||
|
||||
The signal is what links an OSS project to its AMP traces, so it must fire
|
||||
on exactly the runs that landed and on no others.
|
||||
"""
|
||||
|
||||
def _make_manager(self, *, ephemeral: bool) -> TraceBatchManager:
|
||||
with patch(
|
||||
"crewai.events.listeners.tracing.trace_batch_manager.get_auth_token",
|
||||
return_value="mock_token",
|
||||
):
|
||||
bm = TraceBatchManager()
|
||||
bm.trace_batch_id = "batch-id"
|
||||
bm.is_current_batch_ephemeral = ephemeral
|
||||
bm._telemetry = MagicMock()
|
||||
return bm
|
||||
|
||||
def _finalize(self, bm: TraceBatchManager, *, status_code: int = 200) -> bool:
|
||||
endpoint = (
|
||||
"finalize_ephemeral_trace_batch"
|
||||
if bm.is_current_batch_ephemeral
|
||||
else "finalize_trace_batch"
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
bm.plus_api,
|
||||
endpoint,
|
||||
return_value=MagicMock(
|
||||
status_code=status_code,
|
||||
json=MagicMock(return_value={"access_code": "TRACE-test"}),
|
||||
text="error body",
|
||||
),
|
||||
),
|
||||
patch.object(bm, "_mark_batch_as_failed"),
|
||||
patch("crewai.events.listeners.tracing.trace_batch_manager.Console.print"),
|
||||
):
|
||||
return bm._finalize_backend_batch()
|
||||
|
||||
def _features(self, bm: TraceBatchManager) -> list[str]:
|
||||
return [c.args[0] for c in bm._telemetry.feature_usage_span.call_args_list]
|
||||
|
||||
def test_ephemeral_batch_reports_ephemeral_sent(self) -> None:
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
assert self._finalize(bm) is True
|
||||
assert self._features(bm) == ["tracing:ephemeral_sent"]
|
||||
|
||||
def test_authenticated_batch_reports_authenticated_sent(self) -> None:
|
||||
bm = self._make_manager(ephemeral=False)
|
||||
assert self._finalize(bm) is True
|
||||
assert self._features(bm) == ["tracing:authenticated_sent"]
|
||||
|
||||
def test_rejected_batch_reports_nothing(self) -> None:
|
||||
"""A 500 means the batch never landed, so it must not be counted."""
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
assert self._finalize(bm, status_code=500) is False
|
||||
assert self._features(bm) == []
|
||||
|
||||
def test_batch_without_id_reports_nothing(self) -> None:
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
bm.trace_batch_id = None
|
||||
assert self._finalize(bm) is True
|
||||
assert self._features(bm) == []
|
||||
|
||||
def test_already_finalized_batch_reports_nothing(self) -> None:
|
||||
"""Finalization is idempotent; the signal must not double-count."""
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
bm._batch_finalized = True
|
||||
assert self._finalize(bm) is True
|
||||
assert self._features(bm) == []
|
||||
|
||||
def test_finalization_raising_reports_nothing(self) -> None:
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
with (
|
||||
patch.object(
|
||||
bm.plus_api,
|
||||
"finalize_ephemeral_trace_batch",
|
||||
side_effect=RuntimeError("network down"),
|
||||
),
|
||||
patch.object(bm, "_mark_batch_as_failed"),
|
||||
):
|
||||
assert bm._finalize_backend_batch() is False
|
||||
assert self._features(bm) == []
|
||||
|
||||
def test_batch_that_fell_back_to_ephemeral_reports_ephemeral(self) -> None:
|
||||
"""A 401 at init flips the batch to ephemeral; finalize must agree."""
|
||||
bm = self._make_manager(ephemeral=False)
|
||||
bm.current_batch = TraceBatch(
|
||||
user_context={"privacy_level": "standard"},
|
||||
execution_metadata={"execution_type": "crew"},
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
bm.plus_api,
|
||||
"initialize_trace_batch",
|
||||
return_value=MagicMock(status_code=401),
|
||||
),
|
||||
patch.object(
|
||||
bm.plus_api,
|
||||
"initialize_ephemeral_trace_batch",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=MagicMock(return_value={"ephemeral_trace_id": "eph-id"}),
|
||||
),
|
||||
),
|
||||
):
|
||||
bm._initialize_backend_batch({}, {}, skip_context_check=True)
|
||||
|
||||
assert bm.is_current_batch_ephemeral is True
|
||||
assert self._finalize(bm) is True
|
||||
assert self._features(bm) == ["tracing:ephemeral_sent"]
|
||||
|
||||
def test_first_time_handler_path_reports_ephemeral_sent(self) -> None:
|
||||
"""The first-time flow finalizes through its own caller, not finalize_batch."""
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
bm.current_batch = TraceBatch(
|
||||
user_context={"privacy_level": "standard"},
|
||||
execution_metadata={"execution_type": "crew", "crew_name": "test"},
|
||||
)
|
||||
bm.backend_initialized = True
|
||||
|
||||
handler = FirstTimeTraceHandler()
|
||||
handler.is_first_time = True
|
||||
handler.collected_events = True
|
||||
handler.batch_manager = bm
|
||||
|
||||
with (
|
||||
patch.object(bm, "_send_events_to_backend", return_value=200),
|
||||
patch.object(
|
||||
bm.plus_api,
|
||||
"finalize_ephemeral_trace_batch",
|
||||
return_value=MagicMock(
|
||||
status_code=200,
|
||||
json=MagicMock(return_value={"access_code": "TRACE-test"}),
|
||||
),
|
||||
),
|
||||
patch("crewai.events.listeners.tracing.trace_batch_manager.Console.print"),
|
||||
patch.object(handler, "_show_local_trace_message"),
|
||||
):
|
||||
handler._initialize_backend_and_send_events()
|
||||
|
||||
assert self._features(bm) == ["tracing:ephemeral_sent"]
|
||||
|
||||
def test_broken_telemetry_does_not_break_finalization(self) -> None:
|
||||
"""Telemetry is best-effort: a failing exporter must not fail the batch."""
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
bm._telemetry = Telemetry()
|
||||
broken_provider = MagicMock()
|
||||
broken_provider.get_tracer.side_effect = RuntimeError("exporter down")
|
||||
|
||||
with (
|
||||
# `provider` exists only when telemetry initialized, so create it here
|
||||
# to reach the failure this test is about.
|
||||
patch.object(bm._telemetry, "provider", broken_provider, create=True),
|
||||
patch.object(bm._telemetry, "_should_execute_telemetry", return_value=True),
|
||||
):
|
||||
assert self._finalize(bm) is True
|
||||
|
||||
# Proves the assertion above is not vacuous: the span was really attempted.
|
||||
broken_provider.get_tracer.assert_called_once()
|
||||
|
||||
def test_disabled_telemetry_does_not_break_finalization(self) -> None:
|
||||
"""Opted-out users have no provider at all; finalize must still succeed."""
|
||||
bm = self._make_manager(ephemeral=True)
|
||||
bm._telemetry = Telemetry()
|
||||
|
||||
with patch.object(
|
||||
bm._telemetry, "_should_execute_telemetry", return_value=False
|
||||
):
|
||||
assert self._finalize(bm) is True
|
||||
|
||||
@@ -506,7 +506,7 @@ def test_flow_emits_start_event(reset_event_listener_singleton):
|
||||
|
||||
assert event_received.wait(timeout=5), "Timeout waiting for flow started event"
|
||||
mock_telemetry.flow_execution_span.assert_called_once_with(
|
||||
"TestFlow", ["begin"], "user", False
|
||||
"TestFlow", ["begin"], "user", False, False
|
||||
)
|
||||
assert len(received_events) == 1
|
||||
assert received_events[0].flow_name == "TestFlow"
|
||||
|
||||
Reference in New Issue
Block a user