diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 633d6c0e0..b62b0eba5 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -6,7 +6,24 @@ permissions:
contents: read
jobs:
- lint:
+ changes:
+ name: Detect changes
+ runs-on: ubuntu-latest
+ outputs:
+ code: ${{ steps.filter.outputs.code }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ code:
+ - '!docs/**'
+ - '!**/*.md'
+
+ lint-run:
+ needs: changes
+ if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -48,3 +65,23 @@ jobs:
~/.local/share/uv
.venv
key: uv-main-py3.11-${{ hashFiles('uv.lock') }}
+
+ # Summary job to provide single status for branch protection
+ lint:
+ name: lint
+ runs-on: ubuntu-latest
+ needs: [changes, lint-run]
+ if: always()
+ steps:
+ - name: Check results
+ run: |
+ if [ "${{ needs.changes.outputs.code }}" != "true" ]; then
+ echo "Docs-only change, skipping lint"
+ exit 0
+ fi
+ if [ "${{ needs.lint-run.result }}" == "success" ]; then
+ echo "Lint passed"
+ else
+ echo "Lint failed"
+ exit 1
+ fi
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index fb75beb8a..71a7b801a 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -6,8 +6,25 @@ permissions:
contents: read
jobs:
- tests:
+ changes:
+ name: Detect changes
+ runs-on: ubuntu-latest
+ outputs:
+ code: ${{ steps.filter.outputs.code }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ code:
+ - '!docs/**'
+ - '!**/*.md'
+
+ tests-matrix:
name: tests (${{ matrix.python-version }})
+ needs: changes
+ if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
@@ -98,3 +115,23 @@ jobs:
~/.local/share/uv
.venv
key: uv-main-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
+
+ # Summary job to provide single status for branch protection
+ tests:
+ name: tests
+ runs-on: ubuntu-latest
+ needs: [changes, tests-matrix]
+ if: always()
+ steps:
+ - name: Check results
+ run: |
+ if [ "${{ needs.changes.outputs.code }}" != "true" ]; then
+ echo "Docs-only change, skipping tests"
+ exit 0
+ fi
+ if [ "${{ needs.tests-matrix.result }}" == "success" ]; then
+ echo "All tests passed"
+ else
+ echo "Tests failed"
+ exit 1
+ fi
diff --git a/.github/workflows/type-checker.yml b/.github/workflows/type-checker.yml
index 2bab1ebb7..8d88d4b6a 100644
--- a/.github/workflows/type-checker.yml
+++ b/.github/workflows/type-checker.yml
@@ -6,8 +6,25 @@ permissions:
contents: read
jobs:
+ changes:
+ name: Detect changes
+ runs-on: ubuntu-latest
+ outputs:
+ code: ${{ steps.filter.outputs.code }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ code:
+ - '!docs/**'
+ - '!**/*.md'
+
type-checker-matrix:
name: type-checker (${{ matrix.python-version }})
+ needs: changes
+ if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -57,14 +74,18 @@ jobs:
type-checker:
name: type-checker
runs-on: ubuntu-latest
- needs: type-checker-matrix
+ needs: [changes, type-checker-matrix]
if: always()
steps:
- - name: Check matrix results
+ - name: Check results
run: |
- if [ "${{ needs.type-checker-matrix.result }}" == "success" ] || [ "${{ needs.type-checker-matrix.result }}" == "skipped" ]; then
- echo "✅ All type checks passed"
+ if [ "${{ needs.changes.outputs.code }}" != "true" ]; then
+ echo "Docs-only change, skipping type checks"
+ exit 0
+ fi
+ if [ "${{ needs.type-checker-matrix.result }}" == "success" ]; then
+ echo "All type checks passed"
else
- echo "❌ Type checks failed"
+ echo "Type checks failed"
exit 1
fi
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d704c83b1..51d720ebf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,6 +24,14 @@ repos:
rev: 0.11.3
hooks:
- id: uv-lock
+ - repo: local
+ hooks:
+ - id: pip-audit
+ name: pip-audit
+ entry: bash -c 'source .venv/bin/activate && uv run pip-audit --skip-editable --ignore-vuln CVE-2025-69872 --ignore-vuln CVE-2026-25645 --ignore-vuln CVE-2026-27448 --ignore-vuln CVE-2026-27459 --ignore-vuln PYSEC-2023-235' --
+ language: system
+ pass_filenames: false
+ stages: [pre-push, manual]
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.10.1
hooks:
diff --git a/docs/ar/changelog.mdx b/docs/ar/changelog.mdx
index b2f335d6c..7b4ad9f17 100644
--- a/docs/ar/changelog.mdx
+++ b/docs/ar/changelog.mdx
@@ -4,6 +4,214 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2rc1
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2rc1)
+
+ ## ما الذي تغير
+
+ ### إصلاحات الأخطاء
+ - إصلاح معالجة مخططات JSON الدائرية في أداة MCP
+ - إصلاح ثغرة أمنية من خلال تحديث python-multipart إلى 0.0.26
+ - إصلاح ثغرة أمنية من خلال تحديث pypdf إلى 6.10.1
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.2a5
+
+ ## المساهمون
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a5
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
+
+ ## ما الذي تغير
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.2a4
+
+ ## المساهمون
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a4
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## ما الذي تغير
+
+ ### الميزات
+ - إضافة تلميحات استئناف إلى إصدار أدوات المطورين عند الفشل
+
+ ### إصلاحات الأخطاء
+ - إصلاح توجيه وضع الصرامة إلى واجهة برمجة تطبيقات Bedrock Converse
+ - إصلاح إصدار pytest إلى 9.0.3 لثغرة الأمان GHSA-6w46-j5rx-g56g
+ - رفع الحد الأدنى لـ OpenAI إلى >=2.0.0
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.2a3
+
+ ## المساهمون
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a3
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a3)
+
+ ## ما الذي تغير
+
+ ### الميزات
+ - إضافة واجهة سطر الأوامر للتحقق من النشر
+ - تحسين سهولة استخدام تهيئة LLM
+
+ ### إصلاحات الأخطاء
+ - تجاوز pypdf و uv إلى إصدارات مصححة لـ CVE-2026-40260 و GHSA-pjjw-68hj-v9mw
+ - ترقية requests إلى >=2.33.0 لمعالجة ثغرة ملف مؤقت CVE
+ - الحفاظ على معلمات استدعاء أداة Bedrock من خلال إزالة القيمة الافتراضية الصحيحة
+ - تنظيف مخططات الأدوات لوضع صارم
+ - إصلاح اختبار تسلسل تضمين MemoryRecord
+
+ ### الوثائق
+ - تنظيف لغة A2A الخاصة بالمؤسسات
+ - إضافة وثائق ميزات A2A الخاصة بالمؤسسات
+ - تحديث وثائق A2A الخاصة بالمصادر المفتوحة
+ - تحديث سجل التغييرات والإصدار لـ v1.14.2a2
+
+ ## المساهمون
+
+ @Yanhu007, @greysonlalonde
+
+
+
+
+ ## v1.14.2a2
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a2)
+
+ ## ما الذي تغير
+
+ ### الميزات
+ - إضافة واجهة مستخدم نصية لنقطة التحقق مع عرض شجري، ودعم التفرع، ومدخلات/مخرجات قابلة للتعديل
+ - إثراء تتبع رموز LLM مع رموز الاستدلال ورموز إنشاء التخزين المؤقت
+ - إضافة معلمة `from_checkpoint` إلى طرق الانطلاق
+ - تضمين `crewai_version` في نقاط التحقق مع إطار عمل الهجرة
+ - إضافة تفرع نقاط التحقق مع تتبع السلالة
+
+ ### إصلاحات الأخطاء
+ - إصلاح توجيه الوضع الصارم إلى مزودي Anthropic وBedrock
+ - تعزيز NL2SQLTool مع وضع القراءة فقط الافتراضي، والتحقق من الاستعلامات، والاستعلامات المعلمة
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.2a1
+
+ ## المساهمون
+
+ @alex-clawd, @github-actions[bot], @greysonlalonde, @lucasgomide
+
+
+
+
+ ## v1.14.2a1
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a1)
+
+ ## ما الذي تغير
+
+ ### إصلاحات الأخطاء
+ - إصلاح إصدار حدث flow_finished بعد استئناف HITL
+ - إصلاح إصدار التشفير إلى 46.0.7 لمعالجة CVE-2026-39892
+
+ ### إعادة هيكلة
+ - إعادة هيكلة لاستخدام I18N_DEFAULT المشترك
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.1
+
+ ## المساهمون
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.1
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1)
+
+ ## ما الذي تغير
+
+ ### الميزات
+ - إضافة متصفح TUI لنقاط التفتيش غير المتزامنة
+ - إضافة دالة aclose()/close() ومدير سياق غير متزامن لمخرجات البث
+
+ ### إصلاحات الأخطاء
+ - إصلاح التعبير النمطي لزيادة إصدار pyproject.toml
+ - تنظيف أسماء الأدوات في مرشحات زخرفة الخطاف
+ - إصلاح تسجيل معالجات نقاط التفتيش عند إنشاء CheckpointConfig
+ - رفع إصدار transformers إلى 5.5.0 لحل CVE-2026-1839
+ - إزالة غلاف FilteredStream لـ stdout/stderr
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.1rc1
+
+ ### إعادة الهيكلة
+ - استبدال القائمة المحظورة الثابتة باستبعاد حقل BaseTool الديناميكي في توليد المواصفات
+ - استبدال التعبير النمطي بـ tomlkit في واجهة سطر أوامر أدوات التطوير
+ - استخدام كائن PRINTER المشترك
+ - جعل BaseProvider نموذجاً أساسياً مع مميز نوع المزود
+
+ ## المساهمون
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay
+
+
+
+
+ ## v1.14.1rc1
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1)
+
+ ## ما الذي تغير
+
+ ### الميزات
+ - إضافة متصفح TUI لنقطة التحقق غير المتزامنة
+ - إضافة aclose()/close() ومدير سياق غير متزامن لمخرجات البث
+
+ ### إصلاحات الأخطاء
+ - إصلاح زيادة إصدارات pyproject.toml باستخدام التعبيرات العادية
+ - تنظيف أسماء الأدوات في مرشحات ديكور المكونات
+ - زيادة إصدار transformers إلى 5.5.0 لحل CVE-2026-1839
+ - تسجيل معالجات نقطة التحقق عند إنشاء CheckpointConfig
+
+ ### إعادة الهيكلة
+ - استبدال القائمة المحظورة الثابتة باستبعاد حقل BaseTool الديناميكي في توليد المواصفات
+ - استبدال التعبيرات العادية بـ tomlkit في واجهة سطر الأوامر devtools
+ - استخدام كائن PRINTER المشترك
+ - جعل BaseProvider نموذجًا أساسيًا مع مميز نوع المزود
+ - إزالة غلاف stdout/stderr لـ FilteredStream
+ - إزالة flow/config.py غير المستخدمة
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.0
+
+ ## المساهمون
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura
+
+
+
## v1.14.0
diff --git a/docs/ar/installation.mdx b/docs/ar/installation.mdx
index 3a902fae0..6690e72ec 100644
--- a/docs/ar/installation.mdx
+++ b/docs/ar/installation.mdx
@@ -196,7 +196,7 @@ python3 --version
- يدعم أي مزود سحابي بما في ذلك النشر المحلي
- تكامل مع أنظمة الأمان الحالية
-
+
تعرّف على عروض CrewAI للمؤسسات وجدول عرضًا توضيحيًا
diff --git a/docs/ar/learn/streaming-crew-execution.mdx b/docs/ar/learn/streaming-crew-execution.mdx
index 930ef389f..4dfe1859f 100644
--- a/docs/ar/learn/streaming-crew-execution.mdx
+++ b/docs/ar/learn/streaming-crew-execution.mdx
@@ -325,6 +325,34 @@ asyncio.run(interactive_research())
- **تجربة المستخدم**: تقليل زمن الاستجابة المتصور بعرض نتائج تدريجية
- **لوحات المعلومات الحية**: بناء واجهات مراقبة تعرض حالة تنفيذ الطاقم
+## الإلغاء وتنظيف الموارد
+
+يدعم `CrewStreamingOutput` الإلغاء السلس بحيث يتوقف العمل الجاري فوراً عند انقطاع اتصال المستهلك.
+
+### مدير السياق غير المتزامن
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+
+async with streaming:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+```
+
+### الإلغاء الصريح
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+try:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+finally:
+ await streaming.aclose() # غير متزامن
+ # streaming.close() # المكافئ المتزامن
+```
+
+بعد الإلغاء، يكون كل من `streaming.is_cancelled` و `streaming.is_completed` بقيمة `True`. كل من `aclose()` و `close()` متساويان القوة.
+
## ملاحظات مهمة
- يفعّل البث تلقائياً بث LLM لجميع الوكلاء في الطاقم
diff --git a/docs/ar/learn/streaming-flow-execution.mdx b/docs/ar/learn/streaming-flow-execution.mdx
index 53663c111..de4575b1c 100644
--- a/docs/ar/learn/streaming-flow-execution.mdx
+++ b/docs/ar/learn/streaming-flow-execution.mdx
@@ -420,6 +420,34 @@ except Exception as e:
print("Streaming completed but flow encountered an error")
```
+## الإلغاء وتنظيف الموارد
+
+يدعم `FlowStreamingOutput` الإلغاء السلس بحيث يتوقف العمل الجاري فوراً عند انقطاع اتصال المستهلك.
+
+### مدير السياق غير المتزامن
+
+```python Code
+streaming = await flow.kickoff_async()
+
+async with streaming:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+```
+
+### الإلغاء الصريح
+
+```python Code
+streaming = await flow.kickoff_async()
+try:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+finally:
+ await streaming.aclose() # غير متزامن
+ # streaming.close() # المكافئ المتزامن
+```
+
+بعد الإلغاء، يكون كل من `streaming.is_cancelled` و `streaming.is_completed` بقيمة `True`. كل من `aclose()` و `close()` متساويان القوة.
+
## ملاحظات مهمة
- يفعّل البث تلقائياً بث LLM لأي أطقم مستخدمة داخل التدفق
diff --git a/docs/ar/skills.mdx b/docs/ar/skills.mdx
new file mode 100644
index 000000000..4e0bf6e22
--- /dev/null
+++ b/docs/ar/skills.mdx
@@ -0,0 +1,50 @@
+---
+title: Skills
+description: ثبّت crewaiinc/skills من السجل الرسمي على skills.sh—Flows وCrews ووكلاء مرتبطون بالوثائق لـ Claude Code وCursor وCodex وغيرها.
+icon: wand-magic-sparkles
+mode: "wide"
+---
+
+# Skills
+
+**امنح وكيل البرمجة سياق CrewAI في أمر واحد.**
+
+تُنشر **Skills** الخاصة بـ CrewAI على **[skills.sh/crewaiinc/skills](https://skills.sh/crewaiinc/skills)**—السجل الرسمي لـ `crewaiinc/skills`، بما في ذلك كل مهارة (مثل **design-agent** و**getting-started** و**design-task** و**ask-docs**) وإحصاءات التثبيت والتدقيقات. تعلّم وكلاء البرمجة—مثل Claude Code وCursor وCodex—هيكلة Flows وضبط Crews واستخدام الأدوات واتباع أنماط CrewAI. نفّذ الأمر أدناه (أو الصقه في الوكيل).
+
+```shell Terminal
+npx skills add crewaiinc/skills
+```
+
+يضيف ذلك حزمة المهارات إلى سير عمل الوكيل لتطبيق اتفاقيات CrewAI دون إعادة شرح الإطار في كل جلسة. المصدر والقضايا على [GitHub](https://github.com/crewAIInc/skills).
+
+## ما يحصل عليه الوكيل
+
+- **Flows** — تطبيقات ذات حالة وخطوات وkickoffs للـ crew على نمط CrewAI
+- **Crews والوكلاء** — أنماط YAML أولاً، أدوار، مهام، وتفويض
+- **الأدوات والتكاملات** — ربط الوكلاء بالبحث وواجهات API وأدوات CrewAI الشائعة
+- **هيكل المشروع** — مواءمة مع قوالب CLI واتفاقيات المستودع
+- **أنماط محدثة** — تتبع المهارات وثائق CrewAI والممارسات الموصى بها
+
+## تعرّف أكثر على هذا الموقع
+
+
+
+ استخدام `AGENTS.md` وسير عمل وكلاء البرمجة مع CrewAI.
+
+
+ ابنِ أول Flow وcrew من البداية للنهاية.
+
+
+ ثبّت CrewAI CLI وحزمة Python.
+
+
+ القائمة الرسمية لـ `crewaiinc/skills`—المهارات والتثبيتات والتدقيقات.
+
+
+ مصدر الحزمة والتحديثات والقضايا.
+
+
+
+### فيديو: CrewAI مع مهارات وكلاء البرمجة
+
+
diff --git a/docs/ar/tools/database-data/nl2sqltool.mdx b/docs/ar/tools/database-data/nl2sqltool.mdx
index de52a5dd8..6ddc6e058 100644
--- a/docs/ar/tools/database-data/nl2sqltool.mdx
+++ b/docs/ar/tools/database-data/nl2sqltool.mdx
@@ -11,7 +11,7 @@ mode: "wide"
يتيح ذلك سير عمل متعددة مثل أن يقوم وكيل بالوصول إلى قاعدة البيانات واسترجاع المعلومات بناءً على الهدف ثم استخدام تلك المعلومات لتوليد استجابة أو تقرير أو أي مخرجات أخرى. بالإضافة إلى ذلك، يوفر القدرة للوكيل على تحديث قاعدة البيانات بناءً على هدفه.
-**تنبيه**: تأكد من أن الوكيل لديه وصول إلى نسخة قراءة فقط أو أنه من المقبول أن يقوم الوكيل بتنفيذ استعلامات إدراج/تحديث على قاعدة البيانات.
+**تنبيه**: الأداة للقراءة فقط بشكل افتراضي (SELECT/SHOW/DESCRIBE/EXPLAIN فقط). تتطلب عمليات الكتابة تمرير `allow_dml=True` أو ضبط متغير البيئة `CREWAI_NL2SQL_ALLOW_DML=true`. عند تفعيل الكتابة، تأكد من أن الوكيل يستخدم مستخدم قاعدة بيانات محدود الصلاحيات أو نسخة قراءة كلما أمكن.
## نموذج الأمان
@@ -36,6 +36,74 @@ mode: "wide"
- أضف خطافات `before_tool_call` لفرض أنماط الاستعلام المسموح بها
- فعّل تسجيل الاستعلامات والتنبيهات للعبارات التدميرية
+## وضع القراءة فقط وتهيئة DML
+
+تعمل `NL2SQLTool` في **وضع القراءة فقط بشكل افتراضي**. لا يُسمح إلا بأنواع العبارات التالية دون تهيئة إضافية:
+
+- `SELECT`
+- `SHOW`
+- `DESCRIBE`
+- `EXPLAIN`
+
+أي محاولة لتنفيذ عملية كتابة (`INSERT`، `UPDATE`، `DELETE`، `DROP`، `CREATE`، `ALTER`، `TRUNCATE`، إلخ) ستُسبب خطأً ما لم يتم تفعيل DML صراحةً.
+
+كما تُحظر الاستعلامات متعددة العبارات التي تحتوي على فاصلة منقوطة (مثل `SELECT 1; DROP TABLE users`) في وضع القراءة فقط لمنع هجمات الحقن.
+
+### تفعيل عمليات الكتابة
+
+يمكنك تفعيل DML (لغة معالجة البيانات) بطريقتين:
+
+**الخيار الأول — معامل المُنشئ:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+**الخيار الثاني — متغير البيئة:**
+
+```bash
+CREWAI_NL2SQL_ALLOW_DML=true
+```
+
+```python
+from crewai_tools import NL2SQLTool
+
+# DML مفعّل عبر متغير البيئة
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+### أمثلة الاستخدام
+
+**القراءة فقط (الافتراضي) — آمن للتحليلات والتقارير:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# يُسمح فقط بـ SELECT/SHOW/DESCRIBE/EXPLAIN
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+**مع تفعيل DML — مطلوب لأعباء عمل الكتابة:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# يُسمح بـ INSERT وUPDATE وDELETE وDROP وغيرها
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+
+يمنح تفعيل DML للوكيل القدرة على تعديل البيانات أو حذفها. لا تفعّله إلا عندما يتطلب حالة الاستخدام صراحةً وصولاً للكتابة، وتأكد من أن بيانات اعتماد قاعدة البيانات محدودة بالحد الأدنى من الصلاحيات المطلوبة.
+
+
## المتطلبات
- SqlAlchemy
diff --git a/docs/docs.json b/docs/docs.json
index 9d2679a19..3f37157df 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -56,7 +56,7 @@
},
"versions": [
{
- "version": "v1.14.0",
+ "version": "v1.14.1",
"default": true,
"tabs": [
{
@@ -79,6 +79,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -391,7 +392,482 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
+ ]
+ },
+ {
+ "group": "Operate",
+ "pages": [
+ "en/enterprise/features/traces",
+ "en/enterprise/features/webhook-streaming",
+ "en/enterprise/features/hallucination-guardrail",
+ "en/enterprise/features/flow-hitl-management"
+ ]
+ },
+ {
+ "group": "Manage",
+ "pages": [
+ "en/enterprise/features/sso",
+ "en/enterprise/features/rbac"
+ ]
+ },
+ {
+ "group": "Integration Docs",
+ "pages": [
+ "en/enterprise/integrations/asana",
+ "en/enterprise/integrations/box",
+ "en/enterprise/integrations/clickup",
+ "en/enterprise/integrations/github",
+ "en/enterprise/integrations/gmail",
+ "en/enterprise/integrations/google_calendar",
+ "en/enterprise/integrations/google_contacts",
+ "en/enterprise/integrations/google_docs",
+ "en/enterprise/integrations/google_drive",
+ "en/enterprise/integrations/google_sheets",
+ "en/enterprise/integrations/google_slides",
+ "en/enterprise/integrations/hubspot",
+ "en/enterprise/integrations/jira",
+ "en/enterprise/integrations/linear",
+ "en/enterprise/integrations/microsoft_excel",
+ "en/enterprise/integrations/microsoft_onedrive",
+ "en/enterprise/integrations/microsoft_outlook",
+ "en/enterprise/integrations/microsoft_sharepoint",
+ "en/enterprise/integrations/microsoft_teams",
+ "en/enterprise/integrations/microsoft_word",
+ "en/enterprise/integrations/notion",
+ "en/enterprise/integrations/salesforce",
+ "en/enterprise/integrations/shopify",
+ "en/enterprise/integrations/slack",
+ "en/enterprise/integrations/stripe",
+ "en/enterprise/integrations/zendesk"
+ ]
+ },
+ {
+ "group": "Triggers",
+ "pages": [
+ "en/enterprise/guides/automation-triggers",
+ "en/enterprise/guides/gmail-trigger",
+ "en/enterprise/guides/google-calendar-trigger",
+ "en/enterprise/guides/google-drive-trigger",
+ "en/enterprise/guides/outlook-trigger",
+ "en/enterprise/guides/onedrive-trigger",
+ "en/enterprise/guides/microsoft-teams-trigger",
+ "en/enterprise/guides/slack-trigger",
+ "en/enterprise/guides/hubspot-trigger",
+ "en/enterprise/guides/salesforce-trigger",
+ "en/enterprise/guides/zapier-trigger"
+ ]
+ },
+ {
+ "group": "How-To Guides",
+ "pages": [
+ "en/enterprise/guides/build-crew",
+ "en/enterprise/guides/prepare-for-deployment",
+ "en/enterprise/guides/deploy-to-amp",
+ "en/enterprise/guides/private-package-registry",
+ "en/enterprise/guides/kickoff-crew",
+ "en/enterprise/guides/update-crew",
+ "en/enterprise/guides/enable-crew-studio",
+ "en/enterprise/guides/capture_telemetry_logs",
+ "en/enterprise/guides/azure-openai-setup",
+ "en/enterprise/guides/tool-repository",
+ "en/enterprise/guides/custom-mcp-server",
+ "en/enterprise/guides/react-component-export",
+ "en/enterprise/guides/team-management",
+ "en/enterprise/guides/human-in-the-loop",
+ "en/enterprise/guides/webhook-automation"
+ ]
+ },
+ {
+ "group": "Resources",
+ "pages": [
+ "en/enterprise/resources/frequently-asked-questions"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "API Reference",
+ "icon": "magnifying-glass",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": [
+ "en/api-reference/introduction",
+ "en/api-reference/inputs",
+ "en/api-reference/kickoff",
+ "en/api-reference/resume",
+ "en/api-reference/status"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Examples",
+ "icon": "code",
+ "groups": [
+ {
+ "group": "Examples",
+ "pages": [
+ "en/examples/example",
+ "en/examples/cookbooks"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Changelog",
+ "icon": "clock",
+ "groups": [
+ {
+ "group": "Release Notes",
+ "pages": [
+ "en/changelog"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "version": "v1.14.0",
+ "tabs": [
+ {
+ "tab": "Home",
+ "icon": "house",
+ "groups": [
+ {
+ "group": "Welcome",
+ "pages": [
+ "index"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Documentation",
+ "icon": "book-open",
+ "groups": [
+ {
+ "group": "Get Started",
+ "pages": [
+ "en/introduction",
+ "en/skills",
+ "en/installation",
+ "en/quickstart"
+ ]
+ },
+ {
+ "group": "Guides",
+ "pages": [
+ {
+ "group": "Strategy",
+ "icon": "compass",
+ "pages": [
+ "en/guides/concepts/evaluating-use-cases"
+ ]
+ },
+ {
+ "group": "Agents",
+ "icon": "user",
+ "pages": [
+ "en/guides/agents/crafting-effective-agents"
+ ]
+ },
+ {
+ "group": "Crews",
+ "icon": "users",
+ "pages": [
+ "en/guides/crews/first-crew"
+ ]
+ },
+ {
+ "group": "Flows",
+ "icon": "code-branch",
+ "pages": [
+ "en/guides/flows/first-flow",
+ "en/guides/flows/mastering-flow-state"
+ ]
+ },
+ {
+ "group": "Tools",
+ "icon": "wrench",
+ "pages": [
+ "en/guides/tools/publish-custom-tools"
+ ]
+ },
+ {
+ "group": "Coding Tools",
+ "icon": "terminal",
+ "pages": [
+ "en/guides/coding-tools/agents-md"
+ ]
+ },
+ {
+ "group": "Advanced",
+ "icon": "gear",
+ "pages": [
+ "en/guides/advanced/customizing-prompts",
+ "en/guides/advanced/fingerprinting"
+ ]
+ },
+ {
+ "group": "Migration",
+ "icon": "shuffle",
+ "pages": [
+ "en/guides/migration/migrating-from-langgraph"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Core Concepts",
+ "pages": [
+ "en/concepts/agents",
+ "en/concepts/agent-capabilities",
+ "en/concepts/tasks",
+ "en/concepts/crews",
+ "en/concepts/flows",
+ "en/concepts/production-architecture",
+ "en/concepts/knowledge",
+ "en/concepts/skills",
+ "en/concepts/llms",
+ "en/concepts/files",
+ "en/concepts/processes",
+ "en/concepts/collaboration",
+ "en/concepts/training",
+ "en/concepts/memory",
+ "en/concepts/reasoning",
+ "en/concepts/planning",
+ "en/concepts/testing",
+ "en/concepts/cli",
+ "en/concepts/tools",
+ "en/concepts/event-listener",
+ "en/concepts/checkpointing"
+ ]
+ },
+ {
+ "group": "MCP Integration",
+ "pages": [
+ "en/mcp/overview",
+ "en/mcp/dsl-integration",
+ "en/mcp/stdio",
+ "en/mcp/sse",
+ "en/mcp/streamable-http",
+ "en/mcp/multiple-servers",
+ "en/mcp/security"
+ ]
+ },
+ {
+ "group": "Tools",
+ "pages": [
+ "en/tools/overview",
+ {
+ "group": "File & Document",
+ "icon": "folder-open",
+ "pages": [
+ "en/tools/file-document/overview",
+ "en/tools/file-document/filereadtool",
+ "en/tools/file-document/filewritetool",
+ "en/tools/file-document/pdfsearchtool",
+ "en/tools/file-document/docxsearchtool",
+ "en/tools/file-document/mdxsearchtool",
+ "en/tools/file-document/xmlsearchtool",
+ "en/tools/file-document/txtsearchtool",
+ "en/tools/file-document/jsonsearchtool",
+ "en/tools/file-document/csvsearchtool",
+ "en/tools/file-document/directorysearchtool",
+ "en/tools/file-document/directoryreadtool",
+ "en/tools/file-document/ocrtool",
+ "en/tools/file-document/pdf-text-writing-tool"
+ ]
+ },
+ {
+ "group": "Web Scraping & Browsing",
+ "icon": "globe",
+ "pages": [
+ "en/tools/web-scraping/overview",
+ "en/tools/web-scraping/scrapewebsitetool",
+ "en/tools/web-scraping/scrapeelementfromwebsitetool",
+ "en/tools/web-scraping/scrapflyscrapetool",
+ "en/tools/web-scraping/seleniumscrapingtool",
+ "en/tools/web-scraping/scrapegraphscrapetool",
+ "en/tools/web-scraping/spidertool",
+ "en/tools/web-scraping/browserbaseloadtool",
+ "en/tools/web-scraping/hyperbrowserloadtool",
+ "en/tools/web-scraping/stagehandtool",
+ "en/tools/web-scraping/firecrawlcrawlwebsitetool",
+ "en/tools/web-scraping/firecrawlscrapewebsitetool",
+ "en/tools/web-scraping/oxylabsscraperstool",
+ "en/tools/web-scraping/brightdata-tools"
+ ]
+ },
+ {
+ "group": "Search & Research",
+ "icon": "magnifying-glass",
+ "pages": [
+ "en/tools/search-research/overview",
+ "en/tools/search-research/serperdevtool",
+ "en/tools/search-research/bravesearchtool",
+ "en/tools/search-research/exasearchtool",
+ "en/tools/search-research/linkupsearchtool",
+ "en/tools/search-research/githubsearchtool",
+ "en/tools/search-research/websitesearchtool",
+ "en/tools/search-research/codedocssearchtool",
+ "en/tools/search-research/youtubechannelsearchtool",
+ "en/tools/search-research/youtubevideosearchtool",
+ "en/tools/search-research/tavilysearchtool",
+ "en/tools/search-research/tavilyextractortool",
+ "en/tools/search-research/arxivpapertool",
+ "en/tools/search-research/serpapi-googlesearchtool",
+ "en/tools/search-research/serpapi-googleshoppingtool",
+ "en/tools/search-research/databricks-query-tool"
+ ]
+ },
+ {
+ "group": "Database & Data",
+ "icon": "database",
+ "pages": [
+ "en/tools/database-data/overview",
+ "en/tools/database-data/mysqltool",
+ "en/tools/database-data/pgsearchtool",
+ "en/tools/database-data/snowflakesearchtool",
+ "en/tools/database-data/nl2sqltool",
+ "en/tools/database-data/qdrantvectorsearchtool",
+ "en/tools/database-data/weaviatevectorsearchtool",
+ "en/tools/database-data/mongodbvectorsearchtool",
+ "en/tools/database-data/singlestoresearchtool"
+ ]
+ },
+ {
+ "group": "AI & Machine Learning",
+ "icon": "brain",
+ "pages": [
+ "en/tools/ai-ml/overview",
+ "en/tools/ai-ml/dalletool",
+ "en/tools/ai-ml/visiontool",
+ "en/tools/ai-ml/aimindtool",
+ "en/tools/ai-ml/llamaindextool",
+ "en/tools/ai-ml/langchaintool",
+ "en/tools/ai-ml/ragtool",
+ "en/tools/ai-ml/codeinterpretertool"
+ ]
+ },
+ {
+ "group": "Cloud & Storage",
+ "icon": "cloud",
+ "pages": [
+ "en/tools/cloud-storage/overview",
+ "en/tools/cloud-storage/s3readertool",
+ "en/tools/cloud-storage/s3writertool",
+ "en/tools/cloud-storage/bedrockkbretriever"
+ ]
+ },
+ {
+ "group": "Integrations",
+ "icon": "plug",
+ "pages": [
+ "en/tools/integration/overview",
+ "en/tools/integration/bedrockinvokeagenttool",
+ "en/tools/integration/crewaiautomationtool",
+ "en/tools/integration/mergeagenthandlertool"
+ ]
+ },
+ {
+ "group": "Automation",
+ "icon": "bolt",
+ "pages": [
+ "en/tools/automation/overview",
+ "en/tools/automation/apifyactorstool",
+ "en/tools/automation/composiotool",
+ "en/tools/automation/multiontool",
+ "en/tools/automation/zapieractionstool"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Observability",
+ "pages": [
+ "en/observability/tracing",
+ "en/observability/overview",
+ "en/observability/arize-phoenix",
+ "en/observability/braintrust",
+ "en/observability/datadog",
+ "en/observability/galileo",
+ "en/observability/langdb",
+ "en/observability/langfuse",
+ "en/observability/langtrace",
+ "en/observability/maxim",
+ "en/observability/mlflow",
+ "en/observability/neatlogs",
+ "en/observability/openlit",
+ "en/observability/opik",
+ "en/observability/patronus-evaluation",
+ "en/observability/portkey",
+ "en/observability/weave",
+ "en/observability/truefoundry"
+ ]
+ },
+ {
+ "group": "Learn",
+ "pages": [
+ "en/learn/overview",
+ "en/learn/llm-selection-guide",
+ "en/learn/conditional-tasks",
+ "en/learn/coding-agents",
+ "en/learn/create-custom-tools",
+ "en/learn/custom-llm",
+ "en/learn/custom-manager-agent",
+ "en/learn/customizing-agents",
+ "en/learn/dalle-image-generation",
+ "en/learn/force-tool-output-as-result",
+ "en/learn/hierarchical-process",
+ "en/learn/human-input-on-execution",
+ "en/learn/human-in-the-loop",
+ "en/learn/human-feedback-in-flows",
+ "en/learn/kickoff-async",
+ "en/learn/kickoff-for-each",
+ "en/learn/llm-connections",
+ "en/learn/litellm-removal-guide",
+ "en/learn/multimodal-agents",
+ "en/learn/replay-tasks-from-latest-crew-kickoff",
+ "en/learn/sequential-process",
+ "en/learn/using-annotations",
+ "en/learn/execution-hooks",
+ "en/learn/llm-hooks",
+ "en/learn/tool-hooks"
+ ]
+ },
+ {
+ "group": "Telemetry",
+ "pages": [
+ "en/telemetry"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "AMP",
+ "icon": "briefcase",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": [
+ "en/enterprise/introduction"
+ ]
+ },
+ {
+ "group": "Build",
+ "pages": [
+ "en/enterprise/features/automations",
+ "en/enterprise/features/crew-studio",
+ "en/enterprise/features/marketplace",
+ "en/enterprise/features/agent-repositories",
+ "en/enterprise/features/tools-and-integrations",
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -551,6 +1027,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -863,7 +1340,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -1023,6 +1501,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -1335,7 +1814,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -1495,6 +1975,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -1806,7 +2287,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -1965,6 +2447,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -2276,7 +2759,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -2435,6 +2919,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -2746,7 +3231,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -2906,6 +3392,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -3218,7 +3705,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -3378,6 +3866,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -3688,7 +4177,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -3848,6 +4338,7 @@
"group": "Get Started",
"pages": [
"en/introduction",
+ "en/skills",
"en/installation",
"en/quickstart"
]
@@ -4161,7 +4652,8 @@
"en/enterprise/features/marketplace",
"en/enterprise/features/agent-repositories",
"en/enterprise/features/tools-and-integrations",
- "en/enterprise/features/pii-trace-redactions"
+ "en/enterprise/features/pii-trace-redactions",
+ "en/enterprise/features/a2a"
]
},
{
@@ -4328,7 +4820,7 @@
},
"versions": [
{
- "version": "v1.14.0",
+ "version": "v1.14.1",
"default": true,
"tabs": [
{
@@ -4351,6 +4843,465 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
+ "pt-BR/installation",
+ "pt-BR/quickstart"
+ ]
+ },
+ {
+ "group": "Guias",
+ "pages": [
+ {
+ "group": "Estratégia",
+ "icon": "compass",
+ "pages": [
+ "pt-BR/guides/concepts/evaluating-use-cases"
+ ]
+ },
+ {
+ "group": "Agentes",
+ "icon": "user",
+ "pages": [
+ "pt-BR/guides/agents/crafting-effective-agents"
+ ]
+ },
+ {
+ "group": "Crews",
+ "icon": "users",
+ "pages": [
+ "pt-BR/guides/crews/first-crew"
+ ]
+ },
+ {
+ "group": "Flows",
+ "icon": "code-branch",
+ "pages": [
+ "pt-BR/guides/flows/first-flow",
+ "pt-BR/guides/flows/mastering-flow-state"
+ ]
+ },
+ {
+ "group": "Ferramentas",
+ "icon": "wrench",
+ "pages": [
+ "pt-BR/guides/tools/publish-custom-tools"
+ ]
+ },
+ {
+ "group": "Ferramentas de Codificação",
+ "icon": "terminal",
+ "pages": [
+ "pt-BR/guides/coding-tools/agents-md"
+ ]
+ },
+ {
+ "group": "Avançado",
+ "icon": "gear",
+ "pages": [
+ "pt-BR/guides/advanced/customizing-prompts",
+ "pt-BR/guides/advanced/fingerprinting"
+ ]
+ },
+ {
+ "group": "Migração",
+ "icon": "shuffle",
+ "pages": [
+ "pt-BR/guides/migration/migrating-from-langgraph"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Conceitos-Chave",
+ "pages": [
+ "pt-BR/concepts/agents",
+ "pt-BR/concepts/agent-capabilities",
+ "pt-BR/concepts/tasks",
+ "pt-BR/concepts/crews",
+ "pt-BR/concepts/flows",
+ "pt-BR/concepts/production-architecture",
+ "pt-BR/concepts/knowledge",
+ "pt-BR/concepts/skills",
+ "pt-BR/concepts/llms",
+ "pt-BR/concepts/files",
+ "pt-BR/concepts/processes",
+ "pt-BR/concepts/collaboration",
+ "pt-BR/concepts/training",
+ "pt-BR/concepts/memory",
+ "pt-BR/concepts/reasoning",
+ "pt-BR/concepts/planning",
+ "pt-BR/concepts/testing",
+ "pt-BR/concepts/cli",
+ "pt-BR/concepts/tools",
+ "pt-BR/concepts/event-listener",
+ "pt-BR/concepts/checkpointing"
+ ]
+ },
+ {
+ "group": "Integração MCP",
+ "pages": [
+ "pt-BR/mcp/overview",
+ "pt-BR/mcp/dsl-integration",
+ "pt-BR/mcp/stdio",
+ "pt-BR/mcp/sse",
+ "pt-BR/mcp/streamable-http",
+ "pt-BR/mcp/multiple-servers",
+ "pt-BR/mcp/security"
+ ]
+ },
+ {
+ "group": "Ferramentas",
+ "pages": [
+ "pt-BR/tools/overview",
+ {
+ "group": "Arquivo & Documento",
+ "icon": "folder-open",
+ "pages": [
+ "pt-BR/tools/file-document/overview",
+ "pt-BR/tools/file-document/filereadtool",
+ "pt-BR/tools/file-document/filewritetool",
+ "pt-BR/tools/file-document/pdfsearchtool",
+ "pt-BR/tools/file-document/docxsearchtool",
+ "pt-BR/tools/file-document/mdxsearchtool",
+ "pt-BR/tools/file-document/xmlsearchtool",
+ "pt-BR/tools/file-document/txtsearchtool",
+ "pt-BR/tools/file-document/jsonsearchtool",
+ "pt-BR/tools/file-document/csvsearchtool",
+ "pt-BR/tools/file-document/directorysearchtool",
+ "pt-BR/tools/file-document/directoryreadtool"
+ ]
+ },
+ {
+ "group": "Web Scraping & Navegação",
+ "icon": "globe",
+ "pages": [
+ "pt-BR/tools/web-scraping/overview",
+ "pt-BR/tools/web-scraping/scrapewebsitetool",
+ "pt-BR/tools/web-scraping/scrapeelementfromwebsitetool",
+ "pt-BR/tools/web-scraping/scrapflyscrapetool",
+ "pt-BR/tools/web-scraping/seleniumscrapingtool",
+ "pt-BR/tools/web-scraping/scrapegraphscrapetool",
+ "pt-BR/tools/web-scraping/spidertool",
+ "pt-BR/tools/web-scraping/browserbaseloadtool",
+ "pt-BR/tools/web-scraping/hyperbrowserloadtool",
+ "pt-BR/tools/web-scraping/stagehandtool",
+ "pt-BR/tools/web-scraping/firecrawlcrawlwebsitetool",
+ "pt-BR/tools/web-scraping/firecrawlscrapewebsitetool",
+ "pt-BR/tools/web-scraping/oxylabsscraperstool"
+ ]
+ },
+ {
+ "group": "Pesquisa",
+ "icon": "magnifying-glass",
+ "pages": [
+ "pt-BR/tools/search-research/overview",
+ "pt-BR/tools/search-research/serperdevtool",
+ "pt-BR/tools/search-research/bravesearchtool",
+ "pt-BR/tools/search-research/exasearchtool",
+ "pt-BR/tools/search-research/linkupsearchtool",
+ "pt-BR/tools/search-research/githubsearchtool",
+ "pt-BR/tools/search-research/websitesearchtool",
+ "pt-BR/tools/search-research/codedocssearchtool",
+ "pt-BR/tools/search-research/youtubechannelsearchtool",
+ "pt-BR/tools/search-research/youtubevideosearchtool"
+ ]
+ },
+ {
+ "group": "Dados",
+ "icon": "database",
+ "pages": [
+ "pt-BR/tools/database-data/overview",
+ "pt-BR/tools/database-data/mysqltool",
+ "pt-BR/tools/database-data/pgsearchtool",
+ "pt-BR/tools/database-data/snowflakesearchtool",
+ "pt-BR/tools/database-data/nl2sqltool",
+ "pt-BR/tools/database-data/qdrantvectorsearchtool",
+ "pt-BR/tools/database-data/weaviatevectorsearchtool"
+ ]
+ },
+ {
+ "group": "IA & Machine Learning",
+ "icon": "brain",
+ "pages": [
+ "pt-BR/tools/ai-ml/overview",
+ "pt-BR/tools/ai-ml/dalletool",
+ "pt-BR/tools/ai-ml/visiontool",
+ "pt-BR/tools/ai-ml/aimindtool",
+ "pt-BR/tools/ai-ml/llamaindextool",
+ "pt-BR/tools/ai-ml/langchaintool",
+ "pt-BR/tools/ai-ml/ragtool",
+ "pt-BR/tools/ai-ml/codeinterpretertool"
+ ]
+ },
+ {
+ "group": "Cloud & Armazenamento",
+ "icon": "cloud",
+ "pages": [
+ "pt-BR/tools/cloud-storage/overview",
+ "pt-BR/tools/cloud-storage/s3readertool",
+ "pt-BR/tools/cloud-storage/s3writertool",
+ "pt-BR/tools/cloud-storage/bedrockkbretriever"
+ ]
+ },
+ {
+ "group": "Integrations",
+ "icon": "plug",
+ "pages": [
+ "pt-BR/tools/integration/overview",
+ "pt-BR/tools/integration/bedrockinvokeagenttool",
+ "pt-BR/tools/integration/crewaiautomationtool"
+ ]
+ },
+ {
+ "group": "Automação",
+ "icon": "bolt",
+ "pages": [
+ "pt-BR/tools/automation/overview",
+ "pt-BR/tools/automation/apifyactorstool",
+ "pt-BR/tools/automation/composiotool",
+ "pt-BR/tools/automation/multiontool"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Observabilidade",
+ "pages": [
+ "pt-BR/observability/tracing",
+ "pt-BR/observability/overview",
+ "pt-BR/observability/arize-phoenix",
+ "pt-BR/observability/braintrust",
+ "pt-BR/observability/datadog",
+ "pt-BR/observability/galileo",
+ "pt-BR/observability/langdb",
+ "pt-BR/observability/langfuse",
+ "pt-BR/observability/langtrace",
+ "pt-BR/observability/maxim",
+ "pt-BR/observability/mlflow",
+ "pt-BR/observability/openlit",
+ "pt-BR/observability/opik",
+ "pt-BR/observability/patronus-evaluation",
+ "pt-BR/observability/portkey",
+ "pt-BR/observability/weave",
+ "pt-BR/observability/truefoundry"
+ ]
+ },
+ {
+ "group": "Aprenda",
+ "pages": [
+ "pt-BR/learn/overview",
+ "pt-BR/learn/llm-selection-guide",
+ "pt-BR/learn/conditional-tasks",
+ "pt-BR/learn/coding-agents",
+ "pt-BR/learn/create-custom-tools",
+ "pt-BR/learn/custom-llm",
+ "pt-BR/learn/custom-manager-agent",
+ "pt-BR/learn/customizing-agents",
+ "pt-BR/learn/dalle-image-generation",
+ "pt-BR/learn/force-tool-output-as-result",
+ "pt-BR/learn/hierarchical-process",
+ "pt-BR/learn/human-input-on-execution",
+ "pt-BR/learn/human-in-the-loop",
+ "pt-BR/learn/human-feedback-in-flows",
+ "pt-BR/learn/kickoff-async",
+ "pt-BR/learn/kickoff-for-each",
+ "pt-BR/learn/llm-connections",
+ "pt-BR/learn/multimodal-agents",
+ "pt-BR/learn/replay-tasks-from-latest-crew-kickoff",
+ "pt-BR/learn/sequential-process",
+ "pt-BR/learn/using-annotations",
+ "pt-BR/learn/execution-hooks",
+ "pt-BR/learn/llm-hooks",
+ "pt-BR/learn/tool-hooks"
+ ]
+ },
+ {
+ "group": "Telemetria",
+ "pages": [
+ "pt-BR/telemetry"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "AMP",
+ "icon": "briefcase",
+ "groups": [
+ {
+ "group": "Começando",
+ "pages": [
+ "pt-BR/enterprise/introduction"
+ ]
+ },
+ {
+ "group": "Construir",
+ "pages": [
+ "pt-BR/enterprise/features/automations",
+ "pt-BR/enterprise/features/crew-studio",
+ "pt-BR/enterprise/features/marketplace",
+ "pt-BR/enterprise/features/agent-repositories",
+ "pt-BR/enterprise/features/tools-and-integrations",
+ "pt-BR/enterprise/features/pii-trace-redactions"
+ ]
+ },
+ {
+ "group": "Operar",
+ "pages": [
+ "pt-BR/enterprise/features/traces",
+ "pt-BR/enterprise/features/webhook-streaming",
+ "pt-BR/enterprise/features/hallucination-guardrail",
+ "pt-BR/enterprise/features/flow-hitl-management"
+ ]
+ },
+ {
+ "group": "Gerenciar",
+ "pages": [
+ "pt-BR/enterprise/features/rbac"
+ ]
+ },
+ {
+ "group": "Documentação de Integração",
+ "pages": [
+ "pt-BR/enterprise/integrations/asana",
+ "pt-BR/enterprise/integrations/box",
+ "pt-BR/enterprise/integrations/clickup",
+ "pt-BR/enterprise/integrations/github",
+ "pt-BR/enterprise/integrations/gmail",
+ "pt-BR/enterprise/integrations/google_calendar",
+ "pt-BR/enterprise/integrations/google_contacts",
+ "pt-BR/enterprise/integrations/google_docs",
+ "pt-BR/enterprise/integrations/google_drive",
+ "pt-BR/enterprise/integrations/google_sheets",
+ "pt-BR/enterprise/integrations/google_slides",
+ "pt-BR/enterprise/integrations/hubspot",
+ "pt-BR/enterprise/integrations/jira",
+ "pt-BR/enterprise/integrations/linear",
+ "pt-BR/enterprise/integrations/microsoft_excel",
+ "pt-BR/enterprise/integrations/microsoft_onedrive",
+ "pt-BR/enterprise/integrations/microsoft_outlook",
+ "pt-BR/enterprise/integrations/microsoft_sharepoint",
+ "pt-BR/enterprise/integrations/microsoft_teams",
+ "pt-BR/enterprise/integrations/microsoft_word",
+ "pt-BR/enterprise/integrations/notion",
+ "pt-BR/enterprise/integrations/salesforce",
+ "pt-BR/enterprise/integrations/shopify",
+ "pt-BR/enterprise/integrations/slack",
+ "pt-BR/enterprise/integrations/stripe",
+ "pt-BR/enterprise/integrations/zendesk"
+ ]
+ },
+ {
+ "group": "Guias",
+ "pages": [
+ "pt-BR/enterprise/guides/build-crew",
+ "pt-BR/enterprise/guides/prepare-for-deployment",
+ "pt-BR/enterprise/guides/deploy-to-amp",
+ "pt-BR/enterprise/guides/private-package-registry",
+ "pt-BR/enterprise/guides/kickoff-crew",
+ "pt-BR/enterprise/guides/training-crews",
+ "pt-BR/enterprise/guides/update-crew",
+ "pt-BR/enterprise/guides/enable-crew-studio",
+ "pt-BR/enterprise/guides/capture_telemetry_logs",
+ "pt-BR/enterprise/guides/azure-openai-setup",
+ "pt-BR/enterprise/guides/tool-repository",
+ "pt-BR/enterprise/guides/custom-mcp-server",
+ "pt-BR/enterprise/guides/react-component-export",
+ "pt-BR/enterprise/guides/team-management",
+ "pt-BR/enterprise/guides/human-in-the-loop",
+ "pt-BR/enterprise/guides/webhook-automation"
+ ]
+ },
+ {
+ "group": "Triggers",
+ "pages": [
+ "pt-BR/enterprise/guides/automation-triggers",
+ "pt-BR/enterprise/guides/gmail-trigger",
+ "pt-BR/enterprise/guides/google-calendar-trigger",
+ "pt-BR/enterprise/guides/google-drive-trigger",
+ "pt-BR/enterprise/guides/outlook-trigger",
+ "pt-BR/enterprise/guides/onedrive-trigger",
+ "pt-BR/enterprise/guides/microsoft-teams-trigger",
+ "pt-BR/enterprise/guides/slack-trigger",
+ "pt-BR/enterprise/guides/hubspot-trigger",
+ "pt-BR/enterprise/guides/salesforce-trigger",
+ "pt-BR/enterprise/guides/zapier-trigger"
+ ]
+ },
+ {
+ "group": "Recursos",
+ "pages": [
+ "pt-BR/enterprise/resources/frequently-asked-questions"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Referência da API",
+ "icon": "magnifying-glass",
+ "groups": [
+ {
+ "group": "Começando",
+ "pages": [
+ "pt-BR/api-reference/introduction",
+ "pt-BR/api-reference/inputs",
+ "pt-BR/api-reference/kickoff",
+ "pt-BR/api-reference/resume",
+ "pt-BR/api-reference/status"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Exemplos",
+ "icon": "code",
+ "groups": [
+ {
+ "group": "Exemplos",
+ "pages": [
+ "pt-BR/examples/example",
+ "pt-BR/examples/cookbooks"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Notas de Versão",
+ "icon": "clock",
+ "groups": [
+ {
+ "group": "Notas de Versão",
+ "pages": [
+ "pt-BR/changelog"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "version": "v1.14.0",
+ "tabs": [
+ {
+ "tab": "Início",
+ "icon": "house",
+ "groups": [
+ {
+ "group": "Bem-vindo",
+ "pages": [
+ "pt-BR/index"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "Documentação",
+ "icon": "book-open",
+ "groups": [
+ {
+ "group": "Começando",
+ "pages": [
+ "pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -4808,6 +5759,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -5265,6 +6217,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -5722,6 +6675,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -6178,6 +7132,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -6634,6 +7589,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -7090,6 +8046,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -7545,6 +8502,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -8000,6 +8958,7 @@
"group": "Começando",
"pages": [
"pt-BR/introduction",
+ "pt-BR/skills",
"pt-BR/installation",
"pt-BR/quickstart"
]
@@ -8463,7 +9422,7 @@
},
"versions": [
{
- "version": "v1.14.0",
+ "version": "v1.14.1",
"default": true,
"tabs": [
{
@@ -8486,6 +9445,477 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
+ "ko/installation",
+ "ko/quickstart"
+ ]
+ },
+ {
+ "group": "가이드",
+ "pages": [
+ {
+ "group": "전략",
+ "icon": "compass",
+ "pages": [
+ "ko/guides/concepts/evaluating-use-cases"
+ ]
+ },
+ {
+ "group": "에이전트 (Agents)",
+ "icon": "user",
+ "pages": [
+ "ko/guides/agents/crafting-effective-agents"
+ ]
+ },
+ {
+ "group": "크루 (Crews)",
+ "icon": "users",
+ "pages": [
+ "ko/guides/crews/first-crew"
+ ]
+ },
+ {
+ "group": "플로우 (Flows)",
+ "icon": "code-branch",
+ "pages": [
+ "ko/guides/flows/first-flow",
+ "ko/guides/flows/mastering-flow-state"
+ ]
+ },
+ {
+ "group": "도구",
+ "icon": "wrench",
+ "pages": [
+ "ko/guides/tools/publish-custom-tools"
+ ]
+ },
+ {
+ "group": "코딩 도구",
+ "icon": "terminal",
+ "pages": [
+ "ko/guides/coding-tools/agents-md"
+ ]
+ },
+ {
+ "group": "고급",
+ "icon": "gear",
+ "pages": [
+ "ko/guides/advanced/customizing-prompts",
+ "ko/guides/advanced/fingerprinting"
+ ]
+ },
+ {
+ "group": "마이그레이션",
+ "icon": "shuffle",
+ "pages": [
+ "ko/guides/migration/migrating-from-langgraph"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "핵심 개념",
+ "pages": [
+ "ko/concepts/agents",
+ "ko/concepts/tasks",
+ "ko/concepts/agent-capabilities",
+ "ko/concepts/crews",
+ "ko/concepts/flows",
+ "ko/concepts/production-architecture",
+ "ko/concepts/knowledge",
+ "ko/concepts/skills",
+ "ko/concepts/llms",
+ "ko/concepts/files",
+ "ko/concepts/processes",
+ "ko/concepts/collaboration",
+ "ko/concepts/training",
+ "ko/concepts/memory",
+ "ko/concepts/reasoning",
+ "ko/concepts/planning",
+ "ko/concepts/testing",
+ "ko/concepts/cli",
+ "ko/concepts/tools",
+ "ko/concepts/event-listener",
+ "ko/concepts/checkpointing"
+ ]
+ },
+ {
+ "group": "MCP 통합",
+ "pages": [
+ "ko/mcp/overview",
+ "ko/mcp/dsl-integration",
+ "ko/mcp/stdio",
+ "ko/mcp/sse",
+ "ko/mcp/streamable-http",
+ "ko/mcp/multiple-servers",
+ "ko/mcp/security"
+ ]
+ },
+ {
+ "group": "도구 (Tools)",
+ "pages": [
+ "ko/tools/overview",
+ {
+ "group": "파일 & 문서",
+ "icon": "folder-open",
+ "pages": [
+ "ko/tools/file-document/overview",
+ "ko/tools/file-document/filereadtool",
+ "ko/tools/file-document/filewritetool",
+ "ko/tools/file-document/pdfsearchtool",
+ "ko/tools/file-document/docxsearchtool",
+ "ko/tools/file-document/mdxsearchtool",
+ "ko/tools/file-document/xmlsearchtool",
+ "ko/tools/file-document/txtsearchtool",
+ "ko/tools/file-document/jsonsearchtool",
+ "ko/tools/file-document/csvsearchtool",
+ "ko/tools/file-document/directorysearchtool",
+ "ko/tools/file-document/directoryreadtool",
+ "ko/tools/file-document/ocrtool",
+ "ko/tools/file-document/pdf-text-writing-tool"
+ ]
+ },
+ {
+ "group": "웹 스크래핑 & 브라우징",
+ "icon": "globe",
+ "pages": [
+ "ko/tools/web-scraping/overview",
+ "ko/tools/web-scraping/scrapewebsitetool",
+ "ko/tools/web-scraping/scrapeelementfromwebsitetool",
+ "ko/tools/web-scraping/scrapflyscrapetool",
+ "ko/tools/web-scraping/seleniumscrapingtool",
+ "ko/tools/web-scraping/scrapegraphscrapetool",
+ "ko/tools/web-scraping/spidertool",
+ "ko/tools/web-scraping/browserbaseloadtool",
+ "ko/tools/web-scraping/hyperbrowserloadtool",
+ "ko/tools/web-scraping/stagehandtool",
+ "ko/tools/web-scraping/firecrawlcrawlwebsitetool",
+ "ko/tools/web-scraping/firecrawlscrapewebsitetool",
+ "ko/tools/web-scraping/oxylabsscraperstool",
+ "ko/tools/web-scraping/brightdata-tools"
+ ]
+ },
+ {
+ "group": "검색 및 연구",
+ "icon": "magnifying-glass",
+ "pages": [
+ "ko/tools/search-research/overview",
+ "ko/tools/search-research/serperdevtool",
+ "ko/tools/search-research/bravesearchtool",
+ "ko/tools/search-research/exasearchtool",
+ "ko/tools/search-research/linkupsearchtool",
+ "ko/tools/search-research/githubsearchtool",
+ "ko/tools/search-research/websitesearchtool",
+ "ko/tools/search-research/codedocssearchtool",
+ "ko/tools/search-research/youtubechannelsearchtool",
+ "ko/tools/search-research/youtubevideosearchtool",
+ "ko/tools/search-research/tavilysearchtool",
+ "ko/tools/search-research/tavilyextractortool",
+ "ko/tools/search-research/arxivpapertool",
+ "ko/tools/search-research/serpapi-googlesearchtool",
+ "ko/tools/search-research/serpapi-googleshoppingtool",
+ "ko/tools/search-research/databricks-query-tool"
+ ]
+ },
+ {
+ "group": "데이터베이스 & 데이터",
+ "icon": "database",
+ "pages": [
+ "ko/tools/database-data/overview",
+ "ko/tools/database-data/mysqltool",
+ "ko/tools/database-data/pgsearchtool",
+ "ko/tools/database-data/snowflakesearchtool",
+ "ko/tools/database-data/nl2sqltool",
+ "ko/tools/database-data/qdrantvectorsearchtool",
+ "ko/tools/database-data/weaviatevectorsearchtool",
+ "ko/tools/database-data/mongodbvectorsearchtool",
+ "ko/tools/database-data/singlestoresearchtool"
+ ]
+ },
+ {
+ "group": "인공지능 & 머신러닝",
+ "icon": "brain",
+ "pages": [
+ "ko/tools/ai-ml/overview",
+ "ko/tools/ai-ml/dalletool",
+ "ko/tools/ai-ml/visiontool",
+ "ko/tools/ai-ml/aimindtool",
+ "ko/tools/ai-ml/llamaindextool",
+ "ko/tools/ai-ml/langchaintool",
+ "ko/tools/ai-ml/ragtool",
+ "ko/tools/ai-ml/codeinterpretertool"
+ ]
+ },
+ {
+ "group": "클라우드 & 스토리지",
+ "icon": "cloud",
+ "pages": [
+ "ko/tools/cloud-storage/overview",
+ "ko/tools/cloud-storage/s3readertool",
+ "ko/tools/cloud-storage/s3writertool",
+ "ko/tools/cloud-storage/bedrockkbretriever"
+ ]
+ },
+ {
+ "group": "Integrations",
+ "icon": "plug",
+ "pages": [
+ "ko/tools/integration/overview",
+ "ko/tools/integration/bedrockinvokeagenttool",
+ "ko/tools/integration/crewaiautomationtool"
+ ]
+ },
+ {
+ "group": "자동화",
+ "icon": "bolt",
+ "pages": [
+ "ko/tools/automation/overview",
+ "ko/tools/automation/apifyactorstool",
+ "ko/tools/automation/composiotool",
+ "ko/tools/automation/multiontool",
+ "ko/tools/automation/zapieractionstool"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Observability",
+ "pages": [
+ "ko/observability/tracing",
+ "ko/observability/overview",
+ "ko/observability/arize-phoenix",
+ "ko/observability/braintrust",
+ "ko/observability/datadog",
+ "ko/observability/galileo",
+ "ko/observability/langdb",
+ "ko/observability/langfuse",
+ "ko/observability/langtrace",
+ "ko/observability/maxim",
+ "ko/observability/mlflow",
+ "ko/observability/neatlogs",
+ "ko/observability/openlit",
+ "ko/observability/opik",
+ "ko/observability/patronus-evaluation",
+ "ko/observability/portkey",
+ "ko/observability/weave"
+ ]
+ },
+ {
+ "group": "학습",
+ "pages": [
+ "ko/learn/overview",
+ "ko/learn/llm-selection-guide",
+ "ko/learn/conditional-tasks",
+ "ko/learn/coding-agents",
+ "ko/learn/create-custom-tools",
+ "ko/learn/custom-llm",
+ "ko/learn/custom-manager-agent",
+ "ko/learn/customizing-agents",
+ "ko/learn/dalle-image-generation",
+ "ko/learn/force-tool-output-as-result",
+ "ko/learn/hierarchical-process",
+ "ko/learn/human-input-on-execution",
+ "ko/learn/human-in-the-loop",
+ "ko/learn/human-feedback-in-flows",
+ "ko/learn/kickoff-async",
+ "ko/learn/kickoff-for-each",
+ "ko/learn/llm-connections",
+ "ko/learn/multimodal-agents",
+ "ko/learn/replay-tasks-from-latest-crew-kickoff",
+ "ko/learn/sequential-process",
+ "ko/learn/using-annotations",
+ "ko/learn/execution-hooks",
+ "ko/learn/llm-hooks",
+ "ko/learn/tool-hooks"
+ ]
+ },
+ {
+ "group": "Telemetry",
+ "pages": [
+ "ko/telemetry"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "엔터프라이즈",
+ "icon": "briefcase",
+ "groups": [
+ {
+ "group": "시작 안내",
+ "pages": [
+ "ko/enterprise/introduction"
+ ]
+ },
+ {
+ "group": "빌드",
+ "pages": [
+ "ko/enterprise/features/automations",
+ "ko/enterprise/features/crew-studio",
+ "ko/enterprise/features/marketplace",
+ "ko/enterprise/features/agent-repositories",
+ "ko/enterprise/features/tools-and-integrations",
+ "ko/enterprise/features/pii-trace-redactions"
+ ]
+ },
+ {
+ "group": "운영",
+ "pages": [
+ "ko/enterprise/features/traces",
+ "ko/enterprise/features/webhook-streaming",
+ "ko/enterprise/features/hallucination-guardrail",
+ "ko/enterprise/features/flow-hitl-management"
+ ]
+ },
+ {
+ "group": "관리",
+ "pages": [
+ "ko/enterprise/features/rbac"
+ ]
+ },
+ {
+ "group": "통합 문서",
+ "pages": [
+ "ko/enterprise/integrations/asana",
+ "ko/enterprise/integrations/box",
+ "ko/enterprise/integrations/clickup",
+ "ko/enterprise/integrations/github",
+ "ko/enterprise/integrations/gmail",
+ "ko/enterprise/integrations/google_calendar",
+ "ko/enterprise/integrations/google_contacts",
+ "ko/enterprise/integrations/google_docs",
+ "ko/enterprise/integrations/google_drive",
+ "ko/enterprise/integrations/google_sheets",
+ "ko/enterprise/integrations/google_slides",
+ "ko/enterprise/integrations/hubspot",
+ "ko/enterprise/integrations/jira",
+ "ko/enterprise/integrations/linear",
+ "ko/enterprise/integrations/microsoft_excel",
+ "ko/enterprise/integrations/microsoft_onedrive",
+ "ko/enterprise/integrations/microsoft_outlook",
+ "ko/enterprise/integrations/microsoft_sharepoint",
+ "ko/enterprise/integrations/microsoft_teams",
+ "ko/enterprise/integrations/microsoft_word",
+ "ko/enterprise/integrations/notion",
+ "ko/enterprise/integrations/salesforce",
+ "ko/enterprise/integrations/shopify",
+ "ko/enterprise/integrations/slack",
+ "ko/enterprise/integrations/stripe",
+ "ko/enterprise/integrations/zendesk"
+ ]
+ },
+ {
+ "group": "How-To Guides",
+ "pages": [
+ "ko/enterprise/guides/build-crew",
+ "ko/enterprise/guides/prepare-for-deployment",
+ "ko/enterprise/guides/deploy-to-amp",
+ "ko/enterprise/guides/private-package-registry",
+ "ko/enterprise/guides/kickoff-crew",
+ "ko/enterprise/guides/training-crews",
+ "ko/enterprise/guides/update-crew",
+ "ko/enterprise/guides/enable-crew-studio",
+ "ko/enterprise/guides/capture_telemetry_logs",
+ "ko/enterprise/guides/azure-openai-setup",
+ "ko/enterprise/guides/tool-repository",
+ "ko/enterprise/guides/custom-mcp-server",
+ "ko/enterprise/guides/react-component-export",
+ "ko/enterprise/guides/team-management",
+ "ko/enterprise/guides/human-in-the-loop",
+ "ko/enterprise/guides/webhook-automation"
+ ]
+ },
+ {
+ "group": "트리거",
+ "pages": [
+ "ko/enterprise/guides/automation-triggers",
+ "ko/enterprise/guides/gmail-trigger",
+ "ko/enterprise/guides/google-calendar-trigger",
+ "ko/enterprise/guides/google-drive-trigger",
+ "ko/enterprise/guides/outlook-trigger",
+ "ko/enterprise/guides/onedrive-trigger",
+ "ko/enterprise/guides/microsoft-teams-trigger",
+ "ko/enterprise/guides/slack-trigger",
+ "ko/enterprise/guides/hubspot-trigger",
+ "ko/enterprise/guides/salesforce-trigger",
+ "ko/enterprise/guides/zapier-trigger"
+ ]
+ },
+ {
+ "group": "학습 자원",
+ "pages": [
+ "ko/enterprise/resources/frequently-asked-questions"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "API 레퍼런스",
+ "icon": "magnifying-glass",
+ "groups": [
+ {
+ "group": "시작 안내",
+ "pages": [
+ "ko/api-reference/introduction",
+ "ko/api-reference/inputs",
+ "ko/api-reference/kickoff",
+ "ko/api-reference/resume",
+ "ko/api-reference/status"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "예시",
+ "icon": "code",
+ "groups": [
+ {
+ "group": "예시",
+ "pages": [
+ "ko/examples/example",
+ "ko/examples/cookbooks"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "변경 로그",
+ "icon": "clock",
+ "groups": [
+ {
+ "group": "릴리스 노트",
+ "pages": [
+ "ko/changelog"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "version": "v1.14.0",
+ "tabs": [
+ {
+ "tab": "홈",
+ "icon": "house",
+ "groups": [
+ {
+ "group": "환영합니다",
+ "pages": [
+ "ko/index"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "기술 문서",
+ "icon": "book-open",
+ "groups": [
+ {
+ "group": "시작 안내",
+ "pages": [
+ "ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -8955,6 +10385,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -9424,6 +10855,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -9893,6 +11325,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -10361,6 +11794,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -10829,6 +12263,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -11297,6 +12732,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -11764,6 +13200,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -12231,6 +13668,7 @@
"group": "시작 안내",
"pages": [
"ko/introduction",
+ "ko/skills",
"ko/installation",
"ko/quickstart"
]
@@ -12706,7 +14144,7 @@
},
"versions": [
{
- "version": "v1.14.0",
+ "version": "v1.14.1",
"default": true,
"tabs": [
{
@@ -12729,6 +14167,477 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
+ "ar/installation",
+ "ar/quickstart"
+ ]
+ },
+ {
+ "group": "الأدلّة",
+ "pages": [
+ {
+ "group": "الاستراتيجية",
+ "icon": "compass",
+ "pages": [
+ "ar/guides/concepts/evaluating-use-cases"
+ ]
+ },
+ {
+ "group": "الوكلاء",
+ "icon": "user",
+ "pages": [
+ "ar/guides/agents/crafting-effective-agents"
+ ]
+ },
+ {
+ "group": "الطواقم",
+ "icon": "users",
+ "pages": [
+ "ar/guides/crews/first-crew"
+ ]
+ },
+ {
+ "group": "التدفقات",
+ "icon": "code-branch",
+ "pages": [
+ "ar/guides/flows/first-flow",
+ "ar/guides/flows/mastering-flow-state"
+ ]
+ },
+ {
+ "group": "الأدوات",
+ "icon": "wrench",
+ "pages": [
+ "ar/guides/tools/publish-custom-tools"
+ ]
+ },
+ {
+ "group": "أدوات البرمجة",
+ "icon": "terminal",
+ "pages": [
+ "ar/guides/coding-tools/agents-md"
+ ]
+ },
+ {
+ "group": "متقدّم",
+ "icon": "gear",
+ "pages": [
+ "ar/guides/advanced/customizing-prompts",
+ "ar/guides/advanced/fingerprinting"
+ ]
+ },
+ {
+ "group": "الترحيل",
+ "icon": "shuffle",
+ "pages": [
+ "ar/guides/migration/migrating-from-langgraph"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "المفاهيم الأساسية",
+ "pages": [
+ "ar/concepts/agents",
+ "ar/concepts/agent-capabilities",
+ "ar/concepts/tasks",
+ "ar/concepts/crews",
+ "ar/concepts/flows",
+ "ar/concepts/production-architecture",
+ "ar/concepts/knowledge",
+ "ar/concepts/skills",
+ "ar/concepts/llms",
+ "ar/concepts/files",
+ "ar/concepts/processes",
+ "ar/concepts/collaboration",
+ "ar/concepts/training",
+ "ar/concepts/memory",
+ "ar/concepts/reasoning",
+ "ar/concepts/planning",
+ "ar/concepts/testing",
+ "ar/concepts/cli",
+ "ar/concepts/tools",
+ "ar/concepts/event-listener",
+ "ar/concepts/checkpointing"
+ ]
+ },
+ {
+ "group": "تكامل MCP",
+ "pages": [
+ "ar/mcp/overview",
+ "ar/mcp/dsl-integration",
+ "ar/mcp/stdio",
+ "ar/mcp/sse",
+ "ar/mcp/streamable-http",
+ "ar/mcp/multiple-servers",
+ "ar/mcp/security"
+ ]
+ },
+ {
+ "group": "الأدوات",
+ "pages": [
+ "ar/tools/overview",
+ {
+ "group": "الملفات والمستندات",
+ "icon": "folder-open",
+ "pages": [
+ "ar/tools/file-document/overview",
+ "ar/tools/file-document/filereadtool",
+ "ar/tools/file-document/filewritetool",
+ "ar/tools/file-document/pdfsearchtool",
+ "ar/tools/file-document/docxsearchtool",
+ "ar/tools/file-document/mdxsearchtool",
+ "ar/tools/file-document/xmlsearchtool",
+ "ar/tools/file-document/txtsearchtool",
+ "ar/tools/file-document/jsonsearchtool",
+ "ar/tools/file-document/csvsearchtool",
+ "ar/tools/file-document/directorysearchtool",
+ "ar/tools/file-document/directoryreadtool",
+ "ar/tools/file-document/ocrtool",
+ "ar/tools/file-document/pdf-text-writing-tool"
+ ]
+ },
+ {
+ "group": "استخراج بيانات الويب",
+ "icon": "globe",
+ "pages": [
+ "ar/tools/web-scraping/overview",
+ "ar/tools/web-scraping/scrapewebsitetool",
+ "ar/tools/web-scraping/scrapeelementfromwebsitetool",
+ "ar/tools/web-scraping/scrapflyscrapetool",
+ "ar/tools/web-scraping/seleniumscrapingtool",
+ "ar/tools/web-scraping/scrapegraphscrapetool",
+ "ar/tools/web-scraping/spidertool",
+ "ar/tools/web-scraping/browserbaseloadtool",
+ "ar/tools/web-scraping/hyperbrowserloadtool",
+ "ar/tools/web-scraping/stagehandtool",
+ "ar/tools/web-scraping/firecrawlcrawlwebsitetool",
+ "ar/tools/web-scraping/firecrawlscrapewebsitetool",
+ "ar/tools/web-scraping/oxylabsscraperstool",
+ "ar/tools/web-scraping/brightdata-tools"
+ ]
+ },
+ {
+ "group": "البحث والاستكشاف",
+ "icon": "magnifying-glass",
+ "pages": [
+ "ar/tools/search-research/overview",
+ "ar/tools/search-research/serperdevtool",
+ "ar/tools/search-research/bravesearchtool",
+ "ar/tools/search-research/exasearchtool",
+ "ar/tools/search-research/linkupsearchtool",
+ "ar/tools/search-research/githubsearchtool",
+ "ar/tools/search-research/websitesearchtool",
+ "ar/tools/search-research/codedocssearchtool",
+ "ar/tools/search-research/youtubechannelsearchtool",
+ "ar/tools/search-research/youtubevideosearchtool",
+ "ar/tools/search-research/tavilysearchtool",
+ "ar/tools/search-research/tavilyextractortool",
+ "ar/tools/search-research/arxivpapertool",
+ "ar/tools/search-research/serpapi-googlesearchtool",
+ "ar/tools/search-research/serpapi-googleshoppingtool",
+ "ar/tools/search-research/databricks-query-tool"
+ ]
+ },
+ {
+ "group": "قواعد البيانات",
+ "icon": "database",
+ "pages": [
+ "ar/tools/database-data/overview",
+ "ar/tools/database-data/mysqltool",
+ "ar/tools/database-data/pgsearchtool",
+ "ar/tools/database-data/snowflakesearchtool",
+ "ar/tools/database-data/nl2sqltool",
+ "ar/tools/database-data/qdrantvectorsearchtool",
+ "ar/tools/database-data/weaviatevectorsearchtool",
+ "ar/tools/database-data/mongodbvectorsearchtool",
+ "ar/tools/database-data/singlestoresearchtool"
+ ]
+ },
+ {
+ "group": "الذكاء الاصطناعي والتعلّم الآلي",
+ "icon": "brain",
+ "pages": [
+ "ar/tools/ai-ml/overview",
+ "ar/tools/ai-ml/dalletool",
+ "ar/tools/ai-ml/visiontool",
+ "ar/tools/ai-ml/aimindtool",
+ "ar/tools/ai-ml/llamaindextool",
+ "ar/tools/ai-ml/langchaintool",
+ "ar/tools/ai-ml/ragtool",
+ "ar/tools/ai-ml/codeinterpretertool"
+ ]
+ },
+ {
+ "group": "التخزين السحابي",
+ "icon": "cloud",
+ "pages": [
+ "ar/tools/cloud-storage/overview",
+ "ar/tools/cloud-storage/s3readertool",
+ "ar/tools/cloud-storage/s3writertool",
+ "ar/tools/cloud-storage/bedrockkbretriever"
+ ]
+ },
+ {
+ "group": "Integrations",
+ "icon": "plug",
+ "pages": [
+ "ar/tools/integration/overview",
+ "ar/tools/integration/bedrockinvokeagenttool",
+ "ar/tools/integration/crewaiautomationtool"
+ ]
+ },
+ {
+ "group": "الأتمتة",
+ "icon": "bolt",
+ "pages": [
+ "ar/tools/automation/overview",
+ "ar/tools/automation/apifyactorstool",
+ "ar/tools/automation/composiotool",
+ "ar/tools/automation/multiontool",
+ "ar/tools/automation/zapieractionstool"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Observability",
+ "pages": [
+ "ar/observability/tracing",
+ "ar/observability/overview",
+ "ar/observability/arize-phoenix",
+ "ar/observability/braintrust",
+ "ar/observability/datadog",
+ "ar/observability/galileo",
+ "ar/observability/langdb",
+ "ar/observability/langfuse",
+ "ar/observability/langtrace",
+ "ar/observability/maxim",
+ "ar/observability/mlflow",
+ "ar/observability/neatlogs",
+ "ar/observability/openlit",
+ "ar/observability/opik",
+ "ar/observability/patronus-evaluation",
+ "ar/observability/portkey",
+ "ar/observability/weave"
+ ]
+ },
+ {
+ "group": "التعلّم",
+ "pages": [
+ "ar/learn/overview",
+ "ar/learn/llm-selection-guide",
+ "ar/learn/conditional-tasks",
+ "ar/learn/coding-agents",
+ "ar/learn/create-custom-tools",
+ "ar/learn/custom-llm",
+ "ar/learn/custom-manager-agent",
+ "ar/learn/customizing-agents",
+ "ar/learn/dalle-image-generation",
+ "ar/learn/force-tool-output-as-result",
+ "ar/learn/hierarchical-process",
+ "ar/learn/human-input-on-execution",
+ "ar/learn/human-in-the-loop",
+ "ar/learn/human-feedback-in-flows",
+ "ar/learn/kickoff-async",
+ "ar/learn/kickoff-for-each",
+ "ar/learn/llm-connections",
+ "ar/learn/multimodal-agents",
+ "ar/learn/replay-tasks-from-latest-crew-kickoff",
+ "ar/learn/sequential-process",
+ "ar/learn/using-annotations",
+ "ar/learn/execution-hooks",
+ "ar/learn/llm-hooks",
+ "ar/learn/tool-hooks"
+ ]
+ },
+ {
+ "group": "Telemetry",
+ "pages": [
+ "ar/telemetry"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "المؤسسات",
+ "icon": "briefcase",
+ "groups": [
+ {
+ "group": "البدء",
+ "pages": [
+ "ar/enterprise/introduction"
+ ]
+ },
+ {
+ "group": "البناء",
+ "pages": [
+ "ar/enterprise/features/automations",
+ "ar/enterprise/features/crew-studio",
+ "ar/enterprise/features/marketplace",
+ "ar/enterprise/features/agent-repositories",
+ "ar/enterprise/features/tools-and-integrations",
+ "ar/enterprise/features/pii-trace-redactions"
+ ]
+ },
+ {
+ "group": "العمليات",
+ "pages": [
+ "ar/enterprise/features/traces",
+ "ar/enterprise/features/webhook-streaming",
+ "ar/enterprise/features/hallucination-guardrail",
+ "ar/enterprise/features/flow-hitl-management"
+ ]
+ },
+ {
+ "group": "الإدارة",
+ "pages": [
+ "ar/enterprise/features/rbac"
+ ]
+ },
+ {
+ "group": "التكاملات",
+ "pages": [
+ "ar/enterprise/integrations/asana",
+ "ar/enterprise/integrations/box",
+ "ar/enterprise/integrations/clickup",
+ "ar/enterprise/integrations/github",
+ "ar/enterprise/integrations/gmail",
+ "ar/enterprise/integrations/google_calendar",
+ "ar/enterprise/integrations/google_contacts",
+ "ar/enterprise/integrations/google_docs",
+ "ar/enterprise/integrations/google_drive",
+ "ar/enterprise/integrations/google_sheets",
+ "ar/enterprise/integrations/google_slides",
+ "ar/enterprise/integrations/hubspot",
+ "ar/enterprise/integrations/jira",
+ "ar/enterprise/integrations/linear",
+ "ar/enterprise/integrations/microsoft_excel",
+ "ar/enterprise/integrations/microsoft_onedrive",
+ "ar/enterprise/integrations/microsoft_outlook",
+ "ar/enterprise/integrations/microsoft_sharepoint",
+ "ar/enterprise/integrations/microsoft_teams",
+ "ar/enterprise/integrations/microsoft_word",
+ "ar/enterprise/integrations/notion",
+ "ar/enterprise/integrations/salesforce",
+ "ar/enterprise/integrations/shopify",
+ "ar/enterprise/integrations/slack",
+ "ar/enterprise/integrations/stripe",
+ "ar/enterprise/integrations/zendesk"
+ ]
+ },
+ {
+ "group": "How-To Guides",
+ "pages": [
+ "ar/enterprise/guides/build-crew",
+ "ar/enterprise/guides/prepare-for-deployment",
+ "ar/enterprise/guides/deploy-to-amp",
+ "ar/enterprise/guides/private-package-registry",
+ "ar/enterprise/guides/kickoff-crew",
+ "ar/enterprise/guides/training-crews",
+ "ar/enterprise/guides/update-crew",
+ "ar/enterprise/guides/enable-crew-studio",
+ "ar/enterprise/guides/capture_telemetry_logs",
+ "ar/enterprise/guides/azure-openai-setup",
+ "ar/enterprise/guides/tool-repository",
+ "ar/enterprise/guides/custom-mcp-server",
+ "ar/enterprise/guides/react-component-export",
+ "ar/enterprise/guides/team-management",
+ "ar/enterprise/guides/human-in-the-loop",
+ "ar/enterprise/guides/webhook-automation"
+ ]
+ },
+ {
+ "group": "المشغّلات",
+ "pages": [
+ "ar/enterprise/guides/automation-triggers",
+ "ar/enterprise/guides/gmail-trigger",
+ "ar/enterprise/guides/google-calendar-trigger",
+ "ar/enterprise/guides/google-drive-trigger",
+ "ar/enterprise/guides/outlook-trigger",
+ "ar/enterprise/guides/onedrive-trigger",
+ "ar/enterprise/guides/microsoft-teams-trigger",
+ "ar/enterprise/guides/slack-trigger",
+ "ar/enterprise/guides/hubspot-trigger",
+ "ar/enterprise/guides/salesforce-trigger",
+ "ar/enterprise/guides/zapier-trigger"
+ ]
+ },
+ {
+ "group": "موارد التعلّم",
+ "pages": [
+ "ar/enterprise/resources/frequently-asked-questions"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "API المرجع",
+ "icon": "magnifying-glass",
+ "groups": [
+ {
+ "group": "البدء",
+ "pages": [
+ "ar/api-reference/introduction",
+ "ar/api-reference/inputs",
+ "ar/api-reference/kickoff",
+ "ar/api-reference/resume",
+ "ar/api-reference/status"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "أمثلة",
+ "icon": "code",
+ "groups": [
+ {
+ "group": "أمثلة",
+ "pages": [
+ "ar/examples/example",
+ "ar/examples/cookbooks"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "التغييرات السجلات",
+ "icon": "clock",
+ "groups": [
+ {
+ "group": "سجل التغييرات",
+ "pages": [
+ "ar/changelog"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "version": "v1.14.0",
+ "tabs": [
+ {
+ "tab": "الرئيسية",
+ "icon": "house",
+ "groups": [
+ {
+ "group": "مرحباً",
+ "pages": [
+ "ar/index"
+ ]
+ }
+ ]
+ },
+ {
+ "tab": "التقنية التوثيق",
+ "icon": "book-open",
+ "groups": [
+ {
+ "group": "البدء",
+ "pages": [
+ "ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -13198,6 +15107,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -13667,6 +15577,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -14136,6 +16047,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -14604,6 +16516,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -15072,6 +16985,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -15540,6 +17454,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -16007,6 +17922,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -16474,6 +18390,7 @@
"group": "البدء",
"pages": [
"ar/introduction",
+ "ar/skills",
"ar/installation",
"ar/quickstart"
]
@@ -16985,6 +18902,10 @@
"source": "/introduction",
"destination": "/en/introduction"
},
+ {
+ "source": "/skills",
+ "destination": "/en/skills"
+ },
{
"source": "/installation",
"destination": "/en/installation"
diff --git a/docs/en/changelog.mdx b/docs/en/changelog.mdx
index 891d9fc8b..d1a72c2ca 100644
--- a/docs/en/changelog.mdx
+++ b/docs/en/changelog.mdx
@@ -4,6 +4,214 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2rc1
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2rc1)
+
+ ## What's Changed
+
+ ### Bug Fixes
+ - Fix handling of cyclic JSON schemas in MCP tool resolution
+ - Fix vulnerability by bumping python-multipart to 0.0.26
+ - Fix vulnerability by bumping pypdf to 6.10.1
+
+ ### Documentation
+ - Update changelog and version for v1.14.2a5
+
+ ## Contributors
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a5
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
+
+ ## What's Changed
+
+ ### Documentation
+ - Update changelog and version for v1.14.2a4
+
+ ## Contributors
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a4
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## What's Changed
+
+ ### Features
+ - Add resume hints to devtools release on failure
+
+ ### Bug Fixes
+ - Fix strict mode forwarding to Bedrock Converse API
+ - Fix pytest version to 9.0.3 for security vulnerability GHSA-6w46-j5rx-g56g
+ - Bump OpenAI lower bound to >=2.0.0
+
+ ### Documentation
+ - Update changelog and version for v1.14.2a3
+
+ ## Contributors
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a3
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a3)
+
+ ## What's Changed
+
+ ### Features
+ - Add deploy validation CLI
+ - Improve LLM initialization ergonomics
+
+ ### Bug Fixes
+ - Override pypdf and uv to patched versions for CVE-2026-40260 and GHSA-pjjw-68hj-v9mw
+ - Upgrade requests to >=2.33.0 for CVE temp file vulnerability
+ - Preserve Bedrock tool call arguments by removing truthy default
+ - Sanitize tool schemas for strict mode
+ - Deflake MemoryRecord embedding serialization test
+
+ ### Documentation
+ - Clean up enterprise A2A language
+ - Add enterprise A2A feature documentation
+ - Update OSS A2A documentation
+ - Update changelog and version for v1.14.2a2
+
+ ## Contributors
+
+ @Yanhu007, @greysonlalonde
+
+
+
+
+ ## v1.14.2a2
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a2)
+
+ ## What's Changed
+
+ ### Features
+ - Add checkpoint TUI with tree view, fork support, and editable inputs/outputs
+ - Enrich LLM token tracking with reasoning tokens and cache creation tokens
+ - Add `from_checkpoint` parameter to kickoff methods
+ - Embed `crewai_version` in checkpoints with migration framework
+ - Add checkpoint forking with lineage tracking
+
+ ### Bug Fixes
+ - Fix strict mode forwarding to Anthropic and Bedrock providers
+ - Harden NL2SQLTool with read-only default, query validation, and parameterized queries
+
+ ### Documentation
+ - Update changelog and version for v1.14.2a1
+
+ ## Contributors
+
+ @alex-clawd, @github-actions[bot], @greysonlalonde, @lucasgomide
+
+
+
+
+ ## v1.14.2a1
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a1)
+
+ ## What's Changed
+
+ ### Bug Fixes
+ - Fix emission of flow_finished event after HITL resume
+ - Fix cryptography version to 46.0.7 to address CVE-2026-39892
+
+ ### Refactoring
+ - Refactor to use shared I18N_DEFAULT singleton
+
+ ### Documentation
+ - Update changelog and version for v1.14.1
+
+ ## Contributors
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.1
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1)
+
+ ## What's Changed
+
+ ### Features
+ - Add async checkpoint TUI browser
+ - Add aclose()/close() and async context manager to streaming outputs
+
+ ### Bug Fixes
+ - Fix regex for template pyproject.toml version bumps
+ - Sanitize tool names in hook decorator filters
+ - Fix checkpoint handlers registration when CheckpointConfig is created
+ - Bump transformers to 5.5.0 to resolve CVE-2026-1839
+ - Remove FilteredStream stdout/stderr wrapper
+
+ ### Documentation
+ - Update changelog and version for v1.14.1rc1
+
+ ### Refactoring
+ - Replace hardcoded denylist with dynamic BaseTool field exclusion in spec gen
+ - Replace regex with tomlkit in devtools CLI
+ - Use shared PRINTER singleton
+ - Make BaseProvider a BaseModel with provider_type discriminator
+
+ ## Contributors
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay
+
+
+
+
+ ## v1.14.1rc1
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1)
+
+ ## What's Changed
+
+ ### Features
+ - Add async checkpoint TUI browser
+ - Add aclose()/close() and async context manager to streaming outputs
+
+ ### Bug Fixes
+ - Fix template pyproject.toml version bumps using regex
+ - Sanitize tool names in hook decorator filters
+ - Bump transformers to 5.5.0 to resolve CVE-2026-1839
+ - Register checkpoint handlers when CheckpointConfig is created
+
+ ### Refactoring
+ - Replace hardcoded denylist with dynamic BaseTool field exclusion in spec gen
+ - Replace regex with tomlkit in devtools CLI
+ - Use shared PRINTER singleton
+ - Make BaseProvider a BaseModel with provider_type discriminator
+ - Remove FilteredStream stdout/stderr wrapper
+ - Remove unused flow/config.py
+
+ ### Documentation
+ - Update changelog and version for v1.14.0
+
+ ## Contributors
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura
+
+
+
## v1.14.0
diff --git a/docs/en/concepts/checkpointing.mdx b/docs/en/concepts/checkpointing.mdx
index 21ed13905..d6430eb6f 100644
--- a/docs/en/concepts/checkpointing.mdx
+++ b/docs/en/concepts/checkpointing.mdx
@@ -54,6 +54,7 @@ crew = Crew(
| `on_events` | `list[str]` | `["task_completed"]` | Event types that trigger a checkpoint |
| `provider` | `BaseProvider` | `JsonProvider()` | Storage backend |
| `max_checkpoints` | `int \| None` | `None` | Max checkpoints to keep. Oldest are pruned after each write. Pruning is handled by the provider. |
+| `restore_from` | `Path \| str \| None` | `None` | Path to a checkpoint to restore from. Used when passing config via a kickoff method's `from_checkpoint` parameter. |
### Inheritance and Opt-Out
@@ -79,13 +80,42 @@ crew = Crew(
## Resuming from a Checkpoint
+Pass a `CheckpointConfig` with `restore_from` to any kickoff method. The crew restores from that checkpoint, skips completed tasks, and resumes.
+
```python
-# Restore and resume
-crew = Crew.from_checkpoint("./my_checkpoints/20260407T120000_abc123.json")
-result = crew.kickoff() # picks up from last completed task
+from crewai import Crew, CheckpointConfig
+
+crew = Crew(agents=[...], tasks=[...])
+result = crew.kickoff(
+ from_checkpoint=CheckpointConfig(
+ restore_from="./my_checkpoints/20260407T120000_abc123.json",
+ ),
+)
```
-The restored crew skips already-completed tasks and resumes from the first incomplete one.
+Remaining `CheckpointConfig` fields apply to the new run, so checkpointing continues after the restore.
+
+You can also use the classmethod directly:
+
+```python
+config = CheckpointConfig(restore_from="./my_checkpoints/20260407T120000_abc123.json")
+crew = Crew.from_checkpoint(config)
+result = crew.kickoff()
+```
+
+## Forking from a Checkpoint
+
+`fork()` restores a checkpoint and starts a new execution branch. Useful for exploring alternative paths from the same point.
+
+```python
+from crewai import Crew, CheckpointConfig
+
+config = CheckpointConfig(restore_from="./my_checkpoints/20260407T120000_abc123.json")
+crew = Crew.fork(config, branch="experiment-a")
+result = crew.kickoff(inputs={"strategy": "aggressive"})
+```
+
+Each fork gets a unique lineage ID so checkpoints from different branches don't collide. The `branch` label is optional and auto-generated if omitted.
## Works on Crew, Flow, and Agent
@@ -125,7 +155,8 @@ flow = MyFlow(
result = flow.kickoff()
# Resume
-flow = MyFlow.from_checkpoint("./flow_cp/20260407T120000_abc123.json")
+config = CheckpointConfig(restore_from="./flow_cp/20260407T120000_abc123.json")
+flow = MyFlow.from_checkpoint(config)
result = flow.kickoff()
```
@@ -231,3 +262,44 @@ async def on_llm_done_async(source, event, state):
The `state` argument is the `RuntimeState` passed automatically by the event bus when your handler accepts 3 parameters. You can register handlers on any event type listed in the [Event Listeners](/en/concepts/event-listener) documentation.
Checkpointing is best-effort: if a checkpoint write fails, the error is logged but execution continues uninterrupted.
+
+## CLI
+
+The `crewai checkpoint` command gives you a TUI for browsing, inspecting, resuming, and forking checkpoints. It auto-detects whether your checkpoints are JSON files or a SQLite database.
+
+```bash
+# Launch the TUI — auto-detects .checkpoints/ or .checkpoints.db
+crewai checkpoint
+
+# Point at a specific location
+crewai checkpoint --location ./my_checkpoints
+crewai checkpoint --location ./.checkpoints.db
+```
+
+
+
+
+
+The left panel is a tree view. Checkpoints are grouped by branch, and forks nest under the checkpoint they diverged from. Select a checkpoint to see its metadata, entity state, and task progress in the detail panel. Hit **Resume** to pick up where it left off, or **Fork** to start a new branch from that point.
+
+### Editing inputs and task outputs
+
+When a checkpoint is selected, the detail panel shows:
+
+- **Inputs** — if the original kickoff had inputs (e.g. `{topic}`), they appear as editable fields pre-filled with the original values. Change them before resuming or forking.
+- **Task outputs** — completed tasks show their output in editable text areas. Edit a task's output to change the context that downstream tasks receive. When you modify a task output and hit Fork, all subsequent tasks are invalidated and re-run with the new context.
+
+This is useful for "what if" exploration — fork from a checkpoint, tweak a task's result, and see how it changes downstream behavior.
+
+### Subcommands
+
+```bash
+# List all checkpoints
+crewai checkpoint list ./my_checkpoints
+
+# Inspect a specific checkpoint
+crewai checkpoint info ./my_checkpoints/20260407T120000_abc123.json
+
+# Inspect latest in a SQLite database
+crewai checkpoint info ./.checkpoints.db
+```
diff --git a/docs/en/enterprise/features/a2a.mdx b/docs/en/enterprise/features/a2a.mdx
new file mode 100644
index 000000000..e66cbe340
--- /dev/null
+++ b/docs/en/enterprise/features/a2a.mdx
@@ -0,0 +1,227 @@
+---
+title: A2A on AMP
+description: Production-grade Agent-to-Agent communication with distributed state and multi-scheme authentication
+icon: "network-wired"
+mode: "wide"
+---
+
+
+A2A server agents on AMP are in early release. APIs may change in future versions.
+
+
+## Overview
+
+CrewAI AMP extends the open-source [A2A protocol implementation](/en/learn/a2a-agent-delegation) with production infrastructure for deploying distributed agents at scale. AMP supports A2A protocol versions 0.2 and 0.3. When you deploy a crew or agent with A2A server configuration to AMP, the platform automatically provisions distributed state management, authentication, multi-transport endpoints, and lifecycle management.
+
+
+ For A2A protocol fundamentals, client/server configuration, and authentication schemes, see the [A2A Agent Delegation](/en/learn/a2a-agent-delegation) documentation. This page covers what AMP adds on top of the open-source implementation.
+
+
+### Usage
+
+Add `A2AServerConfig` to any agent in your crew and deploy to AMP. The platform detects agents with server configuration and automatically registers A2A endpoints, generates agent cards, and provisions the infrastructure described below.
+
+```python
+from crewai import Agent, Crew, Task
+from crewai.a2a import A2AServerConfig
+from crewai.a2a.auth import EnterpriseTokenAuth
+
+agent = Agent(
+ role="Data Analyst",
+ goal="Analyze datasets and provide insights",
+ backstory="Expert data scientist with statistical analysis skills",
+ llm="gpt-4o",
+ a2a=A2AServerConfig(
+ auth=EnterpriseTokenAuth()
+ )
+)
+
+task = Task(
+ description="Analyze the provided dataset",
+ expected_output="Statistical summary with key insights",
+ agent=agent
+)
+
+crew = Crew(agents=[agent], tasks=[task])
+```
+
+After [deploying to AMP](/en/enterprise/guides/deploy-to-amp), the platform registers two levels of A2A endpoints:
+
+- **Crew-level**: an aggregate agent card at `/.well-known/agent-card.json` where each agent with `A2AServerConfig` is listed as a skill, with a JSON-RPC endpoint at `/a2a`
+- **Per-agent**: isolated agent cards and JSON-RPC endpoints mounted at `/a2a/agents/{role}/`, each with its own tenancy
+
+Clients can interact with the crew as a whole or target a specific agent directly. To route a request to a specific agent through the crew-level endpoint, include `"target_agent"` in the message metadata with the agent's slugified role name (e.g., `"data-analyst"` for an agent with role `"Data Analyst"`). If no `target_agent` is provided, the request is handled by the first agent in the crew.
+
+See [A2A Agent Delegation](/en/learn/a2a-agent-delegation#server-configuration-options) for the full list of `A2AServerConfig` options.
+
+
+ Per the A2A protocol, agent cards are publicly accessible to enable discovery. This includes both the crew-level card at `/.well-known/agent-card.json` and per-agent cards at `/a2a/agents/{role}/.well-known/agent-card.json`. Do not include sensitive information in agent names, descriptions, or skill definitions.
+
+
+### File Inputs and Structured Output
+
+A2A on AMP supports passing files and requesting structured output in both directions. Clients can send files as `FilePart`s and request structured responses by embedding a JSON schema in the message. Server agents receive files as `input_files` on the task, and return structured data as `DataPart`s when a schema is provided. See [File Inputs and Structured Output](/en/learn/a2a-agent-delegation#file-inputs-and-structured-output) for details.
+
+### What AMP Adds
+
+
+
+ Persistent task, context, and result storage
+
+
+ OIDC, OAuth2, mTLS, and Enterprise token validation beyond simple bearer tokens
+
+
+ Full gRPC server with TLS and authentication
+
+
+ Automatic idle detection, expiration, and cleanup of long-running conversations
+
+
+ HMAC-SHA256 signed push notifications with replay protection
+
+
+ REST, JSON-RPC, and gRPC endpoints served simultaneously from a single deployment
+
+
+
+---
+
+## Distributed State Management
+
+In the open-source implementation, task and context state lives in memory on a single process. AMP replaces this with persistent, distributed stores.
+
+### Storage Layers
+
+| Store | Purpose |
+|---|---|
+| **Task Store** | Persists A2A task state and metadata |
+| **Context Store** | Tracks conversation context, creation time, last activity, and associated tasks |
+| **Result Store** | Caches task results for retrieval |
+| **Push Config Store** | Manages webhook subscriptions per task |
+
+Multiple A2A deployments are automatically isolated from each other, preventing data collisions when sharing infrastructure.
+
+---
+
+## Enterprise Authentication
+
+AMP supports six authentication schemes for incoming A2A requests, configurable per deployment. Authentication works across both HTTP and gRPC transports.
+
+| Scheme | Description | Use Case |
+|---|---|---|
+| **SimpleTokenAuth** | Static bearer token from `AUTH_TOKEN` env var | Development, simple deployments |
+| **EnterpriseTokenAuth** | Token verification via CrewAI PlusAPI with integration token claims | AMP-to-AMP agent communication |
+| **OIDCAuth** | OpenID Connect JWT validation with JWKS endpoint caching | Enterprise SSO integration |
+| **OAuth2ServerAuth** | OAuth2 with configurable scopes | Fine-grained access control |
+| **APIKeyServerAuth** | API key validation via header or query parameter | Third-party integrations |
+| **MTLSServerAuth** | Mutual TLS certificate-based authentication | Zero-trust environments |
+
+The configured auth scheme automatically populates the agent card's `securitySchemes` and `security` fields. Clients discover authentication requirements by fetching the agent card before making requests.
+
+---
+
+## Extended Agent Cards
+
+AMP supports role-based skill visibility through extended agent cards. Unauthenticated users see the standard agent card with public skills. Authenticated users receive an extended card with additional capabilities.
+
+This enables patterns like:
+- Public agents that expose basic skills to anyone, with advanced skills available to authenticated clients
+- Internal agents that advertise different capabilities based on the caller's identity
+
+---
+
+## gRPC Transport
+
+If enabled, AMP provides full gRPC support alongside the default JSON-RPC transport.
+
+- **TLS termination** with configurable certificate and key paths
+- **gRPC reflection** for debugging with tools like `grpcurl`
+- **Authentication** using the same schemes available for HTTP
+- **Extension validation** ensuring clients support required protocol extensions
+- **Version negotiation** across A2A protocol versions 0.2 and 0.3
+
+For deployments exposing multiple agents, AMP automatically allocates per-agent gRPC ports and coordinates TLS, startup, and shutdown across all servers.
+
+---
+
+## Context Lifecycle Management
+
+AMP tracks the lifecycle of A2A conversation contexts and automatically manages cleanup.
+
+### Lifecycle States
+
+| State | Condition | Action |
+|---|---|---|
+| **Active** | Context has recent activity | None |
+| **Idle** | No activity for a configured period | Marked idle, event emitted |
+| **Expired** | Context exceeds its maximum lifetime | Marked expired, associated tasks cleaned up, event emitted |
+
+A background cleanup task runs hourly to scan for idle and expired contexts. All state transitions emit CrewAI events that integrate with the platform's observability features.
+
+---
+
+## Signed Push Notifications
+
+When an A2A agent sends push notifications to a client webhook, AMP signs each request with HMAC-SHA256 to ensure integrity and prevent tampering.
+
+### Signature Headers
+
+| Header | Purpose |
+|---|---|
+| `X-A2A-Signature` | HMAC-SHA256 signature in `sha256={hex_digest}` format |
+| `X-A2A-Signature-Timestamp` | Unix timestamp bound to the signature |
+| `X-A2A-Notification-Token` | Optional notification auth token |
+
+### Security Properties
+
+- **Integrity**: payload cannot be modified without invalidating the signature
+- **Replay protection**: signatures are timestamp-bound with a configurable tolerance window
+- **Retry with backoff**: failed deliveries retry with exponential backoff
+
+---
+
+## Distributed Event Streaming
+
+In the open-source implementation, SSE streaming works within a single process. AMP propagates SSE events across instances so that clients receive updates even when the instance holding the streaming connection differs from the instance executing the task.
+
+---
+
+## Multi-Transport Endpoints
+
+AMP serves REST and JSON-RPC by default. gRPC is available as an additional transport if enabled.
+
+| Transport | Path Convention | Description |
+|---|---|---|
+| **REST** | `/v1/message:send`, `/v1/message:stream`, `/v1/tasks` | Google API conventions |
+| **JSON-RPC** | Standard A2A JSON-RPC endpoint | Default A2A protocol transport |
+| **gRPC** | Per-agent port allocation | Optional, high-performance binary protocol |
+
+All active transports share the same authentication, version negotiation, and extension validation. Agent cards are generated from agent and crew metadata — roles, goals, and tools become skills and descriptions — and automatically include interfaces for each active transport. They can also be manually configured via `A2AServerConfig`.
+
+---
+
+## Version and Extension Negotiation
+
+AMP validates A2A protocol versions and extensions at the transport layer.
+
+### Version Negotiation
+
+- Clients send the `A2A-Version` header with their preferred version
+- AMP validates against supported versions (0.2, 0.3) and falls back to 0.3 if unspecified
+- The negotiated version is returned in the response headers
+
+### Extension Validation
+
+- Clients declare supported extensions via the `X-A2A-Extensions` header
+- AMP validates that clients support all extensions the agent requires
+- Requests from clients missing required extensions receive an `UnsupportedExtensionError`
+
+---
+
+## Next Steps
+
+- [A2A Agent Delegation](/en/learn/a2a-agent-delegation) — A2A protocol fundamentals and configuration
+- [A2UI](/en/learn/a2ui) — Interactive UI rendering over A2A
+- [Deploy to AMP](/en/enterprise/guides/deploy-to-amp) — General deployment guide
+- [Webhook Streaming](/en/enterprise/features/webhook-streaming) — Event streaming for deployed automations
diff --git a/docs/en/installation.mdx b/docs/en/installation.mdx
index 727f71220..50f43ff9d 100644
--- a/docs/en/installation.mdx
+++ b/docs/en/installation.mdx
@@ -199,7 +199,7 @@ For teams and organizations, CrewAI offers enterprise deployment options that el
- Supports any hyperscaler including on prem deployments
- Integration with your existing security systems
-
+
Learn about CrewAI's enterprise offerings and schedule a demo
diff --git a/docs/en/learn/a2a-agent-delegation.mdx b/docs/en/learn/a2a-agent-delegation.mdx
index 942ca8bd0..4918e8754 100644
--- a/docs/en/learn/a2a-agent-delegation.mdx
+++ b/docs/en/learn/a2a-agent-delegation.mdx
@@ -7,6 +7,10 @@ mode: "wide"
## A2A Agent Delegation
+
+ Deploying A2A agents to production? See [A2A on AMP](/en/enterprise/features/a2a) for distributed state, enterprise authentication, gRPC transport, and horizontal scaling.
+
+
CrewAI treats [A2A protocol](https://a2a-protocol.org/latest/) as a first-class delegation primitive, enabling agents to delegate tasks, request information, and collaborate with remote agents, as well as act as A2A-compliant server agents.
In client mode, agents autonomously choose between local execution and remote delegation based on task requirements.
@@ -96,24 +100,28 @@ The `A2AClientConfig` class accepts the following parameters:
Update mechanism for receiving task status. Options: `StreamingConfig`, `PollingConfig`, or `PushNotificationConfig`.
-
- Transport protocol for A2A communication. Options: `JSONRPC` (default), `GRPC`, or `HTTP+JSON`.
-
-
Media types the client can accept in responses.
-
- Ordered list of transport protocols the client supports.
-
-
-
- Whether to prioritize client transport preferences over server.
-
-
- Extension URIs the client supports.
+ A2A protocol extension URIs the client supports.
+
+
+
+ Client-side processing hooks for tool injection, prompt augmentation, and response modification.
+
+
+
+ Transport configuration including preferred transport, supported transports for negotiation, and protocol-specific settings (gRPC message sizes, keepalive, etc.).
+
+
+
+ **Deprecated**: Use `transport=ClientTransportConfig(preferred=...)` instead.
+
+
+
+ **Deprecated**: Use `transport=ClientTransportConfig(supported=...)` instead.
## Authentication
@@ -405,11 +413,7 @@ agent = Agent(
Preferred endpoint URL. If set, overrides the URL passed to `to_agent_card()`.
-
- Transport protocol for the preferred endpoint.
-
-
-
+
A2A protocol version this agent supports.
@@ -441,8 +445,36 @@ agent = Agent(
Whether agent provides extended card to authenticated users.
-
- JSON Web Signatures for the AgentCard.
+
+ Additional skills visible only to authenticated users in the extended agent card.
+
+
+
+ Configuration for signing the AgentCard with JWS. Supports RS256, ES256, PS256, and related algorithms.
+
+
+
+ Server-side A2A protocol extensions with `on_request`/`on_response` hooks that modify agent behavior.
+
+
+
+ Configuration for outgoing push notifications, including HMAC-SHA256 signing secret.
+
+
+
+ Transport configuration including preferred transport, gRPC server settings, JSON-RPC paths, and HTTP+JSON settings.
+
+
+
+ Authentication scheme for incoming A2A requests. Defaults to `SimpleTokenAuth` using the `AUTH_TOKEN` environment variable.
+
+
+
+ **Deprecated**: Use `transport=ServerTransportConfig(preferred=...)` instead.
+
+
+
+ **Deprecated**: Use `signing_config=AgentCardSigningConfig(...)` instead.
### Combined Client and Server
@@ -468,6 +500,14 @@ agent = Agent(
)
```
+### File Inputs and Structured Output
+
+A2A supports passing files and requesting structured output in both directions.
+
+**Client side**: When delegating to a remote A2A agent, files from the task's `input_files` are sent as `FilePart`s in the outgoing message. If `response_model` is set on the `A2AClientConfig`, the Pydantic model's JSON schema is embedded in the message metadata, requesting structured output from the remote agent.
+
+**Server side**: Incoming `FilePart`s are extracted and passed to the agent's task as `input_files`. If the client included a JSON schema, the server creates a response model from it and applies it to the task. When the agent returns structured data, the response is sent back as a `DataPart` rather than plain text.
+
## Best Practices
diff --git a/docs/en/learn/streaming-crew-execution.mdx b/docs/en/learn/streaming-crew-execution.mdx
index bfcd0850d..ff0a3cd7f 100644
--- a/docs/en/learn/streaming-crew-execution.mdx
+++ b/docs/en/learn/streaming-crew-execution.mdx
@@ -325,6 +325,34 @@ Streaming is particularly valuable for:
- **User Experience**: Reduce perceived latency by showing incremental results
- **Live Dashboards**: Build monitoring interfaces that display crew execution status
+## Cancellation and Resource Cleanup
+
+`CrewStreamingOutput` supports graceful cancellation so that in-flight work stops promptly when the consumer disconnects.
+
+### Async Context Manager
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+
+async with streaming:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+```
+
+### Explicit Cancellation
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+try:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+finally:
+ await streaming.aclose() # async
+ # streaming.close() # sync equivalent
+```
+
+After cancellation, `streaming.is_cancelled` and `streaming.is_completed` are both `True`. Both `aclose()` and `close()` are idempotent.
+
## Important Notes
- Streaming automatically enables LLM streaming for all agents in the crew
diff --git a/docs/en/learn/streaming-flow-execution.mdx b/docs/en/learn/streaming-flow-execution.mdx
index df0fec91d..31ca0f376 100644
--- a/docs/en/learn/streaming-flow-execution.mdx
+++ b/docs/en/learn/streaming-flow-execution.mdx
@@ -420,6 +420,34 @@ except Exception as e:
print("Streaming completed but flow encountered an error")
```
+## Cancellation and Resource Cleanup
+
+`FlowStreamingOutput` supports graceful cancellation so that in-flight work stops promptly when the consumer disconnects.
+
+### Async Context Manager
+
+```python Code
+streaming = await flow.kickoff_async()
+
+async with streaming:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+```
+
+### Explicit Cancellation
+
+```python Code
+streaming = await flow.kickoff_async()
+try:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+finally:
+ await streaming.aclose() # async
+ # streaming.close() # sync equivalent
+```
+
+After cancellation, `streaming.is_cancelled` and `streaming.is_completed` are both `True`. Both `aclose()` and `close()` are idempotent.
+
## Important Notes
- Streaming automatically enables LLM streaming for any crews used within the flow
diff --git a/docs/en/skills.mdx b/docs/en/skills.mdx
new file mode 100644
index 000000000..81260c88f
--- /dev/null
+++ b/docs/en/skills.mdx
@@ -0,0 +1,50 @@
+---
+title: Skills
+description: Install crewaiinc/skills from the official registry at skills.sh—Flows, Crews, and docs-aware agents for Claude Code, Cursor, Codex, and more.
+icon: wand-magic-sparkles
+mode: "wide"
+---
+
+# Skills
+
+**Give your AI coding agent CrewAI context in one command.**
+
+CrewAI **Skills** are published on **[skills.sh/crewaiinc/skills](https://skills.sh/crewaiinc/skills)**—the official registry for `crewaiinc/skills`, including individual skills (for example **design-agent**, **getting-started**, **design-task**, and **ask-docs**), install stats, and audits. They teach coding agents—like Claude Code, Cursor, and Codex—how to scaffold Flows, configure Crews, use tools, and follow CrewAI patterns. Run the install below (or paste it into your agent).
+
+```shell Terminal
+npx skills add crewaiinc/skills
+```
+
+That pulls the official skill pack into your agent workflow so it can apply CrewAI conventions without you re-explaining the framework each session. Source code and issues live on [GitHub](https://github.com/crewAIInc/skills).
+
+## What your agent gets
+
+- **Flows** — structure stateful apps, steps, and crew kickoffs the CrewAI way
+- **Crews & agents** — YAML-first patterns, roles, tasks, and delegation
+- **Tools & integrations** — hook agents to search, APIs, and common CrewAI tools
+- **Project layout** — align with CLI scaffolds and repo conventions
+- **Up-to-date patterns** — skills track current CrewAI docs and recommended practices
+
+## Learn more on this site
+
+
+
+ How to use `AGENTS.md` and coding-agent workflows with CrewAI.
+
+
+ Build your first Flow and crew end-to-end.
+
+
+ Install the CrewAI CLI and Python package.
+
+
+ Official listing for `crewaiinc/skills`—skills, installs, and audits.
+
+
+ Source, updates, and issues for the skill pack.
+
+
+
+### Video: CrewAI with coding agent skills
+
+
diff --git a/docs/en/tools/database-data/nl2sqltool.mdx b/docs/en/tools/database-data/nl2sqltool.mdx
index ee423e791..833a43cab 100644
--- a/docs/en/tools/database-data/nl2sqltool.mdx
+++ b/docs/en/tools/database-data/nl2sqltool.mdx
@@ -13,7 +13,7 @@ This tool is used to convert natural language to SQL queries. When passed to the
This enables multiple workflows like having an Agent to access the database fetch information based on the goal and then use the information to generate a response, report or any other output.
Along with that provides the ability for the Agent to update the database based on its goal.
-**Attention**: Make sure that the Agent has access to a Read-Replica or that is okay for the Agent to run insert/update queries on the database.
+**Attention**: By default the tool is read-only (SELECT/SHOW/DESCRIBE/EXPLAIN only). Write operations require `allow_dml=True` or the `CREWAI_NL2SQL_ALLOW_DML=true` environment variable. When write access is enabled, make sure the Agent uses a scoped database user or a read replica where possible.
## Security Model
@@ -38,6 +38,74 @@ Use all of the following in production:
- Add `before_tool_call` hooks to enforce allowed query patterns
- Enable query logging and alerting for destructive statements
+## Read-Only Mode & DML Configuration
+
+`NL2SQLTool` operates in **read-only mode by default**. Only the following statement types are permitted without additional configuration:
+
+- `SELECT`
+- `SHOW`
+- `DESCRIBE`
+- `EXPLAIN`
+
+Any attempt to execute a write operation (`INSERT`, `UPDATE`, `DELETE`, `DROP`, `CREATE`, `ALTER`, `TRUNCATE`, etc.) will raise an error unless DML is explicitly enabled.
+
+Multi-statement queries containing semicolons (e.g. `SELECT 1; DROP TABLE users`) are also blocked in read-only mode to prevent injection attacks.
+
+### Enabling Write Operations
+
+You can enable DML (Data Manipulation Language) in two ways:
+
+**Option 1 — constructor parameter:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+**Option 2 — environment variable:**
+
+```bash
+CREWAI_NL2SQL_ALLOW_DML=true
+```
+
+```python
+from crewai_tools import NL2SQLTool
+
+# DML enabled via environment variable
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+### Usage Examples
+
+**Read-only (default) — safe for analytics and reporting:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# Only SELECT/SHOW/DESCRIBE/EXPLAIN are permitted
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+**DML enabled — required for write workloads:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# INSERT, UPDATE, DELETE, DROP, etc. are permitted
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+
+Enabling DML gives the agent the ability to modify or destroy data. Only enable this when your use case explicitly requires write access, and ensure the database credentials are scoped to the minimum required privileges.
+
+
## Requirements
- SqlAlchemy
diff --git a/docs/images/checkpointing.png b/docs/images/checkpointing.png
new file mode 100644
index 000000000..de1f4776a
Binary files /dev/null and b/docs/images/checkpointing.png differ
diff --git a/docs/ko/changelog.mdx b/docs/ko/changelog.mdx
index ad4a3db79..5cb685f3c 100644
--- a/docs/ko/changelog.mdx
+++ b/docs/ko/changelog.mdx
@@ -4,6 +4,214 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2rc1
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2rc1)
+
+ ## 변경 사항
+
+ ### 버그 수정
+ - MCP 도구 해상도에서 순환 JSON 스키마 처리 수정
+ - python-multipart를 0.0.26으로 업데이트하여 취약점 수정
+ - pypdf를 6.10.1로 업데이트하여 취약점 수정
+
+ ### 문서
+ - v1.14.2a5에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a5
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
+
+ ## 변경 사항
+
+ ### 문서
+ - v1.14.2a4의 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a4
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## 변경 사항
+
+ ### 기능
+ - 실패 시 devtools 릴리스에 이력서 힌트 추가
+
+ ### 버그 수정
+ - Bedrock Converse API로의 엄격 모드 포워딩 수정
+ - 보안 취약점 GHSA-6w46-j5rx-g56g에 대해 pytest 버전을 9.0.3으로 수정
+ - OpenAI 하한을 >=2.0.0으로 상향 조정
+
+ ### 문서
+ - v1.14.2a3에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a3
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a3)
+
+ ## 변경 사항
+
+ ### 기능
+ - 배포 검증 CLI 추가
+ - LLM 초기화 사용성 개선
+
+ ### 버그 수정
+ - CVE-2026-40260 및 GHSA-pjjw-68hj-v9mw에 대한 패치된 버전으로 pypdf 및 uv 재정의
+ - CVE 임시 파일 취약점에 대해 requests를 >=2.33.0으로 업그레이드
+ - 진리값 기본값을 제거하여 Bedrock 도구 호출 인수 보존
+ - 엄격 모드를 위한 도구 스키마 정리
+ - MemoryRecord 임베딩 직렬화 테스트의 불안정성 제거
+
+ ### 문서
+ - 기업 A2A 언어 정리
+ - 기업 A2A 기능 문서 추가
+ - OSS A2A 문서 업데이트
+ - v1.14.2a2에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @Yanhu007, @greysonlalonde
+
+
+
+
+ ## v1.14.2a2
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a2)
+
+ ## 변경 사항
+
+ ### 기능
+ - 트리 뷰, 포크 지원 및 편집 가능한 입력/출력을 갖춘 체크포인트 TUI 추가
+ - 추론 토큰 및 캐시 생성 토큰으로 LLM 토큰 추적 강화
+ - 킥오프 메서드에 `from_checkpoint` 매개변수 추가
+ - 마이그레이션 프레임워크와 함께 체크포인트에 `crewai_version` 포함
+ - 계보 추적이 가능한 체크포인트 포킹 추가
+
+ ### 버그 수정
+ - Anthropic 및 Bedrock 공급자로의 엄격 모드 포워딩 수정
+ - 읽기 전용 기본값, 쿼리 검증 및 매개변수화된 쿼리로 NL2SQLTool 강화
+
+ ### 문서
+ - v1.14.2a1에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @alex-clawd, @github-actions[bot], @greysonlalonde, @lucasgomide
+
+
+
+
+ ## v1.14.2a1
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a1)
+
+ ## 변경 사항
+
+ ### 버그 수정
+ - HITL 재개 후 flow_finished 이벤트 방출 수정
+ - CVE-2026-39892 문제를 해결하기 위해 암호화 버전을 46.0.7로 수정
+
+ ### 리팩토링
+ - 공유 I18N_DEFAULT 싱글톤을 사용하도록 리팩토링
+
+ ### 문서
+ - v1.14.1에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.1
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1)
+
+ ## 변경 사항
+
+ ### 기능
+ - 비동기 체크포인트 TUI 브라우저 추가
+ - 스트리밍 출력에 aclose()/close() 및 비동기 컨텍스트 관리자 추가
+
+ ### 버그 수정
+ - 템플릿 pyproject.toml 버전 증가를 위한 정규 표현식 수정
+ - 훅 데코레이터 필터에서 도구 이름 정리
+ - CheckpointConfig 생성 시 체크포인트 핸들러 등록 수정
+ - CVE-2026-1839 해결을 위해 transformers를 5.5.0으로 업데이트
+ - FilteredStream stdout/stderr 래퍼 제거
+
+ ### 문서
+ - v1.14.1rc1에 대한 변경 로그 및 버전 업데이트
+
+ ### 리팩토링
+ - 하드코딩된 거부 목록을 동적 BaseTool 필드 제외로 교체
+ - devtools CLI에서 정규 표현식을 tomlkit으로 교체
+ - 공유 PRINTER 싱글톤 사용
+ - BaseProvider를 provider_type 식별자가 있는 BaseModel로 변경
+
+ ## 기여자
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay
+
+
+
+
+ ## v1.14.1rc1
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1)
+
+ ## 변경 사항
+
+ ### 기능
+ - 비동기 체크포인트 TUI 브라우저 추가
+ - 스트리밍 출력에 aclose()/close() 및 비동기 컨텍스트 관리자 추가
+
+ ### 버그 수정
+ - 정규 표현식을 사용하여 템플릿 pyproject.toml 버전 증가 수정
+ - 후크 데코레이터 필터에서 도구 이름 정리
+ - CVE-2026-1839 해결을 위해 transformers를 5.5.0으로 업데이트
+ - CheckpointConfig가 생성될 때 체크포인트 핸들러 등록
+
+ ### 리팩토링
+ - 하드코딩된 거부 목록을 동적 BaseTool 필드 제외로 교체
+ - devtools CLI에서 정규 표현식을 tomlkit으로 교체
+ - 공유 PRINTER 싱글톤 사용
+ - BaseProvider를 provider_type 구분자가 있는 BaseModel로 변경
+ - FilteredStream stdout/stderr 래퍼 제거
+ - 사용되지 않는 flow/config.py 제거
+
+ ### 문서
+ - v1.14.0에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura
+
+
+
## v1.14.0
diff --git a/docs/ko/installation.mdx b/docs/ko/installation.mdx
index fc47d796b..6363f3271 100644
--- a/docs/ko/installation.mdx
+++ b/docs/ko/installation.mdx
@@ -189,7 +189,7 @@ CrewAI는 의존성 관리와 패키지 처리를 위해 `uv`를 사용합니다
- 온프레미스 배포를 포함하여 모든 하이퍼스케일러 지원
- 기존 보안 시스템과의 통합
-
+
CrewAI의 엔터프라이즈 서비스에 대해 알아보고 데모를 예약하세요
diff --git a/docs/ko/learn/streaming-crew-execution.mdx b/docs/ko/learn/streaming-crew-execution.mdx
index aec56caed..db2ce1c0c 100644
--- a/docs/ko/learn/streaming-crew-execution.mdx
+++ b/docs/ko/learn/streaming-crew-execution.mdx
@@ -325,6 +325,34 @@ asyncio.run(interactive_research())
- **사용자 경험**: 점진적인 결과를 표시하여 체감 지연 시간 감소
- **라이브 대시보드**: crew 실행 상태를 표시하는 모니터링 인터페이스 구축
+## 취소 및 리소스 정리
+
+`CrewStreamingOutput`은 소비자가 연결을 끊을 때 진행 중인 작업을 즉시 중단하는 정상적인 취소를 지원합니다.
+
+### 비동기 컨텍스트 매니저
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+
+async with streaming:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+```
+
+### 명시적 취소
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+try:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+finally:
+ await streaming.aclose() # 비동기
+ # streaming.close() # 동기 버전
+```
+
+취소 후 `streaming.is_cancelled`와 `streaming.is_completed`는 모두 `True`입니다. `aclose()`와 `close()` 모두 멱등성을 가집니다.
+
## 중요 사항
- 스트리밍은 crew의 모든 에이전트에 대해 자동으로 LLM 스트리밍을 활성화합니다
diff --git a/docs/ko/skills.mdx b/docs/ko/skills.mdx
new file mode 100644
index 000000000..0c789c158
--- /dev/null
+++ b/docs/ko/skills.mdx
@@ -0,0 +1,50 @@
+---
+title: Skills
+description: skills.sh의 공식 레지스트리에서 crewaiinc/skills를 설치하세요. Claude Code, Cursor, Codex 등을 위한 Flow, Crew, 문서 연동 스킬.
+icon: wand-magic-sparkles
+mode: "wide"
+---
+
+# Skills
+
+**한 번의 명령으로 코딩 에이전트에 CrewAI 컨텍스트를 제공하세요.**
+
+CrewAI **Skills**는 **[skills.sh/crewaiinc/skills](https://skills.sh/crewaiinc/skills)**에 게시됩니다. `crewaiinc/skills`의 공식 레지스트리로, 개별 스킬(예: **design-agent**, **getting-started**, **design-task**, **ask-docs**), 설치 수, 감사 정보를 확인할 수 있습니다. Claude Code, Cursor, Codex 같은 코딩 에이전트에게 Flow 구성, Crew 설정, 도구 사용, CrewAI 패턴을 가르칩니다. 아래를 실행하거나 에이전트에 붙여 넣으세요.
+
+```shell Terminal
+npx skills add crewaiinc/skills
+```
+
+에이전트 워크플로에 스킬 팩이 추가되어 세션마다 프레임워크를 다시 설명하지 않아도 CrewAI 관례를 적용할 수 있습니다. 소스와 이슈는 [GitHub](https://github.com/crewAIInc/skills)에서 관리합니다.
+
+## 에이전트가 얻는 것
+
+- **Flows** — CrewAI 방식의 상태ful 앱, 단계, crew kickoff
+- **Crew & 에이전트** — YAML 우선 패턴, 역할, 작업, 위임
+- **도구 & 통합** — 검색, API, 일반적인 CrewAI 도구 연결
+- **프로젝트 구조** — CLI 스캐폴드 및 저장소 관례와 정렬
+- **최신 패턴** — 스킬이 현재 CrewAI 문서 및 권장 사항을 반영
+
+## 이 사이트에서 더 알아보기
+
+
+
+ CrewAI와 `AGENTS.md`, 코딩 에이전트 워크플로 사용법.
+
+
+ 첫 Flow와 crew를 처음부터 끝까지 구축합니다.
+
+
+ CrewAI CLI와 Python 패키지를 설치합니다.
+
+
+ `crewaiinc/skills` 공식 목록—스킬, 설치 수, 감사.
+
+
+ 스킬 팩 소스, 업데이트, 이슈.
+
+
+
+### 영상: 코딩 에이전트 스킬과 CrewAI
+
+
diff --git a/docs/ko/tools/database-data/nl2sqltool.mdx b/docs/ko/tools/database-data/nl2sqltool.mdx
index 5f6583155..32894f44c 100644
--- a/docs/ko/tools/database-data/nl2sqltool.mdx
+++ b/docs/ko/tools/database-data/nl2sqltool.mdx
@@ -11,7 +11,75 @@ mode: "wide"
이를 통해 에이전트가 데이터베이스에 접근하여 목표에 따라 정보를 가져오고, 해당 정보를 사용해 응답, 보고서 또는 기타 출력물을 생성하는 다양한 워크플로우가 가능해집니다. 또한 에이전트가 자신의 목표에 맞춰 데이터베이스를 업데이트할 수 있는 기능도 제공합니다.
-**주의**: 에이전트가 Read-Replica에 접근할 수 있거나, 에이전트가 데이터베이스에 insert/update 쿼리를 실행해도 괜찮은지 반드시 확인하십시오.
+**주의**: 도구는 기본적으로 읽기 전용(SELECT/SHOW/DESCRIBE/EXPLAIN만 허용)으로 동작합니다. 쓰기 작업을 수행하려면 `allow_dml=True` 매개변수 또는 `CREWAI_NL2SQL_ALLOW_DML=true` 환경 변수가 필요합니다. 쓰기 접근이 활성화된 경우, 가능하면 권한이 제한된 데이터베이스 사용자나 읽기 복제본을 사용하십시오.
+
+## 읽기 전용 모드 및 DML 구성
+
+`NL2SQLTool`은 기본적으로 **읽기 전용 모드**로 동작합니다. 추가 구성 없이 허용되는 구문 유형은 다음과 같습니다:
+
+- `SELECT`
+- `SHOW`
+- `DESCRIBE`
+- `EXPLAIN`
+
+DML을 명시적으로 활성화하지 않으면 쓰기 작업(`INSERT`, `UPDATE`, `DELETE`, `DROP`, `CREATE`, `ALTER`, `TRUNCATE` 등)을 실행하려고 할 때 오류가 발생합니다.
+
+읽기 전용 모드에서는 세미콜론이 포함된 다중 구문 쿼리(예: `SELECT 1; DROP TABLE users`)도 인젝션 공격을 방지하기 위해 차단됩니다.
+
+### 쓰기 작업 활성화
+
+DML(데이터 조작 언어)을 활성화하는 방법은 두 가지입니다:
+
+**옵션 1 — 생성자 매개변수:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+**옵션 2 — 환경 변수:**
+
+```bash
+CREWAI_NL2SQL_ALLOW_DML=true
+```
+
+```python
+from crewai_tools import NL2SQLTool
+
+# 환경 변수를 통해 DML 활성화
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+### 사용 예시
+
+**읽기 전용(기본값) — 분석 및 보고 워크로드에 안전:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# SELECT/SHOW/DESCRIBE/EXPLAIN만 허용
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+**DML 활성화 — 쓰기 워크로드에 필요:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# INSERT, UPDATE, DELETE, DROP 등이 허용됨
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+
+DML을 활성화하면 에이전트가 데이터를 수정하거나 삭제할 수 있습니다. 사용 사례에서 명시적으로 쓰기 접근이 필요한 경우에만 활성화하고, 데이터베이스 자격 증명이 최소 필요 권한으로 제한되어 있는지 확인하십시오.
+
## 요구 사항
diff --git a/docs/pt-BR/changelog.mdx b/docs/pt-BR/changelog.mdx
index febf0d886..02ab715fb 100644
--- a/docs/pt-BR/changelog.mdx
+++ b/docs/pt-BR/changelog.mdx
@@ -4,6 +4,214 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2rc1
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2rc1)
+
+ ## O que Mudou
+
+ ### Correções de Bugs
+ - Corrigir o manuseio de esquemas JSON cíclicos na resolução da ferramenta MCP
+ - Corrigir vulnerabilidade atualizando python-multipart para 0.0.26
+ - Corrigir vulnerabilidade atualizando pypdf para 6.10.1
+
+ ### Documentação
+ - Atualizar o changelog e a versão para v1.14.2a5
+
+ ## Contribuidores
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a5
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
+
+ ## O que Mudou
+
+ ### Documentação
+ - Atualizar changelog e versão para v1.14.2a4
+
+ ## Contribuidores
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a4
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## O que Mudou
+
+ ### Recursos
+ - Adicionar dicas de retomar ao release do devtools em caso de falha
+
+ ### Correções de Bugs
+ - Corrigir o encaminhamento do modo estrito para a API Bedrock Converse
+ - Corrigir a versão do pytest para 9.0.3 devido à vulnerabilidade de segurança GHSA-6w46-j5rx-g56g
+ - Aumentar o limite inferior do OpenAI para >=2.0.0
+
+ ### Documentação
+ - Atualizar o changelog e a versão para v1.14.2a3
+
+ ## Contribuidores
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.2a3
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a3)
+
+ ## O que Mudou
+
+ ### Recursos
+ - Adicionar CLI de validação de deploy
+ - Melhorar a ergonomia de inicialização do LLM
+
+ ### Correções de Bugs
+ - Substituir pypdf e uv por versões corrigidas para CVE-2026-40260 e GHSA-pjjw-68hj-v9mw
+ - Atualizar requests para >=2.33.0 devido à vulnerabilidade de arquivo temporário CVE
+ - Preservar os argumentos de chamada da ferramenta Bedrock removendo o padrão truthy
+ - Sanitizar esquemas de ferramentas para modo estrito
+ - Remover flakiness do teste de serialização de embedding MemoryRecord
+
+ ### Documentação
+ - Limpar a linguagem do A2A empresarial
+ - Adicionar documentação de recursos do A2A empresarial
+ - Atualizar documentação do A2A OSS
+ - Atualizar changelog e versão para v1.14.2a2
+
+ ## Contribuidores
+
+ @Yanhu007, @greysonlalonde
+
+
+
+
+ ## v1.14.2a2
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a2)
+
+ ## O que Mudou
+
+ ### Funcionalidades
+ - Adicionar TUI de ponto de verificação com visualização em árvore, suporte a bifurcações e entradas/saídas editáveis
+ - Enriquecer o rastreamento de tokens LLM com tokens de raciocínio e tokens de criação de cache
+ - Adicionar parâmetro `from_checkpoint` aos métodos de inicialização
+ - Incorporar `crewai_version` em pontos de verificação com o framework de migração
+ - Adicionar bifurcação de ponto de verificação com rastreamento de linhagem
+
+ ### Correções de Bugs
+ - Corrigir o encaminhamento em modo estrito para os provedores Anthropic e Bedrock
+ - Fortalecer NL2SQLTool com padrão somente leitura, validação de consultas e consultas parametrizadas
+
+ ### Documentação
+ - Atualizar changelog e versão para v1.14.2a1
+
+ ## Contributors
+
+ @alex-clawd, @github-actions[bot], @greysonlalonde, @lucasgomide
+
+
+
+
+ ## v1.14.2a1
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a1)
+
+ ## O que Mudou
+
+ ### Correções de Bugs
+ - Corrigir a emissão do evento flow_finished após a retomada do HITL
+ - Corrigir a versão da criptografia para 46.0.7 para resolver o CVE-2026-39892
+
+ ### Refatoração
+ - Refatorar para usar o singleton I18N_DEFAULT compartilhado
+
+ ### Documentação
+ - Atualizar o changelog e a versão para v1.14.1
+
+ ## Contribuidores
+
+ @greysonlalonde
+
+
+
+
+ ## v1.14.1
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1)
+
+ ## O que Mudou
+
+ ### Funcionalidades
+ - Adicionar navegador TUI de ponto de verificação assíncrono
+ - Adicionar aclose()/close() e gerenciador de contexto assíncrono para saídas de streaming
+
+ ### Correções de Bugs
+ - Corrigir regex para aumentos de versão do template pyproject.toml
+ - Sanitizar nomes de ferramentas nos filtros do decorador de hook
+ - Corrigir registro de manipuladores de ponto de verificação quando CheckpointConfig é criado
+ - Atualizar transformers para 5.5.0 para resolver CVE-2026-1839
+ - Remover wrapper stdout/stderr de FilteredStream
+
+ ### Documentação
+ - Atualizar changelog e versão para v1.14.1rc1
+
+ ### Refatoração
+ - Substituir lista de negação codificada por exclusão dinâmica de campo BaseTool na geração de especificações
+ - Substituir regex por tomlkit na CLI do devtools
+ - Usar singleton PRINTER compartilhado
+ - Fazer BaseProvider um BaseModel com discriminador provider_type
+
+ ## Contribuidores
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay
+
+
+
+
+ ## v1.14.1rc1
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1)
+
+ ## O que Mudou
+
+ ### Recursos
+ - Adicionar navegador TUI de ponto de verificação assíncrono
+ - Adicionar aclose()/close() e gerenciador de contexto assíncrono para saídas de streaming
+
+ ### Correções de Bugs
+ - Corrigir aumentos de versão do template pyproject.toml usando regex
+ - Sanitizar nomes de ferramentas nos filtros do decorador de hook
+ - Atualizar transformers para 5.5.0 para resolver CVE-2026-1839
+ - Registrar manipuladores de ponto de verificação quando CheckpointConfig é criado
+
+ ### Refatoração
+ - Substituir lista de negação codificada por exclusão dinâmica de campo BaseTool na geração de especificações
+ - Substituir regex por tomlkit na CLI do devtools
+ - Usar singleton PRINTER compartilhado
+ - Tornar BaseProvider um BaseModel com discriminador de tipo de provedor
+ - Remover wrapper stdout/stderr de FilteredStream
+ - Remover flow/config.py não utilizado
+
+ ### Documentação
+ - Atualizar changelog e versão para v1.14.0
+
+ ## Contribuidores
+
+ @greysonlalonde, @iris-clawd, @joaomdmoura
+
+
+
## v1.14.0
diff --git a/docs/pt-BR/installation.mdx b/docs/pt-BR/installation.mdx
index 868778af8..74d0445d3 100644
--- a/docs/pt-BR/installation.mdx
+++ b/docs/pt-BR/installation.mdx
@@ -191,7 +191,7 @@ Para equipes e organizações, o CrewAI oferece opções de implantação corpor
- Compatível com qualquer hyperscaler, incluindo ambientes on-premises
- Integração com seus sistemas de segurança existentes
-
+
Saiba mais sobre as soluções enterprise do CrewAI e agende uma demonstração
diff --git a/docs/pt-BR/learn/streaming-crew-execution.mdx b/docs/pt-BR/learn/streaming-crew-execution.mdx
index 85a26e370..4a3df07ef 100644
--- a/docs/pt-BR/learn/streaming-crew-execution.mdx
+++ b/docs/pt-BR/learn/streaming-crew-execution.mdx
@@ -325,6 +325,34 @@ O streaming é particularmente valioso para:
- **Experiência do Usuário**: Reduzir latência percebida mostrando resultados incrementais
- **Dashboards ao Vivo**: Construir interfaces de monitoramento que exibem status de execução da crew
+## Cancelamento e Limpeza de Recursos
+
+`CrewStreamingOutput` suporta cancelamento gracioso para que o trabalho em andamento pare imediatamente quando o consumidor desconecta.
+
+### Gerenciador de Contexto Assíncrono
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+
+async with streaming:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+```
+
+### Cancelamento Explícito
+
+```python Code
+streaming = await crew.akickoff(inputs={"topic": "AI"})
+try:
+ async for chunk in streaming:
+ print(chunk.content, end="", flush=True)
+finally:
+ await streaming.aclose() # assíncrono
+ # streaming.close() # equivalente síncrono
+```
+
+Após o cancelamento, `streaming.is_cancelled` e `streaming.is_completed` são ambos `True`. Tanto `aclose()` quanto `close()` são idempotentes.
+
## Notas Importantes
- O streaming ativa automaticamente o streaming do LLM para todos os agentes na crew
diff --git a/docs/pt-BR/skills.mdx b/docs/pt-BR/skills.mdx
new file mode 100644
index 000000000..acd372d3f
--- /dev/null
+++ b/docs/pt-BR/skills.mdx
@@ -0,0 +1,50 @@
+---
+title: Skills
+description: Instale crewaiinc/skills pelo registro oficial em skills.sh—Flows, Crews e agentes alinhados à documentação para Claude Code, Cursor, Codex e outros.
+icon: wand-magic-sparkles
+mode: "wide"
+---
+
+# Skills
+
+**Dê ao seu agente de código o contexto do CrewAI em um comando.**
+
+As **Skills** do CrewAI são publicadas em **[skills.sh/crewaiinc/skills](https://skills.sh/crewaiinc/skills)**—o registro oficial de `crewaiinc/skills`, com cada skill (por exemplo **design-agent**, **getting-started**, **design-task** e **ask-docs**), estatísticas de instalação e auditorias. Ensinam agentes de código—como Claude Code, Cursor e Codex—a estruturar Flows, configurar Crews, usar ferramentas e seguir os padrões do CrewAI. Execute o comando abaixo (ou cole no seu agente).
+
+```shell Terminal
+npx skills add crewaiinc/skills
+```
+
+Isso adiciona o pacote de skills ao fluxo do seu agente para aplicar convenções do CrewAI sem precisar reexplicar o framework a cada sessão. Código-fonte e issues ficam no [GitHub](https://github.com/crewAIInc/skills).
+
+## O que seu agente ganha
+
+- **Flows** — apps com estado, passos e kickoffs de crew no estilo CrewAI
+- **Crews e agentes** — padrões YAML-first, papéis, tarefas e delegação
+- **Ferramentas e integrações** — conectar agentes a busca, APIs e ferramentas comuns
+- **Layout de projeto** — alinhar com scaffolds da CLI e convenções do repositório
+- **Padrões atualizados** — skills acompanham a documentação e as práticas recomendadas
+
+## Saiba mais neste site
+
+
+
+ Como usar `AGENTS.md` e fluxos de agente de código com o CrewAI.
+
+
+ Construa seu primeiro Flow e crew ponta a ponta.
+
+
+ Instale a CLI e o pacote Python do CrewAI.
+
+
+ Listagem oficial de `crewaiinc/skills`—skills, instalações e auditorias.
+
+
+ Fonte, atualizações e issues do pacote de skills.
+
+
+
+### Vídeo: CrewAI com coding agent skills
+
+
diff --git a/docs/pt-BR/tools/database-data/nl2sqltool.mdx b/docs/pt-BR/tools/database-data/nl2sqltool.mdx
index f414ab4e2..8ef3cc160 100644
--- a/docs/pt-BR/tools/database-data/nl2sqltool.mdx
+++ b/docs/pt-BR/tools/database-data/nl2sqltool.mdx
@@ -11,7 +11,75 @@ Esta ferramenta é utilizada para converter linguagem natural em consultas SQL.
Isso possibilita múltiplos fluxos de trabalho, como por exemplo ter um Agente acessando o banco de dados para buscar informações com base em um objetivo e, então, usar essas informações para gerar uma resposta, relatório ou qualquer outro tipo de saída. Além disso, permite que o Agente atualize o banco de dados de acordo com seu objetivo.
-**Atenção**: Certifique-se de que o Agente tenha acesso a um Read-Replica ou que seja permitido que o Agente execute consultas de inserção/atualização no banco de dados.
+**Atenção**: Por padrão, a ferramenta opera em modo somente leitura (apenas SELECT/SHOW/DESCRIBE/EXPLAIN). Operações de escrita exigem `allow_dml=True` ou a variável de ambiente `CREWAI_NL2SQL_ALLOW_DML=true`. Quando o acesso de escrita estiver habilitado, certifique-se de que o Agente use um usuário de banco de dados com privilégios mínimos ou um Read-Replica sempre que possível.
+
+## Modo Somente Leitura e Configuração de DML
+
+O `NL2SQLTool` opera em **modo somente leitura por padrão**. Apenas os seguintes tipos de instrução são permitidos sem configuração adicional:
+
+- `SELECT`
+- `SHOW`
+- `DESCRIBE`
+- `EXPLAIN`
+
+Qualquer tentativa de executar uma operação de escrita (`INSERT`, `UPDATE`, `DELETE`, `DROP`, `CREATE`, `ALTER`, `TRUNCATE`, etc.) resultará em erro, a menos que o DML seja habilitado explicitamente.
+
+Consultas com múltiplas instruções contendo ponto e vírgula (ex.: `SELECT 1; DROP TABLE users`) também são bloqueadas no modo somente leitura para prevenir ataques de injeção.
+
+### Habilitando Operações de Escrita
+
+Você pode habilitar DML (Linguagem de Manipulação de Dados) de duas formas:
+
+**Opção 1 — parâmetro do construtor:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+**Opção 2 — variável de ambiente:**
+
+```bash
+CREWAI_NL2SQL_ALLOW_DML=true
+```
+
+```python
+from crewai_tools import NL2SQLTool
+
+# DML habilitado via variável de ambiente
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+### Exemplos de Uso
+
+**Somente leitura (padrão) — seguro para análise e relatórios:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# Apenas SELECT/SHOW/DESCRIBE/EXPLAIN são permitidos
+nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db")
+```
+
+**Com DML habilitado — necessário para workloads de escrita:**
+
+```python
+from crewai_tools import NL2SQLTool
+
+# INSERT, UPDATE, DELETE, DROP, etc. são permitidos
+nl2sql = NL2SQLTool(
+ db_uri="postgresql://example@localhost:5432/test_db",
+ allow_dml=True,
+)
+```
+
+
+Habilitar DML concede ao agente a capacidade de modificar ou destruir dados. Ative apenas quando o seu caso de uso exigir explicitamente acesso de escrita e certifique-se de que as credenciais do banco de dados estejam limitadas aos privilégios mínimos necessários.
+
## Requisitos
diff --git a/lib/crewai-files/pyproject.toml b/lib/crewai-files/pyproject.toml
index 99f7c15c5..0302b5900 100644
--- a/lib/crewai-files/pyproject.toml
+++ b/lib/crewai-files/pyproject.toml
@@ -9,7 +9,7 @@ authors = [
requires-python = ">=3.10, <3.14"
dependencies = [
"Pillow~=12.1.1",
- "pypdf~=6.9.1",
+ "pypdf~=6.10.0",
"python-magic>=0.4.27",
"aiocache~=0.12.3",
"aiofiles~=24.1.0",
diff --git a/lib/crewai-files/src/crewai_files/__init__.py b/lib/crewai-files/src/crewai_files/__init__.py
index 7430288b5..9cfa885c2 100644
--- a/lib/crewai-files/src/crewai_files/__init__.py
+++ b/lib/crewai-files/src/crewai_files/__init__.py
@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
-__version__ = "1.14.0"
+__version__ = "1.14.2rc1"
diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml
index 7653f9851..5547d96ef 100644
--- a/lib/crewai-tools/pyproject.toml
+++ b/lib/crewai-tools/pyproject.toml
@@ -9,8 +9,8 @@ authors = [
requires-python = ">=3.10, <3.14"
dependencies = [
"pytube~=15.0.0",
- "requests~=2.32.5",
- "crewai==1.14.0",
+ "requests>=2.33.0,<3",
+ "crewai==1.14.2rc1",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",
diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py
index 2230e9afc..d9ce4e9c2 100644
--- a/lib/crewai-tools/src/crewai_tools/__init__.py
+++ b/lib/crewai-tools/src/crewai_tools/__init__.py
@@ -305,4 +305,4 @@ __all__ = [
"ZapierActionTools",
]
-__version__ = "1.14.0"
+__version__ = "1.14.2rc1"
diff --git a/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py b/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py
index 34d78e074..579adaa30 100644
--- a/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py
+++ b/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py
@@ -154,21 +154,19 @@ class ToolSpecExtractor:
return default_value
+ # Dynamically computed from BaseTool so that any future fields or
+ # computed_fields added to BaseTool are automatically excluded from
+ # the generated spec — no hardcoded denylist to maintain.
+ # ``package_dependencies`` is not a BaseTool field but is extracted
+ # into its own top-level key, so it's also excluded from init_params.
+ _BASE_TOOL_FIELDS: set[str] = (
+ set(BaseTool.model_fields)
+ | set(BaseTool.model_computed_fields)
+ | {"package_dependencies"}
+ )
+
@staticmethod
def _extract_init_params(tool_class: type[BaseTool]) -> dict[str, Any]:
- ignored_init_params = [
- "name",
- "description",
- "env_vars",
- "args_schema",
- "description_updated",
- "cache_function",
- "result_as_answer",
- "max_usage_count",
- "current_usage_count",
- "package_dependencies",
- ]
-
json_schema = tool_class.model_json_schema(
schema_generator=SchemaGenerator, mode="serialization"
)
@@ -176,8 +174,14 @@ class ToolSpecExtractor:
json_schema["properties"] = {
key: value
for key, value in json_schema["properties"].items()
- if key not in ignored_init_params
+ if key not in ToolSpecExtractor._BASE_TOOL_FIELDS
}
+ if "required" in json_schema:
+ json_schema["required"] = [
+ key
+ for key in json_schema["required"]
+ if key not in ToolSpecExtractor._BASE_TOOL_FIELDS
+ ]
return json_schema
def save_to_json(self, output_path: str) -> None:
diff --git a/lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py b/lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
index bfb9c02dd..4e20b4354 100644
--- a/lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
+++ b/lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
@@ -1,7 +1,17 @@
+from collections.abc import Iterator
+import logging
+import os
+import re
from typing import Any
+
+try:
+ from typing import Self
+except ImportError:
+ from typing_extensions import Self
+
from crewai.tools import BaseTool
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, model_validator
try:
@@ -12,6 +22,186 @@ try:
except ImportError:
SQLALCHEMY_AVAILABLE = False
+logger = logging.getLogger(__name__)
+
+# Commands allowed in read-only mode
+# NOTE: WITH is intentionally excluded — writable CTEs start with WITH, so the
+# CTE body must be inspected separately (see _validate_statement).
+_READ_ONLY_COMMANDS = {"SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN"}
+
+# Commands that mutate state and are blocked by default
+_WRITE_COMMANDS = {
+ "INSERT",
+ "UPDATE",
+ "DELETE",
+ "DROP",
+ "ALTER",
+ "CREATE",
+ "TRUNCATE",
+ "GRANT",
+ "REVOKE",
+ "EXEC",
+ "EXECUTE",
+ "CALL",
+ "MERGE",
+ "REPLACE",
+ "UPSERT",
+ "LOAD",
+ "COPY",
+ "VACUUM",
+ "ANALYZE",
+ "ANALYSE",
+ "REINDEX",
+ "CLUSTER",
+ "REFRESH",
+ "COMMENT",
+ "SET",
+ "RESET",
+}
+
+
+# Subset of write commands that can realistically appear *inside* a CTE body.
+# Narrower than _WRITE_COMMANDS to avoid false positives on identifiers like
+# ``comment``, ``set``, or ``reset`` which are common column/table names.
+_CTE_WRITE_INDICATORS = {
+ "INSERT",
+ "UPDATE",
+ "DELETE",
+ "DROP",
+ "ALTER",
+ "CREATE",
+ "TRUNCATE",
+ "MERGE",
+}
+
+
+_AS_PAREN_RE = re.compile(r"\bAS\s*\(", re.IGNORECASE)
+
+
+def _iter_as_paren_matches(stmt: str) -> Iterator[re.Match[str]]:
+ """Yield regex matches for ``AS\\s*(`` outside of string literals."""
+ # Build a set of character positions that are inside string literals.
+ in_string: set[int] = set()
+ i = 0
+ while i < len(stmt):
+ if stmt[i] == "'":
+ start = i
+ end = _skip_string_literal(stmt, i)
+ in_string.update(range(start, end))
+ i = end
+ else:
+ i += 1
+
+ for m in _AS_PAREN_RE.finditer(stmt):
+ if m.start() not in in_string:
+ yield m
+
+
+def _detect_writable_cte(stmt: str) -> str | None:
+ """Return the first write command inside a CTE body, or None.
+
+ Instead of tokenizing the whole statement (which falsely matches column
+ names like ``comment``), this walks through parenthesized CTE bodies and
+ checks only the *first keyword after* an opening ``AS (`` for a write
+ command. Uses a regex to handle any whitespace (spaces, tabs, newlines)
+ between ``AS`` and ``(``. Skips matches inside string literals.
+ """
+ for m in _iter_as_paren_matches(stmt):
+ body = stmt[m.end() :].lstrip()
+ first_word = body.split()[0].upper().strip("()") if body.split() else ""
+ if first_word in _CTE_WRITE_INDICATORS:
+ return first_word
+ return None
+
+
+def _skip_string_literal(stmt: str, pos: int) -> int:
+ """Skip past a string literal starting at pos (single-quoted).
+
+ Handles escaped quotes ('') inside the literal.
+ Returns the index after the closing quote.
+ """
+ quote_char = stmt[pos]
+ i = pos + 1
+ while i < len(stmt):
+ if stmt[i] == quote_char:
+ # Check for escaped quote ('')
+ if i + 1 < len(stmt) and stmt[i + 1] == quote_char:
+ i += 2
+ continue
+ return i + 1
+ i += 1
+ return i # Unterminated literal — return end
+
+
+def _find_matching_close_paren(stmt: str, start: int) -> int:
+ """Find the matching close paren, skipping string literals."""
+ depth = 1
+ i = start
+ while i < len(stmt) and depth > 0:
+ ch = stmt[i]
+ if ch == "'":
+ i = _skip_string_literal(stmt, i)
+ continue
+ if ch == "(":
+ depth += 1
+ elif ch == ")":
+ depth -= 1
+ i += 1
+ return i
+
+
+def _extract_main_query_after_cte(stmt: str) -> str | None:
+ """Extract the main (outer) query that follows all CTE definitions.
+
+ For ``WITH cte AS (SELECT 1) DELETE FROM users``, returns ``DELETE FROM users``.
+ Returns None if no main query is found after the last CTE body.
+ Handles parentheses inside string literals (e.g., ``SELECT '(' FROM t``).
+ """
+ last_cte_end = 0
+ for m in _iter_as_paren_matches(stmt):
+ last_cte_end = _find_matching_close_paren(stmt, m.end())
+
+ if last_cte_end > 0:
+ remainder = stmt[last_cte_end:].strip().lstrip(",").strip()
+ if remainder:
+ return remainder
+ return None
+
+
+def _resolve_explain_command(stmt: str) -> str | None:
+ """Resolve the underlying command from an EXPLAIN [ANALYZE] [VERBOSE] statement.
+
+ Returns the real command (e.g., 'DELETE') if ANALYZE is present, else None.
+ Handles both space-separated and parenthesized syntax.
+ """
+ rest = stmt.strip()[len("EXPLAIN") :].strip()
+ if not rest:
+ return None
+
+ analyze_found = False
+ explain_opts = {"ANALYZE", "ANALYSE", "VERBOSE"}
+
+ if rest.startswith("("):
+ close = rest.find(")")
+ if close != -1:
+ options_str = rest[1:close].upper()
+ analyze_found = any(
+ opt.strip() in ("ANALYZE", "ANALYSE") for opt in options_str.split(",")
+ )
+ rest = rest[close + 1 :].strip()
+ else:
+ while rest:
+ first_opt = rest.split()[0].upper().rstrip(";") if rest.split() else ""
+ if first_opt in ("ANALYZE", "ANALYSE"):
+ analyze_found = True
+ if first_opt not in explain_opts:
+ break
+ rest = rest[len(first_opt) :].strip()
+
+ if analyze_found and rest:
+ return rest.split()[0].upper().rstrip(";")
+ return None
+
class NL2SQLToolInput(BaseModel):
sql_query: str = Field(
@@ -21,20 +211,70 @@ class NL2SQLToolInput(BaseModel):
class NL2SQLTool(BaseTool):
+ """Tool that converts natural language to SQL and executes it against a database.
+
+ By default the tool operates in **read-only mode**: only SELECT, SHOW,
+ DESCRIBE, EXPLAIN, and read-only CTEs (WITH … SELECT) are permitted. Write
+ operations (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, …) are
+ blocked unless ``allow_dml=True`` is set explicitly or the environment
+ variable ``CREWAI_NL2SQL_ALLOW_DML=true`` is present.
+
+ Writable CTEs (``WITH d AS (DELETE …) SELECT …``) and
+ ``EXPLAIN ANALYZE `` are treated as write operations and are
+ blocked in read-only mode.
+
+ The ``_fetch_all_available_columns`` helper uses parameterised queries so
+ that table names coming from the database catalogue cannot be used as an
+ injection vector.
+ """
+
name: str = "NL2SQLTool"
- description: str = "Converts natural language to SQL queries and executes them."
+ description: str = (
+ "Converts natural language to SQL queries and executes them against a "
+ "database. Read-only by default — only SELECT/SHOW/DESCRIBE/EXPLAIN "
+ "queries (and read-only CTEs) are allowed unless configured with "
+ "allow_dml=True."
+ )
db_uri: str = Field(
title="Database URI",
description="The URI of the database to connect to.",
)
+ allow_dml: bool = Field(
+ default=False,
+ title="Allow DML",
+ description=(
+ "When False (default) only read statements are permitted. "
+ "Set to True to allow INSERT/UPDATE/DELETE/DROP and other "
+ "write operations."
+ ),
+ )
tables: list[dict[str, Any]] = Field(default_factory=list)
columns: dict[str, list[dict[str, Any]] | str] = Field(default_factory=dict)
args_schema: type[BaseModel] = NL2SQLToolInput
+ @model_validator(mode="after")
+ def _apply_env_override(self) -> Self:
+ """Allow CREWAI_NL2SQL_ALLOW_DML=true to override allow_dml at runtime."""
+ if os.environ.get("CREWAI_NL2SQL_ALLOW_DML", "").strip().lower() == "true":
+ if not self.allow_dml:
+ logger.warning(
+ "NL2SQLTool: CREWAI_NL2SQL_ALLOW_DML env var is set — "
+ "DML/DDL operations are enabled. Ensure this is intentional."
+ )
+ self.allow_dml = True
+ return self
+
def model_post_init(self, __context: Any) -> None:
if not SQLALCHEMY_AVAILABLE:
raise ImportError(
- "sqlalchemy is not installed. Please install it with `pip install crewai-tools[sqlalchemy]`"
+ "sqlalchemy is not installed. Please install it with "
+ "`pip install crewai-tools[sqlalchemy]`"
+ )
+
+ if self.allow_dml:
+ logger.warning(
+ "NL2SQLTool: allow_dml=True — write operations (INSERT/UPDATE/"
+ "DELETE/DROP/…) are permitted. Use with caution."
)
data: dict[str, list[dict[str, Any]] | str] = {}
@@ -50,42 +290,216 @@ class NL2SQLTool(BaseTool):
self.tables = tables
self.columns = data
+ # ------------------------------------------------------------------
+ # Query validation
+ # ------------------------------------------------------------------
+
+ def _validate_query(self, sql_query: str) -> None:
+ """Raise ValueError if *sql_query* is not permitted under the current config.
+
+ Splits the query on semicolons and validates each statement
+ independently. When ``allow_dml=False`` (the default), multi-statement
+ queries are rejected outright to prevent ``SELECT 1; DROP TABLE users``
+ style bypasses. When ``allow_dml=True`` every statement is checked and
+ a warning is emitted for write operations.
+ """
+ statements = [s.strip() for s in sql_query.split(";") if s.strip()]
+
+ if not statements:
+ raise ValueError("NL2SQLTool received an empty SQL query.")
+
+ if not self.allow_dml and len(statements) > 1:
+ raise ValueError(
+ "NL2SQLTool blocked a multi-statement query in read-only mode. "
+ "Semicolons are not permitted when allow_dml=False."
+ )
+
+ for stmt in statements:
+ self._validate_statement(stmt)
+
+ def _validate_statement(self, stmt: str) -> None:
+ """Validate a single SQL statement (no semicolons)."""
+ command = self._extract_command(stmt)
+
+ # EXPLAIN ANALYZE / EXPLAIN ANALYSE actually *executes* the underlying
+ # query. Resolve the real command so write operations are caught.
+ # Handles both space-separated ("EXPLAIN ANALYZE DELETE …") and
+ # parenthesized ("EXPLAIN (ANALYZE) DELETE …", "EXPLAIN (ANALYZE, VERBOSE) DELETE …").
+ # EXPLAIN ANALYZE actually executes the underlying query — resolve the
+ # real command so write operations are caught.
+ if command == "EXPLAIN":
+ resolved = _resolve_explain_command(stmt)
+ if resolved:
+ command = resolved
+
+ # WITH starts a CTE. Read-only CTEs are fine; writable CTEs
+ # (e.g. WITH d AS (DELETE …) SELECT …) must be blocked in read-only mode.
+ if command == "WITH":
+ # Check for write commands inside CTE bodies.
+ write_found = _detect_writable_cte(stmt)
+ if write_found:
+ found = write_found
+ if not self.allow_dml:
+ raise ValueError(
+ f"NL2SQLTool is configured in read-only mode and blocked a "
+ f"writable CTE containing a '{found}' statement. To allow "
+ f"write operations set allow_dml=True or "
+ f"CREWAI_NL2SQL_ALLOW_DML=true."
+ )
+ logger.warning(
+ "NL2SQLTool: executing writable CTE with '%s' because allow_dml=True.",
+ found,
+ )
+ return
+
+ # Check the main query after the CTE definitions.
+ main_query = _extract_main_query_after_cte(stmt)
+ if main_query:
+ main_cmd = main_query.split()[0].upper().rstrip(";")
+ if main_cmd in _WRITE_COMMANDS:
+ if not self.allow_dml:
+ raise ValueError(
+ f"NL2SQLTool is configured in read-only mode and blocked a "
+ f"'{main_cmd}' statement after a CTE. To allow write "
+ f"operations set allow_dml=True or "
+ f"CREWAI_NL2SQL_ALLOW_DML=true."
+ )
+ logger.warning(
+ "NL2SQLTool: executing '%s' after CTE because allow_dml=True.",
+ main_cmd,
+ )
+ elif main_cmd not in _READ_ONLY_COMMANDS:
+ if not self.allow_dml:
+ raise ValueError(
+ f"NL2SQLTool blocked an unrecognised SQL command '{main_cmd}' "
+ f"after a CTE. Only {sorted(_READ_ONLY_COMMANDS)} are allowed "
+ f"in read-only mode."
+ )
+ return
+
+ if command in _WRITE_COMMANDS:
+ if not self.allow_dml:
+ raise ValueError(
+ f"NL2SQLTool is configured in read-only mode and blocked a "
+ f"'{command}' statement. To allow write operations set "
+ f"allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true."
+ )
+ logger.warning(
+ "NL2SQLTool: executing write statement '%s' because allow_dml=True.",
+ command,
+ )
+ elif command not in _READ_ONLY_COMMANDS:
+ # Unknown command — block by default unless DML is explicitly enabled
+ if not self.allow_dml:
+ raise ValueError(
+ f"NL2SQLTool blocked an unrecognised SQL command '{command}'. "
+ f"Only {sorted(_READ_ONLY_COMMANDS)} are allowed in read-only "
+ f"mode."
+ )
+
+ @staticmethod
+ def _extract_command(sql_query: str) -> str:
+ """Return the uppercased first keyword of *sql_query*."""
+ stripped = sql_query.strip().lstrip("(")
+ first_token = stripped.split()[0] if stripped.split() else ""
+ return first_token.upper().rstrip(";")
+
+ # ------------------------------------------------------------------
+ # Schema introspection helpers
+ # ------------------------------------------------------------------
+
def _fetch_available_tables(self) -> list[dict[str, Any]] | str:
return self.execute_sql(
- "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';"
+ "SELECT table_name FROM information_schema.tables "
+ "WHERE table_schema = 'public';"
)
def _fetch_all_available_columns(
self, table_name: str
) -> list[dict[str, Any]] | str:
+ """Fetch columns for *table_name* using a parameterised query.
+
+ The table name is bound via SQLAlchemy's ``:param`` syntax to prevent
+ SQL injection from catalogue values.
+ """
return self.execute_sql(
- f"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table_name}';" # noqa: S608
+ "SELECT column_name, data_type FROM information_schema.columns "
+ "WHERE table_name = :table_name",
+ params={"table_name": table_name},
)
+ # ------------------------------------------------------------------
+ # Core execution
+ # ------------------------------------------------------------------
+
def _run(self, sql_query: str) -> list[dict[str, Any]] | str:
try:
+ self._validate_query(sql_query)
data = self.execute_sql(sql_query)
+ except ValueError:
+ raise
except Exception as exc:
data = (
f"Based on these tables {self.tables} and columns {self.columns}, "
- "you can create SQL queries to retrieve data from the database."
- f"Get the original request {sql_query} and the error {exc} and create the correct SQL query."
+ "you can create SQL queries to retrieve data from the database. "
+ f"Get the original request {sql_query} and the error {exc} and "
+ "create the correct SQL query."
)
return data
- def execute_sql(self, sql_query: str) -> list[dict[str, Any]] | str:
+ def execute_sql(
+ self,
+ sql_query: str,
+ params: dict[str, Any] | None = None,
+ ) -> list[dict[str, Any]] | str:
+ """Execute *sql_query* and return the results as a list of dicts.
+
+ Parameters
+ ----------
+ sql_query:
+ The SQL statement to run.
+ params:
+ Optional mapping of bind parameters (e.g. ``{"table_name": "users"}``).
+ """
if not SQLALCHEMY_AVAILABLE:
raise ImportError(
- "sqlalchemy is not installed. Please install it with `pip install crewai-tools[sqlalchemy]`"
+ "sqlalchemy is not installed. Please install it with "
+ "`pip install crewai-tools[sqlalchemy]`"
)
+ # Check ALL statements so that e.g. "SELECT 1; DROP TABLE t" triggers a
+ # commit when allow_dml=True, regardless of statement order.
+ _stmts = [s.strip() for s in sql_query.split(";") if s.strip()]
+
+ def _is_write_stmt(s: str) -> bool:
+ cmd = self._extract_command(s)
+ if cmd in _WRITE_COMMANDS:
+ return True
+ if cmd == "EXPLAIN":
+ # Resolve the underlying command for EXPLAIN ANALYZE
+ resolved = _resolve_explain_command(s)
+ if resolved and resolved in _WRITE_COMMANDS:
+ return True
+ if cmd == "WITH":
+ if _detect_writable_cte(s):
+ return True
+ main_q = _extract_main_query_after_cte(s)
+ if main_q:
+ return main_q.split()[0].upper().rstrip(";") in _WRITE_COMMANDS
+ return False
+
+ is_write = any(_is_write_stmt(s) for s in _stmts)
+
engine = create_engine(self.db_uri)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
try:
- result = session.execute(text(sql_query))
- session.commit()
+ result = session.execute(text(sql_query), params or {})
+
+ # Only commit when the operation actually mutates state
+ if self.allow_dml and is_write:
+ session.commit()
if result.returns_rows: # type: ignore[attr-defined]
columns = result.keys()
diff --git a/lib/crewai-tools/tests/test_generate_tool_specs.py b/lib/crewai-tools/tests/test_generate_tool_specs.py
index 7506c4ee4..0841eeda6 100644
--- a/lib/crewai-tools/tests/test_generate_tool_specs.py
+++ b/lib/crewai-tools/tests/test_generate_tool_specs.py
@@ -45,6 +45,26 @@ class MockTool(BaseTool):
)
+# --- Intermediate base class (like RagTool, BraveSearchToolBase) ---
+class MockIntermediateBase(BaseTool):
+ """Simulates an intermediate tool base class (e.g. RagTool, BraveSearchToolBase)."""
+
+ name: str = "Intermediate Base"
+ description: str = "An intermediate tool base"
+ shared_config: str = Field("default_config", description="Config from intermediate base")
+
+ def _run(self, query: str) -> str:
+ return query
+
+
+class MockDerivedTool(MockIntermediateBase):
+ """A tool inheriting from an intermediate base, like CodeDocsSearchTool(RagTool)."""
+
+ name: str = "Derived Tool"
+ description: str = "A tool that inherits from intermediate base"
+ derived_param: str = Field("derived_default", description="Param specific to derived tool")
+
+
@pytest.fixture
def extractor():
ext = ToolSpecExtractor()
@@ -169,6 +189,87 @@ def test_extract_package_dependencies(mock_tool_extractor):
]
+def test_base_tool_fields_excluded_from_init_params(mock_tool_extractor):
+ """BaseTool internal fields (including computed_field like tool_type) must
+ never appear in init_params_schema. Studio reads this schema to render
+ the tool config UI — internal fields confuse users."""
+ init_schema = mock_tool_extractor["init_params_schema"]
+ props = set(init_schema.get("properties", {}).keys())
+ required = set(init_schema.get("required", []))
+
+ # These are all BaseTool's own fields — none should leak
+ base_fields = {"name", "description", "env_vars", "args_schema",
+ "description_updated", "cache_function", "result_as_answer",
+ "max_usage_count", "current_usage_count", "tool_type",
+ "package_dependencies"}
+
+ leaked_props = base_fields & props
+ assert not leaked_props, (
+ f"BaseTool fields leaked into init_params_schema properties: {leaked_props}"
+ )
+ leaked_required = base_fields & required
+ assert not leaked_required, (
+ f"BaseTool fields leaked into init_params_schema required: {leaked_required}"
+ )
+
+
+def test_intermediate_base_fields_preserved_for_derived_tool(extractor):
+ """When a tool inherits from an intermediate base (e.g. RagTool),
+ the intermediate's fields should be included — only BaseTool's own
+ fields are excluded."""
+ with (
+ mock.patch(
+ "crewai_tools.generate_tool_specs.dir",
+ return_value=["MockDerivedTool"],
+ ),
+ mock.patch(
+ "crewai_tools.generate_tool_specs.getattr",
+ return_value=MockDerivedTool,
+ ),
+ ):
+ extractor.extract_all_tools()
+ assert len(extractor.tools_spec) == 1
+ tool_info = extractor.tools_spec[0]
+
+ props = set(tool_info["init_params_schema"].get("properties", {}).keys())
+
+ # Intermediate base's field should be preserved
+ assert "shared_config" in props, (
+ "Intermediate base class fields should be preserved in init_params_schema"
+ )
+ # Derived tool's own field should be preserved
+ assert "derived_param" in props, (
+ "Derived tool's own fields should be preserved in init_params_schema"
+ )
+ # BaseTool internals should still be excluded
+ assert "tool_type" not in props
+ assert "cache_function" not in props
+ assert "result_as_answer" not in props
+
+
+def test_future_base_tool_field_auto_excluded(extractor):
+ """If a new field is added to BaseTool in the future, it should be
+ automatically excluded from spec generation without needing to update
+ the ignored list. This test verifies the allowlist approach works
+ by checking that ONLY non-BaseTool fields appear."""
+ with (
+ mock.patch("crewai_tools.generate_tool_specs.dir", return_value=["MockTool"]),
+ mock.patch("crewai_tools.generate_tool_specs.getattr", return_value=MockTool),
+ ):
+ extractor.extract_all_tools()
+ tool_info = extractor.tools_spec[0]
+
+ props = set(tool_info["init_params_schema"].get("properties", {}).keys())
+ base_all = set(BaseTool.model_fields) | set(BaseTool.model_computed_fields)
+
+ leaked = base_all & props
+ assert not leaked, (
+ f"BaseTool fields should be auto-excluded but found: {leaked}. "
+ "The spec generator should dynamically compute BaseTool's fields "
+ "instead of using a hardcoded denylist."
+ )
+
+
def test_save_to_json(extractor, tmp_path):
extractor.tools_spec = [
{
diff --git a/lib/crewai-tools/tests/tools/test_nl2sql_security.py b/lib/crewai-tools/tests/tools/test_nl2sql_security.py
new file mode 100644
index 000000000..abef973ff
--- /dev/null
+++ b/lib/crewai-tools/tests/tools/test_nl2sql_security.py
@@ -0,0 +1,671 @@
+"""Security tests for NL2SQLTool.
+
+Uses an in-memory SQLite database so no external service is needed.
+SQLite does not have information_schema, so we patch the schema-introspection
+helpers to avoid bootstrap failures and focus purely on the security logic.
+"""
+import os
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+# Skip the entire module if SQLAlchemy is not installed
+pytest.importorskip("sqlalchemy")
+
+from sqlalchemy import create_engine, text # noqa: E402
+
+from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool # noqa: E402
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+SQLITE_URI = "sqlite://" # in-memory
+
+
+def _make_tool(allow_dml: bool = False, **kwargs) -> NL2SQLTool:
+ """Return a NL2SQLTool wired to an in-memory SQLite DB.
+
+ Schema-introspection is patched out so we can create the tool without a
+ real PostgreSQL information_schema.
+ """
+ with (
+ patch.object(NL2SQLTool, "_fetch_available_tables", return_value=[]),
+ patch.object(NL2SQLTool, "_fetch_all_available_columns", return_value=[]),
+ ):
+ return NL2SQLTool(db_uri=SQLITE_URI, allow_dml=allow_dml, **kwargs)
+
+
+# ---------------------------------------------------------------------------
+# Read-only enforcement (allow_dml=False)
+# ---------------------------------------------------------------------------
+
+
+class TestReadOnlyMode:
+ def test_select_allowed_by_default(self):
+ tool = _make_tool()
+ # SQLite supports SELECT without information_schema
+ result = tool.execute_sql("SELECT 1 AS val")
+ assert result == [{"val": 1}]
+
+ @pytest.mark.parametrize(
+ "stmt",
+ [
+ "INSERT INTO t VALUES (1)",
+ "UPDATE t SET col = 1",
+ "DELETE FROM t",
+ "DROP TABLE t",
+ "ALTER TABLE t ADD col TEXT",
+ "CREATE TABLE t (id INTEGER)",
+ "TRUNCATE TABLE t",
+ "GRANT SELECT ON t TO user1",
+ "REVOKE SELECT ON t FROM user1",
+ "EXEC sp_something",
+ "EXECUTE sp_something",
+ "CALL proc()",
+ ],
+ )
+ def test_write_statements_blocked_by_default(self, stmt: str):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(stmt)
+
+ def test_explain_allowed(self):
+ tool = _make_tool()
+ # Should not raise
+ tool._validate_query("EXPLAIN SELECT 1")
+
+ def test_read_only_cte_allowed(self):
+ tool = _make_tool()
+ tool._validate_query("WITH cte AS (SELECT 1) SELECT * FROM cte")
+
+ def test_show_allowed(self):
+ tool = _make_tool()
+ tool._validate_query("SHOW TABLES")
+
+ def test_describe_allowed(self):
+ tool = _make_tool()
+ tool._validate_query("DESCRIBE users")
+
+
+# ---------------------------------------------------------------------------
+# DML enabled (allow_dml=True)
+# ---------------------------------------------------------------------------
+
+
+class TestDMLEnabled:
+ def test_insert_allowed_when_dml_enabled(self):
+ tool = _make_tool(allow_dml=True)
+ # Should not raise
+ tool._validate_query("INSERT INTO t VALUES (1)")
+
+ def test_delete_allowed_when_dml_enabled(self):
+ tool = _make_tool(allow_dml=True)
+ tool._validate_query("DELETE FROM t WHERE id = 1")
+
+ def test_drop_allowed_when_dml_enabled(self):
+ tool = _make_tool(allow_dml=True)
+ tool._validate_query("DROP TABLE t")
+
+ def test_dml_actually_persists(self):
+ """End-to-end: INSERT commits when allow_dml=True."""
+ # Use a file-based SQLite so we can verify persistence across sessions
+ import tempfile, os
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
+ db_path = f.name
+ uri = f"sqlite:///{db_path}"
+ try:
+ tool = _make_tool(allow_dml=True)
+ tool.db_uri = uri
+
+ engine = create_engine(uri)
+ with engine.connect() as conn:
+ conn.execute(text("CREATE TABLE items (id INTEGER PRIMARY KEY)"))
+ conn.commit()
+
+ tool.execute_sql("INSERT INTO items VALUES (42)")
+
+ with engine.connect() as conn:
+ rows = conn.execute(text("SELECT id FROM items")).fetchall()
+ assert (42,) in rows
+ finally:
+ os.unlink(db_path)
+
+
+# ---------------------------------------------------------------------------
+# Parameterised query — SQL injection prevention
+# ---------------------------------------------------------------------------
+
+
+class TestParameterisedQueries:
+ def test_table_name_is_parameterised(self):
+ """_fetch_all_available_columns must not interpolate table_name into SQL."""
+ tool = _make_tool()
+ captured_calls = []
+
+ def recording_execute_sql(self_inner, sql_query, params=None):
+ captured_calls.append((sql_query, params))
+ return []
+
+ with patch.object(NL2SQLTool, "execute_sql", recording_execute_sql):
+ tool._fetch_all_available_columns("users'; DROP TABLE users; --")
+
+ assert len(captured_calls) == 1
+ sql, params = captured_calls[0]
+ # The raw SQL must NOT contain the injected string
+ assert "DROP" not in sql
+ # The table name must be passed as a parameter
+ assert params is not None
+ assert params.get("table_name") == "users'; DROP TABLE users; --"
+ # The SQL template must use the :param syntax
+ assert ":table_name" in sql
+
+ def test_injection_string_not_in_sql_template(self):
+ """The f-string vulnerability is gone — table name never lands in the SQL."""
+ tool = _make_tool()
+ injection = "'; DROP TABLE users; --"
+ captured = {}
+
+ def spy(self_inner, sql_query, params=None):
+ captured["sql"] = sql_query
+ captured["params"] = params
+ return []
+
+ with patch.object(NL2SQLTool, "execute_sql", spy):
+ tool._fetch_all_available_columns(injection)
+
+ assert injection not in captured["sql"]
+ assert captured["params"]["table_name"] == injection
+
+
+# ---------------------------------------------------------------------------
+# session.commit() not called for read-only queries
+# ---------------------------------------------------------------------------
+
+
+class TestNoCommitForReadOnly:
+ def test_select_does_not_commit(self):
+ tool = _make_tool(allow_dml=False)
+
+ mock_session = MagicMock()
+ mock_result = MagicMock()
+ mock_result.returns_rows = True
+ mock_result.keys.return_value = ["val"]
+ mock_result.fetchall.return_value = [(1,)]
+ mock_session.execute.return_value = mock_result
+
+ mock_session_cls = MagicMock(return_value=mock_session)
+
+ with (
+ patch("crewai_tools.tools.nl2sql.nl2sql_tool.create_engine"),
+ patch(
+ "crewai_tools.tools.nl2sql.nl2sql_tool.sessionmaker",
+ return_value=mock_session_cls,
+ ),
+ ):
+ tool.execute_sql("SELECT 1")
+
+ mock_session.commit.assert_not_called()
+
+ def test_write_with_dml_enabled_does_commit(self):
+ tool = _make_tool(allow_dml=True)
+
+ mock_session = MagicMock()
+ mock_result = MagicMock()
+ mock_result.returns_rows = False
+ mock_session.execute.return_value = mock_result
+
+ mock_session_cls = MagicMock(return_value=mock_session)
+
+ with (
+ patch("crewai_tools.tools.nl2sql.nl2sql_tool.create_engine"),
+ patch(
+ "crewai_tools.tools.nl2sql.nl2sql_tool.sessionmaker",
+ return_value=mock_session_cls,
+ ),
+ ):
+ tool.execute_sql("INSERT INTO t VALUES (1)")
+
+ mock_session.commit.assert_called_once()
+
+
+# ---------------------------------------------------------------------------
+# Environment-variable escape hatch
+# ---------------------------------------------------------------------------
+
+
+class TestEnvVarEscapeHatch:
+ def test_env_var_enables_dml(self):
+ with patch.dict(os.environ, {"CREWAI_NL2SQL_ALLOW_DML": "true"}):
+ tool = _make_tool(allow_dml=False)
+ assert tool.allow_dml is True
+
+ def test_env_var_case_insensitive(self):
+ with patch.dict(os.environ, {"CREWAI_NL2SQL_ALLOW_DML": "TRUE"}):
+ tool = _make_tool(allow_dml=False)
+ assert tool.allow_dml is True
+
+ def test_env_var_absent_keeps_default(self):
+ env = {k: v for k, v in os.environ.items() if k != "CREWAI_NL2SQL_ALLOW_DML"}
+ with patch.dict(os.environ, env, clear=True):
+ tool = _make_tool(allow_dml=False)
+ assert tool.allow_dml is False
+
+ def test_env_var_false_does_not_enable_dml(self):
+ with patch.dict(os.environ, {"CREWAI_NL2SQL_ALLOW_DML": "false"}):
+ tool = _make_tool(allow_dml=False)
+ assert tool.allow_dml is False
+
+ def test_dml_write_blocked_without_env_var(self):
+ env = {k: v for k, v in os.environ.items() if k != "CREWAI_NL2SQL_ALLOW_DML"}
+ with patch.dict(os.environ, env, clear=True):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("DROP TABLE sensitive_data")
+
+
+# ---------------------------------------------------------------------------
+# _run() propagates ValueError from _validate_query
+# ---------------------------------------------------------------------------
+
+
+class TestRunValidation:
+ def test_run_raises_on_blocked_query(self):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._run("DELETE FROM users")
+
+ def test_run_returns_results_for_select(self):
+ tool = _make_tool(allow_dml=False)
+ result = tool._run("SELECT 1 AS n")
+ assert result == [{"n": 1}]
+
+
+# ---------------------------------------------------------------------------
+# Multi-statement / semicolon injection prevention
+# ---------------------------------------------------------------------------
+
+
+class TestSemicolonInjection:
+ def test_multi_statement_blocked_in_read_only_mode(self):
+ """SELECT 1; DROP TABLE users must be rejected when allow_dml=False."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="multi-statement"):
+ tool._validate_query("SELECT 1; DROP TABLE users")
+
+ def test_multi_statement_blocked_even_with_only_selects(self):
+ """Two SELECT statements are still rejected in read-only mode."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="multi-statement"):
+ tool._validate_query("SELECT 1; SELECT 2")
+
+ def test_trailing_semicolon_allowed_single_statement(self):
+ """A single statement with a trailing semicolon should pass."""
+ tool = _make_tool(allow_dml=False)
+ # Should not raise — the part after the semicolon is empty
+ tool._validate_query("SELECT 1;")
+
+ def test_multi_statement_allowed_when_dml_enabled(self):
+ """Multiple statements are permitted when allow_dml=True."""
+ tool = _make_tool(allow_dml=True)
+ # Should not raise
+ tool._validate_query("SELECT 1; INSERT INTO t VALUES (1)")
+
+ def test_multi_statement_write_still_blocked_individually(self):
+ """Even with allow_dml=False, a single write statement is blocked."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("DROP TABLE users")
+
+
+# ---------------------------------------------------------------------------
+# Writable CTEs (WITH … DELETE/INSERT/UPDATE)
+# ---------------------------------------------------------------------------
+
+
+class TestWritableCTE:
+ def test_writable_cte_delete_blocked_in_read_only(self):
+ """WITH d AS (DELETE FROM users RETURNING *) SELECT * FROM d — blocked."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH deleted AS (DELETE FROM users RETURNING *) SELECT * FROM deleted"
+ )
+
+ def test_writable_cte_insert_blocked_in_read_only(self):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH ins AS (INSERT INTO t VALUES (1) RETURNING id) SELECT * FROM ins"
+ )
+
+ def test_writable_cte_update_blocked_in_read_only(self):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH upd AS (UPDATE t SET x=1 RETURNING id) SELECT * FROM upd"
+ )
+
+ def test_writable_cte_allowed_when_dml_enabled(self):
+ tool = _make_tool(allow_dml=True)
+ # Should not raise
+ tool._validate_query(
+ "WITH deleted AS (DELETE FROM users RETURNING *) SELECT * FROM deleted"
+ )
+
+ def test_plain_read_only_cte_still_allowed(self):
+ tool = _make_tool(allow_dml=False)
+ # No write commands in the CTE body — must pass
+ tool._validate_query("WITH cte AS (SELECT id FROM users) SELECT * FROM cte")
+
+ def test_cte_with_comment_column_not_false_positive(self):
+ """Column named 'comment' should NOT trigger writable CTE detection."""
+ tool = _make_tool(allow_dml=False)
+ # 'comment' is a column name, not a SQL command
+ tool._validate_query(
+ "WITH cte AS (SELECT comment FROM posts) SELECT * FROM cte"
+ )
+
+ def test_cte_with_set_column_not_false_positive(self):
+ """Column named 'set' should NOT trigger writable CTE detection."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query(
+ "WITH cte AS (SELECT set, reset FROM config) SELECT * FROM cte"
+ )
+
+
+# ---------------------------------------------------------------------------
+# EXPLAIN ANALYZE executes the underlying query
+# ---------------------------------------------------------------------------
+
+
+ def test_cte_with_write_main_query_blocked(self):
+ """WITH cte AS (SELECT 1) DELETE FROM users — main query must be caught."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH cte AS (SELECT 1) DELETE FROM users"
+ )
+
+ def test_cte_with_write_main_query_allowed_with_dml(self):
+ """Main query write after CTE should pass when allow_dml=True."""
+ tool = _make_tool(allow_dml=True)
+ tool._validate_query(
+ "WITH cte AS (SELECT id FROM users) INSERT INTO archive SELECT * FROM cte"
+ )
+
+ def test_cte_with_newline_before_paren_blocked(self):
+ """AS followed by newline then ( should still detect writable CTE."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH cte AS\n(DELETE FROM users RETURNING *) SELECT * FROM cte"
+ )
+
+ def test_cte_with_tab_before_paren_blocked(self):
+ """AS followed by tab then ( should still detect writable CTE."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH cte AS\t(DELETE FROM users RETURNING *) SELECT * FROM cte"
+ )
+
+
+class TestExplainAnalyze:
+ def test_explain_analyze_delete_blocked_in_read_only(self):
+ """EXPLAIN ANALYZE DELETE actually runs the delete — block it."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("EXPLAIN ANALYZE DELETE FROM users")
+
+ def test_explain_analyse_delete_blocked_in_read_only(self):
+ """British spelling ANALYSE is also caught."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("EXPLAIN ANALYSE DELETE FROM users")
+
+ def test_explain_analyze_drop_blocked_in_read_only(self):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("EXPLAIN ANALYZE DROP TABLE users")
+
+ def test_explain_analyze_select_allowed_in_read_only(self):
+ """EXPLAIN ANALYZE on a SELECT is safe — must be permitted."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query("EXPLAIN ANALYZE SELECT * FROM users")
+
+ def test_explain_without_analyze_allowed(self):
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query("EXPLAIN SELECT * FROM users")
+
+ def test_explain_analyze_delete_allowed_when_dml_enabled(self):
+ tool = _make_tool(allow_dml=True)
+ tool._validate_query("EXPLAIN ANALYZE DELETE FROM users")
+
+ def test_explain_paren_analyze_delete_blocked_in_read_only(self):
+ """EXPLAIN (ANALYZE) DELETE actually runs the delete — block it."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("EXPLAIN (ANALYZE) DELETE FROM users")
+
+ def test_explain_paren_analyze_verbose_delete_blocked_in_read_only(self):
+ """EXPLAIN (ANALYZE, VERBOSE) DELETE actually runs the delete — block it."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("EXPLAIN (ANALYZE, VERBOSE) DELETE FROM users")
+
+ def test_explain_paren_verbose_select_allowed_in_read_only(self):
+ """EXPLAIN (VERBOSE) SELECT is safe — no ANALYZE means no execution."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query("EXPLAIN (VERBOSE) SELECT * FROM users")
+
+
+# ---------------------------------------------------------------------------
+# Multi-statement commit covers ALL statements (not just the first)
+# ---------------------------------------------------------------------------
+
+
+class TestMultiStatementCommit:
+ def test_select_then_insert_triggers_commit(self):
+ """SELECT 1; INSERT … — commit must happen because INSERT is a write."""
+ tool = _make_tool(allow_dml=True)
+
+ mock_session = MagicMock()
+ mock_result = MagicMock()
+ mock_result.returns_rows = False
+ mock_session.execute.return_value = mock_result
+ mock_session_cls = MagicMock(return_value=mock_session)
+
+ with (
+ patch("crewai_tools.tools.nl2sql.nl2sql_tool.create_engine"),
+ patch(
+ "crewai_tools.tools.nl2sql.nl2sql_tool.sessionmaker",
+ return_value=mock_session_cls,
+ ),
+ ):
+ tool.execute_sql("SELECT 1; INSERT INTO t VALUES (1)")
+
+ mock_session.commit.assert_called_once()
+
+ def test_select_only_multi_statement_does_not_commit(self):
+ """Two SELECTs must not trigger a commit even when allow_dml=True."""
+ tool = _make_tool(allow_dml=True)
+
+ mock_session = MagicMock()
+ mock_result = MagicMock()
+ mock_result.returns_rows = True
+ mock_result.keys.return_value = ["v"]
+ mock_result.fetchall.return_value = [(1,)]
+ mock_session.execute.return_value = mock_result
+ mock_session_cls = MagicMock(return_value=mock_session)
+
+ with (
+ patch("crewai_tools.tools.nl2sql.nl2sql_tool.create_engine"),
+ patch(
+ "crewai_tools.tools.nl2sql.nl2sql_tool.sessionmaker",
+ return_value=mock_session_cls,
+ ),
+ ):
+ tool.execute_sql("SELECT 1; SELECT 2")
+
+ def test_writable_cte_triggers_commit(self):
+ """WITH d AS (DELETE ...) must trigger commit when allow_dml=True."""
+ tool = _make_tool(allow_dml=True)
+
+ mock_session = MagicMock()
+ mock_result = MagicMock()
+ mock_result.returns_rows = True
+ mock_result.keys.return_value = ["id"]
+ mock_result.fetchall.return_value = [(1,)]
+ mock_session.execute.return_value = mock_result
+ mock_session_cls = MagicMock(return_value=mock_session)
+
+ with (
+ patch("crewai_tools.tools.nl2sql.nl2sql_tool.create_engine"),
+ patch(
+ "crewai_tools.tools.nl2sql.nl2sql_tool.sessionmaker",
+ return_value=mock_session_cls,
+ ),
+ ):
+ tool.execute_sql(
+ "WITH d AS (DELETE FROM users RETURNING *) SELECT * FROM d"
+ )
+ mock_session.commit.assert_called_once()
+
+
+# ---------------------------------------------------------------------------
+# Extended _WRITE_COMMANDS coverage
+# ---------------------------------------------------------------------------
+
+
+class TestExtendedWriteCommands:
+ @pytest.mark.parametrize(
+ "stmt",
+ [
+ "UPSERT INTO t VALUES (1)",
+ "LOAD DATA INFILE 'f.csv' INTO TABLE t",
+ "COPY t FROM '/tmp/f.csv'",
+ "VACUUM ANALYZE t",
+ "ANALYZE t",
+ "ANALYSE t",
+ "REINDEX TABLE t",
+ "CLUSTER t USING idx",
+ "REFRESH MATERIALIZED VIEW v",
+ "COMMENT ON TABLE t IS 'desc'",
+ "SET search_path = myschema",
+ "RESET search_path",
+ ],
+ )
+ def test_extended_write_commands_blocked_by_default(self, stmt: str):
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(stmt)
+
+
+# ---------------------------------------------------------------------------
+# EXPLAIN ANALYZE VERBOSE handling
+# ---------------------------------------------------------------------------
+
+
+class TestExplainAnalyzeVerbose:
+ def test_explain_analyze_verbose_select_allowed(self):
+ """EXPLAIN ANALYZE VERBOSE SELECT should be allowed (read-only)."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query("EXPLAIN ANALYZE VERBOSE SELECT * FROM users")
+
+ def test_explain_analyze_verbose_delete_blocked(self):
+ """EXPLAIN ANALYZE VERBOSE DELETE should be blocked."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query("EXPLAIN ANALYZE VERBOSE DELETE FROM users")
+
+ def test_explain_verbose_select_allowed(self):
+ """EXPLAIN VERBOSE SELECT (no ANALYZE) should be allowed."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query("EXPLAIN VERBOSE SELECT * FROM users")
+
+
+# ---------------------------------------------------------------------------
+# CTE with string literal parens
+# ---------------------------------------------------------------------------
+
+
+class TestCTEStringLiteralParens:
+ def test_cte_string_paren_does_not_bypass(self):
+ """Parens inside string literals should not confuse the paren walker."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH cte AS (SELECT '(' FROM t) DELETE FROM users"
+ )
+
+ def test_cte_string_paren_read_only_allowed(self):
+ """Read-only CTE with string literal parens should be allowed."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query(
+ "WITH cte AS (SELECT '(' FROM t) SELECT * FROM cte"
+ )
+
+
+# ---------------------------------------------------------------------------
+# EXPLAIN ANALYZE commit logic
+# ---------------------------------------------------------------------------
+
+
+class TestExplainAnalyzeCommit:
+ def test_explain_analyze_delete_triggers_commit(self):
+ """EXPLAIN ANALYZE DELETE should trigger commit when allow_dml=True."""
+ tool = _make_tool(allow_dml=True)
+
+ mock_session = MagicMock()
+ mock_result = MagicMock()
+ mock_result.returns_rows = True
+ mock_result.keys.return_value = ["QUERY PLAN"]
+ mock_result.fetchall.return_value = [("Delete on users",)]
+ mock_session.execute.return_value = mock_result
+ mock_session_cls = MagicMock(return_value=mock_session)
+
+ with (
+ patch("crewai_tools.tools.nl2sql.nl2sql_tool.create_engine"),
+ patch(
+ "crewai_tools.tools.nl2sql.nl2sql_tool.sessionmaker",
+ return_value=mock_session_cls,
+ ),
+ ):
+ tool.execute_sql("EXPLAIN ANALYZE DELETE FROM users")
+ mock_session.commit.assert_called_once()
+
+
+# ---------------------------------------------------------------------------
+# AS( inside string literals must not confuse CTE detection
+# ---------------------------------------------------------------------------
+
+
+class TestCTEStringLiteralAS:
+ def test_as_paren_inside_string_does_not_bypass(self):
+ """'AS (' inside a string literal must not be treated as a CTE body."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="read-only mode"):
+ tool._validate_query(
+ "WITH cte AS (SELECT 'AS (' FROM t) DELETE FROM users"
+ )
+
+ def test_as_paren_inside_string_read_only_ok(self):
+ """Read-only CTE with 'AS (' in a string should be allowed."""
+ tool = _make_tool(allow_dml=False)
+ tool._validate_query(
+ "WITH cte AS (SELECT 'AS (' FROM t) SELECT * FROM cte"
+ )
+
+
+# ---------------------------------------------------------------------------
+# Unknown command after CTE should be blocked
+# ---------------------------------------------------------------------------
+
+
+class TestCTEUnknownCommand:
+ def test_unknown_command_after_cte_blocked(self):
+ """WITH cte AS (SELECT 1) FOOBAR should be blocked as unknown."""
+ tool = _make_tool(allow_dml=False)
+ with pytest.raises(ValueError, match="unrecognised"):
+ tool._validate_query("WITH cte AS (SELECT 1) FOOBAR")
diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json
index adc392bab..a00501503 100644
--- a/lib/crewai-tools/tool.specs.json
+++ b/lib/crewai-tools/tool.specs.json
@@ -81,16 +81,9 @@
],
"default": null,
"title": "Mind Name"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "AIMindTool",
"type": "object"
},
@@ -168,20 +161,13 @@
"title": "Save Dir",
"type": "string"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"use_title_as_filename": {
"default": false,
"title": "Use Title As Filename",
"type": "boolean"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "ArxivPaperTool",
"type": "object"
},
@@ -297,16 +283,9 @@
"default": "https://api.search.brave.com/res/v1/images/search",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveImageSearchTool",
"type": "object"
},
@@ -488,16 +467,9 @@
"default": "https://api.search.brave.com/res/v1/llm/context",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveLLMContextTool",
"type": "object"
},
@@ -775,16 +747,9 @@
"default": "https://api.search.brave.com/res/v1/local/descriptions",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveLocalPOIsDescriptionTool",
"type": "object"
},
@@ -896,16 +861,9 @@
"default": "https://api.search.brave.com/res/v1/local/pois",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveLocalPOIsTool",
"type": "object"
},
@@ -1062,16 +1020,9 @@
"default": "https://api.search.brave.com/res/v1/news/search",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveNewsSearchTool",
"type": "object"
},
@@ -1344,16 +1295,9 @@
"default": "https://api.search.brave.com/res/v1/web/search",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveSearchTool",
"type": "object"
},
@@ -1729,16 +1673,9 @@
"default": "https://api.search.brave.com/res/v1/videos/search",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveVideoSearchTool",
"type": "object"
},
@@ -1999,16 +1936,9 @@
"default": "https://api.search.brave.com/res/v1/web/search",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BraveWebSearchTool",
"type": "object"
},
@@ -2380,11 +2310,6 @@
"title": "Format",
"type": "string"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"url": {
"anyOf": [
{
@@ -2410,9 +2335,7 @@
"title": "Zipcode"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BrightDataDatasetTool",
"type": "object"
},
@@ -2590,20 +2513,13 @@
"default": null,
"title": "Search Type"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"zone": {
"default": "",
"title": "Zone",
"type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BrightDataSearchTool",
"type": "object"
},
@@ -2774,11 +2690,6 @@
"title": "Format",
"type": "string"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"url": {
"anyOf": [
{
@@ -2797,9 +2708,7 @@
"type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BrightDataWebUnlockerTool",
"type": "object"
},
@@ -2972,16 +2881,9 @@
],
"default": false,
"title": "Text Content"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "BrowserbaseLoadTool",
"type": "object"
},
@@ -4026,16 +3928,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "CSVSearchTool",
"type": "object"
},
@@ -5085,16 +4980,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "CodeDocsSearchTool",
"type": "object"
},
@@ -5172,18 +5060,8 @@
}
},
"description": "Wrapper for composio tools.",
- "properties": {
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- }
- },
- "required": [
- "name",
- "description",
- "tool_type"
- ],
+ "properties": {},
+ "required": [],
"title": "ComposioTool",
"type": "object"
},
@@ -5246,16 +5124,10 @@
"contextual_client": {
"default": null,
"title": "Contextual Client"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "api_key",
- "tool_type"
+ "api_key"
],
"title": "ContextualAICreateAgentTool",
"type": "object"
@@ -5348,16 +5220,10 @@
"api_key": {
"title": "Api Key",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "api_key",
- "tool_type"
+ "api_key"
],
"title": "ContextualAIParseTool",
"type": "object"
@@ -5475,16 +5341,10 @@
"contextual_client": {
"default": null,
"title": "Contextual Client"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "api_key",
- "tool_type"
+ "api_key"
],
"title": "ContextualAIQueryTool",
"type": "object"
@@ -5575,16 +5435,10 @@
"api_key": {
"title": "Api Key",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "api_key",
- "tool_type"
+ "api_key"
],
"title": "ContextualAIRerankTool",
"type": "object"
@@ -5751,11 +5605,6 @@
"description": "Specify whether the index is scoped. Is True by default.",
"title": "Scoped Index",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
@@ -5763,8 +5612,7 @@
"collection_name",
"scope_name",
"bucket_name",
- "index_name",
- "tool_type"
+ "index_name"
],
"title": "CouchbaseFTSVectorSearchTool",
"type": "object"
@@ -6809,16 +6657,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "DOCXSearchTool",
"type": "object"
},
@@ -6954,16 +6795,9 @@
],
"default": "1024x1024",
"title": "Size"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "DallETool",
"type": "object"
},
@@ -7064,16 +6898,9 @@
],
"default": null,
"title": "Default Warehouse Id"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "DatabricksQueryTool",
"type": "object"
},
@@ -7203,16 +7030,9 @@
],
"default": null,
"title": "Directory"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "DirectoryReadTool",
"type": "object"
},
@@ -8256,16 +8076,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "DirectorySearchTool",
"type": "object"
},
@@ -8409,11 +8222,6 @@
"default": false,
"title": "Summary"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"type": {
"anyOf": [
{
@@ -8427,9 +8235,7 @@
"title": "Type"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "EXASearchTool",
"type": "object"
},
@@ -8536,16 +8342,8 @@
"type": "object"
}
},
- "properties": {
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- }
- },
- "required": [
- "tool_type"
- ],
+ "properties": {},
+ "required": [],
"title": "FileCompressorTool",
"type": "object"
},
@@ -8647,16 +8445,9 @@
],
"default": null,
"title": "File Path"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "FileReadTool",
"type": "object"
},
@@ -8746,16 +8537,8 @@
"type": "object"
}
},
- "properties": {
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- }
- },
- "required": [
- "tool_type"
- ],
+ "properties": {},
+ "required": [],
"title": "FileWriterTool",
"type": "object"
},
@@ -8878,16 +8661,9 @@
}
],
"title": "Config"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "FirecrawlCrawlWebsiteTool",
"type": "object"
},
@@ -8977,16 +8753,9 @@
"additionalProperties": true,
"title": "Config",
"type": "object"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "FirecrawlScrapeWebsiteTool",
"type": "object"
},
@@ -9083,16 +8852,9 @@
}
],
"title": "Config"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "FirecrawlSearchTool",
"type": "object"
},
@@ -9187,16 +8949,9 @@
],
"description": "The user's Personal Access Token to access CrewAI AMP API. If not provided, it will be loaded from the environment variable CREWAI_PERSONAL_ACCESS_TOKEN.",
"title": "Personal Access Token"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "GenerateCrewaiAutomationTool",
"type": "object"
},
@@ -10264,16 +10019,10 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "gh_token",
- "tool_type"
+ "gh_token"
],
"title": "GithubSearchTool",
"type": "object"
@@ -10383,16 +10132,9 @@
],
"default": null,
"title": "Hyperbrowser"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "HyperbrowserLoadTool",
"type": "object"
},
@@ -10495,17 +10237,11 @@
"default": 600,
"title": "Max Polling Time",
"type": "integer"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
"crew_api_url",
- "crew_bearer_token",
- "tool_type"
+ "crew_bearer_token"
],
"title": "InvokeCrewAIAutomationTool",
"type": "object"
@@ -11550,16 +11286,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "JSONSearchTool",
"type": "object"
},
@@ -11649,11 +11378,6 @@
"title": "Headers",
"type": "object"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"website_url": {
"anyOf": [
{
@@ -11667,9 +11391,7 @@
"title": "Website Url"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "JinaScrapeWebsiteTool",
"type": "object"
},
@@ -11740,16 +11462,8 @@
"type": "object"
}
},
- "properties": {
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- }
- },
- "required": [
- "tool_type"
- ],
+ "properties": {},
+ "required": [],
"title": "LinkupSearchTool",
"type": "object"
},
@@ -11809,18 +11523,10 @@
"properties": {
"llama_index_tool": {
"title": "Llama Index Tool"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "name",
- "description",
- "llama_index_tool",
- "tool_type"
+ "llama_index_tool"
],
"title": "LlamaIndexTool",
"type": "object"
@@ -12855,16 +12561,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "MDXSearchTool",
"type": "object"
},
@@ -12976,20 +12675,12 @@
"description": "UUID of the Agent Handler Tool Pack to use",
"title": "Tool Pack Id",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "name",
- "description",
"tool_pack_id",
"registered_user_id",
- "tool_name",
- "tool_type"
+ "tool_name"
],
"title": "MergeAgentHandlerTool",
"type": "object"
@@ -13173,11 +12864,6 @@
"title": "Text Key",
"type": "string"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"vector_index_name": {
"default": "vector_index",
"description": "Name of the Atlas Search vector index",
@@ -13188,8 +12874,7 @@
"required": [
"database_name",
"collection_name",
- "connection_string",
- "tool_type"
+ "connection_string"
],
"title": "MongoDBVectorSearchTool",
"type": "object"
@@ -13296,16 +12981,9 @@
],
"default": null,
"title": "Session Id"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "MultiOnTool",
"type": "object"
},
@@ -14346,16 +14024,10 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "db_uri",
- "tool_type"
+ "db_uri"
],
"title": "MySQLSearchTool",
"type": "object"
@@ -14379,7 +14051,7 @@
}
},
{
- "description": "Converts natural language to SQL queries and executes them.",
+ "description": "Converts natural language to SQL queries and executes them against a database. Read-only by default \u2014 only SELECT/SHOW/DESCRIBE/EXPLAIN queries (and read-only CTEs) are allowed unless configured with allow_dml=True.",
"env_vars": [],
"humanized_name": "NL2SQLTool",
"init_params_schema": {
@@ -14420,7 +14092,14 @@
"type": "object"
}
},
+ "description": "Tool that converts natural language to SQL and executes it against a database.\n\nBy default the tool operates in **read-only mode**: only SELECT, SHOW,\nDESCRIBE, EXPLAIN, and read-only CTEs (WITH \u2026 SELECT) are permitted. Write\noperations (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, \u2026) are\nblocked unless ``allow_dml=True`` is set explicitly or the environment\nvariable ``CREWAI_NL2SQL_ALLOW_DML=true`` is present.\n\nWritable CTEs (``WITH d AS (DELETE \u2026) SELECT \u2026``) and\n``EXPLAIN ANALYZE `` are treated as write operations and are\nblocked in read-only mode.\n\nThe ``_fetch_all_available_columns`` helper uses parameterised queries so\nthat table names coming from the database catalogue cannot be used as an\ninjection vector.",
"properties": {
+ "allow_dml": {
+ "default": false,
+ "description": "When False (default) only read statements are permitted. Set to True to allow INSERT/UPDATE/DELETE/DROP and other write operations.",
+ "title": "Allow DML",
+ "type": "boolean"
+ },
"columns": {
"additionalProperties": {
"anyOf": [
@@ -14451,16 +14130,10 @@
},
"title": "Tables",
"type": "array"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "db_uri",
- "tool_type"
+ "db_uri"
],
"title": "NL2SQLTool",
"type": "object"
@@ -14869,16 +14542,9 @@
"properties": {
"llm": {
"$ref": "#/$defs/LLM"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "OCRTool",
"type": "object"
},
@@ -15075,17 +14741,11 @@
},
"oxylabs_api": {
"title": "Oxylabs Api"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
"oxylabs_api",
- "config",
- "tool_type"
+ "config"
],
"title": "OxylabsAmazonProductScraperTool",
"type": "object"
@@ -15310,17 +14970,11 @@
},
"oxylabs_api": {
"title": "Oxylabs Api"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
"oxylabs_api",
- "config",
- "tool_type"
+ "config"
],
"title": "OxylabsAmazonSearchScraperTool",
"type": "object"
@@ -15558,17 +15212,11 @@
},
"oxylabs_api": {
"title": "Oxylabs Api"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
"oxylabs_api",
- "config",
- "tool_type"
+ "config"
],
"title": "OxylabsGoogleSearchScraperTool",
"type": "object"
@@ -15754,17 +15402,11 @@
},
"oxylabs_api": {
"title": "Oxylabs Api"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
"oxylabs_api",
- "config",
- "tool_type"
+ "config"
],
"title": "OxylabsUniversalScraperTool",
"type": "object"
@@ -16822,16 +16464,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "PDFSearchTool",
"type": "object"
},
@@ -16913,16 +16548,9 @@
"default": "https://api.parallel.ai/v1beta/search",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "ParallelSearchTool",
"type": "object"
},
@@ -17081,16 +16709,9 @@
},
"title": "Evaluators",
"type": "array"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "PatronusEvalTool",
"type": "object"
},
@@ -17156,17 +16777,11 @@
"evaluator": {
"title": "Evaluator",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
"evaluator",
- "evaluated_model_gold_answer",
- "tool_type"
+ "evaluated_model_gold_answer"
],
"title": "PatronusLocalEvaluatorTool",
"type": "object"
@@ -17272,16 +16887,9 @@
},
"title": "Evaluators",
"type": "array"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "PatronusPredefinedCriteriaEvalTool",
"type": "object"
},
@@ -17470,16 +17078,10 @@
"description": "Base package path for Qdrant. Will dynamically import client and models.",
"title": "Qdrant Package",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "qdrant_config",
- "tool_type"
+ "qdrant_config"
],
"title": "QdrantVectorSearchTool",
"type": "object"
@@ -18549,16 +18151,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "RagTool",
"type": "object"
},
@@ -18654,11 +18249,6 @@
],
"title": "Headers"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"website_url": {
"anyOf": [
{
@@ -18672,9 +18262,7 @@
"title": "Website Url"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "ScrapeElementFromWebsiteTool",
"type": "object"
},
@@ -18774,11 +18362,6 @@
],
"title": "Headers"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"website_url": {
"anyOf": [
{
@@ -18792,9 +18375,7 @@
"title": "Website Url"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "ScrapeWebsiteTool",
"type": "object"
},
@@ -18884,11 +18465,6 @@
"title": "Enable Logging",
"type": "boolean"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"user_prompt": {
"anyOf": [
{
@@ -18914,9 +18490,7 @@
"title": "Website Url"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "ScrapegraphScrapeTool",
"type": "object"
},
@@ -19017,16 +18591,9 @@
],
"default": null,
"title": "Scrapfly"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "ScrapflyScrapeWebsiteTool",
"type": "object"
},
@@ -19184,11 +18751,6 @@
"default": false,
"title": "Return Html"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"wait_time": {
"anyOf": [
{
@@ -19214,9 +18776,7 @@
"title": "Website Url"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SeleniumScrapingTool",
"type": "object"
},
@@ -19306,16 +18866,9 @@
],
"default": null,
"title": "Client"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerpApiGoogleSearchTool",
"type": "object"
},
@@ -19411,16 +18964,9 @@
],
"default": null,
"title": "Client"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerpApiGoogleShoppingTool",
"type": "object"
},
@@ -19562,16 +19108,9 @@
"default": "search",
"title": "Search Type",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerperDevTool",
"type": "object"
},
@@ -19642,16 +19181,8 @@
"type": "object"
}
},
- "properties": {
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- }
- },
- "required": [
- "tool_type"
- ],
+ "properties": {},
+ "required": [],
"title": "SerperScrapeWebsiteTool",
"type": "object"
},
@@ -20739,16 +20270,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerplyJobSearchTool",
"type": "object"
},
@@ -20862,16 +20386,9 @@
"default": "https://api.serply.io/v1/news/",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerplyNewsSearchTool",
"type": "object"
},
@@ -20985,16 +20502,9 @@
"default": "https://api.serply.io/v1/scholar/",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerplyScholarSearchTool",
"type": "object"
},
@@ -21144,16 +20654,9 @@
"default": "https://api.serply.io/v1/search/",
"title": "Search Url",
"type": "string"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerplyWebSearchTool",
"type": "object"
},
@@ -22234,16 +21737,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SerplyWebpageToMarkdownTool",
"type": "object"
},
@@ -22384,16 +21880,9 @@
],
"default": null,
"title": "Connection Pool"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SingleStoreSearchTool",
"type": "object"
},
@@ -22604,16 +22093,10 @@
"description": "Delay between retries in seconds",
"title": "Retry Delay",
"type": "number"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
"required": [
- "config",
- "tool_type"
+ "config"
],
"title": "SnowflakeSearchTool",
"type": "object"
@@ -22800,11 +22283,6 @@
"default": null,
"title": "Spider"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"website_url": {
"anyOf": [
{
@@ -22818,9 +22296,7 @@
"title": "Website Url"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "SpiderTool",
"type": "object"
},
@@ -22983,11 +22459,6 @@
"default": "https://api.stagehand.browserbase.com/v1",
"title": "Server Url"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"use_simplified_dom": {
"default": true,
"title": "Use Simplified Dom",
@@ -23004,9 +22475,7 @@
"type": "boolean"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "StagehandTool",
"type": "object"
},
@@ -24084,11 +23553,6 @@
"title": "Summarize",
"type": "boolean"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"txt": {
"anyOf": [
{
@@ -24102,9 +23566,7 @@
"title": "Txt"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "TXTSearchTool",
"type": "object"
},
@@ -24251,16 +23713,9 @@
"description": "The timeout for the extraction request in seconds.",
"title": "Timeout",
"type": "integer"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "TavilyExtractorTool",
"type": "object"
},
@@ -24507,11 +23962,6 @@
"title": "Timeout",
"type": "integer"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"topic": {
"default": "general",
"description": "The topic to focus the search on.",
@@ -24524,9 +23974,7 @@
"type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "TavilySearchTool",
"type": "object"
},
@@ -24600,16 +24048,8 @@
}
},
"description": "Tool for analyzing images using vision models.\n\nArgs:\n llm: Optional LLM instance to use\n model: Model identifier to use if no LLM is provided",
- "properties": {
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- }
- },
- "required": [
- "tool_type"
- ],
+ "properties": {},
+ "required": [],
"title": "VisionTool",
"type": "object"
},
@@ -24731,11 +24171,6 @@
"default": null,
"title": "Query"
},
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
- },
"vectorizer": {
"title": "Vectorizer"
},
@@ -24753,8 +24188,7 @@
"required": [
"collection_name",
"weaviate_cluster_url",
- "weaviate_api_key",
- "tool_type"
+ "weaviate_api_key"
],
"title": "WeaviateVectorSearchTool",
"type": "object"
@@ -25801,16 +25235,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "WebsiteSearchTool",
"type": "object"
},
@@ -26860,16 +26287,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "XMLSearchTool",
"type": "object"
},
@@ -27919,16 +27339,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "YoutubeChannelSearchTool",
"type": "object"
},
@@ -28978,16 +28391,9 @@
"default": false,
"title": "Summarize",
"type": "boolean"
- },
- "tool_type": {
- "readOnly": true,
- "title": "Tool Type",
- "type": "string"
}
},
- "required": [
- "tool_type"
- ],
+ "required": [],
"title": "YoutubeVideoSearchTool",
"type": "object"
},
diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml
index e883035c1..350e1b65f 100644
--- a/lib/crewai/pyproject.toml
+++ b/lib/crewai/pyproject.toml
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
dependencies = [
# Core Dependencies
"pydantic~=2.11.9",
- "openai>=1.83.0,<3",
+ "openai>=2.0.0,<3",
"instructor>=1.3.3",
# Text Processing
"pdfplumber~=0.11.4",
@@ -40,7 +40,7 @@ dependencies = [
"pydantic-settings~=2.10.1",
"httpx~=0.28.1",
"mcp~=1.26.0",
- "uv~=0.9.13",
+ "uv~=0.11.6",
"aiosqlite~=0.21.0",
"pyyaml~=6.0",
"aiofiles~=24.1.0",
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
- "crewai-tools==1.14.0",
+ "crewai-tools==1.14.2rc1",
]
embeddings = [
"tiktoken~=0.8.0"
@@ -68,14 +68,14 @@ openpyxl = [
]
mem0 = ["mem0ai~=0.1.94"]
docling = [
- "docling~=2.75.0",
+ "docling~=2.84.0",
]
qdrant = [
"qdrant-client[fastembed]~=1.14.3",
]
aws = [
- "boto3~=1.40.38",
- "aiobotocore~=2.25.2",
+ "boto3~=1.42.79",
+ "aiobotocore~=3.4.0",
]
watson = [
"ibm-watsonx-ai~=1.3.39",
@@ -87,7 +87,7 @@ litellm = [
"litellm~=1.83.0",
]
bedrock = [
- "boto3~=1.40.45",
+ "boto3~=1.42.79",
]
google-genai = [
"google-genai~=1.65.0",
diff --git a/lib/crewai/src/crewai/__init__.py b/lib/crewai/src/crewai/__init__.py
index b53e09ecd..97f2ca983 100644
--- a/lib/crewai/src/crewai/__init__.py
+++ b/lib/crewai/src/crewai/__init__.py
@@ -46,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
-__version__ = "1.14.0"
+__version__ = "1.14.2rc1"
_telemetry_submitted = False
diff --git a/lib/crewai/src/crewai/a2a/errors.py b/lib/crewai/src/crewai/a2a/errors.py
index b55200708..56d82e110 100644
--- a/lib/crewai/src/crewai/a2a/errors.py
+++ b/lib/crewai/src/crewai/a2a/errors.py
@@ -98,7 +98,6 @@ class A2AErrorCode(IntEnum):
"""The specified artifact was not found."""
-# Error code to default message mapping
ERROR_MESSAGES: dict[int, str] = {
A2AErrorCode.JSON_PARSE_ERROR: "Parse error",
A2AErrorCode.INVALID_REQUEST: "Invalid Request",
diff --git a/lib/crewai/src/crewai/a2a/extensions/base.py b/lib/crewai/src/crewai/a2a/extensions/base.py
index b94e9543e..d9a280506 100644
--- a/lib/crewai/src/crewai/a2a/extensions/base.py
+++ b/lib/crewai/src/crewai/a2a/extensions/base.py
@@ -63,25 +63,21 @@ class A2AExtension(Protocol):
Example:
class MyExtension:
def inject_tools(self, agent: Agent) -> None:
- # Add custom tools to the agent
pass
def extract_state_from_history(
self, conversation_history: Sequence[Message]
) -> ConversationState | None:
- # Extract state from conversation
return None
def augment_prompt(
self, base_prompt: str, conversation_state: ConversationState | None
) -> str:
- # Add custom instructions
return base_prompt
def process_response(
self, agent_response: Any, conversation_state: ConversationState | None
) -> Any:
- # Modify response if needed
return agent_response
"""
diff --git a/lib/crewai/src/crewai/a2a/utils/response_model.py b/lib/crewai/src/crewai/a2a/utils/response_model.py
index 4e65ef2b7..1359e2f10 100644
--- a/lib/crewai/src/crewai/a2a/utils/response_model.py
+++ b/lib/crewai/src/crewai/a2a/utils/response_model.py
@@ -77,7 +77,6 @@ def extract_a2a_agent_ids_from_config(
else:
configs = a2a_config
- # Filter to only client configs (those with endpoint)
client_configs: list[A2AClientConfigTypes] = [
config for config in configs if isinstance(config, (A2AConfig, A2AClientConfig))
]
diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py
index c86d7112c..b83752344 100644
--- a/lib/crewai/src/crewai/agent/core.py
+++ b/lib/crewai/src/crewai/agent/core.py
@@ -84,6 +84,7 @@ from crewai.rag.embeddings.types import EmbedderConfig
from crewai.security.fingerprint import Fingerprint
from crewai.skills.loader import activate_skill, discover_skills
from crewai.skills.models import INSTRUCTIONS, Skill as SkillModel
+from crewai.state.checkpoint_config import CheckpointConfig, apply_checkpoint
from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.types.callback import SerializableCallable
from crewai.utilities.agent_utils import (
@@ -98,6 +99,7 @@ from crewai.utilities.converter import Converter, ConverterError
from crewai.utilities.env import get_env_context
from crewai.utilities.guardrail import process_guardrail
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.prompts import Prompts, StandardPromptResult, SystemPromptResult
from crewai.utilities.pydantic_schema_utils import generate_model_description
@@ -499,8 +501,8 @@ class Agent(BaseAgent):
self.tools_handler.last_used_tool = None
task_prompt = task.prompt()
- task_prompt = build_task_prompt_with_schema(task, task_prompt, self.i18n)
- task_prompt = format_task_with_context(task_prompt, context, self.i18n)
+ task_prompt = build_task_prompt_with_schema(task, task_prompt)
+ task_prompt = format_task_with_context(task_prompt, context)
return self._retrieve_memory_context(task, task_prompt)
def _finalize_task_prompt(
@@ -562,7 +564,7 @@ class Agent(BaseAgent):
m.format() for m in matches
)
if memory.strip() != "":
- task_prompt += self.i18n.slice("memory").format(memory=memory)
+ task_prompt += I18N_DEFAULT.slice("memory").format(memory=memory)
crewai_event_bus.emit(
self,
@@ -968,14 +970,13 @@ class Agent(BaseAgent):
agent=self,
has_tools=len(raw_tools) > 0,
use_native_tool_calling=use_native_tool_calling,
- i18n=self.i18n,
use_system_prompt=self.use_system_prompt,
system_template=self.system_template,
prompt_template=self.prompt_template,
response_template=self.response_template,
).task_execution()
- stop_words = [self.i18n.slice("observation")]
+ stop_words = [I18N_DEFAULT.slice("observation")]
if self.response_template:
stop_words.append(
self.response_template.split("{{ .Response }}")[1].strip()
@@ -1017,7 +1018,6 @@ class Agent(BaseAgent):
self.agent_executor = self.executor_class(
llm=self.llm,
task=task,
- i18n=self.i18n,
agent=self,
crew=self.crew,
tools=parsed_tools,
@@ -1262,10 +1262,10 @@ class Agent(BaseAgent):
from_agent=self,
),
)
- query = self.i18n.slice("knowledge_search_query").format(
+ query = I18N_DEFAULT.slice("knowledge_search_query").format(
task_prompt=task_prompt
)
- rewriter_prompt = self.i18n.slice("knowledge_search_query_system_prompt")
+ rewriter_prompt = I18N_DEFAULT.slice("knowledge_search_query_system_prompt")
if not isinstance(self.llm, BaseLLM):
self._logger.log(
"warning",
@@ -1342,7 +1342,6 @@ class Agent(BaseAgent):
raw_tools: list[BaseTool] = self.tools or []
- # Inject memory tools for standalone kickoff (crew path handles its own)
agent_memory = getattr(self, "memory", None)
if agent_memory is not None:
from crewai.tools.memory_tools import create_memory_tools
@@ -1384,7 +1383,6 @@ class Agent(BaseAgent):
request_within_rpm_limit=rpm_limit_fn,
callbacks=[TokenCalcHandler(self._token_process)],
response_model=response_format,
- i18n=self.i18n,
)
all_files: dict[str, Any] = {}
@@ -1401,7 +1399,6 @@ class Agent(BaseAgent):
if input_files:
all_files.update(input_files)
- # Inject memory context for standalone kickoff (recall before execution)
if agent_memory is not None:
try:
crewai_event_bus.emit(
@@ -1420,7 +1417,7 @@ class Agent(BaseAgent):
m.format() for m in matches
)
if memory_block:
- formatted_messages += "\n\n" + self.i18n.slice("memory").format(
+ formatted_messages += "\n\n" + I18N_DEFAULT.slice("memory").format(
memory=memory_block
)
crewai_event_bus.emit(
@@ -1461,6 +1458,7 @@ class Agent(BaseAgent):
messages: str | list[LLMMessage],
response_format: type[Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> LiteAgentOutput | Coroutine[Any, Any, LiteAgentOutput]:
"""Execute the agent with the given messages using the AgentExecutor.
@@ -1479,6 +1477,9 @@ class Agent(BaseAgent):
response_format: Optional Pydantic model for structured output.
input_files: Optional dict of named files to attach to the message.
Files can be paths, bytes, or File objects from crewai_files.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the agent resumes from that checkpoint. Remaining
+ config fields enable checkpointing for the run.
Returns:
LiteAgentOutput: The result of the agent execution.
@@ -1487,8 +1488,14 @@ class Agent(BaseAgent):
Note:
For explicit async usage outside of Flow, use kickoff_async() directly.
"""
- # Magic auto-async: if inside event loop (e.g., inside a Flow),
- # return coroutine for Flow to await
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return restored.kickoff( # type: ignore[no-any-return]
+ messages=messages,
+ response_format=response_format,
+ input_files=input_files,
+ )
+
if is_inside_event_loop():
return self.kickoff_async(messages, response_format, input_files)
@@ -1624,7 +1631,7 @@ class Agent(BaseAgent):
try:
model_schema = generate_model_description(response_format)
schema = json.dumps(model_schema, indent=2)
- instructions = self.i18n.slice("formatted_task_instructions").format(
+ instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_format=schema
)
@@ -1639,7 +1646,7 @@ class Agent(BaseAgent):
if isinstance(conversion_result, BaseModel):
formatted_result = conversion_result
except ConverterError:
- pass # Keep raw output if conversion fails
+ pass
else:
raw_output = str(output) if not isinstance(output, str) else output
@@ -1721,7 +1728,6 @@ class Agent(BaseAgent):
elif callable(self.guardrail):
guardrail_callable = self.guardrail
else:
- # Should not happen if called from kickoff with guardrail check
return output
guardrail_result = process_guardrail(
@@ -1767,6 +1773,7 @@ class Agent(BaseAgent):
messages: str | list[LLMMessage],
response_format: type[Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> LiteAgentOutput:
"""Execute the agent asynchronously with the given messages.
@@ -1782,10 +1789,20 @@ class Agent(BaseAgent):
response_format: Optional Pydantic model for structured output.
input_files: Optional dict of named files to attach to the message.
Files can be paths, bytes, or File objects from crewai_files.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the agent resumes from that checkpoint.
Returns:
LiteAgentOutput: The result of the agent execution.
"""
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return await restored.kickoff_async( # type: ignore[no-any-return]
+ messages=messages,
+ response_format=response_format,
+ input_files=input_files,
+ )
+
executor, inputs, agent_info, parsed_tools = self._prepare_kickoff(
messages, response_format, input_files
)
@@ -1815,6 +1832,7 @@ class Agent(BaseAgent):
messages: str | list[LLMMessage],
response_format: type[Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> LiteAgentOutput:
"""Async version of kickoff. Alias for kickoff_async.
@@ -1822,8 +1840,12 @@ class Agent(BaseAgent):
messages: Either a string query or a list of message dictionaries.
response_format: Optional Pydantic model for structured output.
input_files: Optional dict of named files to attach to the message.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the agent resumes from that checkpoint.
Returns:
LiteAgentOutput: The result of the agent execution.
"""
- return await self.kickoff_async(messages, response_format, input_files)
+ return await self.kickoff_async(
+ messages, response_format, input_files, from_checkpoint
+ )
diff --git a/lib/crewai/src/crewai/agent/planning_config.py b/lib/crewai/src/crewai/agent/planning_config.py
index d30b0eb46..cd8124b9c 100644
--- a/lib/crewai/src/crewai/agent/planning_config.py
+++ b/lib/crewai/src/crewai/agent/planning_config.py
@@ -41,7 +41,6 @@ class PlanningConfig(BaseModel):
from crewai import Agent
from crewai.agent.planning_config import PlanningConfig
- # Simple usage — fast, linear execution (default)
agent = Agent(
role="Researcher",
goal="Research topics",
@@ -49,7 +48,6 @@ class PlanningConfig(BaseModel):
planning_config=PlanningConfig(),
)
- # Balanced — replan only when steps fail
agent = Agent(
role="Researcher",
goal="Research topics",
@@ -59,7 +57,6 @@ class PlanningConfig(BaseModel):
),
)
- # Full adaptive planning with refinement and replanning
agent = Agent(
role="Researcher",
goal="Research topics",
@@ -69,7 +66,7 @@ class PlanningConfig(BaseModel):
max_attempts=3,
max_steps=10,
plan_prompt="Create a focused plan for: {description}",
- llm="gpt-4o-mini", # Use cheaper model for planning
+ llm="gpt-4o-mini",
),
)
```
diff --git a/lib/crewai/src/crewai/agent/utils.py b/lib/crewai/src/crewai/agent/utils.py
index 8690c8faf..93c861835 100644
--- a/lib/crewai/src/crewai/agent/utils.py
+++ b/lib/crewai/src/crewai/agent/utils.py
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
from crewai.agent.core import Agent
from crewai.task import Task
from crewai.tools.base_tool import BaseTool
- from crewai.utilities.i18n import I18N
def handle_reasoning(agent: Agent, task: Task) -> None:
@@ -40,7 +39,6 @@ def handle_reasoning(agent: Agent, task: Task) -> None:
agent: The agent performing the task.
task: The task to execute.
"""
- # Check if planning is enabled using the planning_enabled property
if not getattr(agent, "planning_enabled", False):
return
@@ -59,46 +57,50 @@ def handle_reasoning(agent: Agent, task: Task) -> None:
agent._logger.log("error", f"Error during planning: {e!s}")
-def build_task_prompt_with_schema(task: Task, task_prompt: str, i18n: I18N) -> str:
+def build_task_prompt_with_schema(task: Task, task_prompt: str) -> str:
"""Build task prompt with JSON/Pydantic schema instructions if applicable.
Args:
task: The task being executed.
task_prompt: The initial task prompt.
- i18n: Internationalization instance.
Returns:
The task prompt potentially augmented with schema instructions.
"""
+ from crewai.utilities.i18n import I18N_DEFAULT
+
if (task.output_json or task.output_pydantic) and not task.response_model:
if task.output_json:
schema_dict = generate_model_description(task.output_json)
schema = json.dumps(schema_dict["json_schema"]["schema"], indent=2)
- task_prompt += "\n" + i18n.slice("formatted_task_instructions").format(
- output_format=schema
- )
+ task_prompt += "\n" + I18N_DEFAULT.slice(
+ "formatted_task_instructions"
+ ).format(output_format=schema)
elif task.output_pydantic:
schema_dict = generate_model_description(task.output_pydantic)
schema = json.dumps(schema_dict["json_schema"]["schema"], indent=2)
- task_prompt += "\n" + i18n.slice("formatted_task_instructions").format(
- output_format=schema
- )
+ task_prompt += "\n" + I18N_DEFAULT.slice(
+ "formatted_task_instructions"
+ ).format(output_format=schema)
return task_prompt
-def format_task_with_context(task_prompt: str, context: str | None, i18n: I18N) -> str:
+def format_task_with_context(task_prompt: str, context: str | None) -> str:
"""Format task prompt with context if provided.
Args:
task_prompt: The task prompt.
context: Optional context string.
- i18n: Internationalization instance.
Returns:
The task prompt formatted with context if provided.
"""
+ from crewai.utilities.i18n import I18N_DEFAULT
+
if context:
- return i18n.slice("task_with_context").format(task=task_prompt, context=context)
+ return I18N_DEFAULT.slice("task_with_context").format(
+ task=task_prompt, context=context
+ )
return task_prompt
diff --git a/lib/crewai/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py b/lib/crewai/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py
index 1710b56cb..33a705728 100644
--- a/lib/crewai/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py
+++ b/lib/crewai/src/crewai/agents/agent_adapters/langgraph/langgraph_adapter.py
@@ -33,6 +33,7 @@ from crewai.tools.base_tool import BaseTool
from crewai.types.callback import SerializableCallable
from crewai.utilities import Logger
from crewai.utilities.converter import Converter
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.import_utils import require
@@ -186,7 +187,7 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
task_prompt = task.prompt() if hasattr(task, "prompt") else str(task)
if context:
- task_prompt = self.i18n.slice("task_with_context").format(
+ task_prompt = I18N_DEFAULT.slice("task_with_context").format(
task=task_prompt, context=context
)
diff --git a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py
index 82eb8640b..169d65af5 100644
--- a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py
+++ b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_adapter.py
@@ -32,6 +32,7 @@ from crewai.events.types.agent_events import (
from crewai.tools import BaseTool
from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.utilities import Logger
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.import_utils import require
@@ -133,7 +134,7 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
try:
task_prompt: str = task.prompt()
if context:
- task_prompt = self.i18n.slice("task_with_context").format(
+ task_prompt = I18N_DEFAULT.slice("task_with_context").format(
task=task_prompt, context=context
)
crewai_event_bus.emit(
diff --git a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py
index 7543305f0..7e0979ba5 100644
--- a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py
+++ b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/openai_agent_tool_adapter.py
@@ -99,12 +99,10 @@ class OpenAIAgentToolAdapter(BaseToolAdapter):
Returns:
Tool execution result.
"""
- # Get the parameter name from the schema
param_name: str = next(
iter(tool.args_schema.model_json_schema()["properties"].keys())
)
- # Handle different argument types
args_dict: dict[str, Any]
if isinstance(arguments, dict):
args_dict = arguments
@@ -116,16 +114,13 @@ class OpenAIAgentToolAdapter(BaseToolAdapter):
else:
args_dict = {param_name: str(arguments)}
- # Run the tool with the processed arguments
output: Any | Awaitable[Any] = tool._run(**args_dict)
- # Await if the tool returned a coroutine
if inspect.isawaitable(output):
result: Any = await output
else:
result = output
- # Ensure the result is JSON serializable
if isinstance(result, (dict, list, str, int, float, bool, type(None))):
return result
return str(result)
diff --git a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py
index 4033c8d50..358281cac 100644
--- a/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py
+++ b/lib/crewai/src/crewai/agents/agent_adapters/openai_agents/structured_output_converter.py
@@ -8,7 +8,7 @@ import json
from typing import Any
from crewai.agents.agent_adapters.base_converter_adapter import BaseConverterAdapter
-from crewai.utilities.i18n import get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
class OpenAIConverterAdapter(BaseConverterAdapter):
@@ -59,10 +59,8 @@ class OpenAIConverterAdapter(BaseConverterAdapter):
if not self._output_format:
return base_prompt
- output_schema: str = (
- get_i18n()
- .slice("formatted_task_instructions")
- .format(output_format=json.dumps(self._schema, indent=2))
+ output_schema: str = I18N_DEFAULT.slice("formatted_task_instructions").format(
+ output_format=json.dumps(self._schema, indent=2)
)
return f"{base_prompt}\n\n{output_schema}"
diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py
index de9379d09..a00f9b49f 100644
--- a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py
+++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py
@@ -43,7 +43,6 @@ from crewai.state.checkpoint_config import CheckpointConfig, _coerce_checkpoint
from crewai.tools.base_tool import BaseTool, Tool
from crewai.types.callback import SerializableCallable
from crewai.utilities.config import process_config
-from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.logger import Logger
from crewai.utilities.rpm_controller import RPMController
from crewai.utilities.string_utils import interpolate_only
@@ -52,7 +51,6 @@ from crewai.utilities.string_utils import interpolate_only
if TYPE_CHECKING:
from crewai.context import ExecutionContext
from crewai.crew import Crew
- from crewai.state.provider.core import BaseProvider
def _validate_crew_ref(value: Any) -> Any:
@@ -179,7 +177,7 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
agent_executor: An instance of the CrewAgentExecutor class.
llm (Any): Language model that will run the agent.
crew (Any): Crew to which the agent belongs.
- i18n (I18N): Internationalization settings.
+
cache_handler ([CacheHandler]): An instance of the CacheHandler class.
tools_handler ([ToolsHandler]): An instance of the ToolsHandler class.
max_tokens: Maximum number of tokens for the agent to generate in a response.
@@ -269,9 +267,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
_serialize_crew_ref, return_type=str | None, when_used="always"
),
] = Field(default=None, description="Crew to which the agent belongs.")
- i18n: I18N = Field(
- default_factory=get_i18n, description="Internationalization settings."
- )
cache_handler: CacheHandler | None = Field(
default=None, description="An instance of the CacheHandler class."
)
@@ -342,19 +337,16 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
execution_context: ExecutionContext | None = Field(default=None)
@classmethod
- def from_checkpoint(
- cls, path: str, *, provider: BaseProvider | None = None
- ) -> Self:
- """Restore an Agent from a checkpoint file."""
+ def from_checkpoint(cls, config: CheckpointConfig) -> Self:
+ """Restore an Agent from a checkpoint.
+
+ Args:
+ config: Checkpoint configuration with ``restore_from`` set.
+ """
from crewai.context import apply_execution_context
- from crewai.state.provider.json_provider import JsonProvider
from crewai.state.runtime import RuntimeState
- state = RuntimeState.from_checkpoint(
- path,
- provider=provider or JsonProvider(),
- context={"from_checkpoint": True},
- )
+ state = RuntimeState.from_checkpoint(config, context={"from_checkpoint": True})
for entity in state.root:
if isinstance(entity, cls):
if entity.execution_context is not None:
@@ -363,7 +355,9 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
entity.agent_executor.agent = entity
entity.agent_executor._resuming = True
return entity
- raise ValueError(f"No {cls.__name__} found in checkpoint: {path}")
+ raise ValueError(
+ f"No {cls.__name__} found in checkpoint: {config.restore_from}"
+ )
@model_validator(mode="before")
@classmethod
@@ -389,7 +383,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
if isinstance(tool, BaseTool):
processed_tools.append(tool)
elif all(hasattr(tool, attr) for attr in required_attrs):
- # Tool has the required attributes, create a Tool instance
processed_tools.append(Tool.from_langchain(tool))
else:
raise ValueError(
@@ -454,14 +447,12 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
@model_validator(mode="after")
def validate_and_set_attributes(self) -> Self:
- # Validate required fields
for field in ["role", "goal", "backstory"]:
if getattr(self, field) is None:
raise ValueError(
f"{field} must be provided either directly or through config"
)
- # Set private attributes
self._logger = Logger(verbose=self.verbose)
if self.max_rpm and not self._rpm_controller:
self._rpm_controller = RPMController(
@@ -470,7 +461,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
if not self._token_process:
self._token_process = TokenProcess()
- # Initialize security_config if not provided
if self.security_config is None:
self.security_config = SecurityConfig()
@@ -572,14 +562,11 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
"actions",
}
- # Copy llm
existing_llm = shallow_copy(self.llm)
copied_knowledge = shallow_copy(self.knowledge)
copied_knowledge_storage = shallow_copy(self.knowledge_storage)
- # Properly copy knowledge sources if they exist
existing_knowledge_sources = None
if self.knowledge_sources:
- # Create a shared storage instance for all knowledge sources
shared_storage = (
self.knowledge_sources[0].storage if self.knowledge_sources else None
)
@@ -591,7 +578,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
if hasattr(source, "model_copy")
else shallow_copy(source)
)
- # Ensure all copied sources use the same storage instance
copied_source.storage = shared_storage
existing_knowledge_sources.append(copied_source)
diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py
index a44b81fc3..d251b1d36 100644
--- a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py
+++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.crew import Crew
from crewai.task import Task
- from crewai.utilities.i18n import I18N
class BaseAgentExecutor(BaseModel):
@@ -28,7 +27,6 @@ class BaseAgentExecutor(BaseModel):
max_iter: int = Field(default=25)
messages: list[LLMMessage] = Field(default_factory=list)
_resuming: bool = PrivateAttr(default=False)
- _i18n: I18N | None = PrivateAttr(default=None)
def _save_to_memory(self, output: AgentFinish) -> None:
"""Save task result to unified memory (memory or crew._memory)."""
diff --git a/lib/crewai/src/crewai/agents/constants.py b/lib/crewai/src/crewai/agents/constants.py
index 326d53d02..7a180f947 100644
--- a/lib/crewai/src/crewai/agents/constants.py
+++ b/lib/crewai/src/crewai/agents/constants.py
@@ -4,8 +4,6 @@ import re
from typing import Final
-# crewai.agents.parser constants
-
FINAL_ANSWER_ACTION: Final[str] = "Final Answer:"
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE: Final[str] = (
"I did it wrong. Invalid Format: I missed the 'Action:' after 'Thought:'. I will do right next, and don't use a tool I have already used.\n"
diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py
index 6307d5b9c..62369bfb9 100644
--- a/lib/crewai/src/crewai/agents/crew_agent_executor.py
+++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py
@@ -67,7 +67,7 @@ from crewai.utilities.agent_utils import (
)
from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.file_store import aget_all_files, get_all_files
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.printer import PRINTER
from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.token_counter_callback import TokenCalcHandler
@@ -135,9 +135,8 @@ class CrewAgentExecutor(BaseAgentExecutor):
model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True)
- def __init__(self, i18n: I18N | None = None, **kwargs: Any) -> None:
+ def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
- self._i18n = i18n or get_i18n()
if not self.before_llm_call_hooks:
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
if not self.after_llm_call_hooks:
@@ -297,7 +296,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
Returns:
Final answer from the agent.
"""
- # Check if model supports native function calling
use_native_tools = (
hasattr(self.llm, "supports_function_calling")
and callable(getattr(self.llm, "supports_function_calling", None))
@@ -308,7 +306,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
if use_native_tools:
return self._invoke_loop_native_tools()
- # Fall back to ReAct text-based pattern
return self._invoke_loop_react()
def _invoke_loop_react(self) -> AgentFinish:
@@ -328,7 +325,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
formatted_answer = handle_max_iterations_exceeded(
formatted_answer,
printer=PRINTER,
- i18n=self._i18n,
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
@@ -349,7 +345,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
executor_context=self,
verbose=self.agent.verbose,
)
- # breakpoint()
if self.response_model is not None:
try:
if isinstance(answer, BaseModel):
@@ -367,7 +362,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
text=answer,
)
except ValidationError:
- # If validation fails, convert BaseModel to JSON string for parsing
answer_str = (
answer.model_dump_json()
if isinstance(answer, BaseModel)
@@ -377,14 +371,12 @@ class CrewAgentExecutor(BaseAgentExecutor):
answer_str, self.use_stop_words
) # type: ignore[assignment]
else:
- # When no response_model, answer should be a string
answer_str = str(answer) if not isinstance(answer, str) else answer
formatted_answer = process_llm_response(
answer_str, self.use_stop_words
) # type: ignore[assignment]
if isinstance(formatted_answer, AgentAction):
- # Extract agent fingerprint if available
fingerprint_context = {}
if (
self.agent
@@ -401,7 +393,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
agent_action=formatted_answer,
fingerprint_context=fingerprint_context,
tools=self.tools,
- i18n=self._i18n,
agent_key=self.agent.key if self.agent else None,
agent_role=self.agent.role if self.agent else None,
tools_handler=self.tools_handler,
@@ -429,7 +420,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
except Exception as e:
if e.__class__.__module__.startswith("litellm"):
- # Do not retry on litellm errors
raise e
if is_context_length_exceeded(e):
handle_context_length(
@@ -438,7 +428,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
- i18n=self._i18n,
verbose=self.agent.verbose,
)
continue
@@ -447,10 +436,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
finally:
self.iterations += 1
- # During the invoke loop, formatted_answer alternates between AgentAction
- # (when the agent is using tools) and eventually becomes AgentFinish
- # (when the agent reaches a final answer). This check confirms we've
- # reached a final answer and helps type checking understand this transition.
if not isinstance(formatted_answer, AgentFinish):
raise RuntimeError(
"Agent execution ended without reaching a final answer. "
@@ -469,9 +454,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
Returns:
Final answer from the agent.
"""
- # Convert tools to OpenAI schema format
if not self.original_tools:
- # No tools available, fall back to simple LLM call
return self._invoke_loop_native_no_tools()
openai_tools, available_functions, self._tool_name_mapping = (
@@ -484,7 +467,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
formatted_answer = handle_max_iterations_exceeded(
None,
printer=PRINTER,
- i18n=self._i18n,
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
@@ -495,10 +477,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
enforce_rpm_limit(self.request_within_rpm_limit)
- # Call LLM with native tools
- # Pass available_functions=None so the LLM returns tool_calls
- # without executing them. The executor handles tool execution
- # via _handle_native_tool_calls to properly manage message history.
answer = get_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
@@ -513,32 +491,26 @@ class CrewAgentExecutor(BaseAgentExecutor):
verbose=self.agent.verbose,
)
- # Check if the response is a list of tool calls
if (
isinstance(answer, list)
and answer
and self._is_tool_call_list(answer)
):
- # Handle tool calls - execute tools and add results to messages
tool_finish = self._handle_native_tool_calls(
answer, available_functions
)
- # If tool has result_as_answer=True, return immediately
if tool_finish is not None:
return tool_finish
- # Continue loop to let LLM analyze results and decide next steps
continue
- # Text or other response - handle as potential final answer
if isinstance(answer, str):
- # Text response - this is the final answer
formatted_answer = AgentFinish(
thought="",
output=answer,
text=answer,
)
self._invoke_step_callback(formatted_answer)
- self._append_message(answer) # Save final answer to messages
+ self._append_message(answer)
self._show_logs(formatted_answer)
return formatted_answer
@@ -554,14 +526,13 @@ class CrewAgentExecutor(BaseAgentExecutor):
self._show_logs(formatted_answer)
return formatted_answer
- # Unexpected response type, treat as final answer
formatted_answer = AgentFinish(
thought="",
output=str(answer),
text=str(answer),
)
self._invoke_step_callback(formatted_answer)
- self._append_message(str(answer)) # Save final answer to messages
+ self._append_message(str(answer))
self._show_logs(formatted_answer)
return formatted_answer
@@ -575,7 +546,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
- i18n=self._i18n,
verbose=self.agent.verbose,
)
continue
@@ -633,12 +603,10 @@ class CrewAgentExecutor(BaseAgentExecutor):
if not response:
return False
first_item = response[0]
- # OpenAI-style
if hasattr(first_item, "function") or (
isinstance(first_item, dict) and "function" in first_item
):
return True
- # Anthropic-style (object with attributes)
if (
hasattr(first_item, "type")
and getattr(first_item, "type", None) == "tool_use"
@@ -646,14 +614,12 @@ class CrewAgentExecutor(BaseAgentExecutor):
return True
if hasattr(first_item, "name") and hasattr(first_item, "input"):
return True
- # Bedrock-style (dict with name and input keys)
if (
isinstance(first_item, dict)
and "name" in first_item
and "input" in first_item
):
return True
- # Gemini-style
if hasattr(first_item, "function_call") and first_item.function_call:
return True
return False
@@ -712,8 +678,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
for _, func_name, _ in parsed_calls
)
- # Preserve historical sequential behavior for result_as_answer batches.
- # Also avoid threading around usage counters for max_usage_count tools.
if has_result_as_answer_in_batch or has_max_usage_count_in_batch:
logger.debug(
"Skipping parallel native execution because batch includes result_as_answer or max_usage_count tool"
@@ -771,7 +735,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
if tool_finish:
return tool_finish
- reasoning_prompt = self._i18n.slice("post_tool_reasoning")
+ reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_message: LLMMessage = {
"role": "user",
"content": reasoning_prompt,
@@ -779,7 +743,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
self.messages.append(reasoning_message)
return None
- # Sequential behavior: process only first tool call, then force reflection.
call_id, func_name, func_args = parsed_calls[0]
self._append_assistant_tool_calls_message([(call_id, func_name, func_args)])
@@ -795,7 +758,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
if tool_finish:
return tool_finish
- reasoning_prompt = self._i18n.slice("post_tool_reasoning")
+ reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_message = {
"role": "user",
"content": reasoning_prompt,
@@ -833,7 +796,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
func_name = sanitize_tool_name(
func_info.get("name", "") or tool_call.get("name", "")
)
- func_args = func_info.get("arguments", "{}") or tool_call.get("input", {})
+ func_args = func_info.get("arguments") or tool_call.get("input", {})
return call_id, func_name, func_args
return None
@@ -1170,7 +1133,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
formatted_answer = handle_max_iterations_exceeded(
formatted_answer,
printer=PRINTER,
- i18n=self._i18n,
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
@@ -1209,7 +1171,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
text=answer,
)
except ValidationError:
- # If validation fails, convert BaseModel to JSON string for parsing
answer_str = (
answer.model_dump_json()
if isinstance(answer, BaseModel)
@@ -1219,7 +1180,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
answer_str, self.use_stop_words
) # type: ignore[assignment]
else:
- # When no response_model, answer should be a string
answer_str = str(answer) if not isinstance(answer, str) else answer
formatted_answer = process_llm_response(
answer_str, self.use_stop_words
@@ -1242,7 +1202,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
agent_action=formatted_answer,
fingerprint_context=fingerprint_context,
tools=self.tools,
- i18n=self._i18n,
agent_key=self.agent.key if self.agent else None,
agent_role=self.agent.role if self.agent else None,
tools_handler=self.tools_handler,
@@ -1278,7 +1237,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
- i18n=self._i18n,
verbose=self.agent.verbose,
)
continue
@@ -1318,7 +1276,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
formatted_answer = handle_max_iterations_exceeded(
None,
printer=PRINTER,
- i18n=self._i18n,
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
@@ -1329,10 +1286,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
enforce_rpm_limit(self.request_within_rpm_limit)
- # Call LLM with native tools
- # Pass available_functions=None so the LLM returns tool_calls
- # without executing them. The executor handles tool execution
- # via _handle_native_tool_calls to properly manage message history.
answer = await aget_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
@@ -1346,32 +1299,26 @@ class CrewAgentExecutor(BaseAgentExecutor):
executor_context=self,
verbose=self.agent.verbose,
)
- # Check if the response is a list of tool calls
if (
isinstance(answer, list)
and answer
and self._is_tool_call_list(answer)
):
- # Handle tool calls - execute tools and add results to messages
tool_finish = self._handle_native_tool_calls(
answer, available_functions
)
- # If tool has result_as_answer=True, return immediately
if tool_finish is not None:
return tool_finish
- # Continue loop to let LLM analyze results and decide next steps
continue
- # Text or other response - handle as potential final answer
if isinstance(answer, str):
- # Text response - this is the final answer
formatted_answer = AgentFinish(
thought="",
output=answer,
text=answer,
)
await self._ainvoke_step_callback(formatted_answer)
- self._append_message(answer) # Save final answer to messages
+ self._append_message(answer)
self._show_logs(formatted_answer)
return formatted_answer
@@ -1387,14 +1334,13 @@ class CrewAgentExecutor(BaseAgentExecutor):
self._show_logs(formatted_answer)
return formatted_answer
- # Unexpected response type, treat as final answer
formatted_answer = AgentFinish(
thought="",
output=str(answer),
text=str(answer),
)
await self._ainvoke_step_callback(formatted_answer)
- self._append_message(str(answer)) # Save final answer to messages
+ self._append_message(str(answer))
self._show_logs(formatted_answer)
return formatted_answer
@@ -1408,7 +1354,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
messages=self.messages,
llm=cast("BaseLLM", self.llm),
callbacks=self.callbacks,
- i18n=self._i18n,
verbose=self.agent.verbose,
)
continue
@@ -1466,8 +1411,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
Returns:
Updated action or final answer.
"""
- # Special case for add_image_tool
- add_image_tool = self._i18n.tools("add_image")
+ add_image_tool = I18N_DEFAULT.tools("add_image")
if (
isinstance(add_image_tool, dict)
and formatted_answer.tool.casefold().strip()
@@ -1586,17 +1530,14 @@ class CrewAgentExecutor(BaseAgentExecutor):
training_handler = CrewTrainingHandler(TRAINING_DATA_FILE)
training_data = training_handler.load() or {}
- # Initialize or retrieve agent's training data
agent_training_data = training_data.get(agent_id, {})
if human_feedback is not None:
- # Save initial output and human feedback
agent_training_data[train_iteration] = {
"initial_output": result.output,
"human_feedback": human_feedback,
}
else:
- # Save improved output
if train_iteration in agent_training_data:
agent_training_data[train_iteration]["improved_output"] = result.output
else:
@@ -1610,7 +1551,6 @@ class CrewAgentExecutor(BaseAgentExecutor):
)
return
- # Update the training data and save
training_data[agent_id] = agent_training_data
training_handler.save(training_data)
@@ -1673,5 +1613,5 @@ class CrewAgentExecutor(BaseAgentExecutor):
Formatted message dict.
"""
return format_message_for_llm(
- self._i18n.slice("feedback_instructions").format(feedback=feedback)
+ I18N_DEFAULT.slice("feedback_instructions").format(feedback=feedback)
)
diff --git a/lib/crewai/src/crewai/agents/parser.py b/lib/crewai/src/crewai/agents/parser.py
index 365443b45..c59719226 100644
--- a/lib/crewai/src/crewai/agents/parser.py
+++ b/lib/crewai/src/crewai/agents/parser.py
@@ -19,10 +19,7 @@ from crewai.agents.constants import (
MISSING_ACTION_INPUT_AFTER_ACTION_ERROR_MESSAGE,
UNABLE_TO_REPAIR_JSON_RESULTS,
)
-from crewai.utilities.i18n import get_i18n
-
-
-_I18N = get_i18n()
+from crewai.utilities.i18n import I18N_DEFAULT as _I18N
@dataclass
@@ -97,11 +94,8 @@ def parse(text: str) -> AgentAction | AgentFinish:
if includes_answer:
final_answer = text.split(FINAL_ANSWER_ACTION)[-1].strip()
- # Check whether the final answer ends with triple backticks.
if final_answer.endswith("```"):
- # Count occurrences of triple backticks in the final answer.
count = final_answer.count("```")
- # If count is odd then it's an unmatched trailing set; remove it.
if count % 2 != 0:
final_answer = final_answer[:-3].rstrip()
return AgentFinish(thought=thought, output=final_answer, text=text)
@@ -149,7 +143,6 @@ def _extract_thought(text: str) -> str:
if thought_index == -1:
return ""
thought = text[:thought_index].strip()
- # Remove any triple backticks from the thought string
return thought.replace("```", "").strip()
@@ -174,18 +167,9 @@ def _safe_repair_json(tool_input: str) -> str:
Returns:
The repaired JSON string or original if repair fails.
"""
- # Skip repair if the input starts and ends with square brackets
- # Explanation: The JSON parser has issues handling inputs that are enclosed in square brackets ('[]').
- # These are typically valid JSON arrays or strings that do not require repair. Attempting to repair such inputs
- # might lead to unintended alterations, such as wrapping the entire input in additional layers or modifying
- # the structure in a way that changes its meaning. By skipping the repair for inputs that start and end with
- # square brackets, we preserve the integrity of these valid JSON structures and avoid unnecessary modifications.
if tool_input.startswith("[") and tool_input.endswith("]"):
return tool_input
- # Before repair, handle common LLM issues:
- # 1. Replace """ with " to avoid JSON parser errors
-
tool_input = tool_input.replace('"""', '"')
result = repair_json(tool_input)
diff --git a/lib/crewai/src/crewai/agents/planner_observer.py b/lib/crewai/src/crewai/agents/planner_observer.py
index 16d1a747e..29d586663 100644
--- a/lib/crewai/src/crewai/agents/planner_observer.py
+++ b/lib/crewai/src/crewai/agents/planner_observer.py
@@ -23,7 +23,7 @@ from crewai.events.types.observation_events import (
StepObservationStartedEvent,
)
from crewai.utilities.agent_utils import extract_task_section
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.planning_types import StepObservation, TodoItem
from crewai.utilities.types import LLMMessage
@@ -64,7 +64,6 @@ class PlannerObserver:
self.task = task
self.kickoff_input = kickoff_input
self.llm = self._resolve_llm()
- self._i18n: I18N = get_i18n()
def _resolve_llm(self) -> Any:
"""Resolve which LLM to use for observation/planning.
@@ -84,10 +83,6 @@ class PlannerObserver:
return create_llm(config.llm)
return self.agent.llm
- # ------------------------------------------------------------------
- # Public API
- # ------------------------------------------------------------------
-
def observe(
self,
completed_step: TodoItem,
@@ -183,9 +178,6 @@ class PlannerObserver:
),
)
- # Don't force a full replan — the step may have succeeded even if the
- # observer LLM failed to parse the result. Defaulting to "continue" is
- # far less disruptive than wiping the entire plan on every observer error.
return StepObservation(
step_completed_successfully=True,
key_information_learned="",
@@ -222,10 +214,6 @@ class PlannerObserver:
return remaining_todos
- # ------------------------------------------------------------------
- # Internal: Message building
- # ------------------------------------------------------------------
-
def _build_observation_messages(
self,
completed_step: TodoItem,
@@ -240,15 +228,11 @@ class PlannerObserver:
task_desc = self.task.description or ""
task_goal = self.task.expected_output or ""
elif self.kickoff_input:
- # Standalone kickoff path — no Task object, but we have the raw input.
- # Extract just the ## Task section so the observer sees the actual goal,
- # not the full enriched instruction with env/tools/verification noise.
task_desc = extract_task_section(self.kickoff_input)
task_goal = "Complete the task successfully"
- system_prompt = self._i18n.retrieve("planning", "observation_system_prompt")
+ system_prompt = I18N_DEFAULT.retrieve("planning", "observation_system_prompt")
- # Build context of what's been done
completed_summary = ""
if all_completed:
completed_lines = []
@@ -262,7 +246,6 @@ class PlannerObserver:
completed_lines
)
- # Build remaining plan
remaining_summary = ""
if remaining_todos:
remaining_lines = [
@@ -273,7 +256,9 @@ class PlannerObserver:
remaining_lines
)
- user_prompt = self._i18n.retrieve("planning", "observation_user_prompt").format(
+ user_prompt = I18N_DEFAULT.retrieve(
+ "planning", "observation_user_prompt"
+ ).format(
task_description=task_desc,
task_goal=task_goal,
completed_summary=completed_summary,
@@ -305,17 +290,14 @@ class PlannerObserver:
if isinstance(response, StepObservation):
return response
- # JSON string path — most common miss before this fix
if isinstance(response, str):
text = response.strip()
try:
return StepObservation.model_validate_json(text)
except Exception: # noqa: S110
pass
- # Some LLMs wrap the JSON in markdown fences
if text.startswith("```"):
lines = text.split("\n")
- # Strip first and last lines (``` markers)
inner = "\n".join(
lines[1:-1] if lines[-1].strip() == "```" else lines[1:]
)
@@ -324,14 +306,12 @@ class PlannerObserver:
except Exception: # noqa: S110
pass
- # Dict path
if isinstance(response, dict):
try:
return StepObservation.model_validate(response)
except Exception: # noqa: S110
pass
- # Last resort — log what we got so it's diagnosable
logger.warning(
"Could not parse observation response (type=%s). "
"Falling back to default failure observation. Preview: %.200s",
diff --git a/lib/crewai/src/crewai/agents/step_executor.py b/lib/crewai/src/crewai/agents/step_executor.py
index 48592efb4..df834e3e4 100644
--- a/lib/crewai/src/crewai/agents/step_executor.py
+++ b/lib/crewai/src/crewai/agents/step_executor.py
@@ -38,7 +38,7 @@ from crewai.utilities.agent_utils import (
process_llm_response,
setup_native_tools,
)
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.planning_types import TodoItem
from crewai.utilities.printer import PRINTER
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
@@ -81,7 +81,7 @@ class StepExecutor:
function_calling_llm: Optional separate LLM for function calling.
request_within_rpm_limit: Optional RPM limit function.
callbacks: Optional list of callbacks.
- i18n: Optional i18n instance.
+
"""
def __init__(
@@ -96,7 +96,6 @@ class StepExecutor:
function_calling_llm: BaseLLM | None = None,
request_within_rpm_limit: Callable[[], bool] | None = None,
callbacks: list[Any] | None = None,
- i18n: I18N | None = None,
) -> None:
self.llm = llm
self.tools = tools
@@ -108,9 +107,7 @@ class StepExecutor:
self.function_calling_llm = function_calling_llm
self.request_within_rpm_limit = request_within_rpm_limit
self.callbacks = callbacks or []
- self._i18n: I18N = i18n or get_i18n()
- # Native tool support — set up once
self._use_native_tools = check_native_tool_support(
self.llm, self.original_tools
)
@@ -123,10 +120,6 @@ class StepExecutor:
_,
) = setup_native_tools(self.original_tools)
- # ------------------------------------------------------------------
- # Public API
- # ------------------------------------------------------------------
-
def execute(
self,
todo: TodoItem,
@@ -192,10 +185,6 @@ class StepExecutor:
execution_time=elapsed,
)
- # ------------------------------------------------------------------
- # Internal: Message building
- # ------------------------------------------------------------------
-
def _build_isolated_messages(
self, todo: TodoItem, context: StepExecutionContext
) -> list[LLMMessage]:
@@ -221,14 +210,14 @@ class StepExecutor:
tools_section = ""
if self.tools and not self._use_native_tools:
tool_names = ", ".join(sanitize_tool_name(t.name) for t in self.tools)
- tools_section = self._i18n.retrieve(
+ tools_section = I18N_DEFAULT.retrieve(
"planning", "step_executor_tools_section"
).format(tool_names=tool_names)
elif self.tools:
tool_names = ", ".join(sanitize_tool_name(t.name) for t in self.tools)
tools_section = f"\n\nAvailable tools: {tool_names}"
- return self._i18n.retrieve("planning", "step_executor_system_prompt").format(
+ return I18N_DEFAULT.retrieve("planning", "step_executor_system_prompt").format(
role=role,
backstory=backstory,
goal=goal,
@@ -239,15 +228,11 @@ class StepExecutor:
"""Build the user prompt for this specific step."""
parts: list[str] = []
- # Include overall task context so the executor knows the full goal and
- # required output format/location — critical for knowing WHAT to produce.
- # We extract only the task body (not tool instructions or verification
- # sections) to avoid duplicating directives already in the system prompt.
if context.task_description:
task_section = extract_task_section(context.task_description)
if task_section:
parts.append(
- self._i18n.retrieve(
+ I18N_DEFAULT.retrieve(
"planning", "step_executor_task_context"
).format(
task_context=task_section,
@@ -255,38 +240,35 @@ class StepExecutor:
)
parts.append(
- self._i18n.retrieve("planning", "step_executor_user_prompt").format(
+ I18N_DEFAULT.retrieve("planning", "step_executor_user_prompt").format(
step_description=todo.description,
)
)
if todo.tool_to_use:
parts.append(
- self._i18n.retrieve("planning", "step_executor_suggested_tool").format(
+ I18N_DEFAULT.retrieve(
+ "planning", "step_executor_suggested_tool"
+ ).format(
tool_to_use=todo.tool_to_use,
)
)
- # Include dependency results (final results only, no traces)
if context.dependency_results:
parts.append(
- self._i18n.retrieve("planning", "step_executor_context_header")
+ I18N_DEFAULT.retrieve("planning", "step_executor_context_header")
)
for step_num, result in sorted(context.dependency_results.items()):
parts.append(
- self._i18n.retrieve(
+ I18N_DEFAULT.retrieve(
"planning", "step_executor_context_entry"
).format(step_number=step_num, result=result)
)
- parts.append(self._i18n.retrieve("planning", "step_executor_complete_step"))
+ parts.append(I18N_DEFAULT.retrieve("planning", "step_executor_complete_step"))
return "\n".join(parts)
- # ------------------------------------------------------------------
- # Internal: Multi-turn execution loop
- # ------------------------------------------------------------------
-
def _execute_text_parsed(
self,
messages: list[LLMMessage],
@@ -306,7 +288,6 @@ class StepExecutor:
last_tool_result = ""
for _ in range(max_step_iterations):
- # Check step timeout
if step_timeout and start_time:
elapsed = time.monotonic() - start_time
if elapsed >= step_timeout:
@@ -331,17 +312,12 @@ class StepExecutor:
tool_calls_made.append(formatted.tool)
tool_result = self._execute_text_tool_with_events(formatted)
last_tool_result = tool_result
- # Append the assistant's reasoning + action, then the observation.
- # _build_observation_message handles vision sentinels so the LLM
- # receives an image content block instead of raw base64 text.
messages.append({"role": "assistant", "content": answer_str})
messages.append(self._build_observation_message(tool_result))
continue
- # Raw text response with no Final Answer marker — treat as done
return answer_str
- # Max iterations reached — return the last tool result we accumulated
return last_tool_result
def _execute_text_tool_with_events(self, formatted: AgentAction) -> str:
@@ -375,7 +351,6 @@ class StepExecutor:
agent_action=formatted,
fingerprint_context=fingerprint_context,
tools=self.tools,
- i18n=self._i18n,
agent_key=self.agent.key if self.agent else None,
agent_role=self.agent.role if self.agent else None,
tools_handler=self.tools_handler,
@@ -430,10 +405,6 @@ class StepExecutor:
return {"input": stripped_input}
return {"input": str(tool_input)}
- # ------------------------------------------------------------------
- # Internal: Vision support
- # ------------------------------------------------------------------
-
@staticmethod
def _parse_vision_sentinel(raw: str) -> tuple[str, str] | None:
"""Parse a VISION_IMAGE sentinel into (media_type, base64_data), or None."""
@@ -518,7 +489,6 @@ class StepExecutor:
accumulated_results: list[str] = []
for _ in range(max_step_iterations):
- # Check step timeout
if step_timeout and start_time:
elapsed = time.monotonic() - start_time
if elapsed >= step_timeout:
@@ -542,19 +512,14 @@ class StepExecutor:
return answer.model_dump_json()
if isinstance(answer, list) and answer and is_tool_call_list(answer):
- # _execute_native_tool_calls appends assistant + tool messages
- # to `messages` as a side-effect, so the next LLM call will
- # see the full conversation history including tool outputs.
result = self._execute_native_tool_calls(
answer, messages, tool_calls_made
)
accumulated_results.append(result)
continue
- # Text answer → LLM decided the step is done
return str(answer)
- # Max iterations reached — return everything we accumulated
return "\n".join(filter(None, accumulated_results))
def _execute_native_tool_calls(
@@ -600,9 +565,6 @@ class StepExecutor:
parsed = self._parse_vision_sentinel(raw_content)
if parsed:
media_type, b64_data = parsed
- # Replace the sentinel with a standard image_url content block.
- # Each provider's _format_messages handles conversion to
- # its native format (e.g. Anthropic image blocks).
modified: LLMMessage = cast(
LLMMessage, dict(call_result.tool_message)
)
diff --git a/lib/crewai/src/crewai/cli/checkpoint_cli.py b/lib/crewai/src/crewai/cli/checkpoint_cli.py
index c61500b20..9db783e0e 100644
--- a/lib/crewai/src/crewai/cli/checkpoint_cli.py
+++ b/lib/crewai/src/crewai/cli/checkpoint_cli.py
@@ -2,16 +2,20 @@
from __future__ import annotations
-from datetime import datetime
+from datetime import datetime, timedelta, timezone
import glob
import json
import os
+import re
import sqlite3
from typing import Any
import click
+_PLACEHOLDER_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_\-]*)}")
+
+
_SQLITE_MAGIC = b"SQLite format 3\x00"
_SELECT_ALL = """
@@ -33,6 +37,45 @@ ORDER BY rowid DESC
LIMIT 1
"""
+_DELETE_OLDER_THAN = """
+DELETE FROM checkpoints
+WHERE created_at < ?
+"""
+
+_DELETE_KEEP_N = """
+DELETE FROM checkpoints WHERE rowid NOT IN (
+ SELECT rowid FROM checkpoints ORDER BY rowid DESC LIMIT ?
+)
+"""
+
+_COUNT_CHECKPOINTS = "SELECT COUNT(*) FROM checkpoints"
+
+_SELECT_LIKE = """
+SELECT id, created_at, json(data)
+FROM checkpoints
+WHERE id LIKE ?
+ORDER BY rowid DESC
+"""
+
+
+_DEFAULT_DIR = "./.checkpoints"
+_DEFAULT_DB = "./.checkpoints.db"
+
+
+def _detect_location(location: str) -> str:
+ """Resolve the default checkpoint location.
+
+ When the caller passes the default directory path, check whether a
+ SQLite database exists at the conventional ``.db`` path and prefer it.
+ """
+ if (
+ location == _DEFAULT_DIR
+ and not os.path.exists(_DEFAULT_DIR)
+ and os.path.exists(_DEFAULT_DB)
+ ):
+ return _DEFAULT_DB
+ return location
+
def _is_sqlite(path: str) -> bool:
"""Check if a file is a SQLite database by reading its magic bytes."""
@@ -52,13 +95,7 @@ def _parse_checkpoint_json(raw: str, source: str) -> dict[str, Any]:
nodes = data.get("event_record", {}).get("nodes", {})
event_count = len(nodes)
- trigger_event = None
- if nodes:
- last_node = max(
- nodes.values(),
- key=lambda n: n.get("event", {}).get("emission_sequence") or 0,
- )
- trigger_event = last_node.get("event", {}).get("type")
+ trigger_event = data.get("trigger")
parsed_entities: list[dict[str, Any]] = []
for entity in entities:
@@ -76,16 +113,47 @@ def _parse_checkpoint_json(raw: str, source: str) -> dict[str, Any]:
{
"description": t.get("description", ""),
"completed": t.get("output") is not None,
+ "output": (t.get("output") or {}).get("raw", ""),
}
for t in tasks
]
parsed_entities.append(info)
+ inputs: dict[str, Any] = {}
+ for entity in entities:
+ cp_inputs = entity.get("checkpoint_inputs")
+ if isinstance(cp_inputs, dict) and cp_inputs:
+ inputs = dict(cp_inputs)
+ break
+
+ for entity in entities:
+ for task in entity.get("tasks", []):
+ for field in (
+ "checkpoint_original_description",
+ "checkpoint_original_expected_output",
+ ):
+ text = task.get(field) or ""
+ for match in _PLACEHOLDER_RE.findall(text):
+ if match not in inputs:
+ inputs[match] = ""
+ for agent in entity.get("agents", []):
+ for field in ("role", "goal", "backstory"):
+ text = agent.get(field) or ""
+ for match in _PLACEHOLDER_RE.findall(text):
+ if match not in inputs:
+ inputs[match] = ""
+
+ branch = data.get("branch", "main")
+ parent_id = data.get("parent_id")
+
return {
"source": source,
"event_count": event_count,
"trigger": trigger_event,
"entities": parsed_entities,
+ "branch": branch,
+ "parent_id": parent_id,
+ "inputs": inputs,
}
@@ -125,9 +193,11 @@ def _entity_summary(entities: list[dict[str, Any]]) -> str:
def _list_json(location: str) -> list[dict[str, Any]]:
- pattern = os.path.join(location, "*.json")
+ pattern = os.path.join(location, "**", "*.json")
results = []
- for path in sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True):
+ for path in sorted(
+ glob.glob(pattern, recursive=True), key=os.path.getmtime, reverse=True
+ ):
name = os.path.basename(path)
try:
with open(path) as f:
@@ -144,8 +214,10 @@ def _list_json(location: str) -> list[dict[str, Any]]:
def _info_json_latest(location: str) -> dict[str, Any] | None:
- pattern = os.path.join(location, "*.json")
- files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
+ pattern = os.path.join(location, "**", "*.json")
+ files = sorted(
+ glob.glob(pattern, recursive=True), key=os.path.getmtime, reverse=True
+ )
if not files:
return None
path = files[0]
@@ -189,6 +261,7 @@ def _list_sqlite(db_path: str) -> list[dict[str, Any]]:
"entities": [],
"source": checkpoint_id,
}
+ meta["db"] = db_path
results.append(meta)
return results
@@ -209,6 +282,8 @@ def _info_sqlite_latest(db_path: str) -> dict[str, Any] | None:
def _info_sqlite_id(db_path: str, checkpoint_id: str) -> dict[str, Any] | None:
with sqlite3.connect(db_path) as conn:
row = conn.execute(_SELECT_ONE, (checkpoint_id,)).fetchone()
+ if not row:
+ row = conn.execute(_SELECT_LIKE, (f"%{checkpoint_id}%",)).fetchone()
if not row:
return None
cid, created_at, raw = row
@@ -311,6 +386,10 @@ def _print_info(meta: dict[str, Any]) -> None:
trigger = meta.get("trigger")
if trigger:
click.echo(f"Trigger: {trigger}")
+ click.echo(f"Branch: {meta.get('branch', 'main')}")
+ parent_id = meta.get("parent_id")
+ if parent_id:
+ click.echo(f"Parent: {parent_id}")
for ent in meta.get("entities", []):
eid = str(ent.get("id", ""))[:8]
@@ -327,3 +406,287 @@ def _print_info(meta: dict[str, Any]) -> None:
if len(desc) > 70:
desc = desc[:67] + "..."
click.echo(f" {i + 1}. [{status}] {desc}")
+
+
+def _resolve_checkpoint(
+ location: str, checkpoint_id: str | None
+) -> dict[str, Any] | None:
+ if _is_sqlite(location):
+ if checkpoint_id:
+ return _info_sqlite_id(location, checkpoint_id)
+ return _info_sqlite_latest(location)
+ if os.path.isdir(location):
+ if checkpoint_id:
+ from crewai.state.provider.json_provider import JsonProvider
+
+ _json_provider: JsonProvider = JsonProvider()
+ pattern: str = os.path.join(location, "**", "*.json")
+ all_files: list[str] = glob.glob(pattern, recursive=True)
+ matches: list[str] = [
+ f for f in all_files if checkpoint_id in _json_provider.extract_id(f)
+ ]
+ matches.sort(key=os.path.getmtime, reverse=True)
+ if matches:
+ return _info_json_file(matches[0])
+ return None
+ return _info_json_latest(location)
+ if os.path.isfile(location):
+ return _info_json_file(location)
+ return None
+
+
+def _entity_type_from_meta(meta: dict[str, Any]) -> str:
+ for ent in meta.get("entities", []):
+ if ent.get("type") == "flow":
+ return "flow"
+ return "crew"
+
+
+def resume_checkpoint(location: str, checkpoint_id: str | None) -> None:
+ import asyncio
+
+ meta: dict[str, Any] | None = _resolve_checkpoint(location, checkpoint_id)
+ if meta is None:
+ if checkpoint_id:
+ click.echo(f"Checkpoint not found: {checkpoint_id}")
+ else:
+ click.echo(f"No checkpoints found in {location}")
+ return
+
+ restore_path: str = meta.get("path") or meta.get("source", "")
+ if meta.get("db"):
+ restore_path = f"{meta['db']}#{meta['name']}"
+
+ click.echo(f"Resuming from: {meta.get('name', restore_path)}")
+ _print_info(meta)
+ click.echo()
+
+ from crewai.state.checkpoint_config import CheckpointConfig
+
+ config: CheckpointConfig = CheckpointConfig(restore_from=restore_path)
+ entity_type: str = _entity_type_from_meta(meta)
+ inputs: dict[str, Any] | None = meta.get("inputs") or None
+
+ if entity_type == "flow":
+ from crewai.flow.flow import Flow
+
+ flow = Flow.from_checkpoint(config)
+ result = asyncio.run(flow.kickoff_async(inputs=inputs))
+ else:
+ from crewai.crew import Crew
+
+ crew = Crew.from_checkpoint(config)
+ result = asyncio.run(crew.akickoff(inputs=inputs))
+
+ click.echo(f"\nResult: {getattr(result, 'raw', result)}")
+
+
+def _task_list_from_meta(meta: dict[str, Any]) -> list[dict[str, Any]]:
+ tasks: list[dict[str, Any]] = []
+ for ent in meta.get("entities", []):
+ tasks.extend(
+ {
+ "entity": ent.get("name", "unnamed"),
+ "description": t.get("description", ""),
+ "completed": t.get("completed", False),
+ "output": t.get("output", ""),
+ }
+ for t in ent.get("tasks", [])
+ )
+ return tasks
+
+
+def diff_checkpoints(location: str, id1: str, id2: str) -> None:
+ meta1: dict[str, Any] | None = _resolve_checkpoint(location, id1)
+ meta2: dict[str, Any] | None = _resolve_checkpoint(location, id2)
+
+ if meta1 is None:
+ click.echo(f"Checkpoint not found: {id1}")
+ return
+ if meta2 is None:
+ click.echo(f"Checkpoint not found: {id2}")
+ return
+
+ name1: str = meta1.get("name", id1)
+ name2: str = meta2.get("name", id2)
+
+ click.echo(f"--- {name1}")
+ click.echo(f"+++ {name2}")
+ click.echo()
+
+ fields: list[tuple[str, str]] = [
+ ("Time", "ts"),
+ ("Branch", "branch"),
+ ("Trigger", "trigger"),
+ ("Events", "event_count"),
+ ]
+ for label, key in fields:
+ v1: str = str(meta1.get(key, ""))
+ v2: str = str(meta2.get(key, ""))
+ if v1 != v2:
+ click.echo(f" {label}:")
+ click.echo(f" - {v1}")
+ click.echo(f" + {v2}")
+
+ inputs1: dict[str, Any] = meta1.get("inputs", {})
+ inputs2: dict[str, Any] = meta2.get("inputs", {})
+ all_keys: list[str] = sorted(set(list(inputs1.keys()) + list(inputs2.keys())))
+ changed_inputs: list[tuple[str, Any, Any]] = [
+ (k, inputs1.get(k, ""), inputs2.get(k, ""))
+ for k in all_keys
+ if inputs1.get(k) != inputs2.get(k)
+ ]
+ if changed_inputs:
+ click.echo("\n Inputs:")
+ for key, v1, v2 in changed_inputs:
+ click.echo(f" {key}:")
+ click.echo(f" - {v1}")
+ click.echo(f" + {v2}")
+
+ tasks1: list[dict[str, Any]] = _task_list_from_meta(meta1)
+ tasks2: list[dict[str, Any]] = _task_list_from_meta(meta2)
+
+ max_tasks: int = max(len(tasks1), len(tasks2))
+ if max_tasks == 0:
+ return
+
+ click.echo("\n Tasks:")
+ for i in range(max_tasks):
+ t1: dict[str, Any] | None = tasks1[i] if i < len(tasks1) else None
+ t2: dict[str, Any] | None = tasks2[i] if i < len(tasks2) else None
+
+ if t1 is None:
+ desc: str = t2["description"][:60] if t2 else ""
+ click.echo(f" + {i + 1}. [new] {desc}")
+ continue
+ if t2 is None:
+ desc = t1["description"][:60]
+ click.echo(f" - {i + 1}. [removed] {desc}")
+ continue
+
+ desc = str(t1["description"][:60])
+ s1: str = "done" if t1["completed"] else "pending"
+ s2: str = "done" if t2["completed"] else "pending"
+
+ if s1 != s2:
+ click.echo(f" {i + 1}. {desc}")
+ click.echo(f" status: {s1} -> {s2}")
+
+ out1: str = (t1.get("output") or "").strip()
+ out2: str = (t2.get("output") or "").strip()
+ if out1 != out2:
+ if s1 == s2:
+ click.echo(f" {i + 1}. {desc}")
+ preview1: str = (
+ out1[:80] + ("..." if len(out1) > 80 else "") if out1 else "(empty)"
+ )
+ preview2: str = (
+ out2[:80] + ("..." if len(out2) > 80 else "") if out2 else "(empty)"
+ )
+ click.echo(" output:")
+ click.echo(f" - {preview1}")
+ click.echo(f" + {preview2}")
+
+
+def _parse_duration(value: str) -> timedelta:
+ match: re.Match[str] | None = re.match(r"^(\d+)([dhm])$", value.strip())
+ if not match:
+ raise click.BadParameter(
+ f"Invalid duration: {value!r}. Use format like '7d', '24h', or '30m'."
+ )
+ amount: int = int(match.group(1))
+ unit: str = match.group(2)
+ if unit == "d":
+ return timedelta(days=amount)
+ if unit == "h":
+ return timedelta(hours=amount)
+ return timedelta(minutes=amount)
+
+
+def _prune_json(location: str, keep: int | None, older_than: timedelta | None) -> int:
+ pattern: str = os.path.join(location, "**", "*.json")
+ files: list[str] = sorted(
+ glob.glob(pattern, recursive=True), key=os.path.getmtime, reverse=True
+ )
+ if not files:
+ return 0
+
+ to_delete: set[str] = set()
+
+ if keep is not None and len(files) > keep:
+ to_delete.update(files[keep:])
+
+ if older_than is not None:
+ cutoff: datetime = datetime.now(timezone.utc) - older_than
+ for path in files:
+ mtime: datetime = datetime.fromtimestamp(
+ os.path.getmtime(path), tz=timezone.utc
+ )
+ if mtime < cutoff:
+ to_delete.add(path)
+
+ deleted: int = 0
+ for path in to_delete:
+ try:
+ os.remove(path)
+ deleted += 1
+ except OSError: # noqa: PERF203
+ pass
+
+ for dirpath, dirnames, filenames in os.walk(location, topdown=False):
+ if dirpath != location and not filenames and not dirnames:
+ try:
+ os.rmdir(dirpath)
+ except OSError:
+ pass
+
+ return deleted
+
+
+def _prune_sqlite(db_path: str, keep: int | None, older_than: timedelta | None) -> int:
+ deleted: int = 0
+ with sqlite3.connect(db_path) as conn:
+ if older_than is not None:
+ cutoff: str = (datetime.now(timezone.utc) - older_than).strftime(
+ "%Y%m%dT%H%M%S"
+ )
+ cursor: sqlite3.Cursor = conn.execute(_DELETE_OLDER_THAN, (cutoff,))
+ deleted += cursor.rowcount
+
+ if keep is not None:
+ cursor = conn.execute(_DELETE_KEEP_N, (keep,))
+ deleted += cursor.rowcount
+
+ conn.commit()
+ return deleted
+
+
+def prune_checkpoints(
+ location: str, keep: int | None, older_than: str | None, dry_run: bool = False
+) -> None:
+ if keep is None and older_than is None:
+ click.echo("Specify --keep N and/or --older-than DURATION (e.g. 7d, 24h)")
+ return
+
+ duration: timedelta | None = _parse_duration(older_than) if older_than else None
+
+ deleted: int
+ if _is_sqlite(location):
+ if dry_run:
+ with sqlite3.connect(location) as conn:
+ total: int = conn.execute(_COUNT_CHECKPOINTS).fetchone()[0]
+ click.echo(f"Would prune from {total} checkpoint(s) in {location}")
+ return
+ deleted = _prune_sqlite(location, keep, duration)
+ elif os.path.isdir(location):
+ if dry_run:
+ files: list[str] = glob.glob(
+ os.path.join(location, "**", "*.json"), recursive=True
+ )
+ click.echo(f"Would prune from {len(files)} checkpoint(s) in {location}")
+ return
+ deleted = _prune_json(location, keep, duration)
+ else:
+ click.echo(f"Not a directory or SQLite database: {location}")
+ return
+ click.echo(f"Pruned {deleted} checkpoint(s) from {location}")
diff --git a/lib/crewai/src/crewai/cli/checkpoint_tui.py b/lib/crewai/src/crewai/cli/checkpoint_tui.py
new file mode 100644
index 000000000..26791af23
--- /dev/null
+++ b/lib/crewai/src/crewai/cli/checkpoint_tui.py
@@ -0,0 +1,686 @@
+"""Textual TUI for browsing checkpoint files."""
+
+from __future__ import annotations
+
+from collections import defaultdict
+from typing import Any, ClassVar, Literal
+
+from textual.app import App, ComposeResult
+from textual.binding import Binding
+from textual.containers import Horizontal, Vertical, VerticalScroll
+from textual.widgets import (
+ Button,
+ Footer,
+ Header,
+ Input,
+ Static,
+ TextArea,
+ Tree,
+)
+
+from crewai.cli.checkpoint_cli import (
+ _format_size,
+ _is_sqlite,
+ _list_json,
+ _list_sqlite,
+)
+
+
+_PRIMARY = "#eb6658"
+_SECONDARY = "#1F7982"
+_TERTIARY = "#ffffff"
+_DIM = "#888888"
+_BG_DARK = "#0d1117"
+_BG_PANEL = "#161b22"
+
+
+def _load_entries(location: str) -> list[dict[str, Any]]:
+ if _is_sqlite(location):
+ return _list_sqlite(location)
+ return _list_json(location)
+
+
+def _short_id(name: str) -> str:
+ """Shorten a checkpoint name for tree display."""
+ if len(name) > 30:
+ return name[:27] + "..."
+ return name
+
+
+def _entry_id(entry: dict[str, Any]) -> str:
+ """Normalize an entry's name into its checkpoint ID.
+
+ JSON filenames are ``{ts}_{uuid}_p-{parent}.json``; SQLite IDs
+ are already ``{ts}_{uuid}``. This strips the JSON suffix so
+ fork-parent lookups work in both providers.
+ """
+ name = str(entry.get("name", ""))
+ if name.endswith(".json"):
+ name = name[: -len(".json")]
+ idx = name.find("_p-")
+ if idx != -1:
+ name = name[:idx]
+ return name
+
+
+def _build_entity_header(ent: dict[str, Any]) -> str:
+ """Build rich text header for an entity (progress bar only)."""
+ lines: list[str] = []
+ tasks = ent.get("tasks")
+ if isinstance(tasks, list):
+ completed = ent.get("tasks_completed", 0)
+ total = ent.get("tasks_total", 0)
+ pct = int(completed / total * 100) if total else 0
+ bar_len = 20
+ filled = int(bar_len * completed / total) if total else 0
+ bar = f"[{_PRIMARY}]{'█' * filled}[/][{_DIM}]{'░' * (bar_len - filled)}[/]"
+ lines.append(f"{bar} {completed}/{total} tasks ({pct}%)")
+ return "\n".join(lines)
+
+
+# Return type: (location, action, inputs, task_output_overrides, entity_type)
+_TuiResult = (
+ tuple[
+ str,
+ str,
+ dict[str, Any] | None,
+ dict[int, str] | None,
+ Literal["crew", "flow"],
+ ]
+ | None
+)
+
+
+class CheckpointTUI(App[_TuiResult]):
+ """TUI to browse and inspect checkpoints.
+
+ Returns ``(location, action, inputs, task_overrides, entity_type)``
+ where action is ``"resume"`` or ``"fork"``, inputs is a parsed dict
+ or ``None``, and entity_type is ``"crew"`` or ``"flow"``;
+ or ``None`` if the user quit without selecting.
+ """
+
+ TITLE = "CrewAI Checkpoints"
+
+ CSS = f"""
+ Screen {{
+ background: {_BG_DARK};
+ }}
+ Header {{
+ background: {_PRIMARY};
+ color: {_TERTIARY};
+ }}
+ Footer {{
+ background: {_SECONDARY};
+ color: {_TERTIARY};
+ }}
+ Footer > .footer-key--key {{
+ background: {_PRIMARY};
+ color: {_TERTIARY};
+ }}
+ #main-layout {{
+ height: 1fr;
+ }}
+ #tree-panel {{
+ width: 45%;
+ background: {_BG_PANEL};
+ border: round {_SECONDARY};
+ padding: 0 1;
+ scrollbar-color: {_PRIMARY};
+ }}
+ #tree-panel:focus-within {{
+ border: round {_PRIMARY};
+ }}
+ #detail-container {{
+ width: 55%;
+ height: 1fr;
+ }}
+ #detail-scroll {{
+ height: 1fr;
+ background: {_BG_PANEL};
+ border: round {_SECONDARY};
+ padding: 1 2;
+ scrollbar-color: {_PRIMARY};
+ }}
+ #detail-scroll:focus-within {{
+ border: round {_PRIMARY};
+ }}
+ #detail-header {{
+ margin-bottom: 1;
+ }}
+ #status {{
+ height: 1;
+ padding: 0 2;
+ color: {_DIM};
+ }}
+ #inputs-section {{
+ display: none;
+ height: auto;
+ max-height: 8;
+ padding: 0 1;
+ }}
+ #inputs-section.visible {{
+ display: block;
+ }}
+ #inputs-label {{
+ height: 1;
+ color: {_DIM};
+ padding: 0 1;
+ }}
+ .input-row {{
+ height: 3;
+ padding: 0 1;
+ }}
+ .input-row Static {{
+ width: auto;
+ min-width: 12;
+ padding: 1 1 0 0;
+ color: {_TERTIARY};
+ }}
+ .input-row Input {{
+ width: 1fr;
+ }}
+ #no-inputs-label {{
+ height: 1;
+ color: {_DIM};
+ padding: 0 1;
+ }}
+ #action-buttons {{
+ height: 3;
+ align: right middle;
+ padding: 0 1;
+ display: none;
+ }}
+ #action-buttons.visible {{
+ display: block;
+ }}
+ #action-buttons Button {{
+ margin: 0 0 0 1;
+ min-width: 10;
+ }}
+ #btn-resume {{
+ background: {_SECONDARY};
+ color: {_TERTIARY};
+ }}
+ #btn-resume:hover {{
+ background: {_PRIMARY};
+ }}
+ #btn-fork {{
+ background: {_PRIMARY};
+ color: {_TERTIARY};
+ }}
+ #btn-fork:hover {{
+ background: {_SECONDARY};
+ }}
+ .entity-title {{
+ padding: 1 1 0 1;
+ }}
+ .entity-detail {{
+ padding: 0 1;
+ }}
+ .task-output-editor {{
+ height: auto;
+ max-height: 10;
+ margin: 0 1 1 1;
+ border: round {_DIM};
+ }}
+ .task-output-editor:focus {{
+ border: round {_PRIMARY};
+ }}
+ .task-label {{
+ padding: 0 1;
+ }}
+ Tree {{
+ background: {_BG_PANEL};
+ }}
+ Tree > .tree--cursor {{
+ background: {_SECONDARY};
+ color: {_TERTIARY};
+ }}
+ """
+
+ BINDINGS: ClassVar[list[Binding | tuple[str, str] | tuple[str, str, str]]] = [
+ ("q", "quit", "Quit"),
+ ("r", "refresh", "Refresh"),
+ ]
+
+ def __init__(self, location: str = "./.checkpoints") -> None:
+ super().__init__()
+ self._location = location
+ self._entries: list[dict[str, Any]] = []
+ self._selected_entry: dict[str, Any] | None = None
+ self._input_keys: list[str] = []
+ self._task_output_ids: list[tuple[int, str, str]] = []
+
+ def compose(self) -> ComposeResult:
+ yield Header(show_clock=False)
+ with Horizontal(id="main-layout"):
+ tree: Tree[dict[str, Any]] = Tree("Checkpoints", id="tree-panel")
+ tree.show_root = True
+ tree.guide_depth = 3
+ yield tree
+ with Vertical(id="detail-container"):
+ yield Static("", id="status")
+ with VerticalScroll(id="detail-scroll"):
+ yield Static(
+ f"[{_DIM}]Select a checkpoint from the tree[/]", # noqa: S608
+ id="detail-header",
+ )
+ with Vertical(id="inputs-section"):
+ yield Static("Inputs", id="inputs-label")
+ with Horizontal(id="action-buttons"):
+ yield Button("Resume", id="btn-resume")
+ yield Button("Fork", id="btn-fork")
+ yield Footer()
+
+ async def on_mount(self) -> None:
+ self._refresh_tree()
+ self.query_one("#tree-panel", Tree).root.expand()
+
+ def _refresh_tree(self) -> None:
+ self._entries = _load_entries(self._location)
+ self._selected_entry = None
+
+ tree = self.query_one("#tree-panel", Tree)
+ tree.clear()
+
+ if not self._entries:
+ self.query_one("#detail-header", Static).update(
+ f"[{_DIM}]No checkpoints in {self._location}[/]"
+ )
+ self.query_one("#status", Static).update("")
+ self.sub_title = self._location
+ return
+
+ # Group by branch
+ branches: dict[str, list[dict[str, Any]]] = defaultdict(list)
+ for entry in self._entries:
+ branch = entry.get("branch", "main")
+ branches[branch].append(entry)
+
+ # Index checkpoint names to tree nodes so forks can attach
+ node_by_name: dict[str, Any] = {}
+
+ def _make_label(e: dict[str, Any]) -> str:
+ name = e.get("name", "")
+ ts = e.get("ts") or ""
+ trigger = e.get("trigger") or ""
+ parts = [f"[bold]{_short_id(name)}[/]"]
+ if ts:
+ time_part = ts.split(" ")[-1] if " " in ts else ts
+ parts.append(f"[{_DIM}]{time_part}[/]")
+ if trigger:
+ parts.append(f"[{_PRIMARY}]{trigger}[/]")
+ return " ".join(parts)
+
+ fork_parents: set[str] = set()
+ for branch_name, entries in branches.items():
+ if branch_name == "main" or not entries:
+ continue
+ oldest = min(entries, key=lambda e: str(e.get("name", "")))
+ first_parent = oldest.get("parent_id")
+ if first_parent:
+ fork_parents.add(str(first_parent))
+
+ def _add_checkpoint(parent_node: Any, e: dict[str, Any]) -> None:
+ """Add a checkpoint node — expandable only if a fork attaches to it."""
+ cp_id = _entry_id(e)
+ if cp_id in fork_parents:
+ node = parent_node.add(
+ _make_label(e), data=e, expand=False, allow_expand=True
+ )
+ else:
+ node = parent_node.add_leaf(_make_label(e), data=e)
+ node_by_name[cp_id] = node
+
+ if "main" in branches:
+ for entry in reversed(branches["main"]):
+ _add_checkpoint(tree.root, entry)
+
+ fork_branches = [
+ (name, sorted(entries, key=lambda e: str(e.get("name", ""))))
+ for name, entries in branches.items()
+ if name != "main"
+ ]
+ remaining = fork_branches
+ max_passes = len(remaining) + 1
+ while remaining and max_passes > 0:
+ max_passes -= 1
+ deferred = []
+ made_progress = False
+ for branch_name, entries in remaining:
+ first_parent = entries[0].get("parent_id") if entries else None
+ if first_parent and str(first_parent) not in node_by_name:
+ deferred.append((branch_name, entries))
+ continue
+ attach_to: Any = tree.root
+ if first_parent:
+ attach_to = node_by_name.get(str(first_parent), tree.root)
+ branch_label = (
+ f"[bold {_SECONDARY}]{branch_name}[/] [{_DIM}]({len(entries)})[/]"
+ )
+ branch_node = attach_to.add(branch_label, expand=False)
+ for entry in entries:
+ _add_checkpoint(branch_node, entry)
+ made_progress = True
+ remaining = deferred
+ if not made_progress:
+ break
+
+ for branch_name, entries in remaining:
+ branch_label = (
+ f"[bold {_SECONDARY}]{branch_name}[/] "
+ f"[{_DIM}]({len(entries)})[/] [{_DIM}](orphaned)[/]"
+ )
+ branch_node = tree.root.add(branch_label, expand=False)
+ for entry in entries:
+ _add_checkpoint(branch_node, entry)
+
+ count = len(self._entries)
+ storage = "SQLite" if _is_sqlite(self._location) else "JSON"
+ self.sub_title = self._location
+ self.query_one("#status", Static).update(f" {count} checkpoint(s) | {storage}")
+
+ async def _show_detail(self, entry: dict[str, Any]) -> None:
+ """Update the detail panel for a checkpoint entry."""
+ self._selected_entry = entry
+ self.query_one("#action-buttons").add_class("visible")
+
+ detail_scroll = self.query_one("#detail-scroll", VerticalScroll)
+
+ # Remove all dynamic children except the header — await so IDs are freed
+ to_remove = [c for c in detail_scroll.children if c.id != "detail-header"]
+ for child in to_remove:
+ await child.remove()
+
+ # Header
+ name = entry.get("name", "")
+ ts = entry.get("ts") or "unknown"
+ trigger = entry.get("trigger") or ""
+ branch = entry.get("branch", "main")
+ parent_id = entry.get("parent_id")
+
+ header_lines = [
+ f"[bold {_PRIMARY}]{name}[/]",
+ f"[{_DIM}]{'─' * 50}[/]",
+ "",
+ f" [bold]Time[/] {ts}",
+ ]
+ if "size" in entry:
+ header_lines.append(f" [bold]Size[/] {_format_size(entry['size'])}")
+ header_lines.append(f" [bold]Events[/] {entry.get('event_count', 0)}")
+ if trigger:
+ header_lines.append(f" [bold]Trigger[/] [{_PRIMARY}]{trigger}[/]")
+ header_lines.append(f" [bold]Branch[/] [{_SECONDARY}]{branch}[/]")
+ if parent_id:
+ header_lines.append(f" [bold]Parent[/] [{_DIM}]{parent_id}[/]")
+ if "path" in entry:
+ header_lines.append(f" [bold]Path[/] [{_DIM}]{entry['path']}[/]")
+ if "db" in entry:
+ header_lines.append(f" [bold]Database[/] [{_DIM}]{entry['db']}[/]")
+
+ self.query_one("#detail-header", Static).update("\n".join(header_lines))
+
+ # Entity details and editable task outputs — mounted flat for scrolling
+ self._task_output_ids = []
+ flat_task_idx = 0
+ for ent_idx, ent in enumerate(entry.get("entities", [])):
+ etype = ent.get("type", "unknown")
+ ename = ent.get("name", "unnamed")
+ completed = ent.get("tasks_completed")
+ total = ent.get("tasks_total")
+ entity_title = f"[bold {_SECONDARY}]{etype}: {ename}[/]"
+ if completed is not None and total is not None:
+ entity_title += f" [{_DIM}]{completed}/{total} tasks[/]"
+ await detail_scroll.mount(Static(entity_title, classes="entity-title"))
+ await detail_scroll.mount(
+ Static(_build_entity_header(ent), classes="entity-detail")
+ )
+
+ tasks = ent.get("tasks", [])
+ for i, task in enumerate(tasks):
+ desc = str(task.get("description", ""))
+ if len(desc) > 55:
+ desc = desc[:52] + "..."
+ if task.get("completed"):
+ icon = "[green]✓[/]"
+ await detail_scroll.mount(
+ Static(f" {icon} {i + 1}. {desc}", classes="task-label")
+ )
+ output_text = task.get("output", "")
+ editor_id = f"task-output-{ent_idx}-{i}"
+ await detail_scroll.mount(
+ TextArea(
+ str(output_text),
+ classes="task-output-editor",
+ id=editor_id,
+ )
+ )
+ self._task_output_ids.append(
+ (flat_task_idx, editor_id, str(output_text))
+ )
+ else:
+ icon = "[yellow]○[/]"
+ await detail_scroll.mount(
+ Static(f" {icon} {i + 1}. {desc}", classes="task-label")
+ )
+ flat_task_idx += 1
+
+ # Build input fields
+ await self._build_input_fields(entry.get("inputs", {}))
+
+ async def _build_input_fields(self, inputs: dict[str, Any]) -> None:
+ """Rebuild the inputs section with one field per input key."""
+ section = self.query_one("#inputs-section")
+
+ # Remove old dynamic children — await so IDs are freed
+ for widget in list(section.query(".input-row, .no-inputs")):
+ await widget.remove()
+
+ self._input_keys = []
+
+ if not inputs:
+ await section.mount(Static(f"[{_DIM}]No inputs[/]", classes="no-inputs"))
+ section.add_class("visible")
+ return
+
+ for key, value in inputs.items():
+ self._input_keys.append(key)
+ row = Horizontal(classes="input-row")
+ row.compose_add_child(Static(f"[bold]{key}[/]"))
+ row.compose_add_child(
+ Input(value=str(value), placeholder=key, id=f"input-{key}")
+ )
+ await section.mount(row)
+
+ section.add_class("visible")
+
+ def _collect_inputs(self) -> dict[str, Any] | None:
+ """Collect current values from input fields."""
+ if not self._input_keys:
+ return None
+ result: dict[str, Any] = {}
+ for key in self._input_keys:
+ widget = self.query_one(f"#input-{key}", Input)
+ result[key] = widget.value
+ return result
+
+ def _collect_task_overrides(self) -> dict[int, str] | None:
+ """Collect edited task outputs. Returns only changed values."""
+ if not self._task_output_ids or self._selected_entry is None:
+ return None
+ overrides: dict[int, str] = {}
+ for task_idx, editor_id, original in self._task_output_ids:
+ editor = self.query_one(f"#{editor_id}", TextArea)
+ if editor.text != original:
+ overrides[task_idx] = editor.text
+ return overrides or None
+
+ def _detect_entity_type(self, entry: dict[str, Any]) -> Literal["crew", "flow"]:
+ """Infer the top-level entity type from checkpoint entities."""
+ for ent in entry.get("entities", []):
+ if ent.get("type") == "flow":
+ return "flow"
+ return "crew"
+
+ def _resolve_location(self, entry: dict[str, Any]) -> str:
+ """Get the restore location string for a checkpoint entry."""
+ if "path" in entry:
+ return str(entry["path"])
+ if _is_sqlite(self._location):
+ return f"{self._location}#{entry['name']}"
+ return str(entry.get("name", ""))
+
+ async def on_tree_node_highlighted(
+ self, event: Tree.NodeHighlighted[dict[str, Any]]
+ ) -> None:
+ if event.node.data is not None:
+ await self._show_detail(event.node.data)
+
+ def on_button_pressed(self, event: Button.Pressed) -> None:
+ if self._selected_entry is None:
+ return
+ inputs = self._collect_inputs()
+ overrides = self._collect_task_overrides()
+ loc = self._resolve_location(self._selected_entry)
+ etype = self._detect_entity_type(self._selected_entry)
+ if event.button.id == "btn-resume":
+ self.exit((loc, "resume", inputs, overrides, etype))
+ elif event.button.id == "btn-fork":
+ self.exit((loc, "fork", inputs, overrides, etype))
+
+ def action_refresh(self) -> None:
+ self._refresh_tree()
+
+
+def _apply_task_overrides(crew: Any, task_overrides: dict[int, str]) -> None:
+ """Apply task output overrides to a restored Crew and print modifications."""
+ import click
+
+ click.echo("Modifications:")
+ overridden_agents: set[int] = set()
+ for task_idx, new_output in task_overrides.items():
+ if task_idx < len(crew.tasks) and crew.tasks[task_idx].output is not None:
+ desc = crew.tasks[task_idx].description or f"Task {task_idx + 1}"
+ if len(desc) > 60:
+ desc = desc[:57] + "..."
+ crew.tasks[task_idx].output.raw = new_output
+ preview = new_output.replace("\n", " ")
+ if len(preview) > 80:
+ preview = preview[:77] + "..."
+ click.echo(f" Task {task_idx + 1}: {desc}")
+ click.echo(f" -> {preview}")
+ agent = crew.tasks[task_idx].agent
+ if agent and agent.agent_executor:
+ nth = sum(1 for t in crew.tasks[:task_idx] if t.agent is agent)
+ messages = agent.agent_executor.messages
+ system_positions = [
+ i for i, m in enumerate(messages) if m.get("role") == "system"
+ ]
+ if nth < len(system_positions):
+ seg_start = system_positions[nth]
+ seg_end = (
+ system_positions[nth + 1]
+ if nth + 1 < len(system_positions)
+ else len(messages)
+ )
+ for j in range(seg_end - 1, seg_start, -1):
+ if messages[j].get("role") == "assistant":
+ messages[j]["content"] = new_output
+ break
+ overridden_agents.add(id(agent))
+
+ earliest = min(task_overrides)
+ for offset, subsequent in enumerate(crew.tasks[earliest + 1 :], start=earliest + 1):
+ if subsequent.output and offset not in task_overrides:
+ subsequent.output = None
+ if subsequent.agent and subsequent.agent.agent_executor:
+ subsequent.agent.agent_executor._resuming = False
+ if id(subsequent.agent) not in overridden_agents:
+ subsequent.agent.agent_executor.messages = []
+ click.echo()
+
+
+async def _run_checkpoint_tui_async(location: str) -> None:
+ """Async implementation of the checkpoint TUI flow."""
+ import click
+
+ app = CheckpointTUI(location=location)
+ selection = await app.run_async()
+
+ if selection is None:
+ return
+
+ selected, action, inputs, task_overrides, entity_type = selection
+
+ from crewai.state.checkpoint_config import CheckpointConfig
+
+ config = CheckpointConfig(restore_from=selected)
+
+ if entity_type == "flow":
+ from crewai.events.event_bus import crewai_event_bus
+ from crewai.flow.flow import Flow
+
+ if action == "fork":
+ click.echo(f"\nForking flow from: {selected}\n")
+ flow = Flow.fork(config)
+ else:
+ click.echo(f"\nResuming flow from: {selected}\n")
+ flow = Flow.from_checkpoint(config)
+
+ if task_overrides:
+ from crewai.crew import Crew as CrewCls
+
+ state = crewai_event_bus._runtime_state
+ if state is not None:
+ flat_offset = 0
+ for entity in state.root:
+ if not isinstance(entity, CrewCls) or not entity.tasks:
+ continue
+ n = len(entity.tasks)
+ local = {
+ idx - flat_offset: out
+ for idx, out in task_overrides.items()
+ if flat_offset <= idx < flat_offset + n
+ }
+ if local:
+ _apply_task_overrides(entity, local)
+ flat_offset += n
+
+ if inputs:
+ click.echo("Inputs:")
+ for k, v in inputs.items():
+ click.echo(f" {k}: {v}")
+ click.echo()
+
+ result = await flow.kickoff_async(inputs=inputs)
+ click.echo(f"\nResult: {getattr(result, 'raw', result)}")
+ return
+
+ from crewai.crew import Crew
+
+ if action == "fork":
+ click.echo(f"\nForking from: {selected}\n")
+ crew = Crew.fork(config)
+ else:
+ click.echo(f"\nResuming from: {selected}\n")
+ crew = Crew.from_checkpoint(config)
+
+ if task_overrides:
+ _apply_task_overrides(crew, task_overrides)
+
+ if inputs:
+ click.echo("Inputs:")
+ for k, v in inputs.items():
+ click.echo(f" {k}: {v}")
+ click.echo()
+
+ result = await crew.akickoff(inputs=inputs)
+ click.echo(f"\nResult: {getattr(result, 'raw', result)}")
+
+
+def run_checkpoint_tui(location: str = "./.checkpoints") -> None:
+ """Launch the checkpoint browser TUI."""
+ import asyncio
+
+ asyncio.run(_run_checkpoint_tui_async(location))
diff --git a/lib/crewai/src/crewai/cli/cli.py b/lib/crewai/src/crewai/cli/cli.py
index 57ff4551a..dc4284677 100644
--- a/lib/crewai/src/crewai/cli/cli.py
+++ b/lib/crewai/src/crewai/cli/cli.py
@@ -18,6 +18,7 @@ from crewai.cli.install_crew import install_crew
from crewai.cli.kickoff_flow import kickoff_flow
from crewai.cli.organization.main import OrganizationCommand
from crewai.cli.plot_flow import plot_flow
+from crewai.cli.remote_template.main import TemplateCommand
from crewai.cli.replay_from_task import replay_task_command
from crewai.cli.reset_memories_command import reset_memories_command
from crewai.cli.run_crew import run_crew
@@ -392,10 +393,15 @@ def deploy() -> None:
@deploy.command(name="create")
@click.option("-y", "--yes", is_flag=True, help="Skip the confirmation prompt")
-def deploy_create(yes: bool) -> None:
+@click.option(
+ "--skip-validate",
+ is_flag=True,
+ help="Skip the pre-deploy validation checks.",
+)
+def deploy_create(yes: bool, skip_validate: bool) -> None:
"""Create a Crew deployment."""
deploy_cmd = DeployCommand()
- deploy_cmd.create_crew(yes)
+ deploy_cmd.create_crew(yes, skip_validate=skip_validate)
@deploy.command(name="list")
@@ -407,10 +413,28 @@ def deploy_list() -> None:
@deploy.command(name="push")
@click.option("-u", "--uuid", type=str, help="Crew UUID parameter")
-def deploy_push(uuid: str | None) -> None:
+@click.option(
+ "--skip-validate",
+ is_flag=True,
+ help="Skip the pre-deploy validation checks.",
+)
+def deploy_push(uuid: str | None, skip_validate: bool) -> None:
"""Deploy the Crew."""
deploy_cmd = DeployCommand()
- deploy_cmd.deploy(uuid=uuid)
+ deploy_cmd.deploy(uuid=uuid, skip_validate=skip_validate)
+
+
+@deploy.command(name="validate")
+def deploy_validate() -> None:
+ """Validate the current project against common deployment failures.
+
+ Runs the same pre-deploy checks that `crewai deploy create` and
+ `crewai deploy push` run automatically, without contacting the platform.
+ Exits non-zero if any blocking issues are found.
+ """
+ from crewai.cli.deploy.validate import run_validate_command
+
+ run_validate_command()
@deploy.command(name="status")
@@ -473,6 +497,33 @@ def tool_publish(is_public: bool, force: bool) -> None:
tool_cmd.publish(is_public, force)
+@crewai.group()
+def template() -> None:
+ """Browse and install project templates."""
+
+
+@template.command(name="list")
+def template_list() -> None:
+ """List available templates and select one to install."""
+ template_cmd = TemplateCommand()
+ template_cmd.list_templates()
+
+
+@template.command(name="add")
+@click.argument("name")
+@click.option(
+ "-o",
+ "--output-dir",
+ type=str,
+ default=None,
+ help="Directory name for the template (defaults to template name)",
+)
+def template_add(name: str, output_dir: str | None) -> None:
+ """Add a template to the current directory."""
+ template_cmd = TemplateCommand()
+ template_cmd.add_template(name, output_dir)
+
+
@crewai.group()
def flow() -> None:
"""Flow related commands."""
@@ -786,27 +837,83 @@ def traces_status() -> None:
console.print(panel)
-@crewai.group()
-def checkpoint() -> None:
- """Inspect checkpoint files."""
+@crewai.group(invoke_without_command=True)
+@click.option(
+ "--location", default="./.checkpoints", help="Checkpoint directory or SQLite file."
+)
+@click.pass_context
+def checkpoint(ctx: click.Context, location: str) -> None:
+ """Browse and inspect checkpoints. Launches a TUI when called without a subcommand."""
+ from crewai.cli.checkpoint_cli import _detect_location
+
+ location = _detect_location(location)
+ ctx.ensure_object(dict)
+ ctx.obj["location"] = location
+ if ctx.invoked_subcommand is None:
+ from crewai.cli.checkpoint_tui import run_checkpoint_tui
+
+ run_checkpoint_tui(location)
@checkpoint.command("list")
@click.argument("location", default="./.checkpoints")
def checkpoint_list(location: str) -> None:
"""List checkpoints in a directory."""
- from crewai.cli.checkpoint_cli import list_checkpoints
+ from crewai.cli.checkpoint_cli import _detect_location, list_checkpoints
- list_checkpoints(location)
+ list_checkpoints(_detect_location(location))
@checkpoint.command("info")
@click.argument("path", default="./.checkpoints")
def checkpoint_info(path: str) -> None:
"""Show details of a checkpoint. Pass a file or directory for latest."""
- from crewai.cli.checkpoint_cli import info_checkpoint
+ from crewai.cli.checkpoint_cli import _detect_location, info_checkpoint
- info_checkpoint(path)
+ info_checkpoint(_detect_location(path))
+
+
+@checkpoint.command("resume")
+@click.argument("checkpoint_id", required=False, default=None)
+@click.pass_context
+def checkpoint_resume(ctx: click.Context, checkpoint_id: str | None) -> None:
+ """Resume from a checkpoint. Defaults to the most recent."""
+ from crewai.cli.checkpoint_cli import resume_checkpoint
+
+ resume_checkpoint(ctx.obj["location"], checkpoint_id)
+
+
+@checkpoint.command("diff")
+@click.argument("id1")
+@click.argument("id2")
+@click.pass_context
+def checkpoint_diff(ctx: click.Context, id1: str, id2: str) -> None:
+ """Compare two checkpoints side-by-side."""
+ from crewai.cli.checkpoint_cli import diff_checkpoints
+
+ diff_checkpoints(ctx.obj["location"], id1, id2)
+
+
+@checkpoint.command("prune")
+@click.option(
+ "--keep", type=int, default=None, help="Keep the N most recent checkpoints."
+)
+@click.option(
+ "--older-than",
+ default=None,
+ help="Remove checkpoints older than duration (e.g. 7d, 24h, 30m).",
+)
+@click.option(
+ "--dry-run", is_flag=True, help="Show what would be pruned without deleting."
+)
+@click.pass_context
+def checkpoint_prune(
+ ctx: click.Context, keep: int | None, older_than: str | None, dry_run: bool
+) -> None:
+ """Remove old checkpoints."""
+ from crewai.cli.checkpoint_cli import prune_checkpoints
+
+ prune_checkpoints(ctx.obj["location"], keep, older_than, dry_run)
if __name__ == "__main__":
diff --git a/lib/crewai/src/crewai/cli/crew_chat.py b/lib/crewai/src/crewai/cli/crew_chat.py
index ad1c65894..61d9b4d9e 100644
--- a/lib/crewai/src/crewai/cli/crew_chat.py
+++ b/lib/crewai/src/crewai/cli/crew_chat.py
@@ -13,7 +13,6 @@ from packaging import version
import tomli
from crewai.cli.utils import read_toml
-from crewai.cli.version import get_crewai_version
from crewai.crew import Crew
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
@@ -21,6 +20,7 @@ from crewai.types.crew_chat import ChatInputField, ChatInputs
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.printer import PRINTER
from crewai.utilities.types import LLMMessage
+from crewai.utilities.version import get_crewai_version
MIN_REQUIRED_VERSION: Final[Literal["0.98.0"]] = "0.98.0"
diff --git a/lib/crewai/src/crewai/cli/deploy/main.py b/lib/crewai/src/crewai/cli/deploy/main.py
index f5a32eb8e..5a677ba5d 100644
--- a/lib/crewai/src/crewai/cli/deploy/main.py
+++ b/lib/crewai/src/crewai/cli/deploy/main.py
@@ -4,12 +4,35 @@ from rich.console import Console
from crewai.cli import git
from crewai.cli.command import BaseCommand, PlusAPIMixin
+from crewai.cli.deploy.validate import validate_project
from crewai.cli.utils import fetch_and_json_env_file, get_project_name
console = Console()
+def _run_predeploy_validation(skip_validate: bool) -> bool:
+ """Run pre-deploy validation unless skipped.
+
+ Returns True if deployment should proceed, False if it should abort.
+ """
+ if skip_validate:
+ console.print(
+ "[yellow]Skipping pre-deploy validation (--skip-validate).[/yellow]"
+ )
+ return True
+
+ console.print("Running pre-deploy validation...", style="bold blue")
+ validator = validate_project()
+ if not validator.ok:
+ console.print(
+ "\n[bold red]Pre-deploy validation failed. "
+ "Fix the issues above or re-run with --skip-validate.[/bold red]"
+ )
+ return False
+ return True
+
+
class DeployCommand(BaseCommand, PlusAPIMixin):
"""
A class to handle deployment-related operations for CrewAI projects.
@@ -60,13 +83,16 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}"
)
- def deploy(self, uuid: str | None = None) -> None:
+ def deploy(self, uuid: str | None = None, skip_validate: bool = False) -> 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.
"""
+ if not _run_predeploy_validation(skip_validate):
+ return
self._telemetry.start_deployment_span(uuid)
console.print("Starting deployment...", style="bold blue")
if uuid:
@@ -80,10 +106,16 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
self._validate_response(response)
self._display_deployment_info(response.json())
- def create_crew(self, confirm: bool = False) -> None:
+ def create_crew(self, confirm: bool = False, skip_validate: bool = False) -> None:
"""
Create a new crew deployment.
+
+ Args:
+ confirm (bool): Whether to skip the interactive confirmation prompt.
+ skip_validate (bool): Skip pre-deploy validation checks.
"""
+ if not _run_predeploy_validation(skip_validate):
+ return
self._telemetry.create_crew_deployment_span()
console.print("Creating deployment...", style="bold blue")
env_vars = fetch_and_json_env_file()
diff --git a/lib/crewai/src/crewai/cli/deploy/validate.py b/lib/crewai/src/crewai/cli/deploy/validate.py
new file mode 100644
index 000000000..55246e102
--- /dev/null
+++ b/lib/crewai/src/crewai/cli/deploy/validate.py
@@ -0,0 +1,845 @@
+"""Pre-deploy validation for CrewAI projects.
+
+Catches locally what a deploy would reject at build or runtime so users
+don't burn deployment attempts on fixable project-structure problems.
+
+Each check is grouped into one of:
+- ERROR: will block a deployment; validator exits non-zero.
+- WARNING: may still deploy but is almost always a deployment bug; printed
+ but does not block.
+
+The individual checks mirror the categories observed in production
+deployment-failure logs:
+
+1. pyproject.toml present with ``[project].name``
+2. lockfile (``uv.lock`` or ``poetry.lock``) present and not stale
+3. package directory at ``src//`` exists (no empty name, no egg-info)
+4. standard crew files: ``crew.py``, ``config/agents.yaml``, ``config/tasks.yaml``
+5. flow entrypoint: ``main.py`` with a Flow subclass
+6. hatch wheel target resolves (packages = [...] or default dir matches name)
+7. crew/flow module imports cleanly (catches ``@CrewBase not found``,
+ ``No Flow subclass found``, provider import errors)
+8. environment variables referenced in code vs ``.env`` / deployment env
+9. installed crewai vs lockfile pin (catches missing-attribute failures from
+ stale pins)
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import Enum
+import json
+import logging
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+from typing import Any
+
+from rich.console import Console
+
+from crewai.cli.utils import parse_toml
+
+
+console = Console()
+logger = logging.getLogger(__name__)
+
+
+class Severity(str, Enum):
+ """Severity of a validation finding."""
+
+ ERROR = "error"
+ WARNING = "warning"
+
+
+@dataclass
+class ValidationResult:
+ """A single finding from a validation check.
+
+ Attributes:
+ severity: whether this blocks deploy or is advisory.
+ code: stable short identifier, used in tests and docs
+ (e.g. ``missing_pyproject``, ``stale_lockfile``).
+ title: one-line summary shown to the user.
+ detail: optional multi-line explanation.
+ hint: optional remediation suggestion.
+ """
+
+ severity: Severity
+ code: str
+ title: str
+ detail: str = ""
+ hint: str = ""
+
+
+# Maps known provider env var names → label used in hint messages.
+_KNOWN_API_KEY_HINTS: dict[str, str] = {
+ "OPENAI_API_KEY": "OpenAI",
+ "ANTHROPIC_API_KEY": "Anthropic",
+ "GOOGLE_API_KEY": "Google",
+ "GEMINI_API_KEY": "Gemini",
+ "AZURE_OPENAI_API_KEY": "Azure OpenAI",
+ "AZURE_API_KEY": "Azure",
+ "AWS_ACCESS_KEY_ID": "AWS",
+ "AWS_SECRET_ACCESS_KEY": "AWS",
+ "COHERE_API_KEY": "Cohere",
+ "GROQ_API_KEY": "Groq",
+ "MISTRAL_API_KEY": "Mistral",
+ "TAVILY_API_KEY": "Tavily",
+ "SERPER_API_KEY": "Serper",
+ "SERPLY_API_KEY": "Serply",
+ "PERPLEXITY_API_KEY": "Perplexity",
+ "DEEPSEEK_API_KEY": "DeepSeek",
+ "OPENROUTER_API_KEY": "OpenRouter",
+ "FIRECRAWL_API_KEY": "Firecrawl",
+ "EXA_API_KEY": "Exa",
+ "BROWSERBASE_API_KEY": "Browserbase",
+}
+
+
+def normalize_package_name(project_name: str) -> str:
+ """Normalize a pyproject project.name into a Python package directory name.
+
+ Mirrors the rules in ``crewai.cli.create_crew.create_crew`` so the
+ validator agrees with the scaffolder about where ``src//`` should
+ live.
+ """
+ folder = project_name.replace(" ", "_").replace("-", "_").lower()
+ return re.sub(r"[^a-zA-Z0-9_]", "", folder)
+
+
+class DeployValidator:
+ """Runs the full pre-deploy validation suite against a project directory."""
+
+ def __init__(self, project_root: Path | None = None) -> None:
+ self.project_root: Path = (project_root or Path.cwd()).resolve()
+ self.results: list[ValidationResult] = []
+ self._pyproject: dict[str, Any] | None = None
+ self._project_name: str | None = None
+ self._package_name: str | None = None
+ self._package_dir: Path | None = None
+ self._is_flow: bool = False
+
+ def _add(
+ self,
+ severity: Severity,
+ code: str,
+ title: str,
+ detail: str = "",
+ hint: str = "",
+ ) -> None:
+ self.results.append(
+ ValidationResult(
+ severity=severity,
+ code=code,
+ title=title,
+ detail=detail,
+ hint=hint,
+ )
+ )
+
+ @property
+ def errors(self) -> list[ValidationResult]:
+ return [r for r in self.results if r.severity is Severity.ERROR]
+
+ @property
+ def warnings(self) -> list[ValidationResult]:
+ return [r for r in self.results if r.severity is Severity.WARNING]
+
+ @property
+ def ok(self) -> bool:
+ return not self.errors
+
+ def run(self) -> list[ValidationResult]:
+ """Run all checks. Later checks are skipped when earlier ones make
+ them impossible (e.g. no pyproject.toml → no lockfile check)."""
+ if not self._check_pyproject():
+ return self.results
+
+ self._check_lockfile()
+
+ if not self._check_package_dir():
+ self._check_hatch_wheel_target()
+ return self.results
+
+ if self._is_flow:
+ self._check_flow_entrypoint()
+ else:
+ self._check_crew_entrypoint()
+ self._check_config_yamls()
+
+ self._check_hatch_wheel_target()
+ self._check_module_imports()
+ self._check_env_vars()
+ self._check_version_vs_lockfile()
+
+ return self.results
+
+ def _check_pyproject(self) -> bool:
+ pyproject_path = self.project_root / "pyproject.toml"
+ if not pyproject_path.exists():
+ self._add(
+ Severity.ERROR,
+ "missing_pyproject",
+ "Cannot find pyproject.toml",
+ detail=(
+ f"Expected pyproject.toml at {pyproject_path}. "
+ "CrewAI projects must be installable Python packages."
+ ),
+ hint="Run `crewai create crew ` to scaffold a valid project layout.",
+ )
+ return False
+
+ try:
+ self._pyproject = parse_toml(pyproject_path.read_text())
+ except Exception as e:
+ self._add(
+ Severity.ERROR,
+ "invalid_pyproject",
+ "pyproject.toml is not valid TOML",
+ detail=str(e),
+ )
+ return False
+
+ project = self._pyproject.get("project") or {}
+ name = project.get("name")
+ if not isinstance(name, str) or not name.strip():
+ self._add(
+ Severity.ERROR,
+ "missing_project_name",
+ "pyproject.toml is missing [project].name",
+ detail=(
+ "Without a project name the platform cannot resolve your "
+ "package directory (this produces errors like "
+ "'Cannot find src//crew.py')."
+ ),
+ hint='Set a `name = "..."` field under `[project]` in pyproject.toml.',
+ )
+ return False
+
+ self._project_name = name
+ self._package_name = normalize_package_name(name)
+ self._is_flow = (self._pyproject.get("tool") or {}).get("crewai", {}).get(
+ "type"
+ ) == "flow"
+ return True
+
+ def _check_lockfile(self) -> None:
+ uv_lock = self.project_root / "uv.lock"
+ poetry_lock = self.project_root / "poetry.lock"
+ pyproject = self.project_root / "pyproject.toml"
+
+ if not uv_lock.exists() and not poetry_lock.exists():
+ self._add(
+ Severity.ERROR,
+ "missing_lockfile",
+ "Expected to find at least one of these files: uv.lock or poetry.lock",
+ hint=(
+ "Run `uv lock` (recommended) or `poetry lock` in your project "
+ "directory, commit the lockfile, then redeploy."
+ ),
+ )
+ return
+
+ lockfile = uv_lock if uv_lock.exists() else poetry_lock
+ try:
+ if lockfile.stat().st_mtime < pyproject.stat().st_mtime:
+ self._add(
+ Severity.WARNING,
+ "stale_lockfile",
+ f"{lockfile.name} is older than pyproject.toml",
+ detail=(
+ "Your lockfile may not reflect recent dependency changes. "
+ "The platform resolves from the lockfile, so deployed "
+ "dependencies may differ from local."
+ ),
+ hint="Run `uv lock` (or `poetry lock`) and commit the result.",
+ )
+ except OSError:
+ pass
+
+ def _check_package_dir(self) -> bool:
+ if self._package_name is None:
+ return False
+
+ src_dir = self.project_root / "src"
+ if not src_dir.is_dir():
+ self._add(
+ Severity.ERROR,
+ "missing_src_dir",
+ "Missing src/ directory",
+ detail=(
+ "CrewAI deployments expect a src-layout project: "
+ f"src/{self._package_name}/crew.py (or main.py for flows)."
+ ),
+ hint="Run `crewai create crew ` to see the expected layout.",
+ )
+ return False
+
+ package_dir = src_dir / self._package_name
+ if not package_dir.is_dir():
+ siblings = [
+ p.name
+ for p in src_dir.iterdir()
+ if p.is_dir() and not p.name.endswith(".egg-info")
+ ]
+ egg_info = [
+ p.name for p in src_dir.iterdir() if p.name.endswith(".egg-info")
+ ]
+
+ hint_parts = [
+ f'Create src/{self._package_name}/ to match [project].name = "{self._project_name}".'
+ ]
+ if siblings:
+ hint_parts.append(
+ f"Found other package directories: {', '.join(siblings)}. "
+ f"Either rename one to '{self._package_name}' or update [project].name."
+ )
+ if egg_info:
+ hint_parts.append(
+ f"Delete stale build artifacts: {', '.join(egg_info)} "
+ "(these confuse the platform's package discovery)."
+ )
+
+ self._add(
+ Severity.ERROR,
+ "missing_package_dir",
+ f"Cannot find src/{self._package_name}/",
+ detail=(
+ "The platform looks for your crew source under "
+ "src//, derived from [project].name."
+ ),
+ hint=" ".join(hint_parts),
+ )
+ return False
+
+ for p in src_dir.iterdir():
+ if p.name.endswith(".egg-info"):
+ self._add(
+ Severity.WARNING,
+ "stale_egg_info",
+ f"Stale build artifact in src/: {p.name}",
+ detail=(
+ ".egg-info directories can be mistaken for your package "
+ "and cause 'Cannot find src/.egg-info/crew.py' errors."
+ ),
+ hint=f"Delete {p} and add `*.egg-info/` to .gitignore.",
+ )
+
+ self._package_dir = package_dir
+ return True
+
+ def _check_crew_entrypoint(self) -> None:
+ if self._package_dir is None:
+ return
+ crew_py = self._package_dir / "crew.py"
+ if not crew_py.is_file():
+ self._add(
+ Severity.ERROR,
+ "missing_crew_py",
+ f"Cannot find {crew_py.relative_to(self.project_root)}",
+ detail=(
+ "Standard crew projects must define a Crew class decorated "
+ "with @CrewBase inside crew.py."
+ ),
+ hint=(
+ "Create crew.py with an @CrewBase-annotated class, or set "
+ '`[tool.crewai] type = "flow"` in pyproject.toml if this is a flow.'
+ ),
+ )
+
+ def _check_config_yamls(self) -> None:
+ if self._package_dir is None:
+ return
+ config_dir = self._package_dir / "config"
+ if not config_dir.is_dir():
+ self._add(
+ Severity.ERROR,
+ "missing_config_dir",
+ f"Cannot find {config_dir.relative_to(self.project_root)}",
+ hint="Create a config/ directory with agents.yaml and tasks.yaml.",
+ )
+ return
+
+ for yaml_name in ("agents.yaml", "tasks.yaml"):
+ yaml_path = config_dir / yaml_name
+ if not yaml_path.is_file():
+ self._add(
+ Severity.ERROR,
+ f"missing_{yaml_name.replace('.', '_')}",
+ f"Cannot find {yaml_path.relative_to(self.project_root)}",
+ detail=(
+ "CrewAI loads agent and task config from these files; "
+ "missing them causes empty-config warnings and runtime crashes."
+ ),
+ )
+
+ def _check_flow_entrypoint(self) -> None:
+ if self._package_dir is None:
+ return
+ main_py = self._package_dir / "main.py"
+ if not main_py.is_file():
+ self._add(
+ Severity.ERROR,
+ "missing_flow_main",
+ f"Cannot find {main_py.relative_to(self.project_root)}",
+ detail=(
+ "Flow projects must define a Flow subclass in main.py. "
+ 'This project has `[tool.crewai] type = "flow"` set.'
+ ),
+ hint="Create main.py with a `class MyFlow(Flow[...])`.",
+ )
+
+ def _check_hatch_wheel_target(self) -> None:
+ if not self._pyproject:
+ return
+
+ build_system = self._pyproject.get("build-system") or {}
+ backend = build_system.get("build-backend", "")
+ if "hatchling" not in backend:
+ return
+
+ hatch_wheel = (
+ (self._pyproject.get("tool") or {})
+ .get("hatch", {})
+ .get("build", {})
+ .get("targets", {})
+ .get("wheel", {})
+ )
+ if hatch_wheel.get("packages") or hatch_wheel.get("only-include"):
+ return
+
+ if self._package_dir and self._package_dir.is_dir():
+ return
+
+ self._add(
+ Severity.ERROR,
+ "hatch_wheel_target_missing",
+ "Hatchling cannot determine which files to ship",
+ detail=(
+ "Your pyproject uses hatchling but has no "
+ "[tool.hatch.build.targets.wheel] configuration and no "
+ "directory matching your project name."
+ ),
+ hint=(
+ "Add:\n"
+ " [tool.hatch.build.targets.wheel]\n"
+ f' packages = ["src/{self._package_name}"]'
+ ),
+ )
+
+ def _check_module_imports(self) -> None:
+ """Import the user's crew/flow via `uv run` so the check sees the same
+ package versions as `crewai run` would. Result is reported as JSON on
+ the subprocess's stdout."""
+ script = (
+ "import json, sys, traceback, os\n"
+ "os.chdir(sys.argv[1])\n"
+ "try:\n"
+ " from crewai.cli.utils import get_crews, get_flows\n"
+ " is_flow = sys.argv[2] == 'flow'\n"
+ " if is_flow:\n"
+ " instances = get_flows()\n"
+ " kind = 'flow'\n"
+ " else:\n"
+ " instances = get_crews()\n"
+ " kind = 'crew'\n"
+ " print(json.dumps({'ok': True, 'kind': kind, 'count': len(instances)}))\n"
+ "except BaseException as e:\n"
+ " print(json.dumps({\n"
+ " 'ok': False,\n"
+ " 'error_type': type(e).__name__,\n"
+ " 'error': str(e),\n"
+ " 'traceback': traceback.format_exc(),\n"
+ " }))\n"
+ )
+
+ uv_path = shutil.which("uv")
+ if uv_path is None:
+ self._add(
+ Severity.WARNING,
+ "uv_not_found",
+ "Skipping import check: `uv` not installed",
+ hint="Install uv: https://docs.astral.sh/uv/",
+ )
+ return
+
+ try:
+ proc = subprocess.run( # noqa: S603 - args constructed from trusted inputs
+ [
+ uv_path,
+ "run",
+ "python",
+ "-c",
+ script,
+ str(self.project_root),
+ "flow" if self._is_flow else "crew",
+ ],
+ cwd=self.project_root,
+ capture_output=True,
+ text=True,
+ timeout=120,
+ check=False,
+ )
+ except subprocess.TimeoutExpired:
+ self._add(
+ Severity.ERROR,
+ "import_timeout",
+ "Importing your crew/flow module timed out after 120s",
+ detail=(
+ "User code may be making network calls or doing heavy work "
+ "at import time. Move that work into agent methods."
+ ),
+ )
+ return
+
+ # The payload is the last JSON object on stdout; user code may print
+ # other lines before it.
+ payload: dict[str, Any] | None = None
+ for line in reversed(proc.stdout.splitlines()):
+ line = line.strip()
+ if line.startswith("{") and line.endswith("}"):
+ try:
+ payload = json.loads(line)
+ break
+ except json.JSONDecodeError:
+ continue
+
+ if payload is None:
+ self._add(
+ Severity.ERROR,
+ "import_failed",
+ "Could not import your crew/flow module",
+ detail=(proc.stderr or proc.stdout or "").strip()[:1500],
+ hint="Run `crewai run` locally first to reproduce the error.",
+ )
+ return
+
+ if payload.get("ok"):
+ if payload.get("count", 0) == 0:
+ kind = payload.get("kind", "crew")
+ if kind == "flow":
+ self._add(
+ Severity.ERROR,
+ "no_flow_subclass",
+ "No Flow subclass found in the module",
+ hint=(
+ "main.py must define a class extending "
+ "`crewai.flow.Flow`, instantiable with no arguments."
+ ),
+ )
+ else:
+ self._add(
+ Severity.ERROR,
+ "no_crewbase_class",
+ "Crew class annotated with @CrewBase not found",
+ hint=(
+ "Decorate your crew class with @CrewBase from "
+ "crewai.project (see `crewai create crew` template)."
+ ),
+ )
+ return
+
+ err_msg = str(payload.get("error", ""))
+ err_type = str(payload.get("error_type", "Exception"))
+ tb = str(payload.get("traceback", ""))
+ self._classify_import_error(err_type, err_msg, tb)
+
+ def _classify_import_error(self, err_type: str, err_msg: str, tb: str) -> None:
+ """Turn a raw import-time exception into a user-actionable finding."""
+ # Must be checked before the generic "native provider" branch below:
+ # the extras-missing message contains the same phrase. Providers
+ # format the install command as plain text (`to install: uv add
+ # "crewai[extra]"`); also tolerate backtick-delimited variants.
+ m = re.search(
+ r"(?P[A-Za-z0-9_ -]+?)\s+native provider not available"
+ r".*?to install:\s*`?(?Puv add [\"']crewai\[[^\]]+\][\"'])`?",
+ err_msg,
+ )
+ if m:
+ self._add(
+ Severity.ERROR,
+ "missing_provider_extra",
+ f"{m.group('pkg').strip()} provider extra not installed",
+ hint=f"Run: {m.group('cmd')}",
+ )
+ return
+
+ # crewai.llm.LLM.__new__ wraps provider init errors as
+ # ImportError("Error importing native provider: ...").
+ if "Error importing native provider" in err_msg or "native provider" in err_msg:
+ missing_key = self._extract_missing_api_key(err_msg)
+ if missing_key:
+ provider = _KNOWN_API_KEY_HINTS.get(missing_key, missing_key)
+ self._add(
+ Severity.WARNING,
+ "llm_init_missing_key",
+ f"LLM is constructed at import time but {missing_key} is not set",
+ detail=(
+ f"Your crew instantiates a {provider} LLM during module "
+ "load (e.g. in a class field default or @crew method). "
+ f"The {provider} provider currently requires {missing_key} "
+ "at construction time, so this will fail on the platform "
+ "unless the key is set in your deployment environment."
+ ),
+ hint=(
+ f"Add {missing_key} to your deployment's Environment "
+ "Variables before deploying, or move LLM construction "
+ "inside agent methods so it runs lazily."
+ ),
+ )
+ return
+ self._add(
+ Severity.ERROR,
+ "llm_provider_init_failed",
+ "LLM native provider failed to initialize",
+ detail=err_msg,
+ hint=(
+ "Check your LLM(model=...) configuration and provider-specific "
+ "extras (e.g. `uv add 'crewai[azure-ai-inference]'` for Azure)."
+ ),
+ )
+ return
+
+ if err_type == "KeyError":
+ key = err_msg.strip("'\"")
+ if key in _KNOWN_API_KEY_HINTS or key.endswith("_API_KEY"):
+ self._add(
+ Severity.WARNING,
+ "env_var_read_at_import",
+ f"{key} is read at import time via os.environ[...]",
+ detail=(
+ "Using os.environ[...] (rather than os.getenv(...)) "
+ "at module scope crashes the build if the key isn't set."
+ ),
+ hint=(
+ f"Either add {key} as a deployment env var, or switch "
+ "to os.getenv() and move the access inside agent methods."
+ ),
+ )
+ return
+
+ if "Crew class annotated with @CrewBase not found" in err_msg:
+ self._add(
+ Severity.ERROR,
+ "no_crewbase_class",
+ "Crew class annotated with @CrewBase not found",
+ detail=err_msg,
+ )
+ return
+ if "No Flow subclass found" in err_msg:
+ self._add(
+ Severity.ERROR,
+ "no_flow_subclass",
+ "No Flow subclass found in the module",
+ detail=err_msg,
+ )
+ return
+
+ if (
+ err_type == "AttributeError"
+ and "has no attribute '_load_response_format'" in err_msg
+ ):
+ self._add(
+ Severity.ERROR,
+ "stale_crewai_pin",
+ "Your lockfile pins a crewai version missing `_load_response_format`",
+ detail=err_msg,
+ hint=(
+ "Run `uv lock --upgrade-package crewai` (or `poetry update crewai`) "
+ "to pin a newer release."
+ ),
+ )
+ return
+
+ if "pydantic" in tb.lower() or "validation error" in err_msg.lower():
+ self._add(
+ Severity.ERROR,
+ "pydantic_validation_error",
+ "Pydantic validation failed while loading your crew",
+ detail=err_msg[:800],
+ hint=(
+ "Check agent/task configuration fields. `crewai run` locally "
+ "will show the full traceback."
+ ),
+ )
+ return
+
+ self._add(
+ Severity.ERROR,
+ "import_failed",
+ f"Importing your crew failed: {err_type}",
+ detail=err_msg[:800],
+ hint="Run `crewai run` locally to see the full traceback.",
+ )
+
+ @staticmethod
+ def _extract_missing_api_key(err_msg: str) -> str | None:
+ """Pull 'FOO_API_KEY' out of '... FOO_API_KEY is required ...'."""
+ m = re.search(r"([A-Z][A-Z0-9_]*_API_KEY)\s+is required", err_msg)
+ if m:
+ return m.group(1)
+ m = re.search(r"['\"]([A-Z][A-Z0-9_]*_API_KEY)['\"]", err_msg)
+ if m:
+ return m.group(1)
+ return None
+
+ def _check_env_vars(self) -> None:
+ """Warn about env vars referenced in user code but missing locally.
+ Best-effort only — the platform sets vars server-side, so we never error.
+ """
+ if not self._package_dir:
+ return
+
+ referenced: set[str] = set()
+ pattern = re.compile(
+ r"""(?x)
+ (?:os\.environ\s*(?:\[\s*|\.get\s*\(\s*)
+ |os\.getenv\s*\(\s*
+ |getenv\s*\(\s*)
+ ['"]([A-Z][A-Z0-9_]*)['"]
+ """
+ )
+
+ for path in self._package_dir.rglob("*.py"):
+ try:
+ text = path.read_text(encoding="utf-8", errors="ignore")
+ except OSError:
+ continue
+ referenced.update(pattern.findall(text))
+
+ for path in self._package_dir.rglob("*.yaml"):
+ try:
+ text = path.read_text(encoding="utf-8", errors="ignore")
+ except OSError:
+ continue
+ referenced.update(re.findall(r"\$\{?([A-Z][A-Z0-9_]+)\}?", text))
+
+ env_file = self.project_root / ".env"
+ env_keys: set[str] = set()
+ if env_file.exists():
+ for line in env_file.read_text(errors="ignore").splitlines():
+ line = line.strip()
+ if not line or line.startswith("#") or "=" not in line:
+ continue
+ env_keys.add(line.split("=", 1)[0].strip())
+
+ missing_known: list[str] = sorted(
+ var
+ for var in referenced
+ if var in _KNOWN_API_KEY_HINTS
+ and var not in env_keys
+ and var not in os.environ
+ )
+ if missing_known:
+ self._add(
+ Severity.WARNING,
+ "env_vars_not_in_dotenv",
+ f"{len(missing_known)} referenced API key(s) not in .env",
+ detail=(
+ "These env vars are referenced in your source but not set "
+ f"locally: {', '.join(missing_known)}. Deploys will fail "
+ "unless they are added to the deployment's Environment "
+ "Variables in the CrewAI dashboard."
+ ),
+ )
+
+ def _check_version_vs_lockfile(self) -> None:
+ """Warn when the lockfile pins a crewai release older than 1.13.0,
+ which is where ``_load_response_format`` was introduced.
+ """
+ uv_lock = self.project_root / "uv.lock"
+ poetry_lock = self.project_root / "poetry.lock"
+ lockfile = (
+ uv_lock
+ if uv_lock.exists()
+ else poetry_lock
+ if poetry_lock.exists()
+ else None
+ )
+ if lockfile is None:
+ return
+
+ try:
+ text = lockfile.read_text(errors="ignore")
+ except OSError:
+ return
+
+ m = re.search(
+ r'name\s*=\s*"crewai"\s*\nversion\s*=\s*"([^"]+)"',
+ text,
+ )
+ if not m:
+ return
+ locked = m.group(1)
+
+ try:
+ from packaging.version import Version
+
+ if Version(locked) < Version("1.13.0"):
+ self._add(
+ Severity.WARNING,
+ "old_crewai_pin",
+ f"Lockfile pins crewai=={locked} (older than 1.13.0)",
+ detail=(
+ "Older pinned versions are missing API surface the "
+ "platform builder expects (e.g. `_load_response_format`)."
+ ),
+ hint="Run `uv lock --upgrade-package crewai` and redeploy.",
+ )
+ except Exception as e:
+ logger.debug("Could not parse crewai pin from lockfile: %s", e)
+
+
+def render_report(results: list[ValidationResult]) -> None:
+ """Pretty-print results to the shared rich console."""
+ if not results:
+ console.print("[bold green]Pre-deploy validation passed.[/bold green]")
+ return
+
+ errors = [r for r in results if r.severity is Severity.ERROR]
+ warnings = [r for r in results if r.severity is Severity.WARNING]
+
+ for result in errors:
+ console.print(f"[bold red]ERROR[/bold red] [{result.code}] {result.title}")
+ if result.detail:
+ console.print(f" {result.detail}")
+ if result.hint:
+ console.print(f" [dim]hint:[/dim] {result.hint}")
+
+ for result in warnings:
+ console.print(
+ f"[bold yellow]WARNING[/bold yellow] [{result.code}] {result.title}"
+ )
+ if result.detail:
+ console.print(f" {result.detail}")
+ if result.hint:
+ console.print(f" [dim]hint:[/dim] {result.hint}")
+
+ summary_parts: list[str] = []
+ if errors:
+ summary_parts.append(f"[bold red]{len(errors)} error(s)[/bold red]")
+ if warnings:
+ summary_parts.append(f"[bold yellow]{len(warnings)} warning(s)[/bold yellow]")
+ console.print(f"\n{' / '.join(summary_parts)}")
+
+
+def validate_project(project_root: Path | None = None) -> DeployValidator:
+ """Entrypoint: run validation, render results, return the validator.
+
+ The caller inspects ``validator.ok`` to decide whether to proceed with a
+ deploy.
+ """
+ validator = DeployValidator(project_root=project_root)
+ validator.run()
+ render_report(validator.results)
+ return validator
+
+
+def run_validate_command() -> None:
+ """Implementation of `crewai deploy validate`."""
+ validator = validate_project()
+ if not validator.ok:
+ sys.exit(1)
diff --git a/lib/crewai/src/crewai/cli/enterprise/main.py b/lib/crewai/src/crewai/cli/enterprise/main.py
index 395de418b..2977868f2 100644
--- a/lib/crewai/src/crewai/cli/enterprise/main.py
+++ b/lib/crewai/src/crewai/cli/enterprise/main.py
@@ -7,7 +7,7 @@ from rich.console import Console
from crewai.cli.authentication.main import Oauth2Settings, ProviderFactory
from crewai.cli.command import BaseCommand
from crewai.cli.settings.main import SettingsCommand
-from crewai.cli.version import get_crewai_version
+from crewai.utilities.version import get_crewai_version
console = Console()
diff --git a/lib/crewai/src/crewai/cli/plus_api.py b/lib/crewai/src/crewai/cli/plus_api.py
index ac7acfda9..862ab81e8 100644
--- a/lib/crewai/src/crewai/cli/plus_api.py
+++ b/lib/crewai/src/crewai/cli/plus_api.py
@@ -6,7 +6,7 @@ import httpx
from crewai.cli.config import Settings
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
-from crewai.cli.version import get_crewai_version
+from crewai.utilities.version import get_crewai_version
class PlusAPI:
diff --git a/lib/crewai/src/crewai/cli/remote_template/__init__.py b/lib/crewai/src/crewai/cli/remote_template/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/crewai/src/crewai/cli/remote_template/main.py b/lib/crewai/src/crewai/cli/remote_template/main.py
new file mode 100644
index 000000000..bbd32184f
--- /dev/null
+++ b/lib/crewai/src/crewai/cli/remote_template/main.py
@@ -0,0 +1,250 @@
+import io
+import logging
+import os
+import shutil
+from typing import Any
+import zipfile
+
+import click
+import httpx
+from rich.console import Console
+from rich.panel import Panel
+from rich.text import Text
+
+from crewai.cli.command import BaseCommand
+
+
+logger = logging.getLogger(__name__)
+console = Console()
+
+GITHUB_ORG = "crewAIInc"
+TEMPLATE_PREFIX = "template_"
+GITHUB_API_BASE = "https://api.github.com"
+
+BANNER = """\
+[bold white] ██████╗██████╗ ███████╗██╗ ██╗[/bold white] [bold red] █████╗ ██╗[/bold red]
+[bold white]██╔════╝██╔══██╗██╔════╝██║ ██║[/bold white] [bold red]██╔══██╗██║[/bold red]
+[bold white]██║ ██████╔╝█████╗ ██║ █╗ ██║[/bold white] [bold red]███████║██║[/bold red]
+[bold white]██║ ██╔══██╗██╔══╝ ██║███╗██║[/bold white] [bold red]██╔══██║██║[/bold red]
+[bold white]╚██████╗██║ ██║███████╗╚███╔███╔╝[/bold white] [bold red]██║ ██║██║[/bold red]
+[bold white] ╚═════╝╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝[/bold white] [bold red]╚═╝ ╚═╝╚═╝[/bold red]
+[dim white]████████╗███████╗███╗ ███╗██████╗ ██╗ █████╗ ████████╗███████╗███████╗[/dim white]
+[dim white]╚══██╔══╝██╔════╝████╗ ████║██╔══██╗██║ ██╔══██╗╚══██╔══╝██╔════╝██╔════╝[/dim white]
+[dim white] ██║ █████╗ ██╔████╔██║██████╔╝██║ ███████║ ██║ █████╗ ███████╗[/dim white]
+[dim white] ██║ ██╔══╝ ██║╚██╔╝██║██╔═══╝ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║[/dim white]
+[dim white] ██║ ███████╗██║ ╚═╝ ██║██║ ███████╗██║ ██║ ██║ ███████╗███████║[/dim white]
+[dim white] ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝[/dim white]"""
+
+
+class TemplateCommand(BaseCommand):
+ """Handle template-related operations for CrewAI projects."""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ def list_templates(self) -> None:
+ """List available templates with an interactive selector to install."""
+ templates = self._fetch_templates()
+ if not templates:
+ click.echo("No templates found.")
+ return
+
+ console.print(f"\n{BANNER}\n")
+ console.print(" [on cyan] templates [/on cyan]\n")
+ console.print(f" [green]o[/green] Source: https://github.com/{GITHUB_ORG}")
+ console.print(
+ f" [green]o[/green] Found [bold]{len(templates)}[/bold] templates\n"
+ )
+ console.print(" [green]o[/green] Select a template to install")
+
+ for idx, repo in enumerate(templates, start=1):
+ name = repo["name"].removeprefix(TEMPLATE_PREFIX)
+ description = repo.get("description") or ""
+ if description:
+ console.print(
+ f" [bold cyan]{idx}.[/bold cyan] [bold white]{name}[/bold white] [dim]({description})[/dim]"
+ )
+ else:
+ console.print(
+ f" [bold cyan]{idx}.[/bold cyan] [bold white]{name}[/bold white]"
+ )
+
+ console.print(" [bold cyan]q.[/bold cyan] [dim]Quit[/dim]\n")
+
+ while True:
+ choice = click.prompt("Enter your choice", type=str)
+
+ if choice.lower() == "q":
+ return
+
+ if choice.isdigit() and 1 <= int(choice) <= len(templates):
+ selected_index = int(choice) - 1
+ break
+
+ click.secho(
+ f"Please enter a number between 1 and {len(templates)}, or 'q' to quit.",
+ fg="yellow",
+ )
+
+ selected = templates[selected_index]
+ repo_name = selected["name"]
+ self._install_repo(repo_name)
+
+ def add_template(self, name: str, output_dir: str | None = None) -> None:
+ """Download a template and copy it into the current working directory.
+
+ Args:
+ name: Template name (with or without the template_ prefix).
+ output_dir: Optional directory name. Defaults to the template name.
+ """
+ repo_name = self._resolve_repo_name(name)
+ if repo_name is None:
+ click.secho(f"Template '{name}' not found.", fg="red")
+ click.echo("Run 'crewai template list' to see available templates.")
+ raise SystemExit(1)
+
+ self._install_repo(repo_name, output_dir)
+
+ def _install_repo(self, repo_name: str, output_dir: str | None = None) -> None:
+ """Download and extract a template repo into the current directory.
+
+ Args:
+ repo_name: Full GitHub repo name (e.g. template_deep_research).
+ output_dir: Optional directory name. Defaults to the template name.
+ """
+ folder_name = output_dir or repo_name.removeprefix(TEMPLATE_PREFIX)
+ dest = os.path.join(os.getcwd(), folder_name)
+
+ while os.path.exists(dest):
+ click.secho(f"Directory '{folder_name}' already exists.", fg="yellow")
+ folder_name = click.prompt(
+ "Enter a different directory name (or 'q' to quit)", type=str
+ )
+ if folder_name.lower() == "q":
+ return
+ dest = os.path.join(os.getcwd(), folder_name)
+
+ click.echo(
+ f"Downloading template '{repo_name.removeprefix(TEMPLATE_PREFIX)}'..."
+ )
+
+ zip_bytes = self._download_zip(repo_name)
+ self._extract_zip(zip_bytes, dest)
+
+ self._telemetry.template_installed_span(repo_name.removeprefix(TEMPLATE_PREFIX))
+
+ console.print(
+ f"\n [green]\u2713[/green] Installed template [bold white]{folder_name}[/bold white]"
+ f" [dim](source: github.com/{GITHUB_ORG}/{repo_name})[/dim]\n"
+ )
+
+ next_steps = Text()
+ next_steps.append(f" cd {folder_name}\n", style="bold white")
+ next_steps.append(" crewai install", style="bold white")
+
+ panel = Panel(
+ next_steps,
+ title="[green]\u25c7 Next steps[/green]",
+ title_align="left",
+ border_style="dim",
+ padding=(1, 2),
+ )
+ console.print(panel)
+
+ def _fetch_templates(self) -> list[dict[str, Any]]:
+ """Fetch all template repos from the GitHub org."""
+ templates: list[dict[str, Any]] = []
+ page = 1
+ while True:
+ url = f"{GITHUB_API_BASE}/orgs/{GITHUB_ORG}/repos"
+ params: dict[str, str | int] = {
+ "per_page": 100,
+ "page": page,
+ "type": "public",
+ }
+ try:
+ response = httpx.get(url, params=params, timeout=15)
+ response.raise_for_status()
+ except httpx.HTTPError as e:
+ click.secho(f"Failed to fetch templates from GitHub: {e}", fg="red")
+ raise SystemExit(1) from e
+
+ repos = response.json()
+ if not repos:
+ break
+
+ templates.extend(
+ repo
+ for repo in repos
+ if repo["name"].startswith(TEMPLATE_PREFIX) and not repo.get("private")
+ )
+
+ page += 1
+
+ templates.sort(key=lambda r: r["name"])
+ return templates
+
+ def _resolve_repo_name(self, name: str) -> str | None:
+ """Resolve user input to a full repo name, or None if not found."""
+ # Accept both 'deep_research' and 'template_deep_research'
+ candidates = [
+ f"{TEMPLATE_PREFIX}{name}"
+ if not name.startswith(TEMPLATE_PREFIX)
+ else name,
+ name,
+ ]
+
+ templates = self._fetch_templates()
+ template_names = {t["name"] for t in templates}
+
+ for candidate in candidates:
+ if candidate in template_names:
+ return candidate
+
+ return None
+
+ def _download_zip(self, repo_name: str) -> bytes:
+ """Download the default branch zipball for a repo."""
+ url = f"{GITHUB_API_BASE}/repos/{GITHUB_ORG}/{repo_name}/zipball"
+ try:
+ response = httpx.get(url, follow_redirects=True, timeout=60)
+ response.raise_for_status()
+ except httpx.HTTPError as e:
+ click.secho(f"Failed to download template: {e}", fg="red")
+ raise SystemExit(1) from e
+
+ return response.content
+
+ def _extract_zip(self, zip_bytes: bytes, dest: str) -> None:
+ """Extract a GitHub zipball into dest, stripping the top-level directory."""
+ with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
+ # GitHub zipballs have a single top-level dir like 'crewAIInc-template_xxx-/'
+ members = zf.namelist()
+ if not members:
+ click.secho("Downloaded archive is empty.", fg="red")
+ raise SystemExit(1)
+
+ top_dir = members[0].split("/")[0] + "/"
+
+ os.makedirs(dest, exist_ok=True)
+
+ for member in members:
+ if member == top_dir or not member.startswith(top_dir):
+ continue
+
+ relative_path = member[len(top_dir) :]
+ if not relative_path:
+ continue
+
+ target = os.path.realpath(os.path.join(dest, relative_path))
+ if not target.startswith(
+ os.path.realpath(dest) + os.sep
+ ) and target != os.path.realpath(dest):
+ continue
+
+ if member.endswith("/"):
+ os.makedirs(target, exist_ok=True)
+ else:
+ os.makedirs(os.path.dirname(target), exist_ok=True)
+ with zf.open(member) as src, open(target, "wb") as dst:
+ shutil.copyfileobj(src, dst)
diff --git a/lib/crewai/src/crewai/cli/run_crew.py b/lib/crewai/src/crewai/cli/run_crew.py
index 6f031f245..ba2202032 100644
--- a/lib/crewai/src/crewai/cli/run_crew.py
+++ b/lib/crewai/src/crewai/cli/run_crew.py
@@ -5,7 +5,7 @@ import click
from packaging import version
from crewai.cli.utils import build_env_with_all_tool_credentials, read_toml
-from crewai.cli.version import get_crewai_version
+from crewai.utilities.version import get_crewai_version
class CrewType(Enum):
diff --git a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml
index 0fabbb1b3..01ce3f12b 100644
--- a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml
+++ b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
- "crewai[tools]==1.14.0"
+ "crewai[tools]==1.14.2rc1"
]
[project.scripts]
diff --git a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml
index e2f3e567e..b91fcbaf1 100644
--- a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml
+++ b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
- "crewai[tools]==1.14.0"
+ "crewai[tools]==1.14.2rc1"
]
[project.scripts]
diff --git a/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml b/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml
index 7f65a59a0..1089ad5c0 100644
--- a/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml
+++ b/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
- "crewai[tools]==1.14.0"
+ "crewai[tools]==1.14.2rc1"
]
[tool.crewai]
diff --git a/lib/crewai/src/crewai/cli/version.py b/lib/crewai/src/crewai/cli/version.py
index 60eb3a95a..232aa2423 100644
--- a/lib/crewai/src/crewai/cli/version.py
+++ b/lib/crewai/src/crewai/cli/version.py
@@ -3,7 +3,6 @@
from collections.abc import Mapping
from datetime import datetime, timedelta
from functools import lru_cache
-import importlib.metadata
import json
from pathlib import Path
from typing import Any
@@ -13,6 +12,8 @@ from urllib.error import URLError
import appdirs
from packaging.version import InvalidVersion, Version, parse
+from crewai.utilities.version import get_crewai_version
+
@lru_cache(maxsize=1)
def _get_cache_file() -> Path:
@@ -25,11 +26,6 @@ def _get_cache_file() -> Path:
return cache_dir / "version_cache.json"
-def get_crewai_version() -> str:
- """Get the version number of CrewAI running the CLI."""
- return importlib.metadata.version("crewai")
-
-
def _is_cache_valid(cache_data: Mapping[str, Any]) -> bool:
"""Check if the cache is still valid, less than 24 hours old."""
if "timestamp" not in cache_data:
diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py
index e630ec5b0..de9a8f73d 100644
--- a/lib/crewai/src/crewai/crew.py
+++ b/lib/crewai/src/crewai/crew.py
@@ -42,7 +42,6 @@ if TYPE_CHECKING:
from opentelemetry.trace import Span
from crewai.context import ExecutionContext
- from crewai.state.provider.core import BaseProvider
try:
from crewai_files import get_supported_content_types
@@ -104,7 +103,11 @@ from crewai.rag.types import SearchResult
from crewai.security.fingerprint import Fingerprint
from crewai.security.security_config import SecurityConfig
from crewai.skills.models import Skill
-from crewai.state.checkpoint_config import CheckpointConfig, _coerce_checkpoint
+from crewai.state.checkpoint_config import (
+ CheckpointConfig,
+ _coerce_checkpoint,
+ apply_checkpoint,
+)
from crewai.task import Task
from crewai.tasks.conditional_task import ConditionalTask
from crewai.tasks.task_output import TaskOutput
@@ -134,6 +137,7 @@ from crewai.utilities.rpm_controller import RPMController
from crewai.utilities.streaming import (
create_async_chunk_generator,
create_chunk_generator,
+ register_cleanup,
signal_end,
signal_error,
)
@@ -364,28 +368,21 @@ class Crew(FlowTrackable, BaseModel):
checkpoint_kickoff_event_id: str | None = Field(default=None)
@classmethod
- def from_checkpoint(
- cls, path: str, *, provider: BaseProvider | None = None
- ) -> Crew:
- """Restore a Crew from a checkpoint file, ready to resume via kickoff().
+ def from_checkpoint(cls, config: CheckpointConfig) -> Crew:
+ """Restore a Crew from a checkpoint, ready to resume via kickoff().
Args:
- path: Path to a checkpoint JSON file.
- provider: Storage backend to read from. Defaults to JsonProvider.
+ config: Checkpoint configuration with ``restore_from`` set to
+ the path of the checkpoint to load.
Returns:
A Crew instance. Call kickoff() to resume from the last completed task.
"""
from crewai.context import apply_execution_context
from crewai.events.event_bus import crewai_event_bus
- from crewai.state.provider.json_provider import JsonProvider
from crewai.state.runtime import RuntimeState
- state = RuntimeState.from_checkpoint(
- path,
- provider=provider or JsonProvider(),
- context={"from_checkpoint": True},
- )
+ state = RuntimeState.from_checkpoint(config, context={"from_checkpoint": True})
crewai_event_bus.set_runtime_state(state)
for entity in state.root:
if isinstance(entity, cls):
@@ -393,7 +390,32 @@ class Crew(FlowTrackable, BaseModel):
apply_execution_context(entity.execution_context)
entity._restore_runtime()
return entity
- raise ValueError(f"No Crew found in checkpoint: {path}")
+ raise ValueError(f"No Crew found in checkpoint: {config.restore_from}")
+
+ @classmethod
+ def fork(
+ cls,
+ config: CheckpointConfig,
+ branch: str | None = None,
+ ) -> Crew:
+ """Fork a Crew from a checkpoint, creating a new execution branch.
+
+ Args:
+ config: Checkpoint configuration with ``restore_from`` set.
+ branch: Branch label for the fork. Auto-generated if not provided.
+
+ Returns:
+ A Crew instance on the new branch. Call kickoff() to run.
+ """
+ crew = cls.from_checkpoint(config)
+ state = crewai_event_bus._runtime_state
+ if state is None:
+ raise RuntimeError(
+ "Cannot fork: no runtime state on the event bus. "
+ "Ensure from_checkpoint() succeeded before calling fork()."
+ )
+ state.fork(branch)
+ return crew
def _restore_runtime(self) -> None:
"""Re-create runtime objects after restoring from a checkpoint."""
@@ -414,6 +436,13 @@ class Crew(FlowTrackable, BaseModel):
if agent.agent_executor is not None and task.output is None:
agent.agent_executor.task = task
break
+ for task in self.tasks:
+ if task.checkpoint_original_description is not None:
+ task._original_description = task.checkpoint_original_description
+ if task.checkpoint_original_expected_output is not None:
+ task._original_expected_output = (
+ task.checkpoint_original_expected_output
+ )
if self.checkpoint_inputs is not None:
self._inputs = self.checkpoint_inputs
if self.checkpoint_kickoff_event_id is not None:
@@ -849,16 +878,23 @@ class Crew(FlowTrackable, BaseModel):
self,
inputs: dict[str, Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> CrewOutput | CrewStreamingOutput:
"""Execute the crew's workflow.
Args:
inputs: Optional input dictionary for task interpolation.
input_files: Optional dict of named file inputs for the crew.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the crew resumes from that checkpoint. Remaining
+ config fields enable checkpointing for the run.
Returns:
CrewOutput or CrewStreamingOutput if streaming is enabled.
"""
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return restored.kickoff(inputs=inputs, input_files=input_files) # type: ignore[no-any-return]
get_env_context()
if self.stream:
enable_agent_streaming(self.agents)
@@ -882,6 +918,7 @@ class Crew(FlowTrackable, BaseModel):
ctx.state, run_crew, ctx.output_holder
)
)
+ register_cleanup(streaming_output, ctx.state)
ctx.output_holder.append(streaming_output)
return streaming_output
@@ -970,12 +1007,15 @@ class Crew(FlowTrackable, BaseModel):
self,
inputs: dict[str, Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> CrewOutput | CrewStreamingOutput:
"""Asynchronous kickoff method to start the crew execution.
Args:
inputs: Optional input dictionary for task interpolation.
input_files: Optional dict of named file inputs for the crew.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the crew resumes from that checkpoint.
Returns:
CrewOutput or CrewStreamingOutput if streaming is enabled.
@@ -984,6 +1024,9 @@ class Crew(FlowTrackable, BaseModel):
to get stream chunks. After iteration completes, access the final result
via .result.
"""
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return await restored.kickoff_async(inputs=inputs, input_files=input_files) # type: ignore[no-any-return]
inputs = inputs or {}
if self.stream:
@@ -1007,6 +1050,7 @@ class Crew(FlowTrackable, BaseModel):
ctx.state, run_crew, ctx.output_holder
)
)
+ register_cleanup(streaming_output, ctx.state)
ctx.output_holder.append(streaming_output)
return streaming_output
@@ -1043,6 +1087,7 @@ class Crew(FlowTrackable, BaseModel):
self,
inputs: dict[str, Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> CrewOutput | CrewStreamingOutput:
"""Native async kickoff method using async task execution throughout.
@@ -1053,10 +1098,15 @@ class Crew(FlowTrackable, BaseModel):
Args:
inputs: Optional input dictionary for task interpolation.
input_files: Optional dict of named file inputs for the crew.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the crew resumes from that checkpoint.
Returns:
CrewOutput or CrewStreamingOutput if streaming is enabled.
"""
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return await restored.akickoff(inputs=inputs, input_files=input_files) # type: ignore[no-any-return]
if self.stream:
enable_agent_streaming(self.agents)
ctx = StreamingContext(use_async=True)
@@ -1078,6 +1128,7 @@ class Crew(FlowTrackable, BaseModel):
ctx.state, run_crew, ctx.output_holder
)
)
+ register_cleanup(streaming_output, ctx.state)
ctx.output_holder.append(streaming_output)
return streaming_output
diff --git a/lib/crewai/src/crewai/crews/utils.py b/lib/crewai/src/crewai/crews/utils.py
index 4077a9a19..e85a48b05 100644
--- a/lib/crewai/src/crewai/crews/utils.py
+++ b/lib/crewai/src/crewai/crews/utils.py
@@ -431,6 +431,7 @@ async def run_for_each_async(
from crewai.types.usage_metrics import UsageMetrics
from crewai.utilities.streaming import (
create_async_chunk_generator,
+ register_cleanup,
signal_end,
signal_error,
)
@@ -480,6 +481,7 @@ async def run_for_each_async(
streaming_output._set_results(result)
streaming_output._set_result = set_results_wrapper # type: ignore[method-assign]
+ register_cleanup(streaming_output, ctx.state)
ctx.output_holder.append(streaming_output)
return streaming_output
diff --git a/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py b/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py
index 1a25b68a9..d2a0912f6 100644
--- a/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py
+++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py
@@ -13,13 +13,13 @@ from crewai.cli.authentication.token import AuthError, get_auth_token
from crewai.cli.config import Settings
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai.cli.plus_api import PlusAPI
-from crewai.cli.version import get_crewai_version
from crewai.events.listeners.tracing.types import TraceEvent
from crewai.events.listeners.tracing.utils import (
get_user_id,
is_tracing_enabled_in_context,
should_auto_collect_first_time_traces,
)
+from crewai.utilities.version import get_crewai_version
logger = getLogger(__name__)
diff --git a/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py
index 0e3b284c0..c4cc6cb71 100644
--- a/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py
+++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py
@@ -7,7 +7,6 @@ import uuid
from typing_extensions import Self
from crewai.cli.authentication.token import AuthError, get_auth_token
-from crewai.cli.version import get_crewai_version
from crewai.events.base_event_listener import BaseEventListener
from crewai.events.base_events import BaseEvent
from crewai.events.event_bus import CrewAIEventsBus
@@ -127,6 +126,7 @@ from crewai.events.types.tool_usage_events import (
ToolUsageStartedEvent,
)
from crewai.events.utils.console_formatter import ConsoleFormatter
+from crewai.utilities.version import get_crewai_version
class TraceCollectionListener(BaseEventListener):
diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py
index 72b732766..ef33fab43 100644
--- a/lib/crewai/src/crewai/experimental/agent_executor.py
+++ b/lib/crewai/src/crewai/experimental/agent_executor.py
@@ -91,7 +91,7 @@ from crewai.utilities.agent_utils import (
track_delegation_if_needed,
)
from crewai.utilities.constants import TRAINING_DATA_FILE
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.planning_types import (
PlanStep,
StepObservation,
@@ -189,7 +189,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
)
callbacks: list[Any] = Field(default_factory=list, exclude=True)
response_model: type[BaseModel] | None = Field(default=None, exclude=True)
- i18n: I18N | None = Field(default=None, exclude=True)
log_error_after: int = Field(default=3, exclude=True)
before_llm_call_hooks: list[BeforeLLMCallHookType | BeforeLLMCallHookCallable] = (
Field(default_factory=list, exclude=True)
@@ -198,7 +197,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
default_factory=list, exclude=True
)
- _i18n: I18N = PrivateAttr(default_factory=get_i18n)
_console: Console = PrivateAttr(default_factory=Console)
_last_parser_error: OutputParserError | None = PrivateAttr(default=None)
_last_context_error: Exception | None = PrivateAttr(default=None)
@@ -214,7 +212,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
@model_validator(mode="after")
def _setup_executor(self) -> Self:
"""Configure executor after Pydantic field initialization."""
- self._i18n = self.i18n or get_i18n()
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
self.after_llm_call_hooks.extend(get_after_llm_call_hooks())
@@ -363,7 +360,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
function_calling_llm=self.function_calling_llm,
request_within_rpm_limit=self.request_within_rpm_limit,
callbacks=self.callbacks,
- i18n=self._i18n,
)
return self._step_executor
@@ -1203,7 +1199,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
formatted_answer = handle_max_iterations_exceeded(
formatted_answer=None,
printer=PRINTER,
- i18n=self._i18n,
messages=list(self.state.messages),
llm=self.llm,
callbacks=self.callbacks,
@@ -1430,7 +1425,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
agent_action=action,
fingerprint_context=fingerprint_context,
tools=self.tools,
- i18n=self._i18n,
agent_key=self.agent.key if self.agent else None,
agent_role=self.agent.role if self.agent else None,
tools_handler=self.tools_handler,
@@ -1450,7 +1444,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
action.result = str(e)
self._append_message_to_state(action.text)
- reasoning_prompt = self._i18n.slice("post_tool_reasoning")
+ reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_message: LLMMessage = {
"role": "user",
"content": reasoning_prompt,
@@ -1471,7 +1465,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
self.state.is_finished = True
return "tool_result_is_final"
- reasoning_prompt = self._i18n.slice("post_tool_reasoning")
+ reasoning_prompt = I18N_DEFAULT.slice("post_tool_reasoning")
reasoning_message_post: LLMMessage = {
"role": "user",
"content": reasoning_prompt,
@@ -2222,10 +2216,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
# Build synthesis prompt
role = self.agent.role if self.agent else "Assistant"
- system_prompt = self._i18n.retrieve(
+ system_prompt = I18N_DEFAULT.retrieve(
"planning", "synthesis_system_prompt"
).format(role=role)
- user_prompt = self._i18n.retrieve("planning", "synthesis_user_prompt").format(
+ user_prompt = I18N_DEFAULT.retrieve("planning", "synthesis_user_prompt").format(
task_description=task_description,
combined_steps=combined_steps,
)
@@ -2472,7 +2466,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
self.task.description if self.task else getattr(self, "_kickoff_input", "")
)
- enhancement = self._i18n.retrieve(
+ enhancement = I18N_DEFAULT.retrieve(
"planning", "replan_enhancement_prompt"
).format(previous_context=previous_context)
@@ -2535,7 +2529,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
messages=self.state.messages,
llm=self.llm,
callbacks=self.callbacks,
- i18n=self._i18n,
verbose=self.agent.verbose,
)
@@ -2746,7 +2739,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
Returns:
Updated action or final answer.
"""
- add_image_tool = self._i18n.tools("add_image")
+ add_image_tool = I18N_DEFAULT.tools("add_image")
if (
isinstance(add_image_tool, dict)
and formatted_answer.tool.casefold().strip()
diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py
index 60d03b069..88457f7aa 100644
--- a/lib/crewai/src/crewai/flow/flow.py
+++ b/lib/crewai/src/crewai/flow/flow.py
@@ -113,7 +113,11 @@ from crewai.flow.utils import (
)
from crewai.memory.memory_scope import MemoryScope, MemorySlice
from crewai.memory.unified_memory import Memory
-from crewai.state.checkpoint_config import CheckpointConfig, _coerce_checkpoint
+from crewai.state.checkpoint_config import (
+ CheckpointConfig,
+ _coerce_checkpoint,
+ apply_checkpoint,
+)
if TYPE_CHECKING:
@@ -122,7 +126,6 @@ if TYPE_CHECKING:
from crewai.context import ExecutionContext
from crewai.flow.async_feedback.types import PendingFeedbackContext
from crewai.llms.base_llm import BaseLLM
- from crewai.state.provider.core import BaseProvider
from crewai.flow.visualization import build_flow_structure, render_interactive
from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput
@@ -132,6 +135,7 @@ from crewai.utilities.streaming import (
create_async_chunk_generator,
create_chunk_generator,
create_streaming_state,
+ register_cleanup,
signal_end,
signal_error,
)
@@ -927,20 +931,21 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
] = Field(default=None)
@classmethod
- def from_checkpoint(
- cls, path: str, *, provider: BaseProvider | None = None
- ) -> Flow: # type: ignore[type-arg]
- """Restore a Flow from a checkpoint file."""
+ def from_checkpoint(cls, config: CheckpointConfig) -> Flow: # type: ignore[type-arg]
+ """Restore a Flow from a checkpoint.
+
+ Args:
+ config: Checkpoint configuration with ``restore_from`` set to
+ the path of the checkpoint to load.
+
+ Returns:
+ A Flow instance ready to resume.
+ """
from crewai.context import apply_execution_context
from crewai.events.event_bus import crewai_event_bus
- from crewai.state.provider.json_provider import JsonProvider
from crewai.state.runtime import RuntimeState
- state = RuntimeState.from_checkpoint(
- path,
- provider=provider or JsonProvider(),
- context={"from_checkpoint": True},
- )
+ state = RuntimeState.from_checkpoint(config, context={"from_checkpoint": True})
crewai_event_bus.set_runtime_state(state)
for entity in state.root:
if not isinstance(entity, Flow):
@@ -957,7 +962,32 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
instance.checkpoint_state = entity.checkpoint_state
instance._restore_from_checkpoint()
return instance
- raise ValueError(f"No Flow found in checkpoint: {path}")
+ raise ValueError(f"No Flow found in checkpoint: {config.restore_from}")
+
+ @classmethod
+ def fork(
+ cls,
+ config: CheckpointConfig,
+ branch: str | None = None,
+ ) -> Flow: # type: ignore[type-arg]
+ """Fork a Flow from a checkpoint, creating a new execution branch.
+
+ Args:
+ config: Checkpoint configuration with ``restore_from`` set.
+ branch: Branch label for the fork. Auto-generated if not provided.
+
+ Returns:
+ A Flow instance on the new branch. Call kickoff() to run.
+ """
+ flow = cls.from_checkpoint(config)
+ state = crewai_event_bus._runtime_state
+ if state is None:
+ raise RuntimeError(
+ "Cannot fork: no runtime state on the event bus. "
+ "Ensure from_checkpoint() succeeded before calling fork()."
+ )
+ state.fork(branch)
+ return flow
checkpoint_completed_methods: set[str] | None = Field(default=None)
checkpoint_method_outputs: list[Any] | None = Field(default=None)
@@ -1454,6 +1484,25 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
"No pending feedback context. Use from_pending() to restore a paused flow."
)
+ if get_current_parent_id() is None:
+ reset_emission_counter()
+ reset_last_event_id()
+
+ if not self.suppress_flow_events:
+ future = crewai_event_bus.emit(
+ self,
+ FlowStartedEvent(
+ type="flow_started",
+ flow_name=self.name or self.__class__.__name__,
+ inputs=None,
+ ),
+ )
+ if future and isinstance(future, Future):
+ try:
+ await asyncio.wrap_future(future)
+ except Exception:
+ logger.warning("FlowStartedEvent handler failed", exc_info=True)
+
context = self._pending_feedback_context
emit = context.emit
default_outcome = context.default_outcome
@@ -1593,16 +1642,39 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
final_result = self._method_outputs[-1] if self._method_outputs else result
- # Emit flow finished
- crewai_event_bus.emit(
- self,
- FlowFinishedEvent(
- type="flow_finished",
- flow_name=self.name or self.__class__.__name__,
- result=final_result,
- state=self._state,
- ),
- )
+ if self._event_futures:
+ await asyncio.gather(
+ *[
+ asyncio.wrap_future(f)
+ for f in self._event_futures
+ if isinstance(f, Future)
+ ]
+ )
+ self._event_futures.clear()
+
+ if not self.suppress_flow_events:
+ future = crewai_event_bus.emit(
+ self,
+ FlowFinishedEvent(
+ type="flow_finished",
+ flow_name=self.name or self.__class__.__name__,
+ result=final_result,
+ state=self._copy_and_serialize_state(),
+ ),
+ )
+ if future and isinstance(future, Future):
+ try:
+ await asyncio.wrap_future(future)
+ except Exception:
+ logger.warning("FlowFinishedEvent handler failed", exc_info=True)
+
+ trace_listener = TraceCollectionListener()
+ if trace_listener.batch_manager.batch_owner_type == "flow":
+ if trace_listener.first_time_handler.is_first_time:
+ trace_listener.first_time_handler.mark_events_collected()
+ trace_listener.first_time_handler.handle_execution_completion()
+ else:
+ trace_listener.batch_manager.finalize_batch()
return final_result
@@ -1913,6 +1985,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
self,
inputs: dict[str, Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> Any | FlowStreamingOutput:
"""Start the flow execution in a synchronous context.
@@ -1922,10 +1995,15 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
Args:
inputs: Optional dictionary containing input values and/or a state ID.
input_files: Optional dict of named file inputs for the flow.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the flow resumes from that checkpoint.
Returns:
The final output from the flow or FlowStreamingOutput if streaming.
"""
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return restored.kickoff(inputs=inputs, input_files=input_files)
get_env_context()
if self.stream:
result_holder: list[Any] = []
@@ -1962,6 +2040,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
streaming_output = FlowStreamingOutput(
sync_iterator=create_chunk_generator(state, run_flow, output_holder)
)
+ register_cleanup(streaming_output, state)
output_holder.append(streaming_output)
return streaming_output
@@ -1981,6 +2060,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
self,
inputs: dict[str, Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> Any | FlowStreamingOutput:
"""Start the flow execution asynchronously.
@@ -1992,10 +2072,15 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
Args:
inputs: Optional dictionary containing input values and/or a state ID for restoration.
input_files: Optional dict of named file inputs for the flow.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the flow resumes from that checkpoint.
Returns:
The final output from the flow, which is the result of the last executed method.
"""
+ restored = apply_checkpoint(self, from_checkpoint)
+ if restored is not None:
+ return await restored.kickoff_async(inputs=inputs, input_files=input_files)
if self.stream:
result_holder: list[Any] = []
current_task_info: TaskInfo = {
@@ -2035,6 +2120,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
state, run_flow, output_holder
)
)
+ register_cleanup(streaming_output, state)
output_holder.append(streaming_output)
return streaming_output
@@ -2052,7 +2138,9 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
try:
# Reset flow state for fresh execution unless restoring from persistence
- is_restoring = inputs and "id" in inputs and self.persistence is not None
+ is_restoring = (
+ inputs and "id" in inputs and self.persistence is not None
+ ) or self.checkpoint_completed_methods is not None
if not is_restoring:
# Clear completed methods and outputs for a fresh start
self._completed_methods.clear()
@@ -2253,17 +2341,20 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
self,
inputs: dict[str, Any] | None = None,
input_files: dict[str, FileInput] | None = None,
+ from_checkpoint: CheckpointConfig | None = None,
) -> Any | FlowStreamingOutput:
"""Native async method to start the flow execution. Alias for kickoff_async.
Args:
inputs: Optional dictionary containing input values and/or a state ID for restoration.
input_files: Optional dict of named file inputs for the flow.
+ from_checkpoint: Optional checkpoint config. If ``restore_from``
+ is set, the flow resumes from that checkpoint.
Returns:
The final output from the flow, which is the result of the last executed method.
"""
- return await self.kickoff_async(inputs, input_files)
+ return await self.kickoff_async(inputs, input_files, from_checkpoint)
async def _execute_start_method(self, start_method_name: FlowMethodName) -> None:
"""Executes a flow's start method and its triggered listeners.
@@ -3191,7 +3282,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM as BaseLLMClass
- from crewai.utilities.i18n import get_i18n
+ from crewai.utilities.i18n import I18N_DEFAULT
llm_instance: BaseLLMClass
if isinstance(llm, str):
@@ -3211,9 +3302,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
description=f"The outcome that best matches the feedback. Must be one of: {', '.join(outcomes)}"
)
- # Load prompt from translations (using cached instance)
- i18n = get_i18n()
- prompt_template = i18n.slice("human_feedback_collapse")
+ prompt_template = I18N_DEFAULT.slice("human_feedback_collapse")
prompt = prompt_template.format(
feedback=feedback,
diff --git a/lib/crewai/src/crewai/flow/human_feedback.py b/lib/crewai/src/crewai/flow/human_feedback.py
index 5fedbd3a2..e6a51d9da 100644
--- a/lib/crewai/src/crewai/flow/human_feedback.py
+++ b/lib/crewai/src/crewai/flow/human_feedback.py
@@ -350,9 +350,9 @@ def human_feedback(
def _get_hitl_prompt(key: str) -> str:
"""Read a HITL prompt from the i18n translations."""
- from crewai.utilities.i18n import get_i18n
+ from crewai.utilities.i18n import I18N_DEFAULT
- return get_i18n().slice(key)
+ return I18N_DEFAULT.slice(key)
def _resolve_llm_instance() -> Any:
"""Resolve the ``llm`` parameter to a BaseLLM instance.
diff --git a/lib/crewai/src/crewai/hooks/decorators.py b/lib/crewai/src/crewai/hooks/decorators.py
index 6007f19bb..4f1da08f5 100644
--- a/lib/crewai/src/crewai/hooks/decorators.py
+++ b/lib/crewai/src/crewai/hooks/decorators.py
@@ -5,6 +5,8 @@ from functools import wraps
import inspect
from typing import TYPE_CHECKING, Any, TypeVar, overload
+from crewai.utilities.string_utils import sanitize_tool_name
+
if TYPE_CHECKING:
from crewai.hooks.llm_hooks import LLMCallHookContext
@@ -37,6 +39,9 @@ def _create_hook_decorator(
tools: list[str] | None = None,
agents: list[str] | None = None,
) -> Callable[..., Any]:
+ if tools:
+ tools = [sanitize_tool_name(t) for t in tools]
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
setattr(f, marker_attribute, True)
diff --git a/lib/crewai/src/crewai/lite_agent.py b/lib/crewai/src/crewai/lite_agent.py
index f96c84493..5ddddc89e 100644
--- a/lib/crewai/src/crewai/lite_agent.py
+++ b/lib/crewai/src/crewai/lite_agent.py
@@ -16,7 +16,6 @@ from typing import (
get_origin,
)
import uuid
-import warnings
from pydantic import (
UUID4,
@@ -26,7 +25,7 @@ from pydantic import (
field_validator,
model_validator,
)
-from typing_extensions import Self
+from typing_extensions import Self, deprecated
if TYPE_CHECKING:
@@ -89,7 +88,7 @@ from crewai.utilities.converter import (
)
from crewai.utilities.guardrail import process_guardrail
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.printer import PRINTER
from crewai.utilities.pydantic_schema_utils import generate_model_description
@@ -173,9 +172,12 @@ def _kickoff_with_a2a_support(
)
+@deprecated(
+ "LiteAgent is deprecated and will be removed in v2.0.0.",
+ category=FutureWarning,
+)
class LiteAgent(FlowTrackable, BaseModel):
- """
- A lightweight agent that can process messages and use tools.
+ """A lightweight agent that can process messages and use tools.
.. deprecated::
LiteAgent is deprecated and will be removed in a future version.
@@ -227,9 +229,6 @@ class LiteAgent(FlowTrackable, BaseModel):
default=None,
description="Callback to check if the request is within the RPM8 limit",
)
- i18n: I18N = Field(
- default_factory=get_i18n, description="Internationalization settings."
- )
response_format: type[BaseModel] | None = Field(
default=None, description="Pydantic model for structured output"
)
@@ -281,18 +280,6 @@ class LiteAgent(FlowTrackable, BaseModel):
)
_memory: Any = PrivateAttr(default=None)
- @model_validator(mode="after")
- def emit_deprecation_warning(self) -> Self:
- """Emit deprecation warning for LiteAgent usage."""
- warnings.warn(
- "LiteAgent is deprecated and will be removed in a future version. "
- "Use Agent().kickoff(messages) instead, which provides the same "
- "functionality with additional features like memory and knowledge support.",
- DeprecationWarning,
- stacklevel=2,
- )
- return self
-
@model_validator(mode="after")
def setup_llm(self) -> Self:
"""Set up the LLM and other components after initialization."""
@@ -571,7 +558,7 @@ class LiteAgent(FlowTrackable, BaseModel):
f"- {m.record.content}" for m in matches
)
if memory_block:
- formatted = self.i18n.slice("memory").format(memory=memory_block)
+ formatted = I18N_DEFAULT.slice("memory").format(memory=memory_block)
if self._messages and self._messages[0].get("role") == "system":
existing_content = self._messages[0].get("content", "")
if not isinstance(existing_content, str):
@@ -644,7 +631,7 @@ class LiteAgent(FlowTrackable, BaseModel):
try:
model_schema = generate_model_description(active_response_format)
schema = json.dumps(model_schema, indent=2)
- instructions = self.i18n.slice("formatted_task_instructions").format(
+ instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_format=schema
)
@@ -793,7 +780,9 @@ class LiteAgent(FlowTrackable, BaseModel):
base_prompt = ""
if self._parsed_tools:
# Use the prompt template for agents with tools
- base_prompt = self.i18n.slice("lite_agent_system_prompt_with_tools").format(
+ base_prompt = I18N_DEFAULT.slice(
+ "lite_agent_system_prompt_with_tools"
+ ).format(
role=self.role,
backstory=self.backstory,
goal=self.goal,
@@ -802,7 +791,7 @@ class LiteAgent(FlowTrackable, BaseModel):
)
else:
# Use the prompt template for agents without tools
- base_prompt = self.i18n.slice(
+ base_prompt = I18N_DEFAULT.slice(
"lite_agent_system_prompt_without_tools"
).format(
role=self.role,
@@ -814,7 +803,7 @@ class LiteAgent(FlowTrackable, BaseModel):
if active_response_format:
model_description = generate_model_description(active_response_format)
schema_json = json.dumps(model_description, indent=2)
- base_prompt += self.i18n.slice("lite_agent_response_format").format(
+ base_prompt += I18N_DEFAULT.slice("lite_agent_response_format").format(
response_format=schema_json
)
@@ -875,7 +864,6 @@ class LiteAgent(FlowTrackable, BaseModel):
formatted_answer = handle_max_iterations_exceeded(
formatted_answer,
printer=PRINTER,
- i18n=self.i18n,
messages=self._messages,
llm=cast(LLM, self.llm),
callbacks=self._callbacks,
@@ -914,7 +902,6 @@ class LiteAgent(FlowTrackable, BaseModel):
tool_result = execute_tool_and_check_finality(
agent_action=formatted_answer,
tools=self._parsed_tools,
- i18n=self.i18n,
agent_key=self.key,
agent_role=self.role,
agent=self.original_agent,
@@ -956,7 +943,6 @@ class LiteAgent(FlowTrackable, BaseModel):
messages=self._messages,
llm=cast(LLM, self.llm),
callbacks=self._callbacks,
- i18n=self.i18n,
verbose=self.verbose,
)
continue
diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py
index e6f5cc68b..db126954e 100644
--- a/lib/crewai/src/crewai/llm.py
+++ b/lib/crewai/src/crewai/llm.py
@@ -51,6 +51,7 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
)
from crewai.utilities.logger_utils import suppress_warnings
from crewai.utilities.string_utils import sanitize_tool_name
+from crewai.utilities.token_counter_callback import TokenCalcHandler
try:
@@ -75,8 +76,13 @@ try:
from litellm.types.utils import (
ChatCompletionDeltaToolCall,
Choices,
+ Delta as LiteLLMDelta,
Function,
+ Message,
ModelResponse,
+ ModelResponseBase,
+ ModelResponseStream,
+ StreamingChoices as LiteLLMStreamingChoices,
)
from litellm.utils import supports_response_schema
@@ -85,6 +91,11 @@ except ImportError:
LITELLM_AVAILABLE = False
litellm = None # type: ignore[assignment]
Choices = None # type: ignore[assignment, misc]
+ LiteLLMDelta = None # type: ignore[assignment, misc]
+ Message = None # type: ignore[assignment, misc]
+ ModelResponseBase = None # type: ignore[assignment, misc]
+ ModelResponseStream = None # type: ignore[assignment, misc]
+ LiteLLMStreamingChoices = None # type: ignore[assignment, misc]
get_supported_openai_params = None # type: ignore[assignment]
ChatCompletionDeltaToolCall = None # type: ignore[assignment, misc]
Function = None # type: ignore[assignment, misc]
@@ -709,7 +720,7 @@ class LLM(BaseLLM):
chunk_content = None
response_id = None
- if hasattr(chunk, "id"):
+ if isinstance(chunk, ModelResponseBase):
response_id = chunk.id
# Safely extract content from various chunk formats
@@ -718,18 +729,16 @@ class LLM(BaseLLM):
choices = None
if isinstance(chunk, dict) and "choices" in chunk:
choices = chunk["choices"]
- elif hasattr(chunk, "choices"):
- # Check if choices is not a type but an actual attribute with value
- if not isinstance(chunk.choices, type):
- choices = chunk.choices
+ elif isinstance(chunk, ModelResponseStream):
+ choices = chunk.choices
# Try to extract usage information if available
+ # NOTE: usage is a pydantic extra field on ModelResponseBase,
+ # so it must be accessed via model_extra.
if isinstance(chunk, dict) and "usage" in chunk:
usage_info = chunk["usage"]
- elif hasattr(chunk, "usage"):
- # Check if usage is not a type but an actual attribute with value
- if not isinstance(chunk.usage, type):
- usage_info = chunk.usage
+ elif isinstance(chunk, ModelResponseBase) and chunk.model_extra:
+ usage_info = chunk.model_extra.get("usage") or usage_info
if choices and len(choices) > 0:
choice = choices[0]
@@ -738,7 +747,7 @@ class LLM(BaseLLM):
delta = None
if isinstance(choice, dict) and "delta" in choice:
delta = choice["delta"]
- elif hasattr(choice, "delta"):
+ elif isinstance(choice, LiteLLMStreamingChoices):
delta = choice.delta
# Extract content from delta
@@ -748,7 +757,7 @@ class LLM(BaseLLM):
if "content" in delta and delta["content"] is not None:
chunk_content = delta["content"]
# Handle object format
- elif hasattr(delta, "content"):
+ elif isinstance(delta, LiteLLMDelta):
chunk_content = delta.content
# Handle case where content might be None or empty
@@ -821,9 +830,8 @@ class LLM(BaseLLM):
choices = None
if isinstance(last_chunk, dict) and "choices" in last_chunk:
choices = last_chunk["choices"]
- elif hasattr(last_chunk, "choices"):
- if not isinstance(last_chunk.choices, type):
- choices = last_chunk.choices
+ elif isinstance(last_chunk, ModelResponseStream):
+ choices = last_chunk.choices
if choices and len(choices) > 0:
choice = choices[0]
@@ -832,14 +840,14 @@ class LLM(BaseLLM):
message = None
if isinstance(choice, dict) and "message" in choice:
message = choice["message"]
- elif hasattr(choice, "message"):
+ elif isinstance(choice, Choices):
message = choice.message
if message:
content = None
if isinstance(message, dict) and "content" in message:
content = message["content"]
- elif hasattr(message, "content"):
+ elif isinstance(message, Message):
content = message.content
if content:
@@ -866,24 +874,23 @@ class LLM(BaseLLM):
choices = None
if isinstance(last_chunk, dict) and "choices" in last_chunk:
choices = last_chunk["choices"]
- elif hasattr(last_chunk, "choices"):
- if not isinstance(last_chunk.choices, type):
- choices = last_chunk.choices
+ elif isinstance(last_chunk, ModelResponseStream):
+ choices = last_chunk.choices
if choices and len(choices) > 0:
choice = choices[0]
- message = None
- if isinstance(choice, dict) and "message" in choice:
- message = choice["message"]
- elif hasattr(choice, "message"):
- message = choice.message
+ delta = None
+ if isinstance(choice, dict) and "delta" in choice:
+ delta = choice["delta"]
+ elif isinstance(choice, LiteLLMStreamingChoices):
+ delta = choice.delta
- if message:
- if isinstance(message, dict) and "tool_calls" in message:
- tool_calls = message["tool_calls"]
- elif hasattr(message, "tool_calls"):
- tool_calls = message.tool_calls
+ if delta:
+ if isinstance(delta, dict) and "tool_calls" in delta:
+ tool_calls = delta["tool_calls"]
+ elif isinstance(delta, LiteLLMDelta):
+ tool_calls = delta.tool_calls
except Exception as e:
logging.debug(f"Error checking for tool calls: {e}")
@@ -1037,7 +1044,7 @@ class LLM(BaseLLM):
"""
if callbacks and len(callbacks) > 0:
for callback in callbacks:
- if hasattr(callback, "log_success_event"):
+ if isinstance(callback, TokenCalcHandler):
# Use the usage_info we've been tracking
if not usage_info:
# Try to get usage from the last chunk if we haven't already
@@ -1048,9 +1055,14 @@ class LLM(BaseLLM):
and "usage" in last_chunk
):
usage_info = last_chunk["usage"]
- elif hasattr(last_chunk, "usage"):
- if not isinstance(last_chunk.usage, type):
- usage_info = last_chunk.usage
+ elif (
+ isinstance(last_chunk, ModelResponseBase)
+ and last_chunk.model_extra
+ ):
+ usage_info = (
+ last_chunk.model_extra.get("usage")
+ or usage_info
+ )
except Exception as e:
logging.debug(f"Error extracting usage info: {e}")
@@ -1123,13 +1135,10 @@ class LLM(BaseLLM):
params["response_model"] = response_model
response = litellm.completion(**params)
- if (
- hasattr(response, "usage")
- and not isinstance(response.usage, type)
- and response.usage
- ):
- usage_info = response.usage
- self._track_token_usage_internal(usage_info)
+ if isinstance(response, ModelResponseBase) and response.model_extra:
+ usage_info = response.model_extra.get("usage")
+ if usage_info:
+ self._track_token_usage_internal(usage_info)
except LLMContextLengthExceededError:
# Re-raise our own context length error
@@ -1141,7 +1150,11 @@ class LLM(BaseLLM):
raise LLMContextLengthExceededError(error_msg) from e
raise
- response_usage = self._usage_to_dict(getattr(response, "usage", None))
+ response_usage = self._usage_to_dict(
+ response.model_extra.get("usage")
+ if isinstance(response, ModelResponseBase) and response.model_extra
+ else None
+ )
# --- 2) Handle structured output response (when response_model is provided)
if response_model is not None:
@@ -1166,8 +1179,13 @@ class LLM(BaseLLM):
# --- 3) Handle callbacks with usage info
if callbacks and len(callbacks) > 0:
for callback in callbacks:
- if hasattr(callback, "log_success_event"):
- usage_info = getattr(response, "usage", None)
+ if isinstance(callback, TokenCalcHandler):
+ usage_info = (
+ response.model_extra.get("usage")
+ if isinstance(response, ModelResponseBase)
+ and response.model_extra
+ else None
+ )
if usage_info:
callback.log_success_event(
kwargs=params,
@@ -1176,7 +1194,7 @@ class LLM(BaseLLM):
end_time=0,
)
# --- 4) Check for tool calls
- tool_calls = getattr(response_message, "tool_calls", [])
+ tool_calls = response_message.tool_calls or []
# --- 5) If no tool calls or no available functions, return the text response directly as long as there is a text response
if (not tool_calls or not available_functions) and text_response:
@@ -1269,13 +1287,10 @@ class LLM(BaseLLM):
params["response_model"] = response_model
response = await litellm.acompletion(**params)
- if (
- hasattr(response, "usage")
- and not isinstance(response.usage, type)
- and response.usage
- ):
- usage_info = response.usage
- self._track_token_usage_internal(usage_info)
+ if isinstance(response, ModelResponseBase) and response.model_extra:
+ usage_info = response.model_extra.get("usage")
+ if usage_info:
+ self._track_token_usage_internal(usage_info)
except LLMContextLengthExceededError:
# Re-raise our own context length error
@@ -1287,7 +1302,11 @@ class LLM(BaseLLM):
raise LLMContextLengthExceededError(error_msg) from e
raise
- response_usage = self._usage_to_dict(getattr(response, "usage", None))
+ response_usage = self._usage_to_dict(
+ response.model_extra.get("usage")
+ if isinstance(response, ModelResponseBase) and response.model_extra
+ else None
+ )
if response_model is not None:
if isinstance(response, BaseModel):
@@ -1309,8 +1328,13 @@ class LLM(BaseLLM):
if callbacks and len(callbacks) > 0:
for callback in callbacks:
- if hasattr(callback, "log_success_event"):
- usage_info = getattr(response, "usage", None)
+ if isinstance(callback, TokenCalcHandler):
+ usage_info = (
+ response.model_extra.get("usage")
+ if isinstance(response, ModelResponseBase)
+ and response.model_extra
+ else None
+ )
if usage_info:
callback.log_success_event(
kwargs=params,
@@ -1319,7 +1343,7 @@ class LLM(BaseLLM):
end_time=0,
)
- tool_calls = getattr(response_message, "tool_calls", [])
+ tool_calls = response_message.tool_calls or []
if (not tool_calls or not available_functions) and text_response:
self._handle_emit_call_events(
@@ -1394,18 +1418,19 @@ class LLM(BaseLLM):
async for chunk in await litellm.acompletion(**params):
chunk_count += 1
chunk_content = None
- response_id = chunk.id if hasattr(chunk, "id") else None
+ response_id = chunk.id if isinstance(chunk, ModelResponseBase) else None
try:
choices = None
if isinstance(chunk, dict) and "choices" in chunk:
choices = chunk["choices"]
- elif hasattr(chunk, "choices"):
- if not isinstance(chunk.choices, type):
- choices = chunk.choices
+ elif isinstance(chunk, ModelResponseStream):
+ choices = chunk.choices
- if hasattr(chunk, "usage") and chunk.usage is not None:
- usage_info = chunk.usage
+ if isinstance(chunk, ModelResponseBase) and chunk.model_extra:
+ chunk_usage = chunk.model_extra.get("usage")
+ if chunk_usage is not None:
+ usage_info = chunk_usage
if choices and len(choices) > 0:
first_choice = choices[0]
@@ -1413,19 +1438,19 @@ class LLM(BaseLLM):
if isinstance(first_choice, dict):
delta = first_choice.get("delta", {})
- elif hasattr(first_choice, "delta"):
+ elif isinstance(first_choice, LiteLLMStreamingChoices):
delta = first_choice.delta
if delta:
if isinstance(delta, dict):
chunk_content = delta.get("content")
- elif hasattr(delta, "content"):
+ elif isinstance(delta, LiteLLMDelta):
chunk_content = delta.content
tool_calls: list[ChatCompletionDeltaToolCall] | None = None
if isinstance(delta, dict):
tool_calls = delta.get("tool_calls")
- elif hasattr(delta, "tool_calls"):
+ elif isinstance(delta, LiteLLMDelta):
tool_calls = delta.tool_calls
if tool_calls:
@@ -1461,7 +1486,7 @@ class LLM(BaseLLM):
if callbacks and len(callbacks) > 0 and usage_info:
for callback in callbacks:
- if hasattr(callback, "log_success_event"):
+ if isinstance(callback, TokenCalcHandler):
callback.log_success_event(
kwargs=params,
response_obj={"usage": usage_info},
@@ -1920,7 +1945,7 @@ class LLM(BaseLLM):
return None
if isinstance(usage, dict):
return usage
- if hasattr(usage, "model_dump"):
+ if isinstance(usage, BaseModel):
result: dict[str, Any] = usage.model_dump()
return result
if hasattr(usage, "__dict__"):
@@ -1984,7 +2009,7 @@ class LLM(BaseLLM):
)
return messages
- provider = getattr(self, "provider", None) or self.model
+ provider = self.provider or self.model
for msg in messages:
files = msg.get("files")
@@ -2035,7 +2060,7 @@ class LLM(BaseLLM):
)
return messages
- provider = getattr(self, "provider", None) or self.model
+ provider = self.provider or self.model
for msg in messages:
files = msg.get("files")
diff --git a/lib/crewai/src/crewai/llms/base_llm.py b/lib/crewai/src/crewai/llms/base_llm.py
index 41ce1d2cd..4f45572ee 100644
--- a/lib/crewai/src/crewai/llms/base_llm.py
+++ b/lib/crewai/src/crewai/llms/base_llm.py
@@ -172,6 +172,8 @@ class BaseLLM(BaseModel, ABC):
"completion_tokens": 0,
"successful_requests": 0,
"cached_prompt_tokens": 0,
+ "reasoning_tokens": 0,
+ "cache_creation_tokens": 0,
}
)
@@ -808,14 +810,24 @@ class BaseLLM(BaseModel, ABC):
cached_tokens = (
usage_data.get("cached_tokens")
or usage_data.get("cached_prompt_tokens")
+ or usage_data.get("cache_read_input_tokens")
or 0
)
+ if not cached_tokens:
+ prompt_details = usage_data.get("prompt_tokens_details")
+ if isinstance(prompt_details, dict):
+ cached_tokens = prompt_details.get("cached_tokens", 0) or 0
+
+ reasoning_tokens = usage_data.get("reasoning_tokens", 0) or 0
+ cache_creation_tokens = usage_data.get("cache_creation_tokens", 0) or 0
self._token_usage["prompt_tokens"] += prompt_tokens
self._token_usage["completion_tokens"] += completion_tokens
self._token_usage["total_tokens"] += prompt_tokens + completion_tokens
self._token_usage["successful_requests"] += 1
self._token_usage["cached_prompt_tokens"] += cached_tokens
+ self._token_usage["reasoning_tokens"] += reasoning_tokens
+ self._token_usage["cache_creation_tokens"] += cache_creation_tokens
def get_token_usage_summary(self) -> UsageMetrics:
"""Get summary of token usage for this LLM instance.
diff --git a/lib/crewai/src/crewai/llms/providers/anthropic/completion.py b/lib/crewai/src/crewai/llms/providers/anthropic/completion.py
index b6df34b94..b627a8539 100644
--- a/lib/crewai/src/crewai/llms/providers/anthropic/completion.py
+++ b/lib/crewai/src/crewai/llms/providers/anthropic/completion.py
@@ -11,10 +11,14 @@ from crewai.events.types.llm_events import LLMCallType
from crewai.llms.base_llm import BaseLLM, JsonResponseFormat, llm_call_context
from crewai.llms.hooks.base import BaseInterceptor
from crewai.llms.hooks.transport import AsyncHTTPTransport, HTTPTransport
+from crewai.llms.providers.utils.common import safe_tool_conversion
from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
+from crewai.utilities.pydantic_schema_utils import (
+ sanitize_tool_params_for_anthropic_strict,
+)
from crewai.utilities.types import LLMMessage
@@ -189,16 +193,41 @@ class AnthropicCompletion(BaseLLM):
@model_validator(mode="after")
def _init_clients(self) -> AnthropicCompletion:
- self._client = Anthropic(**self._get_client_params())
+ """Eagerly build clients when the API key is available, otherwise
+ defer so ``LLM(model="anthropic/...")`` can be constructed at module
+ import time even before deployment env vars are set.
+ """
+ try:
+ self._client = self._build_sync_client()
+ self._async_client = self._build_async_client()
+ except ValueError:
+ pass
+ return self
- async_client_params = self._get_client_params()
+ def _build_sync_client(self) -> Any:
+ return Anthropic(**self._get_client_params())
+
+ def _build_async_client(self) -> Any:
+ # Skip the sync httpx.Client that `_get_client_params` would
+ # otherwise construct under `interceptor`; we attach an async one
+ # below and would leak the sync one if both were built.
+ async_client_params = self._get_client_params(include_http_client=False)
if self.interceptor:
async_transport = AsyncHTTPTransport(interceptor=self.interceptor)
- async_http_client = httpx.AsyncClient(transport=async_transport)
- async_client_params["http_client"] = async_http_client
+ async_client_params["http_client"] = httpx.AsyncClient(
+ transport=async_transport
+ )
+ return AsyncAnthropic(**async_client_params)
- self._async_client = AsyncAnthropic(**async_client_params)
- return self
+ def _get_sync_client(self) -> Any:
+ if self._client is None:
+ self._client = self._build_sync_client()
+ return self._client
+
+ def _get_async_client(self) -> Any:
+ if self._async_client is None:
+ self._async_client = self._build_async_client()
+ return self._async_client
def to_config_dict(self) -> dict[str, Any]:
"""Extend base config with Anthropic-specific fields."""
@@ -213,8 +242,15 @@ class AnthropicCompletion(BaseLLM):
config["timeout"] = self.timeout
return config
- def _get_client_params(self) -> dict[str, Any]:
- """Get client parameters."""
+ def _get_client_params(self, include_http_client: bool = True) -> dict[str, Any]:
+ """Get client parameters.
+
+ Args:
+ include_http_client: When True (default) and an interceptor is
+ set, attach a sync ``httpx.Client``. The async builder
+ passes ``False`` so it can attach its own async client
+ without leaking a sync one.
+ """
if self.api_key is None:
self.api_key = os.getenv("ANTHROPIC_API_KEY")
@@ -228,7 +264,7 @@ class AnthropicCompletion(BaseLLM):
"max_retries": self.max_retries,
}
- if self.interceptor:
+ if include_http_client and self.interceptor:
transport = HTTPTransport(interceptor=self.interceptor)
http_client = httpx.Client(transport=transport)
client_params["http_client"] = http_client # type: ignore[assignment]
@@ -473,10 +509,8 @@ class AnthropicCompletion(BaseLLM):
continue
try:
- from crewai.llms.providers.utils.common import safe_tool_conversion
-
name, description, parameters = safe_tool_conversion(tool, "Anthropic")
- except (ImportError, KeyError, ValueError) as e:
+ except (KeyError, ValueError) as e:
logging.error(f"Error converting tool to Anthropic format: {e}")
raise e
@@ -485,8 +519,15 @@ class AnthropicCompletion(BaseLLM):
"description": description,
}
+ func_info = tool.get("function", {})
+ strict_enabled = bool(func_info.get("strict"))
+
if parameters and isinstance(parameters, dict):
- anthropic_tool["input_schema"] = parameters
+ anthropic_tool["input_schema"] = (
+ sanitize_tool_params_for_anthropic_strict(parameters)
+ if strict_enabled
+ else parameters
+ )
else:
anthropic_tool["input_schema"] = {
"type": "object",
@@ -494,6 +535,9 @@ class AnthropicCompletion(BaseLLM):
"required": [],
}
+ if strict_enabled:
+ anthropic_tool["strict"] = True
+
anthropic_tools.append(anthropic_tool)
return anthropic_tools
@@ -786,11 +830,11 @@ class AnthropicCompletion(BaseLLM):
try:
if betas:
params["betas"] = betas
- response = self._client.beta.messages.create(
+ response = self._get_sync_client().beta.messages.create(
**params, extra_body=extra_body
)
else:
- response = self._client.messages.create(**params)
+ response = self._get_sync_client().messages.create(**params)
except Exception as e:
if is_context_length_exceeded(e):
@@ -938,9 +982,11 @@ class AnthropicCompletion(BaseLLM):
current_tool_calls: dict[int, dict[str, Any]] = {}
stream_context = (
- self._client.beta.messages.stream(**stream_params, extra_body=extra_body)
+ self._get_sync_client().beta.messages.stream(
+ **stream_params, extra_body=extra_body
+ )
if betas
- else self._client.messages.stream(**stream_params)
+ else self._get_sync_client().messages.stream(**stream_params)
)
with stream_context as stream:
response_id = None
@@ -1219,7 +1265,9 @@ class AnthropicCompletion(BaseLLM):
try:
# Send tool results back to Claude for final response
- final_response: Message = self._client.messages.create(**follow_up_params)
+ final_response: Message = self._get_sync_client().messages.create(
+ **follow_up_params
+ )
# Track token usage for follow-up call
follow_up_usage = self._extract_anthropic_token_usage(final_response)
@@ -1315,11 +1363,11 @@ class AnthropicCompletion(BaseLLM):
try:
if betas:
params["betas"] = betas
- response = await self._async_client.beta.messages.create(
+ response = await self._get_async_client().beta.messages.create(
**params, extra_body=extra_body
)
else:
- response = await self._async_client.messages.create(**params)
+ response = await self._get_async_client().messages.create(**params)
except Exception as e:
if is_context_length_exceeded(e):
@@ -1453,11 +1501,11 @@ class AnthropicCompletion(BaseLLM):
current_tool_calls: dict[int, dict[str, Any]] = {}
stream_context = (
- self._async_client.beta.messages.stream(
+ self._get_async_client().beta.messages.stream(
**stream_params, extra_body=extra_body
)
if betas
- else self._async_client.messages.stream(**stream_params)
+ else self._get_async_client().messages.stream(**stream_params)
)
async with stream_context as stream:
response_id = None
@@ -1622,7 +1670,7 @@ class AnthropicCompletion(BaseLLM):
]
try:
- final_response: Message = await self._async_client.messages.create(
+ final_response: Message = await self._get_async_client().messages.create(
**follow_up_params
)
@@ -1704,18 +1752,23 @@ class AnthropicCompletion(BaseLLM):
def _extract_anthropic_token_usage(
response: Message | BetaMessage,
) -> dict[str, Any]:
- """Extract token usage from Anthropic response."""
+ """Extract token usage and response metadata from Anthropic response."""
if hasattr(response, "usage") and response.usage:
usage = response.usage
input_tokens = getattr(usage, "input_tokens", 0)
output_tokens = getattr(usage, "output_tokens", 0)
cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0) or 0
- return {
+ cache_creation_tokens = (
+ getattr(usage, "cache_creation_input_tokens", 0) or 0
+ )
+ result: dict[str, Any] = {
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"cached_prompt_tokens": cache_read_tokens,
+ "cache_creation_tokens": cache_creation_tokens,
}
+ return result
return {"total_tokens": 0}
def supports_multimodal(self) -> bool:
@@ -1745,8 +1798,8 @@ class AnthropicCompletion(BaseLLM):
from crewai_files.uploaders.anthropic import AnthropicFileUploader
return AnthropicFileUploader(
- client=self._client,
- async_client=self._async_client,
+ client=self._get_sync_client(),
+ async_client=self._get_async_client(),
)
except ImportError:
return None
diff --git a/lib/crewai/src/crewai/llms/providers/azure/completion.py b/lib/crewai/src/crewai/llms/providers/azure/completion.py
index db7ab7e73..4b8d842a5 100644
--- a/lib/crewai/src/crewai/llms/providers/azure/completion.py
+++ b/lib/crewai/src/crewai/llms/providers/azure/completion.py
@@ -116,43 +116,100 @@ class AzureCompletion(BaseLLM):
data.get("api_version") or os.getenv("AZURE_API_VERSION") or "2024-06-01"
)
- if not data["api_key"]:
- raise ValueError(
- "Azure API key is required. Set AZURE_API_KEY environment variable or pass api_key parameter."
- )
- if not data["endpoint"]:
- raise ValueError(
- "Azure endpoint is required. Set AZURE_ENDPOINT environment variable or pass endpoint parameter."
- )
-
+ # Credentials and endpoint are validated lazily in `_init_clients`
+ # so the LLM can be constructed before deployment env vars are set.
model = data.get("model", "")
- data["endpoint"] = AzureCompletion._validate_and_fix_endpoint(
- data["endpoint"], model
+ if data["endpoint"]:
+ data["endpoint"] = AzureCompletion._validate_and_fix_endpoint(
+ data["endpoint"], model
+ )
+ data["is_azure_openai_endpoint"] = AzureCompletion._is_azure_openai_endpoint(
+ data["endpoint"]
)
data["is_openai_model"] = any(
prefix in model.lower() for prefix in ["gpt-", "o1-", "text-"]
)
- parsed = urlparse(data["endpoint"])
- hostname = parsed.hostname or ""
- data["is_azure_openai_endpoint"] = (
- hostname == "openai.azure.com" or hostname.endswith(".openai.azure.com")
- ) and "/openai/deployments/" in data["endpoint"]
return data
+ @staticmethod
+ def _is_azure_openai_endpoint(endpoint: str | None) -> bool:
+ if not endpoint:
+ return False
+ hostname = urlparse(endpoint).hostname or ""
+ return (
+ hostname == "openai.azure.com" or hostname.endswith(".openai.azure.com")
+ ) and "/openai/deployments/" in endpoint
+
@model_validator(mode="after")
def _init_clients(self) -> AzureCompletion:
+ """Eagerly build clients when credentials are available, otherwise
+ defer so ``LLM(model="azure/...")`` can be constructed at module
+ import time even before deployment env vars are set.
+ """
+ try:
+ self._client = self._build_sync_client()
+ self._async_client = self._build_async_client()
+ except ValueError:
+ pass
+ return self
+
+ def _build_sync_client(self) -> Any:
+ return ChatCompletionsClient(**self._make_client_kwargs())
+
+ def _build_async_client(self) -> Any:
+ return AsyncChatCompletionsClient(**self._make_client_kwargs())
+
+ def _make_client_kwargs(self) -> dict[str, Any]:
+ # Re-read env vars so that a deferred build can pick up credentials
+ # that weren't set at instantiation time (e.g. LLM constructed at
+ # module import before deployment env vars were injected).
if not self.api_key:
- raise ValueError("Azure API key is required.")
+ self.api_key = os.getenv("AZURE_API_KEY")
+ if not self.endpoint:
+ endpoint = (
+ os.getenv("AZURE_ENDPOINT")
+ or os.getenv("AZURE_OPENAI_ENDPOINT")
+ or os.getenv("AZURE_API_BASE")
+ )
+ if endpoint:
+ self.endpoint = AzureCompletion._validate_and_fix_endpoint(
+ endpoint, self.model
+ )
+ # Recompute the routing flag now that the endpoint is known —
+ # _prepare_completion_params uses it to decide whether to
+ # include `model` in the request body (Azure OpenAI endpoints
+ # embed the deployment name in the URL and reject it).
+ self.is_azure_openai_endpoint = (
+ AzureCompletion._is_azure_openai_endpoint(self.endpoint)
+ )
+
+ if not self.api_key:
+ raise ValueError(
+ "Azure API key is required. Set AZURE_API_KEY environment "
+ "variable or pass api_key parameter."
+ )
+ if not self.endpoint:
+ raise ValueError(
+ "Azure endpoint is required. Set AZURE_ENDPOINT environment "
+ "variable or pass endpoint parameter."
+ )
client_kwargs: dict[str, Any] = {
"endpoint": self.endpoint,
"credential": AzureKeyCredential(self.api_key),
}
if self.api_version:
client_kwargs["api_version"] = self.api_version
+ return client_kwargs
- self._client = ChatCompletionsClient(**client_kwargs)
- self._async_client = AsyncChatCompletionsClient(**client_kwargs)
- return self
+ def _get_sync_client(self) -> Any:
+ if self._client is None:
+ self._client = self._build_sync_client()
+ return self._client
+
+ def _get_async_client(self) -> Any:
+ if self._async_client is None:
+ self._async_client = self._build_async_client()
+ return self._async_client
def to_config_dict(self) -> dict[str, Any]:
"""Extend base config with Azure-specific fields."""
@@ -713,8 +770,7 @@ class AzureCompletion(BaseLLM):
) -> str | Any:
"""Handle non-streaming chat completion."""
try:
- # Cast params to Any to avoid type checking issues with TypedDict unpacking
- response: ChatCompletions = self._client.complete(**params)
+ response: ChatCompletions = self._get_sync_client().complete(**params)
return self._process_completion_response(
response=response,
params=params,
@@ -913,7 +969,7 @@ class AzureCompletion(BaseLLM):
tool_calls: dict[int, dict[str, Any]] = {}
usage_data: dict[str, Any] | None = None
- for update in self._client.complete(**params):
+ for update in self._get_sync_client().complete(**params):
if isinstance(update, StreamingChatCompletionsUpdate):
if update.usage:
usage = update.usage
@@ -953,8 +1009,9 @@ class AzureCompletion(BaseLLM):
) -> str | Any:
"""Handle non-streaming chat completion asynchronously."""
try:
- # Cast params to Any to avoid type checking issues with TypedDict unpacking
- response: ChatCompletions = await self._async_client.complete(**params)
+ response: ChatCompletions = await self._get_async_client().complete(
+ **params
+ )
return self._process_completion_response(
response=response,
params=params,
@@ -980,7 +1037,7 @@ class AzureCompletion(BaseLLM):
usage_data: dict[str, Any] | None = None
- stream = await self._async_client.complete(**params)
+ stream = await self._get_async_client().complete(**params)
async for update in stream:
if isinstance(update, StreamingChatCompletionsUpdate):
if hasattr(update, "usage") and update.usage:
@@ -1076,28 +1133,39 @@ class AzureCompletion(BaseLLM):
@staticmethod
def _extract_azure_token_usage(response: ChatCompletions) -> dict[str, Any]:
- """Extract token usage from Azure response."""
+ """Extract token usage and response metadata from Azure response."""
if hasattr(response, "usage") and response.usage:
usage = response.usage
cached_tokens = 0
prompt_details = getattr(usage, "prompt_tokens_details", None)
if prompt_details:
cached_tokens = getattr(prompt_details, "cached_tokens", 0) or 0
- return {
+ reasoning_tokens = 0
+ completion_details = getattr(usage, "completion_tokens_details", None)
+ if completion_details:
+ reasoning_tokens = (
+ getattr(completion_details, "reasoning_tokens", 0) or 0
+ )
+ result: dict[str, Any] = {
"prompt_tokens": getattr(usage, "prompt_tokens", 0),
"completion_tokens": getattr(usage, "completion_tokens", 0),
"total_tokens": getattr(usage, "total_tokens", 0),
"cached_prompt_tokens": cached_tokens,
+ "reasoning_tokens": reasoning_tokens,
}
+ return result
return {"total_tokens": 0}
async def aclose(self) -> None:
"""Close the async client and clean up resources.
This ensures proper cleanup of the underlying aiohttp session
- to avoid unclosed connector warnings.
+ to avoid unclosed connector warnings. Accesses the cached client
+ directly rather than going through `_get_async_client` so a
+ cleanup on an uninitialized LLM is a harmless no-op rather than
+ a credential-required error.
"""
- if hasattr(self._async_client, "close"):
+ if self._async_client is not None and hasattr(self._async_client, "close"):
await self._async_client.close()
async def __aenter__(self) -> Self:
diff --git a/lib/crewai/src/crewai/llms/providers/bedrock/completion.py b/lib/crewai/src/crewai/llms/providers/bedrock/completion.py
index c25c9bfec..54c222c85 100644
--- a/lib/crewai/src/crewai/llms/providers/bedrock/completion.py
+++ b/lib/crewai/src/crewai/llms/providers/bedrock/completion.py
@@ -12,6 +12,7 @@ from typing_extensions import Required
from crewai.events.types.llm_events import LLMCallType
from crewai.llms.base_llm import BaseLLM, llm_call_context
+from crewai.llms.providers.utils.common import safe_tool_conversion
from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
@@ -302,6 +303,22 @@ class BedrockCompletion(BaseLLM):
@model_validator(mode="after")
def _init_clients(self) -> BedrockCompletion:
+ """Eagerly build the sync client when AWS credentials resolve,
+ otherwise defer so ``LLM(model="bedrock/...")`` can be constructed
+ at module import time even before deployment env vars are set.
+
+ Only credential/SDK errors are caught — programming errors like
+ ``TypeError`` or ``AttributeError`` propagate so real bugs aren't
+ silently swallowed.
+ """
+ try:
+ self._client = self._build_sync_client()
+ except (BotoCoreError, ClientError, ValueError) as e:
+ logging.debug("Deferring Bedrock client construction: %s", e)
+ self._async_exit_stack = AsyncExitStack() if AIOBOTOCORE_AVAILABLE else None
+ return self
+
+ def _build_sync_client(self) -> Any:
config = Config(
read_timeout=300,
retries={"max_attempts": 3, "mode": "adaptive"},
@@ -313,9 +330,17 @@ class BedrockCompletion(BaseLLM):
aws_session_token=self.aws_session_token,
region_name=self.region_name,
)
- self._client = session.client("bedrock-runtime", config=config)
- self._async_exit_stack = AsyncExitStack() if AIOBOTOCORE_AVAILABLE else None
- return self
+ return session.client("bedrock-runtime", config=config)
+
+ def _get_sync_client(self) -> Any:
+ if self._client is None:
+ self._client = self._build_sync_client()
+ return self._client
+
+ def _get_async_client(self) -> Any:
+ """Async client is set up separately by ``_ensure_async_client``
+ using ``aiobotocore`` inside an exit stack."""
+ return self._async_client
def to_config_dict(self) -> dict[str, Any]:
"""Extend base config with Bedrock-specific fields."""
@@ -655,7 +680,7 @@ class BedrockCompletion(BaseLLM):
raise ValueError(f"Invalid message format at index {i}")
# Call Bedrock Converse API with proper error handling
- response = self._client.converse(
+ response = self._get_sync_client().converse(
modelId=self.model_id,
messages=cast(
"Sequence[MessageTypeDef | MessageOutputTypeDef]",
@@ -944,7 +969,7 @@ class BedrockCompletion(BaseLLM):
usage_data: dict[str, Any] | None = None
try:
- response = self._client.converse_stream(
+ response = self._get_sync_client().converse_stream(
modelId=self.model_id,
messages=cast(
"Sequence[MessageTypeDef | MessageOutputTypeDef]",
@@ -1948,8 +1973,6 @@ class BedrockCompletion(BaseLLM):
tools: list[dict[str, Any]],
) -> list[ConverseToolTypeDef]:
"""Convert CrewAI tools to Converse API format following AWS specification."""
- from crewai.llms.providers.utils.common import safe_tool_conversion
-
converse_tools: list[ConverseToolTypeDef] = []
for tool in tools:
@@ -2025,11 +2048,18 @@ class BedrockCompletion(BaseLLM):
input_tokens = usage.get("inputTokens", 0)
output_tokens = usage.get("outputTokens", 0)
total_tokens = usage.get("totalTokens", input_tokens + output_tokens)
+ raw_cached = (
+ usage.get("cacheReadInputTokenCount")
+ or usage.get("cacheReadInputTokens")
+ or 0
+ )
+ cached_tokens = raw_cached if isinstance(raw_cached, int) else 0
self._token_usage["prompt_tokens"] += input_tokens
self._token_usage["completion_tokens"] += output_tokens
self._token_usage["total_tokens"] += total_tokens
self._token_usage["successful_requests"] += 1
+ self._token_usage["cached_prompt_tokens"] += cached_tokens
def supports_function_calling(self) -> bool:
"""Check if the model supports function calling."""
diff --git a/lib/crewai/src/crewai/llms/providers/gemini/completion.py b/lib/crewai/src/crewai/llms/providers/gemini/completion.py
index c84f7f5fd..1b2fb26cb 100644
--- a/lib/crewai/src/crewai/llms/providers/gemini/completion.py
+++ b/lib/crewai/src/crewai/llms/providers/gemini/completion.py
@@ -118,9 +118,33 @@ class GeminiCompletion(BaseLLM):
@model_validator(mode="after")
def _init_client(self) -> GeminiCompletion:
- self._client = self._initialize_client(self.use_vertexai)
+ """Eagerly build the client when credentials resolve, otherwise defer
+ so ``LLM(model="gemini/...")`` can be constructed at module import time
+ even before deployment env vars are set.
+ """
+ try:
+ self._client = self._initialize_client(self.use_vertexai)
+ except ValueError:
+ pass
return self
+ def _get_sync_client(self) -> Any:
+ if self._client is None:
+ # Re-read env vars so a deferred build can pick up credentials
+ # that weren't set at instantiation time.
+ if not self.api_key:
+ self.api_key = os.getenv("GOOGLE_API_KEY") or os.getenv(
+ "GEMINI_API_KEY"
+ )
+ if not self.project:
+ self.project = os.getenv("GOOGLE_CLOUD_PROJECT")
+ self._client = self._initialize_client(self.use_vertexai)
+ return self._client
+
+ def _get_async_client(self) -> Any:
+ """Gemini uses a single client for both sync and async calls."""
+ return self._get_sync_client()
+
def to_config_dict(self) -> dict[str, Any]:
"""Extend base config with Gemini/Vertex-specific fields."""
config = super().to_config_dict()
@@ -228,6 +252,7 @@ class GeminiCompletion(BaseLLM):
if (
hasattr(self, "client")
+ and self._client is not None
and hasattr(self._client, "vertexai")
and self._client.vertexai
):
@@ -1112,7 +1137,7 @@ class GeminiCompletion(BaseLLM):
try:
# The API accepts list[Content] but mypy is overly strict about variance
contents_for_api: Any = contents
- response = self._client.models.generate_content(
+ response = self._get_sync_client().models.generate_content(
model=self.model,
contents=contents_for_api,
config=config,
@@ -1153,7 +1178,7 @@ class GeminiCompletion(BaseLLM):
# The API accepts list[Content] but mypy is overly strict about variance
contents_for_api: Any = contents
- for chunk in self._client.models.generate_content_stream(
+ for chunk in self._get_sync_client().models.generate_content_stream(
model=self.model,
contents=contents_for_api,
config=config,
@@ -1191,7 +1216,7 @@ class GeminiCompletion(BaseLLM):
try:
# The API accepts list[Content] but mypy is overly strict about variance
contents_for_api: Any = contents
- response = await self._client.aio.models.generate_content(
+ response = await self._get_async_client().aio.models.generate_content(
model=self.model,
contents=contents_for_api,
config=config,
@@ -1232,7 +1257,7 @@ class GeminiCompletion(BaseLLM):
# The API accepts list[Content] but mypy is overly strict about variance
contents_for_api: Any = contents
- stream = await self._client.aio.models.generate_content_stream(
+ stream = await self._get_async_client().aio.models.generate_content_stream(
model=self.model,
contents=contents_for_api,
config=config,
@@ -1306,17 +1331,20 @@ class GeminiCompletion(BaseLLM):
@staticmethod
def _extract_token_usage(response: GenerateContentResponse) -> dict[str, Any]:
- """Extract token usage from Gemini response."""
+ """Extract token usage and response metadata from Gemini response."""
if response.usage_metadata:
usage = response.usage_metadata
cached_tokens = getattr(usage, "cached_content_token_count", 0) or 0
- return {
+ thinking_tokens = getattr(usage, "thoughts_token_count", 0) or 0
+ result: dict[str, Any] = {
"prompt_token_count": getattr(usage, "prompt_token_count", 0),
"candidates_token_count": getattr(usage, "candidates_token_count", 0),
"total_token_count": getattr(usage, "total_token_count", 0),
"total_tokens": getattr(usage, "total_token_count", 0),
"cached_prompt_tokens": cached_tokens,
+ "reasoning_tokens": thinking_tokens,
}
+ return result
return {"total_tokens": 0}
@staticmethod
@@ -1436,6 +1464,6 @@ class GeminiCompletion(BaseLLM):
try:
from crewai_files.uploaders.gemini import GeminiFileUploader
- return GeminiFileUploader(client=self._client)
+ return GeminiFileUploader(client=self._get_sync_client())
except ImportError:
return None
diff --git a/lib/crewai/src/crewai/llms/providers/openai/completion.py b/lib/crewai/src/crewai/llms/providers/openai/completion.py
index b76f552df..ce3567fb8 100644
--- a/lib/crewai/src/crewai/llms/providers/openai/completion.py
+++ b/lib/crewai/src/crewai/llms/providers/openai/completion.py
@@ -32,11 +32,15 @@ from crewai.events.types.llm_events import LLMCallType
from crewai.llms.base_llm import BaseLLM, JsonResponseFormat, llm_call_context
from crewai.llms.hooks.base import BaseInterceptor
from crewai.llms.hooks.transport import AsyncHTTPTransport, HTTPTransport
+from crewai.llms.providers.utils.common import safe_tool_conversion
from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
-from crewai.utilities.pydantic_schema_utils import generate_model_description
+from crewai.utilities.pydantic_schema_utils import (
+ generate_model_description,
+ sanitize_tool_params_for_openai_strict,
+)
from crewai.utilities.types import LLMMessage
@@ -253,22 +257,40 @@ class OpenAICompletion(BaseLLM):
@model_validator(mode="after")
def _init_clients(self) -> OpenAICompletion:
+ """Eagerly build clients when the API key is available, otherwise
+ defer so ``LLM(model="openai/...")`` can be constructed at module
+ import time even before deployment env vars are set.
+ """
+ try:
+ self._client = self._build_sync_client()
+ self._async_client = self._build_async_client()
+ except ValueError:
+ pass
+ return self
+
+ def _build_sync_client(self) -> Any:
client_config = self._get_client_params()
if self.interceptor:
transport = HTTPTransport(interceptor=self.interceptor)
- http_client = httpx.Client(transport=transport)
- client_config["http_client"] = http_client
+ client_config["http_client"] = httpx.Client(transport=transport)
+ return OpenAI(**client_config)
- self._client = OpenAI(**client_config)
-
- async_client_config = self._get_client_params()
+ def _build_async_client(self) -> Any:
+ client_config = self._get_client_params()
if self.interceptor:
- async_transport = AsyncHTTPTransport(interceptor=self.interceptor)
- async_http_client = httpx.AsyncClient(transport=async_transport)
- async_client_config["http_client"] = async_http_client
+ transport = AsyncHTTPTransport(interceptor=self.interceptor)
+ client_config["http_client"] = httpx.AsyncClient(transport=transport)
+ return AsyncOpenAI(**client_config)
- self._async_client = AsyncOpenAI(**async_client_config)
- return self
+ def _get_sync_client(self) -> Any:
+ if self._client is None:
+ self._client = self._build_sync_client()
+ return self._client
+
+ def _get_async_client(self) -> Any:
+ if self._async_client is None:
+ self._async_client = self._build_async_client()
+ return self._async_client
@property
def last_response_id(self) -> str | None:
@@ -764,8 +786,6 @@ class OpenAICompletion(BaseLLM):
"function": {"name": "...", "description": "...", "parameters": {...}}
}
"""
- from crewai.llms.providers.utils.common import safe_tool_conversion
-
responses_tools = []
for tool in tools:
@@ -797,7 +817,7 @@ class OpenAICompletion(BaseLLM):
) -> str | ResponsesAPIResult | Any:
"""Handle non-streaming Responses API call."""
try:
- response: Response = self._client.responses.create(**params)
+ response: Response = self._get_sync_client().responses.create(**params)
# Track response ID for auto-chaining
if self.auto_chain and response.id:
@@ -933,7 +953,9 @@ class OpenAICompletion(BaseLLM):
) -> str | ResponsesAPIResult | Any:
"""Handle async non-streaming Responses API call."""
try:
- response: Response = await self._async_client.responses.create(**params)
+ response: Response = await self._get_async_client().responses.create(
+ **params
+ )
# Track response ID for auto-chaining
if self.auto_chain and response.id:
@@ -1069,7 +1091,7 @@ class OpenAICompletion(BaseLLM):
final_response: Response | None = None
usage: dict[str, Any] | None = None
- stream = self._client.responses.create(**params)
+ stream = self._get_sync_client().responses.create(**params)
response_id_stream = None
for event in stream:
@@ -1197,7 +1219,7 @@ class OpenAICompletion(BaseLLM):
final_response: Response | None = None
usage: dict[str, Any] | None = None
- stream = await self._async_client.responses.create(**params)
+ stream = await self._get_async_client().responses.create(**params)
response_id_stream = None
async for event in stream:
@@ -1324,19 +1346,23 @@ class OpenAICompletion(BaseLLM):
]
def _extract_responses_token_usage(self, response: Response) -> dict[str, Any]:
- """Extract token usage from Responses API response."""
+ """Extract token usage and response metadata from Responses API response."""
if response.usage:
- result = {
+ result: dict[str, Any] = {
"prompt_tokens": response.usage.input_tokens,
"completion_tokens": response.usage.output_tokens,
"total_tokens": response.usage.total_tokens,
}
- # Extract cached prompt tokens from input_tokens_details
input_details = getattr(response.usage, "input_tokens_details", None)
if input_details:
result["cached_prompt_tokens"] = (
getattr(input_details, "cached_tokens", 0) or 0
)
+ output_details = getattr(response.usage, "output_tokens_details", None)
+ if output_details:
+ result["reasoning_tokens"] = (
+ getattr(output_details, "reasoning_tokens", 0) or 0
+ )
return result
return {"total_tokens": 0}
@@ -1544,11 +1570,6 @@ class OpenAICompletion(BaseLLM):
self, tools: list[dict[str, BaseTool]]
) -> list[dict[str, Any]]:
"""Convert CrewAI tool format to OpenAI function calling format."""
- from crewai.llms.providers.utils.common import safe_tool_conversion
- from crewai.utilities.pydantic_schema_utils import (
- force_additional_properties_false,
- )
-
openai_tools = []
for tool in tools:
@@ -1567,8 +1588,9 @@ class OpenAICompletion(BaseLLM):
params_dict = (
parameters if isinstance(parameters, dict) else dict(parameters)
)
- params_dict = force_additional_properties_false(params_dict)
- openai_tool["function"]["parameters"] = params_dict
+ openai_tool["function"]["parameters"] = (
+ sanitize_tool_params_for_openai_strict(params_dict)
+ )
openai_tools.append(openai_tool)
return openai_tools
@@ -1587,7 +1609,7 @@ class OpenAICompletion(BaseLLM):
parse_params = {
k: v for k, v in params.items() if k != "response_format"
}
- parsed_response = self._client.beta.chat.completions.parse(
+ parsed_response = self._get_sync_client().beta.chat.completions.parse(
**parse_params,
response_format=response_model,
)
@@ -1611,7 +1633,9 @@ class OpenAICompletion(BaseLLM):
)
return parsed_object
- response: ChatCompletion = self._client.chat.completions.create(**params)
+ response: ChatCompletion = self._get_sync_client().chat.completions.create(
+ **params
+ )
usage = self._extract_openai_token_usage(response)
@@ -1838,7 +1862,7 @@ class OpenAICompletion(BaseLLM):
}
stream: ChatCompletionStream[BaseModel]
- with self._client.beta.chat.completions.stream(
+ with self._get_sync_client().beta.chat.completions.stream(
**parse_params, response_format=response_model
) as stream:
for chunk in stream:
@@ -1875,7 +1899,7 @@ class OpenAICompletion(BaseLLM):
return ""
completion_stream: Stream[ChatCompletionChunk] = (
- self._client.chat.completions.create(**params)
+ self._get_sync_client().chat.completions.create(**params)
)
usage_data: dict[str, Any] | None = None
@@ -1972,9 +1996,11 @@ class OpenAICompletion(BaseLLM):
parse_params = {
k: v for k, v in params.items() if k != "response_format"
}
- parsed_response = await self._async_client.beta.chat.completions.parse(
- **parse_params,
- response_format=response_model,
+ parsed_response = (
+ await self._get_async_client().beta.chat.completions.parse(
+ **parse_params,
+ response_format=response_model,
+ )
)
math_reasoning = parsed_response.choices[0].message
@@ -1996,8 +2022,8 @@ class OpenAICompletion(BaseLLM):
)
return parsed_object
- response: ChatCompletion = await self._async_client.chat.completions.create(
- **params
+ response: ChatCompletion = (
+ await self._get_async_client().chat.completions.create(**params)
)
usage = self._extract_openai_token_usage(response)
@@ -2123,7 +2149,7 @@ class OpenAICompletion(BaseLLM):
if response_model:
completion_stream: AsyncIterator[
ChatCompletionChunk
- ] = await self._async_client.chat.completions.create(**params)
+ ] = await self._get_async_client().chat.completions.create(**params)
accumulated_content = ""
usage_data: dict[str, Any] | None = None
@@ -2179,7 +2205,7 @@ class OpenAICompletion(BaseLLM):
stream: AsyncIterator[
ChatCompletionChunk
- ] = await self._async_client.chat.completions.create(**params)
+ ] = await self._get_async_client().chat.completions.create(**params)
usage_data = None
@@ -2307,20 +2333,24 @@ class OpenAICompletion(BaseLLM):
def _extract_openai_token_usage(
self, response: ChatCompletion | ChatCompletionChunk
) -> dict[str, Any]:
- """Extract token usage from OpenAI ChatCompletion or ChatCompletionChunk response."""
+ """Extract token usage and response metadata from OpenAI ChatCompletion."""
if hasattr(response, "usage") and response.usage:
usage = response.usage
- result = {
+ result: dict[str, Any] = {
"prompt_tokens": getattr(usage, "prompt_tokens", 0),
"completion_tokens": getattr(usage, "completion_tokens", 0),
"total_tokens": getattr(usage, "total_tokens", 0),
}
- # Extract cached prompt tokens from prompt_tokens_details
prompt_details = getattr(usage, "prompt_tokens_details", None)
if prompt_details:
result["cached_prompt_tokens"] = (
getattr(prompt_details, "cached_tokens", 0) or 0
)
+ completion_details = getattr(usage, "completion_tokens_details", None)
+ if completion_details:
+ result["reasoning_tokens"] = (
+ getattr(completion_details, "reasoning_tokens", 0) or 0
+ )
return result
return {"total_tokens": 0}
@@ -2371,8 +2401,8 @@ class OpenAICompletion(BaseLLM):
from crewai_files.uploaders.openai import OpenAIFileUploader
return OpenAIFileUploader(
- client=self._client,
- async_client=self._async_client,
+ client=self._get_sync_client(),
+ async_client=self._get_async_client(),
)
except ImportError:
return None
diff --git a/lib/crewai/src/crewai/mcp/tool_resolver.py b/lib/crewai/src/crewai/mcp/tool_resolver.py
index 92b1e488c..a394741fd 100644
--- a/lib/crewai/src/crewai/mcp/tool_resolver.py
+++ b/lib/crewai/src/crewai/mcp/tool_resolver.py
@@ -417,9 +417,18 @@ class MCPToolResolver:
args_schema = None
if tool_def.get("inputSchema"):
- args_schema = self._json_schema_to_pydantic(
- tool_name, tool_def["inputSchema"]
- )
+ try:
+ args_schema = self._json_schema_to_pydantic(
+ tool_name, tool_def["inputSchema"]
+ )
+ except Exception as e:
+ self._logger.log(
+ "warning",
+ f"Failed to build args schema for MCP tool "
+ f"'{tool_name}': {e}. Registering tool without a "
+ "typed schema.",
+ )
+ args_schema = None
tool_schema = {
"description": tool_def.get("description", ""),
diff --git a/lib/crewai/src/crewai/memory/analyze.py b/lib/crewai/src/crewai/memory/analyze.py
index 2709c57fc..19d110bf5 100644
--- a/lib/crewai/src/crewai/memory/analyze.py
+++ b/lib/crewai/src/crewai/memory/analyze.py
@@ -9,7 +9,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from crewai.memory.types import MemoryPromptConfig, MemoryRecord, ScopeInfo
-from crewai.utilities.i18n import get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
_logger = logging.getLogger(__name__)
@@ -149,7 +149,7 @@ def _memory_prompt_line(
raw = getattr(memory_prompt, key, None)
if isinstance(raw, str) and raw.strip():
return raw
- return get_i18n().memory(key)
+ return I18N_DEFAULT.memory(key)
def extract_memories_from_content(
diff --git a/lib/crewai/src/crewai/state/checkpoint_config.py b/lib/crewai/src/crewai/state/checkpoint_config.py
index 38c6b0490..e03964c05 100644
--- a/lib/crewai/src/crewai/state/checkpoint_config.py
+++ b/lib/crewai/src/crewai/state/checkpoint_config.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+from pathlib import Path
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, model_validator
@@ -201,11 +202,20 @@ class CheckpointConfig(BaseModel):
description="Maximum checkpoints to keep. Oldest are pruned after "
"each write. None means keep all.",
)
+ restore_from: Path | str | None = Field(
+ default=None,
+ description="Path or location of a checkpoint to restore from. "
+ "When passed via a kickoff method's from_checkpoint parameter, "
+ "the crew or flow resumes from this checkpoint.",
+ )
@model_validator(mode="after")
def _register_handlers(self) -> CheckpointConfig:
from crewai.state.checkpoint_listener import _ensure_handlers_registered
+ if isinstance(self.provider, SqliteProvider) and not Path(self.location).suffix:
+ self.location = f"{self.location}.db"
+
_ensure_handlers_registered()
return self
@@ -216,3 +226,25 @@ class CheckpointConfig(BaseModel):
@property
def trigger_events(self) -> set[str]:
return set(self.on_events)
+
+
+def apply_checkpoint(instance: Any, from_checkpoint: CheckpointConfig | None) -> Any:
+ """Handle checkpoint config for a kickoff method.
+
+ If *from_checkpoint* carries a ``restore_from`` path, builds and returns a
+ restored instance (with ``restore_from`` cleared). The caller should
+ dispatch into its own kickoff variant on that restored instance.
+
+ If *from_checkpoint* is present but has no ``restore_from``, sets
+ ``instance.checkpoint`` and returns ``None`` (proceed normally).
+
+ If *from_checkpoint* is ``None``, returns ``None`` immediately.
+ """
+ if from_checkpoint is None:
+ return None
+ if from_checkpoint.restore_from is not None:
+ restored = type(instance).from_checkpoint(from_checkpoint)
+ restored.checkpoint = from_checkpoint.model_copy(update={"restore_from": None})
+ return restored
+ instance.checkpoint = from_checkpoint
+ return None
diff --git a/lib/crewai/src/crewai/state/checkpoint_listener.py b/lib/crewai/src/crewai/state/checkpoint_listener.py
index 6471b9bde..674a8436a 100644
--- a/lib/crewai/src/crewai/state/checkpoint_listener.py
+++ b/lib/crewai/src/crewai/state/checkpoint_listener.py
@@ -7,6 +7,7 @@ avoids per-event overhead when no entity uses checkpointing.
from __future__ import annotations
+import json
import logging
import threading
from typing import Any
@@ -102,14 +103,31 @@ def _find_checkpoint(source: Any) -> CheckpointConfig | None:
return None
-def _do_checkpoint(state: RuntimeState, cfg: CheckpointConfig) -> None:
+def _do_checkpoint(
+ state: RuntimeState, cfg: CheckpointConfig, event: BaseEvent | None = None
+) -> None:
"""Write a checkpoint and prune old ones if configured."""
_prepare_entities(state.root)
- data = state.model_dump_json()
- cfg.provider.checkpoint(data, cfg.location)
+ payload = state.model_dump(mode="json")
+ if event is not None:
+ payload["trigger"] = event.type
+ data = json.dumps(payload)
+ location = cfg.provider.checkpoint(
+ data,
+ cfg.location,
+ parent_id=state._parent_id,
+ branch=state._branch,
+ )
+ state._chain_lineage(cfg.provider, location)
+
+ checkpoint_id: str = cfg.provider.extract_id(location)
+ msg: str = (
+ f"Checkpoint saved. Resume with: crewai checkpoint resume {checkpoint_id}"
+ )
+ logger.info(msg)
if cfg.max_checkpoints is not None:
- cfg.provider.prune(cfg.location, cfg.max_checkpoints)
+ cfg.provider.prune(cfg.location, cfg.max_checkpoints, branch=state._branch)
def _should_checkpoint(source: Any, event: BaseEvent) -> CheckpointConfig | None:
@@ -128,7 +146,7 @@ def _on_any_event(source: Any, event: BaseEvent, state: Any) -> None:
if cfg is None:
return
try:
- _do_checkpoint(state, cfg)
+ _do_checkpoint(state, cfg, event)
except Exception:
logger.warning("Auto-checkpoint failed for event %s", event.type, exc_info=True)
diff --git a/lib/crewai/src/crewai/state/provider/core.py b/lib/crewai/src/crewai/state/provider/core.py
index 0b12364c0..c386d519f 100644
--- a/lib/crewai/src/crewai/state/provider/core.py
+++ b/lib/crewai/src/crewai/state/provider/core.py
@@ -17,12 +17,21 @@ class BaseProvider(BaseModel, ABC):
provider_type: str = "base"
@abstractmethod
- def checkpoint(self, data: str, location: str) -> str:
+ def checkpoint(
+ self,
+ data: str,
+ location: str,
+ *,
+ parent_id: str | None = None,
+ branch: str = "main",
+ ) -> str:
"""Persist a snapshot synchronously.
Args:
data: The serialized string to persist.
location: Storage destination (directory, file path, URI, etc.).
+ parent_id: ID of the parent checkpoint for lineage tracking.
+ branch: Branch label for this checkpoint.
Returns:
A location identifier for the saved checkpoint.
@@ -30,12 +39,21 @@ class BaseProvider(BaseModel, ABC):
...
@abstractmethod
- async def acheckpoint(self, data: str, location: str) -> str:
+ async def acheckpoint(
+ self,
+ data: str,
+ location: str,
+ *,
+ parent_id: str | None = None,
+ branch: str = "main",
+ ) -> str:
"""Persist a snapshot asynchronously.
Args:
data: The serialized string to persist.
location: Storage destination (directory, file path, URI, etc.).
+ parent_id: ID of the parent checkpoint for lineage tracking.
+ branch: Branch label for this checkpoint.
Returns:
A location identifier for the saved checkpoint.
@@ -43,12 +61,25 @@ class BaseProvider(BaseModel, ABC):
...
@abstractmethod
- def prune(self, location: str, max_keep: int) -> None:
- """Remove old checkpoints, keeping at most *max_keep*.
+ def prune(self, location: str, max_keep: int, *, branch: str = "main") -> None:
+ """Remove old checkpoints, keeping at most *max_keep* per branch.
Args:
location: The storage destination passed to ``checkpoint``.
max_keep: Maximum number of checkpoints to retain.
+ branch: Only prune checkpoints on this branch.
+ """
+ ...
+
+ @abstractmethod
+ def extract_id(self, location: str) -> str:
+ """Extract the checkpoint ID from a location string.
+
+ Args:
+ location: The identifier returned by a previous ``checkpoint`` call.
+
+ Returns:
+ The checkpoint ID.
"""
...
diff --git a/lib/crewai/src/crewai/state/provider/json_provider.py b/lib/crewai/src/crewai/state/provider/json_provider.py
index f9763e6f3..0f18a5901 100644
--- a/lib/crewai/src/crewai/state/provider/json_provider.py
+++ b/lib/crewai/src/crewai/state/provider/json_provider.py
@@ -19,48 +19,87 @@ from crewai.state.provider.core import BaseProvider
logger = logging.getLogger(__name__)
+def _safe_branch(base: str, branch: str) -> None:
+ """Validate that a branch name doesn't escape the base directory.
+
+ Raises:
+ ValueError: If the branch resolves outside the base directory.
+ """
+ base_resolved = str(Path(base).resolve())
+ target_resolved = str((Path(base) / branch).resolve())
+ if (
+ not target_resolved.startswith(base_resolved + os.sep)
+ and target_resolved != base_resolved
+ ):
+ raise ValueError(f"Branch name escapes checkpoint directory: {branch!r}")
+
+
class JsonProvider(BaseProvider):
"""Persists runtime state checkpoints as JSON files on the local filesystem."""
provider_type: Literal["json"] = "json"
- def checkpoint(self, data: str, location: str) -> str:
+ def checkpoint(
+ self,
+ data: str,
+ location: str,
+ *,
+ parent_id: str | None = None,
+ branch: str = "main",
+ ) -> str:
"""Write a JSON checkpoint file.
Args:
data: The serialized JSON string to persist.
- location: Directory where the checkpoint will be saved.
+ location: Base directory where checkpoints are saved.
+ parent_id: ID of the parent checkpoint for lineage tracking.
+ Encoded in the filename for queryable lineage without
+ parsing the blob.
+ branch: Branch label. Files are stored under ``location/branch/``.
Returns:
The path to the written checkpoint file.
"""
- file_path = _build_path(location)
+ file_path = _build_path(location, branch, parent_id)
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w") as f:
f.write(data)
return str(file_path)
- async def acheckpoint(self, data: str, location: str) -> str:
+ async def acheckpoint(
+ self,
+ data: str,
+ location: str,
+ *,
+ parent_id: str | None = None,
+ branch: str = "main",
+ ) -> str:
"""Write a JSON checkpoint file asynchronously.
Args:
data: The serialized JSON string to persist.
- location: Directory where the checkpoint will be saved.
+ location: Base directory where checkpoints are saved.
+ parent_id: ID of the parent checkpoint for lineage tracking.
+ Encoded in the filename for queryable lineage without
+ parsing the blob.
+ branch: Branch label. Files are stored under ``location/branch/``.
Returns:
The path to the written checkpoint file.
"""
- file_path = _build_path(location)
+ file_path = _build_path(location, branch, parent_id)
await aiofiles.os.makedirs(str(file_path.parent), exist_ok=True)
async with aiofiles.open(file_path, "w") as f:
await f.write(data)
return str(file_path)
- def prune(self, location: str, max_keep: int) -> None:
- """Remove oldest checkpoint files beyond *max_keep*."""
- pattern = os.path.join(location, "*.json")
+ def prune(self, location: str, max_keep: int, *, branch: str = "main") -> None:
+ """Remove oldest checkpoint files beyond *max_keep* on a branch."""
+ _safe_branch(location, branch)
+ branch_dir = os.path.join(location, branch)
+ pattern = os.path.join(branch_dir, "*.json")
files = sorted(glob.glob(pattern), key=os.path.getmtime)
for path in files if max_keep == 0 else files[:-max_keep]:
try:
@@ -68,6 +107,16 @@ class JsonProvider(BaseProvider):
except OSError: # noqa: PERF203
logger.debug("Failed to remove %s", path, exc_info=True)
+ def extract_id(self, location: str) -> str:
+ """Extract the checkpoint ID from a file path.
+
+ The filename format is ``{ts}_{uuid8}_p-{parent}.json``.
+ The checkpoint ID is the ``{ts}_{uuid8}`` prefix.
+ """
+ stem = Path(location).stem
+ idx = stem.find("_p-")
+ return stem[:idx] if idx != -1 else stem
+
def from_checkpoint(self, location: str) -> str:
"""Read a JSON checkpoint file.
@@ -92,15 +141,24 @@ class JsonProvider(BaseProvider):
return await f.read()
-def _build_path(directory: str) -> Path:
- """Build a timestamped checkpoint file path.
+def _build_path(
+ directory: str, branch: str = "main", parent_id: str | None = None
+) -> Path:
+ """Build a timestamped checkpoint file path under a branch subdirectory.
+
+ Filename format: ``{ts}_{uuid8}_p-{parent_id}.json``
Args:
- directory: Parent directory for the checkpoint file.
+ directory: Base directory for checkpoints.
+ branch: Branch label used as a subdirectory name.
+ parent_id: Parent checkpoint ID to encode in the filename.
Returns:
The target file path.
"""
+ _safe_branch(directory, branch)
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
- filename = f"{ts}_{uuid.uuid4().hex[:8]}.json"
- return Path(directory) / filename
+ short_uuid = uuid.uuid4().hex[:8]
+ parent_suffix = parent_id or "none"
+ filename = f"{ts}_{short_uuid}_p-{parent_suffix}.json"
+ return Path(directory) / branch / filename
diff --git a/lib/crewai/src/crewai/state/provider/sqlite_provider.py b/lib/crewai/src/crewai/state/provider/sqlite_provider.py
index e54f56180..5ee4dca26 100644
--- a/lib/crewai/src/crewai/state/provider/sqlite_provider.py
+++ b/lib/crewai/src/crewai/state/provider/sqlite_provider.py
@@ -17,15 +17,20 @@ _CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS checkpoints (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
+ parent_id TEXT,
+ branch TEXT NOT NULL DEFAULT 'main',
data JSONB NOT NULL
)
"""
-_INSERT = "INSERT INTO checkpoints (id, created_at, data) VALUES (?, ?, jsonb(?))"
+_INSERT = (
+ "INSERT INTO checkpoints (id, created_at, parent_id, branch, data) "
+ "VALUES (?, ?, ?, ?, jsonb(?))"
+)
_SELECT = "SELECT json(data) FROM checkpoints WHERE id = ?"
_PRUNE = """
-DELETE FROM checkpoints WHERE rowid NOT IN (
- SELECT rowid FROM checkpoints ORDER BY rowid DESC LIMIT ?
+DELETE FROM checkpoints WHERE branch = ? AND rowid NOT IN (
+ SELECT rowid FROM checkpoints WHERE branch = ? ORDER BY rowid DESC LIMIT ?
)
"""
@@ -50,12 +55,21 @@ class SqliteProvider(BaseProvider):
provider_type: Literal["sqlite"] = "sqlite"
- def checkpoint(self, data: str, location: str) -> str:
+ def checkpoint(
+ self,
+ data: str,
+ location: str,
+ *,
+ parent_id: str | None = None,
+ branch: str = "main",
+ ) -> str:
"""Write a checkpoint to the SQLite database.
Args:
data: The serialized JSON string to persist.
location: Path to the SQLite database file.
+ parent_id: ID of the parent checkpoint for lineage tracking.
+ branch: Branch label for this checkpoint.
Returns:
A location string in the format ``"db_path#checkpoint_id"``.
@@ -65,16 +79,25 @@ class SqliteProvider(BaseProvider):
with sqlite3.connect(location) as conn:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute(_CREATE_TABLE)
- conn.execute(_INSERT, (checkpoint_id, ts, data))
+ conn.execute(_INSERT, (checkpoint_id, ts, parent_id, branch, data))
conn.commit()
return f"{location}#{checkpoint_id}"
- async def acheckpoint(self, data: str, location: str) -> str:
+ async def acheckpoint(
+ self,
+ data: str,
+ location: str,
+ *,
+ parent_id: str | None = None,
+ branch: str = "main",
+ ) -> str:
"""Write a checkpoint to the SQLite database asynchronously.
Args:
data: The serialized JSON string to persist.
location: Path to the SQLite database file.
+ parent_id: ID of the parent checkpoint for lineage tracking.
+ branch: Branch label for this checkpoint.
Returns:
A location string in the format ``"db_path#checkpoint_id"``.
@@ -84,16 +107,20 @@ class SqliteProvider(BaseProvider):
async with aiosqlite.connect(location) as db:
await db.execute("PRAGMA journal_mode=WAL")
await db.execute(_CREATE_TABLE)
- await db.execute(_INSERT, (checkpoint_id, ts, data))
+ await db.execute(_INSERT, (checkpoint_id, ts, parent_id, branch, data))
await db.commit()
return f"{location}#{checkpoint_id}"
- def prune(self, location: str, max_keep: int) -> None:
- """Remove oldest checkpoint rows beyond *max_keep*."""
+ def prune(self, location: str, max_keep: int, *, branch: str = "main") -> None:
+ """Remove oldest checkpoint rows beyond *max_keep* on a branch."""
with sqlite3.connect(location) as conn:
- conn.execute(_PRUNE, (max_keep,))
+ conn.execute(_PRUNE, (branch, branch, max_keep))
conn.commit()
+ def extract_id(self, location: str) -> str:
+ """Extract the checkpoint ID from a ``db_path#id`` string."""
+ return location.rsplit("#", 1)[1]
+
def from_checkpoint(self, location: str) -> str:
"""Read a checkpoint from the SQLite database.
diff --git a/lib/crewai/src/crewai/state/provider/utils.py b/lib/crewai/src/crewai/state/provider/utils.py
new file mode 100644
index 000000000..f4854cbe5
--- /dev/null
+++ b/lib/crewai/src/crewai/state/provider/utils.py
@@ -0,0 +1,34 @@
+"""Provider detection utilities."""
+
+from __future__ import annotations
+
+from crewai.state.provider.core import BaseProvider
+
+
+_SQLITE_MAGIC = b"SQLite format 3\x00"
+
+
+def detect_provider(path: str) -> BaseProvider:
+ """Detect the storage provider from a checkpoint path.
+
+ Reads the file's magic bytes to determine if it's a SQLite database.
+ For paths containing ``#``, checks the portion before the ``#``.
+ Falls back to JsonProvider.
+
+ Args:
+ path: A checkpoint file path, directory, or ``db_path#checkpoint_id``.
+
+ Returns:
+ The appropriate provider instance.
+ """
+ from crewai.state.provider.json_provider import JsonProvider
+ from crewai.state.provider.sqlite_provider import SqliteProvider
+
+ file_path = path.split("#")[0] if "#" in path else path
+ try:
+ with open(file_path, "rb") as f:
+ if f.read(16) == _SQLITE_MAGIC:
+ return SqliteProvider()
+ except OSError:
+ pass
+ return JsonProvider()
diff --git a/lib/crewai/src/crewai/state/runtime.py b/lib/crewai/src/crewai/state/runtime.py
index 6f1c5de80..daae0620e 100644
--- a/lib/crewai/src/crewai/state/runtime.py
+++ b/lib/crewai/src/crewai/state/runtime.py
@@ -9,8 +9,11 @@ via ``RuntimeState.model_rebuild()``.
from __future__ import annotations
+import logging
from typing import TYPE_CHECKING, Any
+import uuid
+from packaging.version import Version
from pydantic import (
ModelWrapValidatorHandler,
PrivateAttr,
@@ -20,9 +23,14 @@ from pydantic import (
)
from crewai.context import capture_execution_context
+from crewai.state.checkpoint_config import CheckpointConfig
from crewai.state.event_record import EventRecord
from crewai.state.provider.core import BaseProvider
from crewai.state.provider.json_provider import JsonProvider
+from crewai.utilities.version import get_crewai_version
+
+
+logger = logging.getLogger(__name__)
if TYPE_CHECKING:
@@ -58,12 +66,51 @@ def _sync_checkpoint_fields(entity: object) -> None:
entity.checkpoint_inputs = entity._inputs
entity.checkpoint_train = entity._train
entity.checkpoint_kickoff_event_id = entity._kickoff_event_id
+ for task in entity.tasks:
+ task.checkpoint_original_description = task._original_description
+ task.checkpoint_original_expected_output = task._original_expected_output
+
+
+def _migrate(data: dict[str, Any]) -> dict[str, Any]:
+ """Apply version-based migrations to checkpoint data.
+
+ Each block handles checkpoints older than a specific version,
+ transforming them forward to the current format. Blocks run in
+ version order so migrations compose.
+
+ Args:
+ data: The raw deserialized checkpoint dict.
+
+ Returns:
+ The migrated checkpoint dict.
+ """
+ raw = data.get("crewai_version")
+ current = Version(get_crewai_version())
+ stored = Version(raw) if raw else Version("0.0.0")
+
+ if raw is None:
+ logger.warning("Checkpoint has no crewai_version — treating as 0.0.0")
+ elif stored != current:
+ logger.debug(
+ "Migrating checkpoint from crewAI %s to %s",
+ stored,
+ current,
+ )
+
+ # --- migrations in version order ---
+ # if stored < Version("X.Y.Z"):
+ # data.setdefault("some_field", "default")
+
+ return data
class RuntimeState(RootModel): # type: ignore[type-arg]
root: list[Entity]
_provider: BaseProvider = PrivateAttr(default_factory=JsonProvider)
_event_record: EventRecord = PrivateAttr(default_factory=EventRecord)
+ _checkpoint_id: str | None = PrivateAttr(default=None)
+ _parent_id: str | None = PrivateAttr(default=None)
+ _branch: str = PrivateAttr(default="main")
@property
def event_record(self) -> EventRecord:
@@ -73,8 +120,11 @@ class RuntimeState(RootModel): # type: ignore[type-arg]
@model_serializer(mode="plain")
def _serialize(self) -> dict[str, Any]:
return {
+ "crewai_version": get_crewai_version(),
+ "parent_id": self._parent_id,
+ "branch": self._branch,
"entities": [e.model_dump(mode="json") for e in self.root],
- "event_record": self._event_record.model_dump(),
+ "event_record": self._event_record.model_dump(mode="json"),
}
@model_validator(mode="wrap")
@@ -83,13 +133,29 @@ class RuntimeState(RootModel): # type: ignore[type-arg]
cls, data: Any, handler: ModelWrapValidatorHandler[RuntimeState]
) -> RuntimeState:
if isinstance(data, dict) and "entities" in data:
+ data = _migrate(data)
record_data = data.get("event_record")
state = handler(data["entities"])
if record_data:
state._event_record = EventRecord.model_validate(record_data)
+ state._parent_id = data.get("parent_id")
+ state._branch = data.get("branch", "main")
return state
return handler(data)
+ def _chain_lineage(self, provider: BaseProvider, location: str) -> None:
+ """Update lineage fields after a successful checkpoint write.
+
+ Sets ``_checkpoint_id`` and ``_parent_id`` so the next write
+ records the correct parent in the lineage chain.
+
+ Args:
+ provider: The provider that performed the write.
+ location: The location string returned by the provider.
+ """
+ self._checkpoint_id = provider.extract_id(location)
+ self._parent_id = self._checkpoint_id
+
def checkpoint(self, location: str) -> str:
"""Write a checkpoint.
@@ -101,7 +167,14 @@ class RuntimeState(RootModel): # type: ignore[type-arg]
A location identifier for the saved checkpoint.
"""
_prepare_entities(self.root)
- return self._provider.checkpoint(self.model_dump_json(), location)
+ result = self._provider.checkpoint(
+ self.model_dump_json(),
+ location,
+ parent_id=self._parent_id,
+ branch=self._branch,
+ )
+ self._chain_lineage(self._provider, result)
+ return result
async def acheckpoint(self, location: str) -> str:
"""Async version of :meth:`checkpoint`.
@@ -114,41 +187,84 @@ class RuntimeState(RootModel): # type: ignore[type-arg]
A location identifier for the saved checkpoint.
"""
_prepare_entities(self.root)
- return await self._provider.acheckpoint(self.model_dump_json(), location)
+ result = await self._provider.acheckpoint(
+ self.model_dump_json(),
+ location,
+ parent_id=self._parent_id,
+ branch=self._branch,
+ )
+ self._chain_lineage(self._provider, result)
+ return result
+
+ def fork(self, branch: str | None = None) -> None:
+ """Create a new execution branch and write an initial checkpoint.
+
+ If this state was restored from a checkpoint, an initial checkpoint
+ is written on the new branch so the fork point is recorded.
+
+ Args:
+ branch: Branch label. Auto-generated from the current checkpoint
+ ID if not provided. Always unique — safe to call multiple
+ times without collisions.
+ """
+ if branch:
+ self._branch = branch
+ elif self._checkpoint_id:
+ self._branch = f"fork/{self._checkpoint_id}_{uuid.uuid4().hex[:6]}"
+ else:
+ self._branch = f"fork/{uuid.uuid4().hex[:8]}"
@classmethod
- def from_checkpoint(
- cls, location: str, provider: BaseProvider, **kwargs: Any
- ) -> RuntimeState:
+ def from_checkpoint(cls, config: CheckpointConfig, **kwargs: Any) -> RuntimeState:
"""Restore a RuntimeState from a checkpoint.
Args:
- location: The identifier returned by a previous ``checkpoint`` call.
- provider: The storage backend to read from.
+ config: Checkpoint configuration with ``restore_from`` set.
**kwargs: Passed to ``model_validate_json``.
Returns:
A restored RuntimeState.
"""
+ from crewai.state.provider.utils import detect_provider
+
+ if config.restore_from is None:
+ raise ValueError("CheckpointConfig.restore_from must be set")
+ location = str(config.restore_from)
+ provider = detect_provider(location)
raw = provider.from_checkpoint(location)
- return cls.model_validate_json(raw, **kwargs)
+ state = cls.model_validate_json(raw, **kwargs)
+ state._provider = provider
+ checkpoint_id = provider.extract_id(location)
+ state._checkpoint_id = checkpoint_id
+ state._parent_id = checkpoint_id
+ return state
@classmethod
async def afrom_checkpoint(
- cls, location: str, provider: BaseProvider, **kwargs: Any
+ cls, config: CheckpointConfig, **kwargs: Any
) -> RuntimeState:
"""Async version of :meth:`from_checkpoint`.
Args:
- location: The identifier returned by a previous ``acheckpoint`` call.
- provider: The storage backend to read from.
+ config: Checkpoint configuration with ``restore_from`` set.
**kwargs: Passed to ``model_validate_json``.
Returns:
A restored RuntimeState.
"""
+ from crewai.state.provider.utils import detect_provider
+
+ if config.restore_from is None:
+ raise ValueError("CheckpointConfig.restore_from must be set")
+ location = str(config.restore_from)
+ provider = detect_provider(location)
raw = await provider.afrom_checkpoint(location)
- return cls.model_validate_json(raw, **kwargs)
+ state = cls.model_validate_json(raw, **kwargs)
+ state._provider = provider
+ checkpoint_id = provider.extract_id(location)
+ state._checkpoint_id = checkpoint_id
+ state._parent_id = checkpoint_id
+ return state
def _prepare_entities(root: list[Entity]) -> None:
diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py
index 5671282dc..e12caa2af 100644
--- a/lib/crewai/src/crewai/task.py
+++ b/lib/crewai/src/crewai/task.py
@@ -45,6 +45,7 @@ from crewai.events.types.task_events import (
TaskStartedEvent,
)
from crewai.llms.base_llm import BaseLLM
+from crewai.llms.providers.openai.completion import OpenAICompletion
from crewai.security import Fingerprint, SecurityConfig
from crewai.tasks.output_format import OutputFormat
from crewai.tasks.task_output import TaskOutput
@@ -80,7 +81,7 @@ from crewai.utilities.guardrail_types import (
GuardrailType,
GuardrailsType,
)
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.printer import PRINTER
from crewai.utilities.string_utils import interpolate_only
@@ -115,7 +116,6 @@ class Task(BaseModel):
used_tools: int = 0
tools_errors: int = 0
delegations: int = 0
- i18n: I18N = Field(default_factory=get_i18n)
name: str | None = Field(default=None)
prompt_context: str | None = None
description: str = Field(description="Description of the actual task.")
@@ -231,6 +231,8 @@ class Task(BaseModel):
_original_description: str | None = PrivateAttr(default=None)
_original_expected_output: str | None = PrivateAttr(default=None)
_original_output_file: str | None = PrivateAttr(default=None)
+ checkpoint_original_description: str | None = Field(default=None, exclude=False)
+ checkpoint_original_expected_output: str | None = Field(default=None, exclude=False)
_thread: threading.Thread | None = PrivateAttr(default=None)
model_config = {"arbitrary_types_allowed": True}
@@ -300,12 +302,14 @@ class Task(BaseModel):
@model_validator(mode="after")
def validate_required_fields(self) -> Self:
- required_fields = ["description", "expected_output"]
- for field in required_fields:
- if getattr(self, field) is None:
- raise ValueError(
- f"{field} must be provided either directly or through config"
- )
+ if self.description is None:
+ raise ValueError(
+ "description must be provided either directly or through config"
+ )
+ if self.expected_output is None:
+ raise ValueError(
+ "expected_output must be provided either directly or through config"
+ )
return self
@model_validator(mode="after")
@@ -837,8 +841,8 @@ class Task(BaseModel):
should_inject = self.allow_crewai_trigger_context
if should_inject and self.agent:
- crew = getattr(self.agent, "crew", None)
- if crew and hasattr(crew, "_inputs") and crew._inputs:
+ crew = self.agent.crew
+ if crew and not isinstance(crew, str) and crew._inputs:
trigger_payload = crew._inputs.get("crewai_trigger_payload")
if trigger_payload is not None:
description += f"\n\nTrigger Payload: {trigger_payload}"
@@ -851,11 +855,12 @@ class Task(BaseModel):
isinstance(self.agent.llm, BaseLLM)
and self.agent.llm.supports_multimodal()
):
- provider: str = str(
- getattr(self.agent.llm, "provider", None)
- or getattr(self.agent.llm, "model", "openai")
+ provider: str = self.agent.llm.provider or self.agent.llm.model
+ api: str | None = (
+ self.agent.llm.api
+ if isinstance(self.agent.llm, OpenAICompletion)
+ else None
)
- api: str | None = getattr(self.agent.llm, "api", None)
supported_types = get_supported_content_types(provider, api)
def is_auto_injected(content_type: str) -> bool:
@@ -896,7 +901,7 @@ class Task(BaseModel):
tasks_slices = [description]
- output = self.i18n.slice("expected_output").format(
+ output = I18N_DEFAULT.slice("expected_output").format(
expected_output=self.expected_output
)
tasks_slices = [description, output]
@@ -968,7 +973,7 @@ Follow these guidelines:
raise ValueError(f"Error interpolating output_file path: {e!s}") from e
if inputs.get("crew_chat_messages"):
- conversation_instruction = self.i18n.slice(
+ conversation_instruction = I18N_DEFAULT.slice(
"conversation_history_instruction"
)
@@ -1219,7 +1224,7 @@ Follow these guidelines:
self.retry_count += 1
current_retry_count = self.retry_count
- context = self.i18n.errors("validation_error").format(
+ context = I18N_DEFAULT.errors("validation_error").format(
guardrail_result_error=guardrail_result.error,
task_output=task_output.raw,
)
@@ -1316,7 +1321,7 @@ Follow these guidelines:
self.retry_count += 1
current_retry_count = self.retry_count
- context = self.i18n.errors("validation_error").format(
+ context = I18N_DEFAULT.errors("validation_error").format(
guardrail_result_error=guardrail_result.error,
task_output=task_output.raw,
)
diff --git a/lib/crewai/src/crewai/telemetry/telemetry.py b/lib/crewai/src/crewai/telemetry/telemetry.py
index c93a06ff2..17d34f419 100644
--- a/lib/crewai/src/crewai/telemetry/telemetry.py
+++ b/lib/crewai/src/crewai/telemetry/telemetry.py
@@ -53,6 +53,7 @@ from crewai.telemetry.utils import (
close_span,
crew_memory_span_attribute_value,
)
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.logger_utils import suppress_warnings
from crewai.utilities.string_utils import sanitize_tool_name
@@ -319,7 +320,7 @@ class Telemetry:
"verbose?": agent.verbose,
"max_iter": agent.max_iter,
"max_rpm": agent.max_rpm,
- "i18n": agent.i18n.prompt_file,
+ "i18n": I18N_DEFAULT.prompt_file,
"function_calling_llm": (
getattr(
getattr(agent, "function_calling_llm", None),
@@ -849,7 +850,7 @@ class Telemetry:
"verbose?": agent.verbose,
"max_iter": agent.max_iter,
"max_rpm": agent.max_rpm,
- "i18n": agent.i18n.prompt_file,
+ "i18n": I18N_DEFAULT.prompt_file,
"llm": agent.llm.model
if isinstance(agent.llm, BaseLLM)
else str(agent.llm),
@@ -1062,3 +1063,20 @@ class Telemetry:
close_span(span)
self._safe_telemetry_operation(_operation)
+
+ def template_installed_span(self, template_name: str) -> None:
+ """Records when a template is downloaded and installed.
+
+ Args:
+ template_name: Name of the template that was installed
+ (without the template_ prefix).
+ """
+
+ def _operation() -> None:
+ tracer = trace.get_tracer("crewai.telemetry")
+ span = tracer.start_span("Template Installed")
+ self._add_attribute(span, "crewai_version", version("crewai"))
+ self._add_attribute(span, "template_name", template_name)
+ close_span(span)
+
+ self._safe_telemetry_operation(_operation)
diff --git a/lib/crewai/src/crewai/tools/agent_tools/add_image_tool.py b/lib/crewai/src/crewai/tools/agent_tools/add_image_tool.py
index e9ef66e81..8191144d9 100644
--- a/lib/crewai/src/crewai/tools/agent_tools/add_image_tool.py
+++ b/lib/crewai/src/crewai/tools/agent_tools/add_image_tool.py
@@ -3,10 +3,7 @@ from typing import Any
from pydantic import BaseModel, Field
from crewai.tools.base_tool import BaseTool
-from crewai.utilities import I18N
-
-
-i18n = I18N()
+from crewai.utilities.i18n import I18N_DEFAULT
class AddImageToolSchema(BaseModel):
@@ -19,9 +16,9 @@ class AddImageToolSchema(BaseModel):
class AddImageTool(BaseTool):
"""Tool for adding images to the content"""
- name: str = Field(default_factory=lambda: i18n.tools("add_image")["name"]) # type: ignore[index]
+ name: str = Field(default_factory=lambda: I18N_DEFAULT.tools("add_image")["name"]) # type: ignore[index]
description: str = Field(
- default_factory=lambda: i18n.tools("add_image")["description"] # type: ignore[index]
+ default_factory=lambda: I18N_DEFAULT.tools("add_image")["description"] # type: ignore[index]
)
args_schema: type[BaseModel] = AddImageToolSchema
@@ -31,7 +28,7 @@ class AddImageTool(BaseTool):
action: str | None = None,
**kwargs: Any,
) -> dict[str, Any]:
- action = action or i18n.tools("add_image")["default_action"] # type: ignore
+ action = action or I18N_DEFAULT.tools("add_image")["default_action"] # type: ignore
content = [
{"type": "text", "text": action},
{
diff --git a/lib/crewai/src/crewai/tools/agent_tools/agent_tools.py b/lib/crewai/src/crewai/tools/agent_tools/agent_tools.py
index 51552f7a8..533217456 100644
--- a/lib/crewai/src/crewai/tools/agent_tools/agent_tools.py
+++ b/lib/crewai/src/crewai/tools/agent_tools/agent_tools.py
@@ -5,21 +5,19 @@ from typing import TYPE_CHECKING
from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool
from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool
-from crewai.utilities.i18n import get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
if TYPE_CHECKING:
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.tools.base_tool import BaseTool
- from crewai.utilities.i18n import I18N
class AgentTools:
"""Manager class for agent-related tools"""
- def __init__(self, agents: Sequence[BaseAgent], i18n: I18N | None = None) -> None:
+ def __init__(self, agents: Sequence[BaseAgent]) -> None:
self.agents = agents
- self.i18n = i18n if i18n is not None else get_i18n()
def tools(self) -> list[BaseTool]:
"""Get all available agent tools"""
@@ -27,14 +25,12 @@ class AgentTools:
delegate_tool = DelegateWorkTool(
agents=self.agents,
- i18n=self.i18n,
- description=self.i18n.tools("delegate_work").format(coworkers=coworkers), # type: ignore
+ description=I18N_DEFAULT.tools("delegate_work").format(coworkers=coworkers), # type: ignore
)
ask_tool = AskQuestionTool(
agents=self.agents,
- i18n=self.i18n,
- description=self.i18n.tools("ask_question").format(coworkers=coworkers), # type: ignore
+ description=I18N_DEFAULT.tools("ask_question").format(coworkers=coworkers), # type: ignore
)
return [delegate_tool, ask_tool]
diff --git a/lib/crewai/src/crewai/tools/agent_tools/base_agent_tools.py b/lib/crewai/src/crewai/tools/agent_tools/base_agent_tools.py
index 8e5b959a4..17e44e57a 100644
--- a/lib/crewai/src/crewai/tools/agent_tools/base_agent_tools.py
+++ b/lib/crewai/src/crewai/tools/agent_tools/base_agent_tools.py
@@ -6,7 +6,7 @@ from pydantic import Field
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.task import Task
from crewai.tools.base_tool import BaseTool
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
logger = logging.getLogger(__name__)
@@ -16,9 +16,6 @@ class BaseAgentTool(BaseTool):
"""Base class for agent-related tools"""
agents: list[BaseAgent] = Field(description="List of available agents")
- i18n: I18N = Field(
- default_factory=get_i18n, description="Internationalization settings"
- )
def sanitize_agent_name(self, name: str) -> str:
"""
@@ -93,7 +90,7 @@ class BaseAgentTool(BaseTool):
)
except (AttributeError, ValueError) as e:
# Handle specific exceptions that might occur during role name processing
- return self.i18n.errors("agent_tool_unexisting_coworker").format(
+ return I18N_DEFAULT.errors("agent_tool_unexisting_coworker").format(
coworkers="\n".join(
[
f"- {self.sanitize_agent_name(agent.role)}"
@@ -105,7 +102,7 @@ class BaseAgentTool(BaseTool):
if not agent:
# No matching agent found after sanitization
- return self.i18n.errors("agent_tool_unexisting_coworker").format(
+ return I18N_DEFAULT.errors("agent_tool_unexisting_coworker").format(
coworkers="\n".join(
[
f"- {self.sanitize_agent_name(agent.role)}"
@@ -120,8 +117,7 @@ class BaseAgentTool(BaseTool):
task_with_assigned_agent = Task(
description=task,
agent=selected_agent,
- expected_output=selected_agent.i18n.slice("manager_request"),
- i18n=selected_agent.i18n,
+ expected_output=I18N_DEFAULT.slice("manager_request"),
)
logger.debug(
f"Created task for agent '{self.sanitize_agent_name(selected_agent.role)}': {task}"
@@ -129,6 +125,6 @@ class BaseAgentTool(BaseTool):
return selected_agent.execute_task(task_with_assigned_agent, context)
except Exception as e:
# Handle task creation or execution errors
- return self.i18n.errors("agent_tool_execution_error").format(
+ return I18N_DEFAULT.errors("agent_tool_execution_error").format(
agent_role=self.sanitize_agent_name(selected_agent.role), error=str(e)
)
diff --git a/lib/crewai/src/crewai/tools/memory_tools.py b/lib/crewai/src/crewai/tools/memory_tools.py
index c1874a532..e790c93f1 100644
--- a/lib/crewai/src/crewai/tools/memory_tools.py
+++ b/lib/crewai/src/crewai/tools/memory_tools.py
@@ -7,7 +7,7 @@ from typing import Any
from pydantic import BaseModel, Field
from crewai.tools.base_tool import BaseTool
-from crewai.utilities.i18n import get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
class RecallMemorySchema(BaseModel):
@@ -114,18 +114,17 @@ def create_memory_tools(memory: Any) -> list[BaseTool]:
Returns:
List containing a RecallMemoryTool and, if not read-only, a RememberTool.
"""
- i18n = get_i18n()
tools: list[BaseTool] = [
RecallMemoryTool(
memory=memory,
- description=i18n.tools("recall_memory"),
+ description=I18N_DEFAULT.tools("recall_memory"),
),
]
if not memory.read_only:
tools.append(
RememberTool(
memory=memory,
- description=i18n.tools("save_to_memory"),
+ description=I18N_DEFAULT.tools("save_to_memory"),
)
)
return tools
diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py
index c99b32cf5..09b44be17 100644
--- a/lib/crewai/src/crewai/tools/tool_usage.py
+++ b/lib/crewai/src/crewai/tools/tool_usage.py
@@ -28,7 +28,7 @@ from crewai.utilities.agent_utils import (
render_text_description_and_args,
)
from crewai.utilities.converter import Converter
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.printer import PRINTER
from crewai.utilities.string_utils import sanitize_tool_name
@@ -93,7 +93,6 @@ class ToolUsage:
action: Any = None,
fingerprint_context: dict[str, str] | None = None,
) -> None:
- self._i18n: I18N = agent.i18n if agent else get_i18n()
self._telemetry: Telemetry = Telemetry()
self._run_attempts: int = 1
self._max_parsing_attempts: int = 3
@@ -146,7 +145,7 @@ class ToolUsage:
if (
isinstance(tool, CrewStructuredTool)
and sanitize_tool_name(tool.name)
- == sanitize_tool_name(self._i18n.tools("add_image")["name"]) # type: ignore
+ == sanitize_tool_name(I18N_DEFAULT.tools("add_image")["name"]) # type: ignore
):
try:
return self._use(tool_string=tool_string, tool=tool, calling=calling)
@@ -194,7 +193,7 @@ class ToolUsage:
if (
isinstance(tool, CrewStructuredTool)
and sanitize_tool_name(tool.name)
- == sanitize_tool_name(self._i18n.tools("add_image")["name"]) # type: ignore
+ == sanitize_tool_name(I18N_DEFAULT.tools("add_image")["name"]) # type: ignore
):
try:
return await self._ause(
@@ -230,7 +229,7 @@ class ToolUsage:
"""
if self._check_tool_repeated_usage(calling=calling):
try:
- result = self._i18n.errors("task_repeated_usage").format(
+ result = I18N_DEFAULT.errors("task_repeated_usage").format(
tool_names=self.tools_names
)
self._telemetry.tool_repeated_usage(
@@ -415,7 +414,7 @@ class ToolUsage:
self._run_attempts += 1
if self._run_attempts > self._max_parsing_attempts:
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
- error_message = self._i18n.errors(
+ error_message = I18N_DEFAULT.errors(
"tool_usage_exception"
).format(
error=e,
@@ -423,7 +422,7 @@ class ToolUsage:
tool_inputs=tool.description,
)
result = ToolUsageError(
- f"\n{error_message}.\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
+ f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
).message
if self.task:
self.task.increment_tools_errors()
@@ -461,7 +460,7 @@ class ToolUsage:
# Repeated usage check happens before event emission - safe to return early
if self._check_tool_repeated_usage(calling=calling):
try:
- result = self._i18n.errors("task_repeated_usage").format(
+ result = I18N_DEFAULT.errors("task_repeated_usage").format(
tool_names=self.tools_names
)
self._telemetry.tool_repeated_usage(
@@ -648,7 +647,7 @@ class ToolUsage:
self._run_attempts += 1
if self._run_attempts > self._max_parsing_attempts:
self._telemetry.tool_usage_error(llm=self.function_calling_llm)
- error_message = self._i18n.errors(
+ error_message = I18N_DEFAULT.errors(
"tool_usage_exception"
).format(
error=e,
@@ -656,7 +655,7 @@ class ToolUsage:
tool_inputs=tool.description,
)
result = ToolUsageError(
- f"\n{error_message}.\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
+ f"\n{error_message}.\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
).message
if self.task:
self.task.increment_tools_errors()
@@ -699,7 +698,7 @@ class ToolUsage:
def _remember_format(self, result: str) -> str:
result = str(result)
- result += "\n\n" + self._i18n.slice("tools").format(
+ result += "\n\n" + I18N_DEFAULT.slice("tools").format(
tools=self.tools_description, tool_names=self.tools_names
)
return result
@@ -825,12 +824,12 @@ class ToolUsage:
except Exception:
if raise_error:
raise
- return ToolUsageError(f"{self._i18n.errors('tool_arguments_error')}")
+ return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
if not isinstance(arguments, dict):
if raise_error:
raise
- return ToolUsageError(f"{self._i18n.errors('tool_arguments_error')}")
+ return ToolUsageError(f"{I18N_DEFAULT.errors('tool_arguments_error')}")
return ToolCalling(
tool_name=sanitize_tool_name(tool.name),
@@ -856,7 +855,7 @@ class ToolUsage:
if self.agent and self.agent.verbose:
PRINTER.print(content=f"\n\n{e}\n", color="red")
return ToolUsageError(
- f"{self._i18n.errors('tool_usage_error').format(error=e)}\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}"
+ f"{I18N_DEFAULT.errors('tool_usage_error').format(error=e)}\nMoving on then. {I18N_DEFAULT.slice('format').format(tool_names=self.tools_names)}"
)
return self._tool_calling(tool_string)
diff --git a/lib/crewai/src/crewai/types/streaming.py b/lib/crewai/src/crewai/types/streaming.py
index a1f6e4ef7..eb3ddbde1 100644
--- a/lib/crewai/src/crewai/types/streaming.py
+++ b/lib/crewai/src/crewai/types/streaming.py
@@ -2,11 +2,12 @@
from __future__ import annotations
-from collections.abc import AsyncIterator, Iterator
+from collections.abc import AsyncIterator, Callable, Iterator
from enum import Enum
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from pydantic import BaseModel, Field
+from typing_extensions import Self
if TYPE_CHECKING:
@@ -78,12 +79,21 @@ class StreamingOutputBase(Generic[T]):
via the .result property after streaming completes.
"""
- def __init__(self) -> None:
+ def __init__(
+ self,
+ sync_iterator: Iterator[StreamChunk] | None = None,
+ async_iterator: AsyncIterator[StreamChunk] | None = None,
+ ) -> None:
"""Initialize streaming output base."""
self._result: T | None = None
self._completed: bool = False
self._chunks: list[StreamChunk] = []
self._error: Exception | None = None
+ self._cancelled: bool = False
+ self._exhausted: bool = False
+ self._on_cleanup: Callable[[], None] | None = None
+ self._sync_iterator = sync_iterator
+ self._async_iterator = async_iterator
@property
def result(self) -> T:
@@ -112,6 +122,11 @@ class StreamingOutputBase(Generic[T]):
"""Check if streaming has completed."""
return self._completed
+ @property
+ def is_cancelled(self) -> bool:
+ """Check if streaming was cancelled."""
+ return self._cancelled
+
@property
def chunks(self) -> list[StreamChunk]:
"""Get all collected chunks so far."""
@@ -129,6 +144,98 @@ class StreamingOutputBase(Generic[T]):
if chunk.chunk_type == StreamChunkType.TEXT
)
+ async def __aenter__(self) -> Self:
+ """Enter async context manager."""
+ return self
+
+ async def __aexit__(self, *exc_info: Any) -> None:
+ """Exit async context manager, cancelling if still running."""
+ await self.aclose()
+
+ async def aclose(self) -> None:
+ """Cancel streaming and clean up resources.
+
+ Cancels any in-flight tasks and closes the underlying async iterator.
+ Safe to call multiple times. No-op if already cancelled or fully consumed.
+ """
+ if self._cancelled or self._exhausted or self._error is not None:
+ return
+ self._cancelled = True
+ self._completed = True
+ if self._async_iterator is not None and hasattr(self._async_iterator, "aclose"):
+ await self._async_iterator.aclose()
+ if self._on_cleanup is not None:
+ self._on_cleanup()
+ self._on_cleanup = None
+
+ def close(self) -> None:
+ """Cancel streaming and clean up resources (sync).
+
+ Closes the underlying sync iterator. Safe to call multiple times.
+ No-op if already cancelled, fully consumed, or errored.
+ """
+ if self._cancelled or self._exhausted or self._error is not None:
+ return
+ self._cancelled = True
+ self._completed = True
+ if self._sync_iterator is not None and hasattr(self._sync_iterator, "close"):
+ self._sync_iterator.close()
+ if self._on_cleanup is not None:
+ self._on_cleanup()
+ self._on_cleanup = None
+
+ def __iter__(self) -> Iterator[StreamChunk]:
+ """Iterate over stream chunks synchronously.
+
+ Yields:
+ StreamChunk objects as they arrive.
+
+ Raises:
+ RuntimeError: If sync iterator not available.
+ """
+ if self._sync_iterator is None:
+ raise RuntimeError("Sync iterator not available")
+ try:
+ for chunk in self._sync_iterator:
+ self._chunks.append(chunk)
+ yield chunk
+ self._exhausted = True
+ except Exception as e:
+ self._error = e
+ raise
+ finally:
+ self._completed = True
+
+ def __aiter__(self) -> AsyncIterator[StreamChunk]:
+ """Return async iterator for stream chunks.
+
+ Returns:
+ Async iterator for StreamChunk objects.
+ """
+ return self._async_iterate()
+
+ async def _async_iterate(self) -> AsyncIterator[StreamChunk]:
+ """Iterate over stream chunks asynchronously.
+
+ Yields:
+ StreamChunk objects as they arrive.
+
+ Raises:
+ RuntimeError: If async iterator not available.
+ """
+ if self._async_iterator is None:
+ raise RuntimeError("Async iterator not available")
+ try:
+ async for chunk in self._async_iterator:
+ self._chunks.append(chunk)
+ yield chunk
+ self._exhausted = True
+ except Exception as e:
+ self._error = e
+ raise
+ finally:
+ self._completed = True
+
class CrewStreamingOutput(StreamingOutputBase["CrewOutput"]):
"""Streaming output wrapper for crew execution.
@@ -167,9 +274,7 @@ class CrewStreamingOutput(StreamingOutputBase["CrewOutput"]):
sync_iterator: Synchronous iterator for chunks.
async_iterator: Asynchronous iterator for chunks.
"""
- super().__init__()
- self._sync_iterator = sync_iterator
- self._async_iterator = async_iterator
+ super().__init__(sync_iterator=sync_iterator, async_iterator=async_iterator)
self._results: list[CrewOutput] | None = None
@property
@@ -204,56 +309,6 @@ class CrewStreamingOutput(StreamingOutputBase["CrewOutput"]):
self._results = results
self._completed = True
- def __iter__(self) -> Iterator[StreamChunk]:
- """Iterate over stream chunks synchronously.
-
- Yields:
- StreamChunk objects as they arrive.
-
- Raises:
- RuntimeError: If sync iterator not available.
- """
- if self._sync_iterator is None:
- raise RuntimeError("Sync iterator not available")
- try:
- for chunk in self._sync_iterator:
- self._chunks.append(chunk)
- yield chunk
- except Exception as e:
- self._error = e
- raise
- finally:
- self._completed = True
-
- def __aiter__(self) -> AsyncIterator[StreamChunk]:
- """Return async iterator for stream chunks.
-
- Returns:
- Async iterator for StreamChunk objects.
- """
- return self._async_iterate()
-
- async def _async_iterate(self) -> AsyncIterator[StreamChunk]:
- """Iterate over stream chunks asynchronously.
-
- Yields:
- StreamChunk objects as they arrive.
-
- Raises:
- RuntimeError: If async iterator not available.
- """
- if self._async_iterator is None:
- raise RuntimeError("Async iterator not available")
- try:
- async for chunk in self._async_iterator:
- self._chunks.append(chunk)
- yield chunk
- except Exception as e:
- self._error = e
- raise
- finally:
- self._completed = True
-
def _set_result(self, result: CrewOutput) -> None:
"""Set the final result after streaming completes.
@@ -286,71 +341,6 @@ class FlowStreamingOutput(StreamingOutputBase[Any]):
```
"""
- def __init__(
- self,
- sync_iterator: Iterator[StreamChunk] | None = None,
- async_iterator: AsyncIterator[StreamChunk] | None = None,
- ) -> None:
- """Initialize flow streaming output.
-
- Args:
- sync_iterator: Synchronous iterator for chunks.
- async_iterator: Asynchronous iterator for chunks.
- """
- super().__init__()
- self._sync_iterator = sync_iterator
- self._async_iterator = async_iterator
-
- def __iter__(self) -> Iterator[StreamChunk]:
- """Iterate over stream chunks synchronously.
-
- Yields:
- StreamChunk objects as they arrive.
-
- Raises:
- RuntimeError: If sync iterator not available.
- """
- if self._sync_iterator is None:
- raise RuntimeError("Sync iterator not available")
- try:
- for chunk in self._sync_iterator:
- self._chunks.append(chunk)
- yield chunk
- except Exception as e:
- self._error = e
- raise
- finally:
- self._completed = True
-
- def __aiter__(self) -> AsyncIterator[StreamChunk]:
- """Return async iterator for stream chunks.
-
- Returns:
- Async iterator for StreamChunk objects.
- """
- return self._async_iterate()
-
- async def _async_iterate(self) -> AsyncIterator[StreamChunk]:
- """Iterate over stream chunks asynchronously.
-
- Yields:
- StreamChunk objects as they arrive.
-
- Raises:
- RuntimeError: If async iterator not available.
- """
- if self._async_iterator is None:
- raise RuntimeError("Async iterator not available")
- try:
- async for chunk in self._async_iterator:
- self._chunks.append(chunk)
- yield chunk
- except Exception as e:
- self._error = e
- raise
- finally:
- self._completed = True
-
def _set_result(self, result: Any) -> None:
"""Set the final result after streaming completes.
diff --git a/lib/crewai/src/crewai/types/usage_metrics.py b/lib/crewai/src/crewai/types/usage_metrics.py
index 77e9ef598..76fa7dca0 100644
--- a/lib/crewai/src/crewai/types/usage_metrics.py
+++ b/lib/crewai/src/crewai/types/usage_metrics.py
@@ -29,6 +29,14 @@ class UsageMetrics(BaseModel):
completion_tokens: int = Field(
default=0, description="Number of tokens used in completions."
)
+ reasoning_tokens: int = Field(
+ default=0,
+ description="Number of reasoning/thinking tokens (e.g. OpenAI o-series, Gemini thinking).",
+ )
+ cache_creation_tokens: int = Field(
+ default=0,
+ description="Number of cache creation tokens (e.g. Anthropic cache writes).",
+ )
successful_requests: int = Field(
default=0, description="Number of successful requests made."
)
@@ -43,4 +51,6 @@ class UsageMetrics(BaseModel):
self.prompt_tokens += usage_metrics.prompt_tokens
self.cached_prompt_tokens += usage_metrics.cached_prompt_tokens
self.completion_tokens += usage_metrics.completion_tokens
+ self.reasoning_tokens += usage_metrics.reasoning_tokens
+ self.cache_creation_tokens += usage_metrics.cache_creation_tokens
self.successful_requests += usage_metrics.successful_requests
diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py
index d448cd162..684fd9287 100644
--- a/lib/crewai/src/crewai/utilities/agent_utils.py
+++ b/lib/crewai/src/crewai/utilities/agent_utils.py
@@ -31,7 +31,7 @@ from crewai.utilities.errors import AgentRepositoryError
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
-from crewai.utilities.i18n import I18N
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.printer import PRINTER, ColoredText, Printer
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.string_utils import sanitize_tool_name
@@ -254,7 +254,6 @@ def has_reached_max_iterations(iterations: int, max_iterations: int) -> bool:
def handle_max_iterations_exceeded(
formatted_answer: AgentAction | AgentFinish | None,
printer: Printer,
- i18n: I18N,
messages: list[LLMMessage],
llm: LLM | BaseLLM,
callbacks: list[TokenCalcHandler],
@@ -265,7 +264,6 @@ def handle_max_iterations_exceeded(
Args:
formatted_answer: The last formatted answer from the agent.
printer: Printer instance for output.
- i18n: I18N instance for internationalization.
messages: List of messages to send to the LLM.
llm: The LLM instance to call.
callbacks: List of callbacks for the LLM call.
@@ -282,10 +280,10 @@ def handle_max_iterations_exceeded(
if formatted_answer and hasattr(formatted_answer, "text"):
assistant_message = (
- formatted_answer.text + f"\n{i18n.errors('force_final_answer')}"
+ formatted_answer.text + f"\n{I18N_DEFAULT.errors('force_final_answer')}"
)
else:
- assistant_message = i18n.errors("force_final_answer")
+ assistant_message = I18N_DEFAULT.errors("force_final_answer")
messages.append(format_message_for_llm(assistant_message, role="assistant"))
@@ -687,7 +685,6 @@ def handle_context_length(
messages: list[LLMMessage],
llm: LLM | BaseLLM,
callbacks: list[TokenCalcHandler],
- i18n: I18N,
verbose: bool = True,
) -> None:
"""Handle context length exceeded by either summarizing or raising an error.
@@ -698,7 +695,6 @@ def handle_context_length(
messages: List of messages to summarize
llm: LLM instance for summarization
callbacks: List of callbacks for LLM
- i18n: I18N instance for messages
Raises:
SystemExit: If context length is exceeded and user opts not to summarize
@@ -710,7 +706,7 @@ def handle_context_length(
color="yellow",
)
summarize_messages(
- messages=messages, llm=llm, callbacks=callbacks, i18n=i18n, verbose=verbose
+ messages=messages, llm=llm, callbacks=callbacks, verbose=verbose
)
else:
if verbose:
@@ -863,7 +859,6 @@ async def _asummarize_chunks(
chunks: list[list[LLMMessage]],
llm: LLM | BaseLLM,
callbacks: list[TokenCalcHandler],
- i18n: I18N,
) -> list[SummaryContent]:
"""Summarize multiple message chunks concurrently using asyncio.
@@ -871,7 +866,6 @@ async def _asummarize_chunks(
chunks: List of message chunks to summarize.
llm: LLM instance (must support ``acall``).
callbacks: List of callbacks for the LLM.
- i18n: I18N instance for prompt templates.
Returns:
Ordered list of summary contents, one per chunk.
@@ -881,10 +875,10 @@ async def _asummarize_chunks(
conversation_text = _format_messages_for_summary(chunk)
summarization_messages = [
format_message_for_llm(
- i18n.slice("summarizer_system_message"), role="system"
+ I18N_DEFAULT.slice("summarizer_system_message"), role="system"
),
format_message_for_llm(
- i18n.slice("summarize_instruction").format(
+ I18N_DEFAULT.slice("summarize_instruction").format(
conversation=conversation_text
),
),
@@ -901,7 +895,6 @@ def summarize_messages(
messages: list[LLMMessage],
llm: LLM | BaseLLM,
callbacks: list[TokenCalcHandler],
- i18n: I18N,
verbose: bool = True,
) -> None:
"""Summarize messages to fit within context window.
@@ -917,7 +910,6 @@ def summarize_messages(
messages: List of messages to summarize (modified in-place)
llm: LLM instance for summarization
callbacks: List of callbacks for LLM
- i18n: I18N instance for messages
verbose: Whether to print progress.
"""
# 1. Extract & preserve file attachments from user messages
@@ -953,10 +945,10 @@ def summarize_messages(
conversation_text = _format_messages_for_summary(chunk)
summarization_messages = [
format_message_for_llm(
- i18n.slice("summarizer_system_message"), role="system"
+ I18N_DEFAULT.slice("summarizer_system_message"), role="system"
),
format_message_for_llm(
- i18n.slice("summarize_instruction").format(
+ I18N_DEFAULT.slice("summarize_instruction").format(
conversation=conversation_text
),
),
@@ -971,9 +963,7 @@ def summarize_messages(
content=f"Summarizing {total_chunks} chunks in parallel...",
color="yellow",
)
- coro = _asummarize_chunks(
- chunks=chunks, llm=llm, callbacks=callbacks, i18n=i18n
- )
+ coro = _asummarize_chunks(chunks=chunks, llm=llm, callbacks=callbacks)
if is_inside_event_loop():
ctx = contextvars.copy_context()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
@@ -988,7 +978,7 @@ def summarize_messages(
messages.extend(system_messages)
summary_message = format_message_for_llm(
- i18n.slice("summary").format(merged_summary=merged_summary)
+ I18N_DEFAULT.slice("summary").format(merged_summary=merged_summary)
)
if preserved_files:
summary_message["files"] = preserved_files
diff --git a/lib/crewai/src/crewai/utilities/converter.py b/lib/crewai/src/crewai/utilities/converter.py
index 328ecbdf9..26dce6bd0 100644
--- a/lib/crewai/src/crewai/utilities/converter.py
+++ b/lib/crewai/src/crewai/utilities/converter.py
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ValidationError
from typing_extensions import Unpack
from crewai.agents.agent_builder.utilities.base_output_converter import OutputConverter
-from crewai.utilities.i18n import get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.internal_instructor import InternalInstructor
from crewai.utilities.printer import PRINTER
from crewai.utilities.pydantic_schema_utils import generate_model_description
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from crewai.llms.base_llm import BaseLLM
_JSON_PATTERN: Final[re.Pattern[str]] = re.compile(r"({.*})", re.DOTALL)
-_I18N = get_i18n()
+_I18N = I18N_DEFAULT
class ConverterError(Exception):
diff --git a/lib/crewai/src/crewai/utilities/evaluators/task_evaluator.py b/lib/crewai/src/crewai/utilities/evaluators/task_evaluator.py
index 0a76c2a6c..5915c2346 100644
--- a/lib/crewai/src/crewai/utilities/evaluators/task_evaluator.py
+++ b/lib/crewai/src/crewai/utilities/evaluators/task_evaluator.py
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.task_events import TaskEvaluationEvent
from crewai.utilities.converter import Converter
-from crewai.utilities.i18n import get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.training_converter import TrainingConverter
@@ -98,11 +98,9 @@ class TaskEvaluator:
if not self.llm.supports_function_calling(): # type: ignore[union-attr]
schema_dict = generate_model_description(TaskEvaluation)
- output_schema: str = (
- get_i18n()
- .slice("formatted_task_instructions")
- .format(output_format=json.dumps(schema_dict, indent=2))
- )
+ output_schema: str = I18N_DEFAULT.slice(
+ "formatted_task_instructions"
+ ).format(output_format=json.dumps(schema_dict, indent=2))
instructions = f"{instructions}\n\n{output_schema}"
converter = Converter(
@@ -174,11 +172,9 @@ class TaskEvaluator:
if not self.llm.supports_function_calling(): # type: ignore[union-attr]
schema_dict = generate_model_description(TrainingTaskEvaluation)
- output_schema: str = (
- get_i18n()
- .slice("formatted_task_instructions")
- .format(output_format=json.dumps(schema_dict, indent=2))
- )
+ output_schema: str = I18N_DEFAULT.slice(
+ "formatted_task_instructions"
+ ).format(output_format=json.dumps(schema_dict, indent=2))
instructions = f"{instructions}\n\n{output_schema}"
converter = TrainingConverter(
diff --git a/lib/crewai/src/crewai/utilities/i18n.py b/lib/crewai/src/crewai/utilities/i18n.py
index 623d8a22e..8d091dd52 100644
--- a/lib/crewai/src/crewai/utilities/i18n.py
+++ b/lib/crewai/src/crewai/utilities/i18n.py
@@ -142,3 +142,6 @@ def get_i18n(prompt_file: str | None = None) -> I18N:
Cached I18N instance.
"""
return I18N(prompt_file=prompt_file)
+
+
+I18N_DEFAULT: I18N = get_i18n()
diff --git a/lib/crewai/src/crewai/utilities/prompts.py b/lib/crewai/src/crewai/utilities/prompts.py
index 821623b89..31c1a1b27 100644
--- a/lib/crewai/src/crewai/utilities/prompts.py
+++ b/lib/crewai/src/crewai/utilities/prompts.py
@@ -6,7 +6,7 @@ from typing import Any, Literal
from pydantic import BaseModel, Field
-from crewai.utilities.i18n import I18N, get_i18n
+from crewai.utilities.i18n import I18N_DEFAULT
class StandardPromptResult(BaseModel):
@@ -49,7 +49,6 @@ class Prompts(BaseModel):
- Need to refactor so that prompt is not tightly coupled to agent.
"""
- i18n: I18N = Field(default_factory=get_i18n)
has_tools: bool = Field(
default=False, description="Indicates if the agent has access to tools"
)
@@ -140,13 +139,13 @@ class Prompts(BaseModel):
if not system_template or not prompt_template:
# If any of the required templates are missing, fall back to the default format
prompt_parts: list[str] = [
- self.i18n.slice(component) for component in components
+ I18N_DEFAULT.slice(component) for component in components
]
prompt = "".join(prompt_parts)
else:
# All templates are provided, use them
template_parts: list[str] = [
- self.i18n.slice(component)
+ I18N_DEFAULT.slice(component)
for component in components
if component != "task"
]
@@ -154,7 +153,7 @@ class Prompts(BaseModel):
"{{ .System }}", "".join(template_parts)
)
prompt = prompt_template.replace(
- "{{ .Prompt }}", "".join(self.i18n.slice("task"))
+ "{{ .Prompt }}", "".join(I18N_DEFAULT.slice("task"))
)
# Handle missing response_template
if response_template:
diff --git a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py
index 62536cbe7..a45c1635a 100644
--- a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py
+++ b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py
@@ -19,7 +19,18 @@ from collections.abc import Callable
from copy import deepcopy
import datetime
import logging
-from typing import TYPE_CHECKING, Annotated, Any, Final, Literal, TypedDict, Union
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+ Final,
+ ForwardRef,
+ Literal,
+ Optional,
+ TypedDict,
+ Union,
+ cast,
+)
import uuid
import jsonref # type: ignore[import-untyped]
@@ -99,15 +110,26 @@ def resolve_refs(schema: dict[str, Any]) -> dict[str, Any]:
"""
defs = schema.get("$defs", {})
schema_copy = deepcopy(schema)
+ expanding: set[str] = set()
def _resolve(node: Any) -> Any:
if isinstance(node, dict):
ref = node.get("$ref")
if isinstance(ref, str) and ref.startswith("#/$defs/"):
def_name = ref.replace("#/$defs/", "")
- if def_name in defs:
+ if def_name not in defs:
+ raise KeyError(f"Definition '{def_name}' not found in $defs.")
+ if def_name in expanding:
+ def_schema = defs[def_name]
+ stub: dict[str, Any] = {"type": def_schema.get("type", "object")}
+ if "description" in def_schema:
+ stub["description"] = def_schema["description"]
+ return stub
+ expanding.add(def_name)
+ try:
return _resolve(deepcopy(defs[def_name]))
- raise KeyError(f"Definition '{def_name}' not found in $defs.")
+ finally:
+ expanding.discard(def_name)
return {k: _resolve(v) for k, v in node.items()}
if isinstance(node, list):
@@ -119,7 +141,11 @@ def resolve_refs(schema: dict[str, Any]) -> dict[str, Any]:
def add_key_in_dict_recursively(
- d: dict[str, Any], key: str, value: Any, criteria: Callable[[dict[str, Any]], bool]
+ d: dict[str, Any],
+ key: str,
+ value: Any,
+ criteria: Callable[[dict[str, Any]], bool],
+ _seen: set[int] | None = None,
) -> dict[str, Any]:
"""Recursively adds a key/value pair to all nested dicts matching `criteria`.
@@ -128,22 +154,31 @@ def add_key_in_dict_recursively(
key: The key to add.
value: The value to add.
criteria: A function that returns True for dicts that should receive the key.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
The modified dictionary.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(d, dict):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
if criteria(d) and key not in d:
d[key] = value
for v in d.values():
- add_key_in_dict_recursively(v, key, value, criteria)
+ add_key_in_dict_recursively(v, key, value, criteria, _seen)
elif isinstance(d, list):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
for i in d:
- add_key_in_dict_recursively(i, key, value, criteria)
+ add_key_in_dict_recursively(i, key, value, criteria, _seen)
return d
-def force_additional_properties_false(d: Any) -> Any:
+def force_additional_properties_false(d: Any, _seen: set[int] | None = None) -> Any:
"""Force additionalProperties=false on all object-type dicts recursively.
OpenAI strict mode requires all objects to have additionalProperties=false.
@@ -154,11 +189,17 @@ def force_additional_properties_false(d: Any) -> Any:
Args:
d: The dictionary/list to modify.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
The modified dictionary/list.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(d, dict):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
if d.get("type") == "object":
d["additionalProperties"] = False
if "properties" not in d:
@@ -166,10 +207,13 @@ def force_additional_properties_false(d: Any) -> Any:
if "required" not in d:
d["required"] = []
for v in d.values():
- force_additional_properties_false(v)
+ force_additional_properties_false(v, _seen)
elif isinstance(d, list):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
for i in d:
- force_additional_properties_false(i)
+ force_additional_properties_false(i, _seen)
return d
@@ -183,7 +227,7 @@ OPENAI_SUPPORTED_FORMATS: Final[
}
-def strip_unsupported_formats(d: Any) -> Any:
+def strip_unsupported_formats(d: Any, _seen: set[int] | None = None) -> Any:
"""Remove format annotations that OpenAI strict mode doesn't support.
OpenAI only supports: date-time, date, time, duration.
@@ -191,11 +235,17 @@ def strip_unsupported_formats(d: Any) -> Any:
Args:
d: The dictionary/list to modify.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
The modified dictionary/list.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(d, dict):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
format_value = d.get("format")
if (
isinstance(format_value, str)
@@ -203,14 +253,17 @@ def strip_unsupported_formats(d: Any) -> Any:
):
del d["format"]
for v in d.values():
- strip_unsupported_formats(v)
+ strip_unsupported_formats(v, _seen)
elif isinstance(d, list):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
for i in d:
- strip_unsupported_formats(i)
+ strip_unsupported_formats(i, _seen)
return d
-def ensure_type_in_schemas(d: Any) -> Any:
+def ensure_type_in_schemas(d: Any, _seen: set[int] | None = None) -> Any:
"""Ensure all schema objects in anyOf/oneOf have a 'type' key.
OpenAI strict mode requires every schema to have a 'type' key.
@@ -218,11 +271,17 @@ def ensure_type_in_schemas(d: Any) -> Any:
Args:
d: The dictionary/list to modify.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
The modified dictionary/list.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(d, dict):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
for key in ("anyOf", "oneOf"):
if key in d:
schema_list = d[key]
@@ -230,12 +289,15 @@ def ensure_type_in_schemas(d: Any) -> Any:
if isinstance(schema, dict) and schema == {}:
schema_list[i] = {"type": "object"}
else:
- ensure_type_in_schemas(schema)
+ ensure_type_in_schemas(schema, _seen)
for v in d.values():
- ensure_type_in_schemas(v)
+ ensure_type_in_schemas(v, _seen)
elif isinstance(d, list):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
for item in d:
- ensure_type_in_schemas(item)
+ ensure_type_in_schemas(item, _seen)
return d
@@ -318,7 +380,9 @@ def add_const_to_oneof_variants(schema: dict[str, Any]) -> dict[str, Any]:
return _process_oneof(deepcopy(schema))
-def convert_oneof_to_anyof(schema: dict[str, Any]) -> dict[str, Any]:
+def convert_oneof_to_anyof(
+ schema: dict[str, Any], _seen: set[int] | None = None
+) -> dict[str, Any]:
"""Convert oneOf to anyOf for OpenAI compatibility.
OpenAI's Structured Outputs support anyOf better than oneOf.
@@ -326,26 +390,37 @@ def convert_oneof_to_anyof(schema: dict[str, Any]) -> dict[str, Any]:
Args:
schema: JSON schema dictionary.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
Modified schema with anyOf instead of oneOf.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(schema, dict):
+ if id(schema) in _seen:
+ return schema
+ _seen.add(id(schema))
if "oneOf" in schema:
schema["anyOf"] = schema.pop("oneOf")
for value in schema.values():
if isinstance(value, dict):
- convert_oneof_to_anyof(value)
+ convert_oneof_to_anyof(value, _seen)
elif isinstance(value, list):
+ if id(value) in _seen:
+ continue
+ _seen.add(id(value))
for item in value:
if isinstance(item, dict):
- convert_oneof_to_anyof(item)
+ convert_oneof_to_anyof(item, _seen)
return schema
-def ensure_all_properties_required(schema: dict[str, Any]) -> dict[str, Any]:
+def ensure_all_properties_required(
+ schema: dict[str, Any], _seen: set[int] | None = None
+) -> dict[str, Any]:
"""Ensure all properties are in the required array for OpenAI strict mode.
OpenAI's strict structured outputs require all properties to be listed
@@ -354,11 +429,17 @@ def ensure_all_properties_required(schema: dict[str, Any]) -> dict[str, Any]:
Args:
schema: JSON schema dictionary.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
Modified schema with all properties marked as required.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(schema, dict):
+ if id(schema) in _seen:
+ return schema
+ _seen.add(id(schema))
if schema.get("type") == "object" and "properties" in schema:
properties = schema["properties"]
if properties:
@@ -366,16 +447,21 @@ def ensure_all_properties_required(schema: dict[str, Any]) -> dict[str, Any]:
for value in schema.values():
if isinstance(value, dict):
- ensure_all_properties_required(value)
+ ensure_all_properties_required(value, _seen)
elif isinstance(value, list):
+ if id(value) in _seen:
+ continue
+ _seen.add(id(value))
for item in value:
if isinstance(item, dict):
- ensure_all_properties_required(item)
+ ensure_all_properties_required(item, _seen)
return schema
-def strip_null_from_types(schema: dict[str, Any]) -> dict[str, Any]:
+def strip_null_from_types(
+ schema: dict[str, Any], _seen: set[int] | None = None
+) -> dict[str, Any]:
"""Remove null type from anyOf/type arrays.
Pydantic generates `T | None` for optional fields, which creates schemas with
@@ -384,11 +470,17 @@ def strip_null_from_types(schema: dict[str, Any]) -> dict[str, Any]:
Args:
schema: JSON schema dictionary.
+ _seen: Internal set of visited ``id()``s, used to guard cyclic schemas.
Returns:
Modified schema with null types removed.
"""
+ if _seen is None:
+ _seen = set()
if isinstance(schema, dict):
+ if id(schema) in _seen:
+ return schema
+ _seen.add(id(schema))
if "anyOf" in schema:
any_of = schema["anyOf"]
non_null = [opt for opt in any_of if opt.get("type") != "null"]
@@ -408,15 +500,141 @@ def strip_null_from_types(schema: dict[str, Any]) -> dict[str, Any]:
for value in schema.values():
if isinstance(value, dict):
- strip_null_from_types(value)
+ strip_null_from_types(value, _seen)
elif isinstance(value, list):
+ if id(value) in _seen:
+ continue
+ _seen.add(id(value))
for item in value:
if isinstance(item, dict):
- strip_null_from_types(item)
+ strip_null_from_types(item, _seen)
return schema
+_STRICT_METADATA_KEYS: Final[tuple[str, ...]] = (
+ "title",
+ "default",
+ "examples",
+ "example",
+ "$comment",
+ "readOnly",
+ "writeOnly",
+ "deprecated",
+)
+
+_CLAUDE_STRICT_UNSUPPORTED: Final[tuple[str, ...]] = (
+ "minimum",
+ "maximum",
+ "exclusiveMinimum",
+ "exclusiveMaximum",
+ "multipleOf",
+ "minLength",
+ "maxLength",
+ "pattern",
+ "minItems",
+ "maxItems",
+ "uniqueItems",
+ "minContains",
+ "maxContains",
+ "minProperties",
+ "maxProperties",
+ "patternProperties",
+ "propertyNames",
+ "dependentRequired",
+ "dependentSchemas",
+)
+
+
+def _strip_keys_recursive(
+ d: Any, keys: tuple[str, ...], _seen: set[int] | None = None
+) -> Any:
+ """Recursively delete a fixed set of keys from a schema."""
+ if _seen is None:
+ _seen = set()
+ if isinstance(d, dict):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
+ for key in keys:
+ d.pop(key, None)
+ for v in d.values():
+ _strip_keys_recursive(v, keys, _seen)
+ elif isinstance(d, list):
+ if id(d) in _seen:
+ return d
+ _seen.add(id(d))
+ for i in d:
+ _strip_keys_recursive(i, keys, _seen)
+ return d
+
+
+def lift_top_level_anyof(schema: dict[str, Any]) -> dict[str, Any]:
+ """Unwrap a top-level anyOf/oneOf/allOf wrapping a single object variant.
+
+ Anthropic's strict ``input_schema`` rejects top-level union keywords. When
+ exactly one variant is an object schema, lift it so the root is a plain
+ object; otherwise leave the schema alone.
+ """
+ for key in ("anyOf", "oneOf", "allOf"):
+ variants = schema.get(key)
+ if not isinstance(variants, list):
+ continue
+ object_variants = [
+ v for v in variants if isinstance(v, dict) and v.get("type") == "object"
+ ]
+ if len(object_variants) == 1:
+ lifted = deepcopy(object_variants[0])
+ schema.pop(key)
+ schema.update(lifted)
+ break
+ return schema
+
+
+def _common_strict_pipeline(params: dict[str, Any]) -> dict[str, Any]:
+ """Shared strict sanitization: inline refs, close objects, require all properties."""
+ sanitized = resolve_refs(deepcopy(params))
+ sanitized.pop("$defs", None)
+ sanitized = convert_oneof_to_anyof(sanitized)
+ sanitized = ensure_type_in_schemas(sanitized)
+ sanitized = force_additional_properties_false(sanitized)
+ sanitized = ensure_all_properties_required(sanitized)
+ return cast(dict[str, Any], _strip_keys_recursive(sanitized, _STRICT_METADATA_KEYS))
+
+
+def sanitize_tool_params_for_openai_strict(
+ params: dict[str, Any],
+) -> dict[str, Any]:
+ """Sanitize a JSON schema for OpenAI strict function calling."""
+ if not isinstance(params, dict):
+ return params
+ return cast(
+ dict[str, Any], strip_unsupported_formats(_common_strict_pipeline(params))
+ )
+
+
+def sanitize_tool_params_for_anthropic_strict(
+ params: dict[str, Any],
+) -> dict[str, Any]:
+ """Sanitize a JSON schema for Anthropic strict tool use."""
+ if not isinstance(params, dict):
+ return params
+ sanitized = lift_top_level_anyof(_common_strict_pipeline(params))
+ sanitized = _strip_keys_recursive(sanitized, _CLAUDE_STRICT_UNSUPPORTED)
+ return cast(dict[str, Any], strip_unsupported_formats(sanitized))
+
+
+def sanitize_tool_params_for_bedrock_strict(
+ params: dict[str, Any],
+) -> dict[str, Any]:
+ """Sanitize a JSON schema for Bedrock Converse strict tool use.
+
+ Bedrock Converse uses the same grammar compiler as the underlying Claude
+ model, so the constraints match Anthropic's.
+ """
+ return sanitize_tool_params_for_anthropic_strict(params)
+
+
def generate_model_description(
model: type[BaseModel],
*,
@@ -545,6 +763,25 @@ def build_rich_field_description(prop_schema: dict[str, Any]) -> str:
return ". ".join(parts) if parts else ""
+def _inline_top_level_ref(schema: dict[str, Any]) -> dict[str, Any]:
+ """Resolve only the top-level ``$ref``, preserving ``$defs`` for lazy inner resolution.
+
+ Used as a fallback when ``jsonref.replace_refs`` fails on circular schemas.
+ Inner ``$ref`` pointers are left intact so that :func:`_resolve_ref` can
+ resolve them during model construction, with cycle detection via ``in_progress``.
+ """
+ schema = deepcopy(schema)
+ ref = schema.get("$ref")
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
+ def_name = ref[len("#/$defs/") :]
+ defs = schema.get("$defs", {})
+ if def_name in defs:
+ resolved: dict[str, Any] = deepcopy(defs[def_name])
+ resolved.setdefault("$defs", defs)
+ return resolved
+ return schema
+
+
def create_model_from_schema( # type: ignore[no-any-unimported]
json_schema: dict[str, Any],
*,
@@ -599,19 +836,80 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
>>> person.name
'John'
"""
- json_schema = dict(jsonref.replace_refs(json_schema, proxies=False))
+ try:
+ json_schema = dict(jsonref.replace_refs(json_schema, proxies=False))
+ except (jsonref.JsonRefError, RecursionError):
+ json_schema = _inline_top_level_ref(json_schema)
effective_root = root_schema or json_schema
json_schema = force_additional_properties_false(json_schema)
effective_root = force_additional_properties_false(effective_root)
+ in_progress: dict[int, Any] = {}
+ model = _build_model_from_schema(
+ json_schema,
+ effective_root,
+ model_name=model_name,
+ enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
+ __config__=__config__,
+ __base__=__base__,
+ __module__=__module__,
+ __validators__=__validators__,
+ __cls_kwargs__=__cls_kwargs__,
+ )
+
+ types_namespace: dict[str, Any] = {
+ entry.__name__: entry
+ for entry in in_progress.values()
+ if isinstance(entry, type) and issubclass(entry, BaseModel)
+ }
+ for entry in in_progress.values():
+ if (
+ isinstance(entry, type)
+ and issubclass(entry, BaseModel)
+ and not getattr(entry, "__pydantic_complete__", True)
+ ):
+ try:
+ entry.model_rebuild(_types_namespace=types_namespace)
+ except Exception as e:
+ logger.debug("model_rebuild failed for %s: %s", entry.__name__, e)
+ return model
+
+
+def _build_model_from_schema( # type: ignore[no-any-unimported]
+ json_schema: dict[str, Any],
+ effective_root: dict[str, Any],
+ *,
+ model_name: str | None,
+ enrich_descriptions: bool,
+ in_progress: dict[int, Any],
+ __config__: ConfigDict | None = None,
+ __base__: type[BaseModel] | None = None,
+ __module__: str = __name__,
+ __validators__: dict[str, AnyClassMethod] | None = None,
+ __cls_kwargs__: dict[str, Any] | None = None,
+) -> type[BaseModel]:
+ """Inner builder shared by the public entry point and recursive nested-object creation.
+
+ Preprocessing via ``jsonref.replace_refs`` and the sanitization walkers is
+ run once by the public entry; this helper walks the already-normalized
+ schema and emits Pydantic models. ``in_progress`` maps ``id(schema)`` to
+ the model being built for that schema, so a cyclic ``$ref`` graph
+ degrades to a ``ForwardRef`` back-edge instead of blowing the stack.
+ """
+ original_id = id(json_schema)
if "allOf" in json_schema:
json_schema = _merge_all_of_schemas(json_schema["allOf"], effective_root)
- if "title" not in json_schema and "title" in (root_schema or {}):
- json_schema["title"] = (root_schema or {}).get("title")
effective_name = model_name or json_schema.get("title") or "DynamicModel"
+
+ schema_id = id(json_schema)
+ in_progress[original_id] = effective_name
+ if schema_id != original_id:
+ in_progress[schema_id] = effective_name
+
field_definitions = {
name: _json_schema_to_pydantic_field(
name,
@@ -619,13 +917,14 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
json_schema.get("required", []),
effective_root,
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
for name, prop in (json_schema.get("properties", {}) or {}).items()
}
effective_config = __config__ or ConfigDict(extra="forbid")
- return create_model_base(
+ model = create_model_base(
effective_name,
__config__=effective_config,
__base__=__base__,
@@ -634,6 +933,10 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
__cls_kwargs__=__cls_kwargs__,
**field_definitions,
)
+ in_progress[original_id] = model
+ if schema_id != original_id:
+ in_progress[schema_id] = model
+ return model
def _json_schema_to_pydantic_field(
@@ -643,6 +946,7 @@ def _json_schema_to_pydantic_field(
root_schema: dict[str, Any],
*,
enrich_descriptions: bool = False,
+ in_progress: dict[int, Any] | None = None,
) -> Any:
"""Convert a JSON schema property to a Pydantic field definition.
@@ -661,6 +965,7 @@ def _json_schema_to_pydantic_field(
root_schema,
name_=name.title(),
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
is_required = name in required
@@ -720,7 +1025,7 @@ def _json_schema_to_pydantic_field(
field_params["pattern"] = json_schema["pattern"]
if not is_required:
- type_ = type_ | None
+ type_ = Optional[type_] # noqa: UP045 - ForwardRef does not support `|`
if schema_extra:
field_params["json_schema_extra"] = schema_extra
@@ -793,6 +1098,7 @@ def _json_schema_to_pydantic_type(
*,
name_: str | None = None,
enrich_descriptions: bool = False,
+ in_progress: dict[int, Any] | None = None,
) -> Any:
"""Convert a JSON schema to a Python/Pydantic type.
@@ -801,10 +1107,23 @@ def _json_schema_to_pydantic_type(
root_schema: The root schema for resolving $ref.
name_: Optional name for nested models.
enrich_descriptions: Propagated to nested model creation.
+ in_progress: Map of ``id(schema_dict)`` to the Pydantic model
+ currently being built for that schema, or to a placeholder name
+ as a plain ``str`` while the model is still being constructed.
+ Populated by :func:`_build_model_from_schema`. Enables cycle
+ detection so a self-referential ``$ref`` graph resolves to a
+ :class:`ForwardRef` back-edge rather than recursing forever.
Returns:
A Python type corresponding to the JSON schema.
"""
+ if in_progress is not None:
+ cached = in_progress.get(id(json_schema))
+ if isinstance(cached, str):
+ return ForwardRef(cached)
+ if cached is not None:
+ return cached
+
ref = json_schema.get("$ref")
if ref:
ref_schema = _resolve_ref(ref, root_schema)
@@ -813,6 +1132,7 @@ def _json_schema_to_pydantic_type(
root_schema,
name_=name_,
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
enum_values = json_schema.get("enum")
@@ -832,6 +1152,7 @@ def _json_schema_to_pydantic_type(
root_schema,
name_=f"{name_ or 'Union'}Option{i}",
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
for i, schema in enumerate(any_of_schemas)
]
@@ -845,6 +1166,15 @@ def _json_schema_to_pydantic_type(
root_schema,
name_=name_,
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
+ )
+ if in_progress is not None:
+ return _build_model_from_schema(
+ json_schema,
+ root_schema,
+ model_name=name_,
+ enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
merged = _merge_all_of_schemas(all_of_schemas, root_schema)
return _json_schema_to_pydantic_type(
@@ -852,6 +1182,7 @@ def _json_schema_to_pydantic_type(
root_schema,
name_=name_,
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
type_ = json_schema.get("type")
@@ -872,12 +1203,21 @@ def _json_schema_to_pydantic_type(
root_schema,
name_=name_,
enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
)
return list[item_type] # type: ignore[valid-type]
return list
if type_ == "object":
properties = json_schema.get("properties")
if properties:
+ if in_progress is not None:
+ return _build_model_from_schema(
+ json_schema,
+ root_schema,
+ model_name=name_,
+ enrich_descriptions=enrich_descriptions,
+ in_progress=in_progress,
+ )
json_schema_ = json_schema.copy()
if json_schema_.get("title") is None:
json_schema_["title"] = name_ or "DynamicModel"
diff --git a/lib/crewai/src/crewai/utilities/reasoning_handler.py b/lib/crewai/src/crewai/utilities/reasoning_handler.py
index eecd8ee9a..ab3cbba16 100644
--- a/lib/crewai/src/crewai/utilities/reasoning_handler.py
+++ b/lib/crewai/src/crewai/utilities/reasoning_handler.py
@@ -15,6 +15,7 @@ from crewai.events.types.reasoning_events import (
AgentReasoningStartedEvent,
)
from crewai.llm import LLM
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.planning_types import PlanStep
from crewai.utilities.string_utils import sanitize_tool_name
@@ -481,17 +482,17 @@ class AgentReasoning:
"""Get the system prompt for planning.
Returns:
- The system prompt, either custom or from i18n.
+ The system prompt, either custom or from I18N_DEFAULT.
"""
if self.config.system_prompt is not None:
return self.config.system_prompt
# Try new "planning" section first, fall back to "reasoning" for compatibility
try:
- return self.agent.i18n.retrieve("planning", "system_prompt")
+ return I18N_DEFAULT.retrieve("planning", "system_prompt")
except (KeyError, AttributeError):
# Fallback to reasoning section for backward compatibility
- return self.agent.i18n.retrieve("reasoning", "initial_plan").format(
+ return I18N_DEFAULT.retrieve("reasoning", "initial_plan").format(
role=self.agent.role,
goal=self.agent.goal,
backstory=self._get_agent_backstory(),
@@ -527,7 +528,7 @@ class AgentReasoning:
# Try new "planning" section first
try:
- return self.agent.i18n.retrieve("planning", "create_plan_prompt").format(
+ return I18N_DEFAULT.retrieve("planning", "create_plan_prompt").format(
description=self.description,
expected_output=self.expected_output,
tools=available_tools,
@@ -535,7 +536,7 @@ class AgentReasoning:
)
except (KeyError, AttributeError):
# Fallback to reasoning section for backward compatibility
- return self.agent.i18n.retrieve("reasoning", "create_plan_prompt").format(
+ return I18N_DEFAULT.retrieve("reasoning", "create_plan_prompt").format(
role=self.agent.role,
goal=self.agent.goal,
backstory=self._get_agent_backstory(),
@@ -584,12 +585,12 @@ class AgentReasoning:
# Try new "planning" section first
try:
- return self.agent.i18n.retrieve("planning", "refine_plan_prompt").format(
+ return I18N_DEFAULT.retrieve("planning", "refine_plan_prompt").format(
current_plan=current_plan,
)
except (KeyError, AttributeError):
# Fallback to reasoning section for backward compatibility
- return self.agent.i18n.retrieve("reasoning", "refine_plan_prompt").format(
+ return I18N_DEFAULT.retrieve("reasoning", "refine_plan_prompt").format(
role=self.agent.role,
goal=self.agent.goal,
backstory=self._get_agent_backstory(),
@@ -642,7 +643,7 @@ def _call_llm_with_reasoning_prompt(
Returns:
The LLM response.
"""
- system_prompt = reasoning_agent.i18n.retrieve("reasoning", plan_type).format(
+ system_prompt = I18N_DEFAULT.retrieve("reasoning", plan_type).format(
role=reasoning_agent.role,
goal=reasoning_agent.goal,
backstory=backstory,
diff --git a/lib/crewai/src/crewai/utilities/streaming.py b/lib/crewai/src/crewai/utilities/streaming.py
index dd0992684..99bc9b199 100644
--- a/lib/crewai/src/crewai/utilities/streaming.py
+++ b/lib/crewai/src/crewai/utilities/streaming.py
@@ -3,9 +3,11 @@
import asyncio
from collections.abc import AsyncIterator, Callable, Iterator
import contextvars
+import logging
import queue
import threading
from typing import Any, NamedTuple
+import uuid
from typing_extensions import TypedDict
@@ -22,6 +24,13 @@ from crewai.types.streaming import (
from crewai.utilities.string_utils import sanitize_tool_name
+logger = logging.getLogger(__name__)
+
+_current_stream_ids: contextvars.ContextVar[tuple[str, ...]] = contextvars.ContextVar(
+ "_current_stream_ids", default=()
+)
+
+
class TaskInfo(TypedDict):
"""Task context information for streaming."""
@@ -41,6 +50,7 @@ class StreamingState(NamedTuple):
async_queue: asyncio.Queue[StreamChunk | None | Exception] | None
loop: asyncio.AbstractEventLoop | None
handler: Callable[[Any, BaseEvent], None]
+ stream_id: str | None = None
def _extract_tool_call_info(
@@ -102,6 +112,7 @@ def _create_stream_handler(
sync_queue: queue.Queue[StreamChunk | None | Exception],
async_queue: asyncio.Queue[StreamChunk | None | Exception] | None = None,
loop: asyncio.AbstractEventLoop | None = None,
+ stream_id: str | None = None,
) -> Callable[[Any, BaseEvent], None]:
"""Create a stream handler function.
@@ -110,21 +121,19 @@ def _create_stream_handler(
sync_queue: Synchronous queue for chunks.
async_queue: Optional async queue for chunks.
loop: Optional event loop for async operations.
+ stream_id: Stream scope ID for concurrent isolation.
Returns:
Handler function that can be registered with the event bus.
"""
def stream_handler(_: Any, event: BaseEvent) -> None:
- """Handle LLM stream chunk events and enqueue them.
-
- Args:
- _: Event source (unused).
- event: The event to process.
- """
if not isinstance(event, LLMStreamChunkEvent):
return
+ if stream_id is not None and stream_id not in _current_stream_ids.get():
+ return
+
chunk = _create_stream_chunk(event, current_task_info)
if async_queue is not None and loop is not None:
@@ -159,10 +168,23 @@ def _finalize_streaming(
streaming_output: The streaming output to set the result on.
"""
_unregister_handler(state.handler)
+ streaming_output._on_cleanup = None
if state.result_holder:
streaming_output._set_result(state.result_holder[0])
+def register_cleanup(
+ streaming_output: CrewStreamingOutput | FlowStreamingOutput,
+ state: StreamingState,
+) -> None:
+ """Register a cleanup callback on the streaming output.
+
+ Ensures the event handler is unregistered even if aclose()/close()
+ is called before iteration starts.
+ """
+ streaming_output._on_cleanup = lambda: _unregister_handler(state.handler)
+
+
def create_streaming_state(
current_task_info: TaskInfo,
result_holder: list[Any],
@@ -186,7 +208,11 @@ def create_streaming_state(
async_queue = asyncio.Queue()
loop = asyncio.get_event_loop()
- handler = _create_stream_handler(current_task_info, sync_queue, async_queue, loop)
+ stream_id = str(uuid.uuid4())
+
+ handler = _create_stream_handler(
+ current_task_info, sync_queue, async_queue, loop, stream_id=stream_id
+ )
crewai_event_bus.register_handler(LLMStreamChunkEvent, handler)
return StreamingState(
@@ -196,6 +222,7 @@ def create_streaming_state(
async_queue=async_queue,
loop=loop,
handler=handler,
+ stream_id=stream_id,
)
@@ -243,7 +270,12 @@ def create_chunk_generator(
Yields:
StreamChunk objects as they arrive.
"""
- ctx = contextvars.copy_context()
+ if state.stream_id is not None:
+ token = _current_stream_ids.set((*_current_stream_ids.get(), state.stream_id))
+ ctx = contextvars.copy_context()
+ _current_stream_ids.reset(token)
+ else:
+ ctx = contextvars.copy_context()
thread = threading.Thread(target=ctx.run, args=(run_func,), daemon=True)
thread.start()
@@ -283,7 +315,12 @@ async def create_async_chunk_generator(
"Async queue not initialized. Use create_streaming_state(use_async=True)."
)
- task = asyncio.create_task(run_coro())
+ if state.stream_id is not None:
+ token = _current_stream_ids.set((*_current_stream_ids.get(), state.stream_id))
+ task = asyncio.create_task(run_coro())
+ _current_stream_ids.reset(token)
+ else:
+ task = asyncio.create_task(run_coro())
try:
while True:
@@ -294,7 +331,14 @@ async def create_async_chunk_generator(
raise item
yield item
finally:
- await task
+ if not task.done():
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ pass
+ except Exception:
+ logger.debug("Background streaming task failed", exc_info=True)
if output_holder:
_finalize_streaming(state, output_holder[0])
else:
diff --git a/lib/crewai/src/crewai/utilities/tool_utils.py b/lib/crewai/src/crewai/utilities/tool_utils.py
index 027f136ed..c7a469468 100644
--- a/lib/crewai/src/crewai/utilities/tool_utils.py
+++ b/lib/crewai/src/crewai/utilities/tool_utils.py
@@ -13,7 +13,7 @@ from crewai.security.fingerprint import Fingerprint
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.tools.tool_types import ToolResult
from crewai.tools.tool_usage import ToolUsage, ToolUsageError
-from crewai.utilities.i18n import I18N
+from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.logger import Logger
from crewai.utilities.string_utils import sanitize_tool_name
@@ -30,7 +30,6 @@ if TYPE_CHECKING:
async def aexecute_tool_and_check_finality(
agent_action: AgentAction,
tools: list[CrewStructuredTool],
- i18n: I18N,
agent_key: str | None = None,
agent_role: str | None = None,
tools_handler: ToolsHandler | None = None,
@@ -49,7 +48,6 @@ async def aexecute_tool_and_check_finality(
Args:
agent_action: The action containing the tool to execute.
tools: List of available tools.
- i18n: Internationalization settings.
agent_key: Optional key for event emission.
agent_role: Optional role for event emission.
tools_handler: Optional tools handler for tool execution.
@@ -96,7 +94,7 @@ async def aexecute_tool_and_check_finality(
if tool:
tool_input = tool_calling.arguments if tool_calling.arguments else {}
hook_context = ToolCallHookContext(
- tool_name=tool_calling.tool_name,
+ tool_name=sanitized_tool_name,
tool_input=tool_input,
tool=tool,
agent=agent,
@@ -120,7 +118,7 @@ async def aexecute_tool_and_check_finality(
tool_result = await tool_usage.ause(tool_calling, agent_action.text)
after_hook_context = ToolCallHookContext(
- tool_name=tool_calling.tool_name,
+ tool_name=sanitized_tool_name,
tool_input=tool_input,
tool=tool,
agent=agent,
@@ -142,7 +140,7 @@ async def aexecute_tool_and_check_finality(
return ToolResult(modified_result, tool.result_as_answer)
- tool_result = i18n.errors("wrong_tool_name").format(
+ tool_result = I18N_DEFAULT.errors("wrong_tool_name").format(
tool=sanitized_tool_name,
tools=", ".join(tool_name_to_tool_map.keys()),
)
@@ -152,7 +150,6 @@ async def aexecute_tool_and_check_finality(
def execute_tool_and_check_finality(
agent_action: AgentAction,
tools: list[CrewStructuredTool],
- i18n: I18N,
agent_key: str | None = None,
agent_role: str | None = None,
tools_handler: ToolsHandler | None = None,
@@ -170,7 +167,6 @@ def execute_tool_and_check_finality(
Args:
agent_action: The action containing the tool to execute
tools: List of available tools
- i18n: Internationalization settings
agent_key: Optional key for event emission
agent_role: Optional role for event emission
tools_handler: Optional tools handler for tool execution
@@ -216,7 +212,7 @@ def execute_tool_and_check_finality(
if tool:
tool_input = tool_calling.arguments if tool_calling.arguments else {}
hook_context = ToolCallHookContext(
- tool_name=tool_calling.tool_name,
+ tool_name=sanitized_tool_name,
tool_input=tool_input,
tool=tool,
agent=agent,
@@ -240,7 +236,7 @@ def execute_tool_and_check_finality(
tool_result = tool_usage.use(tool_calling, agent_action.text)
after_hook_context = ToolCallHookContext(
- tool_name=tool_calling.tool_name,
+ tool_name=sanitized_tool_name,
tool_input=tool_input,
tool=tool,
agent=agent,
@@ -263,7 +259,7 @@ def execute_tool_and_check_finality(
return ToolResult(modified_result, tool.result_as_answer)
- tool_result = i18n.errors("wrong_tool_name").format(
+ tool_result = I18N_DEFAULT.errors("wrong_tool_name").format(
tool=sanitized_tool_name,
tools=", ".join(tool_name_to_tool_map.keys()),
)
diff --git a/lib/crewai/src/crewai/utilities/version.py b/lib/crewai/src/crewai/utilities/version.py
new file mode 100644
index 000000000..57a5c562d
--- /dev/null
+++ b/lib/crewai/src/crewai/utilities/version.py
@@ -0,0 +1,12 @@
+"""Version utilities for crewAI."""
+
+from __future__ import annotations
+
+from functools import cache
+import importlib.metadata
+
+
+@cache
+def get_crewai_version() -> str:
+ """Get the installed crewAI version string."""
+ return importlib.metadata.version("crewai")
diff --git a/lib/crewai/tests/agents/test_agent.py b/lib/crewai/tests/agents/test_agent.py
index 7706f9ade..4681c8842 100644
--- a/lib/crewai/tests/agents/test_agent.py
+++ b/lib/crewai/tests/agents/test_agent.py
@@ -1208,12 +1208,10 @@ def test_llm_call_with_error():
def test_handle_context_length_exceeds_limit():
# Import necessary modules
from crewai.utilities.agent_utils import handle_context_length
- from crewai.utilities.i18n import I18N
from crewai.utilities.printer import Printer
# Create mocks for dependencies
printer = Printer()
- i18n = I18N()
# Create an agent just for its LLM
agent = Agent(
@@ -1249,7 +1247,6 @@ def test_handle_context_length_exceeds_limit():
messages=messages,
llm=llm,
callbacks=callbacks,
- i18n=i18n,
)
# Verify our patch was called and raised the correct error
@@ -1994,7 +1991,7 @@ def test_litellm_anthropic_error_handling():
@pytest.mark.vcr()
def test_get_knowledge_search_query():
"""Test that _get_knowledge_search_query calls the LLM with the correct prompts."""
- from crewai.utilities.i18n import I18N
+ from crewai.utilities.i18n import I18N_DEFAULT
content = "The capital of France is Paris."
string_source = StringKnowledgeSource(content=content)
@@ -2013,7 +2010,6 @@ def test_get_knowledge_search_query():
agent=agent,
)
- i18n = I18N()
task_prompt = task.prompt()
with (
@@ -2050,13 +2046,13 @@ def test_get_knowledge_search_query():
[
{
"role": "system",
- "content": i18n.slice(
+ "content": I18N_DEFAULT.slice(
"knowledge_search_query_system_prompt"
).format(task_prompt=task.description),
},
{
"role": "user",
- "content": i18n.slice("knowledge_search_query").format(
+ "content": I18N_DEFAULT.slice("knowledge_search_query").format(
task_prompt=task_prompt
),
},
diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py
index 7a6260a44..3413e30ac 100644
--- a/lib/crewai/tests/agents/test_agent_executor.py
+++ b/lib/crewai/tests/agents/test_agent_executor.py
@@ -48,8 +48,6 @@ def _build_executor(**kwargs: Any) -> AgentExecutor:
executor._last_context_error = None
executor._step_executor = None
executor._planner_observer = None
- from crewai.utilities.i18n import get_i18n
- executor._i18n = kwargs.get("i18n") or get_i18n()
return executor
from crewai.agents.planner_observer import PlannerObserver
from crewai.experimental.agent_executor import (
diff --git a/lib/crewai/tests/agents/test_lite_agent.py b/lib/crewai/tests/agents/test_lite_agent.py
index b42e2c1ec..37d115228 100644
--- a/lib/crewai/tests/agents/test_lite_agent.py
+++ b/lib/crewai/tests/agents/test_lite_agent.py
@@ -1051,7 +1051,7 @@ def test_lite_agent_verbose_false_suppresses_printer_output():
successful_requests=1,
)
- with pytest.warns(DeprecationWarning):
+ with pytest.warns(FutureWarning):
agent = LiteAgent(
role="Test Agent",
goal="Test goal",
diff --git a/lib/crewai/tests/cassettes/test_hierarchical_verbose_false_manager_agent.yaml b/lib/crewai/tests/cassettes/test_hierarchical_verbose_false_manager_agent.yaml
index 6bd4c405c..994e3b9e6 100644
--- a/lib/crewai/tests/cassettes/test_hierarchical_verbose_false_manager_agent.yaml
+++ b/lib/crewai/tests/cassettes/test_hierarchical_verbose_false_manager_agent.yaml
@@ -55,7 +55,7 @@ interactions:
x-stainless-os:
- X-STAINLESS-OS-XXX
x-stainless-package-version:
- - 1.83.0
+ - 2.31.0
x-stainless-read-timeout:
- X-STAINLESS-READ-TIMEOUT-XXX
x-stainless-retry-count:
@@ -63,50 +63,51 @@ interactions:
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- - 3.13.3
+ - 3.13.12
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
- string: "{\n \"id\": \"chatcmpl-DIqxWpJbbFJoV8WlXhb9UYFbCmdPk\",\n \"object\":
- \"chat.completion\",\n \"created\": 1773385850,\n \"model\": \"gpt-4o-2024-08-06\",\n
+ string: "{\n \"id\": \"chatcmpl-DTApYQx2LepfeRL1XcDKPgrhMFnQr\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1775845516,\n \"model\": \"gpt-4o-2024-08-06\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
\"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
- \ \"id\": \"call_G2i9RJGNXKVfnd8ZTaBG8Fwi\",\n \"type\":
- \"function\",\n \"function\": {\n \"name\": \"ask_question_to_coworker\",\n
- \ \"arguments\": \"{\\\"question\\\": \\\"What are some trending
- topics or ideas in various fields that could be explored for an article?\\\",
- \\\"context\\\": \\\"We need to generate a list of 5 interesting ideas to
- explore for an article. These ideas should be engaging and relevant to current
- trends or captivating subjects.\\\", \\\"coworker\\\": \\\"Researcher\\\"}\"\n
- \ }\n },\n {\n \"id\": \"call_j4KH2SGZvNeioql0HcRQ9NTp\",\n
+ \ \"id\": \"call_BCh6lXsBTdixRuRh6OTBPoIJ\",\n \"type\":
+ \"function\",\n \"function\": {\n \"name\": \"delegate_work_to_coworker\",\n
+ \ \"arguments\": \"{\\\"task\\\": \\\"Come up with a list of 5
+ interesting ideas to explore for an article.\\\", \\\"context\\\": \\\"We
+ need five intriguing ideas worth exploring for an article. Each idea should
+ have potential for in-depth exploration and appeal to a broad audience, possibly
+ touching on current trends, historical insights, future possibilities, or
+ human interest stories.\\\", \\\"coworker\\\": \\\"Researcher\\\"}\"\n }\n
+ \ },\n {\n \"id\": \"call_rAQFeCrS4ogsqvIWRGAYFHGI\",\n
\ \"type\": \"function\",\n \"function\": {\n \"name\":
- \"ask_question_to_coworker\",\n \"arguments\": \"{\\\"question\\\":
- \\\"What unique angles or perspectives could we explore to make articles more
- compelling and engaging?\\\", \\\"context\\\": \\\"Our task involves coming
- up with 5 ideas for articles, each with an exciting paragraph highlight that
- illustrates the promise and intrigue of the topic. We want them to be more
- than generic concepts, shining for readers with fresh insights or engaging
- twists.\\\", \\\"coworker\\\": \\\"Senior Writer\\\"}\"\n }\n }\n
- \ ],\n \"refusal\": null,\n \"annotations\": []\n },\n
- \ \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n }\n
- \ ],\n \"usage\": {\n \"prompt_tokens\": 476,\n \"completion_tokens\":
- 183,\n \"total_tokens\": 659,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
- 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
+ \"delegate_work_to_coworker\",\n \"arguments\": \"{\\\"task\\\":
+ \\\"Write one amazing paragraph highlight for each of 5 ideas that showcases
+ how good an article about this topic could be.\\\", \\\"context\\\": \\\"Upon
+ receiving five intriguing ideas from the Researcher, create a compelling paragraph
+ for each idea that highlights its potential as a fascinating article. These
+ paragraphs must capture the essence of the topic and explain why it would
+ captivate readers, incorporating possible themes and insights.\\\", \\\"coworker\\\":
+ \\\"Senior Writer\\\"}\"\n }\n }\n ],\n \"refusal\":
+ null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
+ \ \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
+ 476,\n \"completion_tokens\": 201,\n \"total_tokens\": 677,\n \"prompt_tokens_details\":
+ {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
- \"default\",\n \"system_fingerprint\": \"fp_b7c8e3f100\"\n}\n"
+ \"default\",\n \"system_fingerprint\": \"fp_2ca5b70601\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- - 9db9389a3f9e424c-EWR
+ - 9ea3cb06ba66b301-TPE
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- - Fri, 13 Mar 2026 07:10:53 GMT
+ - Fri, 10 Apr 2026 18:25:18 GMT
Server:
- cloudflare
Strict-Transport-Security:
@@ -122,7 +123,7 @@ interactions:
openai-organization:
- OPENAI-ORG-XXX
openai-processing-ms:
- - '2402'
+ - '1981'
openai-project:
- OPENAI-PROJECT-XXX
openai-version:
@@ -154,13 +155,14 @@ interactions:
You work as a freelancer and is now working on doing research and analysis for
a new customer.\nYour personal goal is: Make the best research and analysis
on content about AI and AI agents"},{"role":"user","content":"\nCurrent Task:
- What are some trending topics or ideas in various fields that could be explored
- for an article?\n\nThis is the expected criteria for your final answer: Your
- best answer to your coworker asking you this, accounting for the context shared.\nyou
- MUST return the actual complete content as the final answer, not a summary.\n\nThis
- is the context you''re working with:\nWe need to generate a list of 5 interesting
- ideas to explore for an article. These ideas should be engaging and relevant
- to current trends or captivating subjects.\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}'
+ Come up with a list of 5 interesting ideas to explore for an article.\n\nThis
+ is the expected criteria for your final answer: Your best answer to your coworker
+ asking you this, accounting for the context shared.\nyou MUST return the actual
+ complete content as the final answer, not a summary.\n\nThis is the context
+ you''re working with:\nWe need five intriguing ideas worth exploring for an
+ article. Each idea should have potential for in-depth exploration and appeal
+ to a broad audience, possibly touching on current trends, historical insights,
+ future possibilities, or human interest stories.\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -173,7 +175,7 @@ interactions:
connection:
- keep-alive
content-length:
- - '978'
+ - '1046'
content-type:
- application/json
host:
@@ -187,7 +189,7 @@ interactions:
x-stainless-os:
- X-STAINLESS-OS-XXX
x-stainless-package-version:
- - 1.83.0
+ - 2.31.0
x-stainless-read-timeout:
- X-STAINLESS-READ-TIMEOUT-XXX
x-stainless-retry-count:
@@ -195,63 +197,69 @@ interactions:
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- - 3.13.3
+ - 3.13.12
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
- string: "{\n \"id\": \"chatcmpl-DIqxak88AexErt9PGFGHnWPIJLwNV\",\n \"object\":
- \"chat.completion\",\n \"created\": 1773385854,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
+ string: "{\n \"id\": \"chatcmpl-DTApalbfnYkqIc8slLS3DKwo9KXbc\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1775845518,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"Here are five trending and engaging
- topics across various fields that could be explored for an article:\\n\\n1.
- **The Rise of Autonomous AI Agents and Their Impact on the Future of Work**
- \ \\nExplore how autonomous AI agents\u2014systems capable of performing complex
- tasks independently\u2014are transforming industries such as customer service,
- software development, and logistics. Discuss implications for job automation,
- human-AI collaboration, and ethical considerations surrounding decision-making
- autonomy.\\n\\n2. **Generative AI Beyond Text: Innovations in Audio, Video,
- and 3D Content Creation** \\nDelve into advancements in generative AI models
- that create not only text but also realistic audio, video content, virtual
- environments, and 3D models. Highlight applications in gaming, entertainment,
- education, and digital marketing, as well as challenges like misinformation
- and deepfake detection.\\n\\n3. **AI-Driven Climate Modeling: Enhancing Predictive
- Accuracy to Combat Climate Change** \\nExamine how AI and machine learning
- are improving climate models by analyzing vast datasets, uncovering patterns,
- and simulating environmental scenarios. Discuss how these advances are aiding
- policymakers in making informed decisions to address climate risks and sustainability
- goals.\\n\\n4. **The Ethical Frontiers of AI in Healthcare: Balancing Innovation
- with Patient Privacy** \\nInvestigate ethical challenges posed by AI applications
- in healthcare, including diagnosis, personalized treatment, and patient data
- management. Focus on balancing rapid technological innovation with privacy,
- bias mitigation, and regulatory frameworks to ensure equitable access and
- trust.\\n\\n5. **Quantum Computing Meets AI: Exploring the Next Leap in Computational
- Power** \\nCover the intersection of quantum computing and artificial intelligence,
- exploring how quantum algorithms could accelerate AI training processes and
- solve problems beyond the reach of classical computers. Outline current research,
- potential breakthroughs, and the timeline for real-world applications.\\n\\nEach
- of these topics is timely, relevant, and has the potential to engage readers
- interested in cutting-edge technology, societal impact, and future trends.
- Let me know if you want me to help develop an outline or deeper research into
- any of these areas!\",\n \"refusal\": null,\n \"annotations\":
- []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
- \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 178,\n \"completion_tokens\":
- 402,\n \"total_tokens\": 580,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ \"assistant\",\n \"content\": \"Certainly! Here are five intriguing
+ article ideas that offer rich potential for deep exploration and broad audience
+ appeal, especially aligned with current trends and human interest in AI and
+ technology:\\n\\n1. **The Evolution of AI Agents: From Rule-Based Bots to
+ Autonomous Decision Makers** \\n Explore the historical development of
+ AI agents, tracing the journey from simple scripted chatbots to advanced autonomous
+ systems capable of complex decision-making and learning. Dive into key technological
+ milestones, breakthroughs in machine learning, and current state-of-the-art
+ AI agents. Discuss implications for industries such as customer service, healthcare,
+ and autonomous vehicles, highlighting both opportunities and ethical concerns.\\n\\n2.
+ **AI in Daily Life: How Intelligent Agents Are Reshaping Human Routines**
+ \ \\n Investigate the integration of AI agents in everyday life\u2014from
+ virtual assistants like Siri and Alexa to personalized recommendation systems
+ and smart home devices. Analyze how these AI tools influence productivity,
+ privacy, and social behavior. Include human interest elements through stories
+ of individuals or communities who have embraced or resisted these technologies.\\n\\n3.
+ **The Future of Work: AI Agents as Collaborative Colleagues** \\n Examine
+ how AI agents are transforming workplaces by acting as collaborators rather
+ than just tools. Cover applications in creative fields, data analysis, and
+ decision support, while addressing potential challenges such as job displacement,
+ new skill requirements, and the evolving definition of teamwork. Use expert
+ opinions and case studies to paint a nuanced future outlook.\\n\\n4. **Ethics
+ and Accountability in AI Agent Development** \\n Delve into the ethical
+ dilemmas posed by increasingly autonomous AI agents\u2014topics like bias
+ in algorithms, data privacy, and accountability for AI-driven decisions. Explore
+ measures being taken globally to regulate AI, frameworks for responsible AI
+ development, and the role of public awareness. Include historical context
+ about technology ethics to provide depth.\\n\\n5. **Human-AI Symbiosis: Stories
+ of Innovative Partnerships Shaping Our World** \\n Tell compelling human
+ interest stories about individuals or organizations pioneering collaborative
+ projects with AI agents that lead to breakthroughs in science, art, or social
+ good. Highlight how these partnerships transcend traditional human-machine
+ interaction and open new creative and problem-solving possibilities, inspiring
+ readers about the potential of human-AI synergy.\\n\\nThese ideas are designed
+ to be both engaging and informative, offering multiple angles\u2014technical,
+ historical, ethical, and personal\u2014to keep readers captivated while providing
+ substantial content for in-depth analysis.\",\n \"refusal\": null,\n
+ \ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\":
+ \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 189,\n \"completion_tokens\":
+ 472,\n \"total_tokens\": 661,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
- \"default\",\n \"system_fingerprint\": \"fp_e76a310957\"\n}\n"
+ \"default\",\n \"system_fingerprint\": \"fp_fbf43a1ff3\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- - 9db938b0493c4b9f-EWR
+ - 9ea3cb1b5c943323-TPE
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- - Fri, 13 Mar 2026 07:10:59 GMT
+ - Fri, 10 Apr 2026 18:25:25 GMT
Server:
- cloudflare
Strict-Transport-Security:
@@ -267,7 +275,7 @@ interactions:
openai-organization:
- OPENAI-ORG-XXX
openai-processing-ms:
- - '5699'
+ - '6990'
openai-project:
- OPENAI-PROJECT-XXX
openai-version:
@@ -298,15 +306,16 @@ interactions:
a senior writer, specialized in technology, software engineering, AI and startups.
You work as a freelancer and are now working on writing content for a new customer.\nYour
personal goal is: Write the best content about AI and AI agents."},{"role":"user","content":"\nCurrent
- Task: What unique angles or perspectives could we explore to make articles more
- compelling and engaging?\n\nThis is the expected criteria for your final answer:
- Your best answer to your coworker asking you this, accounting for the context
- shared.\nyou MUST return the actual complete content as the final answer, not
- a summary.\n\nThis is the context you''re working with:\nOur task involves coming
- up with 5 ideas for articles, each with an exciting paragraph highlight that
- illustrates the promise and intrigue of the topic. We want them to be more than
- generic concepts, shining for readers with fresh insights or engaging twists.\n\nProvide
- your complete response:"}],"model":"gpt-4.1-mini"}'
+ Task: Write one amazing paragraph highlight for each of 5 ideas that showcases
+ how good an article about this topic could be.\n\nThis is the expected criteria
+ for your final answer: Your best answer to your coworker asking you this, accounting
+ for the context shared.\nyou MUST return the actual complete content as the
+ final answer, not a summary.\n\nThis is the context you''re working with:\nUpon
+ receiving five intriguing ideas from the Researcher, create a compelling paragraph
+ for each idea that highlights its potential as a fascinating article. These
+ paragraphs must capture the essence of the topic and explain why it would captivate
+ readers, incorporating possible themes and insights.\n\nProvide your complete
+ response:"}],"model":"gpt-4.1-mini"}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -319,7 +328,7 @@ interactions:
connection:
- keep-alive
content-length:
- - '1041'
+ - '1103'
content-type:
- application/json
host:
@@ -333,7 +342,7 @@ interactions:
x-stainless-os:
- X-STAINLESS-OS-XXX
x-stainless-package-version:
- - 1.83.0
+ - 2.31.0
x-stainless-read-timeout:
- X-STAINLESS-READ-TIMEOUT-XXX
x-stainless-retry-count:
@@ -341,78 +350,83 @@ interactions:
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- - 3.13.3
+ - 3.13.12
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
- string: "{\n \"id\": \"chatcmpl-DIqxZCl1kFIE7WXznIKow9QFNZ2QT\",\n \"object\":
- \"chat.completion\",\n \"created\": 1773385853,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
+ string: "{\n \"id\": \"chatcmpl-DTApbrh9Z4yFAKPHIR48ubdB1R5xK\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1775845519,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"Absolutely! To create compelling and
- engaging AI articles that stand out, we need to go beyond surface-level discussions
- and deliver fresh perspectives that challenge assumptions and spark curiosity.
- Here are five unique angles with their highlight paragraphs that could really
- captivate our readers:\\n\\n1. **The Hidden Psychology of AI Agents: How They
- Learn Human Biases and What That Means for Our Future** \\n*Highlight:* AI
- agents don\u2019t just process data\u2014they absorb the subtle nuances and
- biases embedded in human language, behavior, and culture. This article dives
- deep into the psychological parallels between AI learning mechanisms and human
- cognitive biases, revealing surprising ways AI can both mirror and amplify
- our prejudices. Understanding these dynamics is crucial for building trustworthy
- AI systems and reshaping the future relationship between humans and machines.\\n\\n2.
- **From Assistants to Autonomous Creators: The Rise of AI Agents as Artists,
- Writers, and Innovators** \\n*Highlight:* What do we lose and gain when AI
- agents start producing original art, literature, and innovations? This piece
- explores groundbreaking examples where AI isn\u2019t just a tool but a creative
- partner that challenges our definition of authorship and genius. We\u2019ll
- examine ethical dilemmas, collaborative workflows, and the exciting frontier
- where human intuition meets algorithmic originality.\\n\\n3. **AI Agents in
- the Wild: How Decentralized Autonomous Organizations Could Redefine Economy
- and Governance** \\n*Highlight:* Imagine AI agents operating autonomously
- in decentralized networks, making real-time decisions that affect finances,
- resource management, and governance without human intervention. This article
- uncovers how DAOs powered by AI agents might spontaneously evolve new forms
- of organization\u2014transparent, efficient, and resistant to traditional
- corruption. We\u2019ll investigate early case studies and speculate on how
- this might disrupt centuries-old societal structures.\\n\\n4. **Beyond Chatbots:
- The Next Generation of AI Agents as Empathetic Digital Companions** \\n*Highlight:*
- Moving past scripted conversations, emerging AI agents simulate empathy and
- emotional intelligence in ways that can transform mental health care, education,
- and companionship. This article provides an insider look at the complex algorithms
- and biofeedback mechanisms enabling AI to recognize, respond to, and foster
- human emotions\u2014potentially filling gaps in underserved populations while
- raising profound questions about authenticity and connection.\\n\\n5. **The
- Environmental Toll of AI Agents: Unmasking the Ecological Cost of Intelligent
- Automation** \\n*Highlight:* While AI promises efficiency and innovation,
- the environmental footprint of training and deploying millions of AI agents
- is rarely discussed. This eye-opening article quantifies the energy demands
- of current models, challenges the narrative of AI as an unequivocal green
- solution, and explores emerging approaches pathing toward sustainable intelligent
- automation\u2014an urgent conversation for an increasingly eco-conscious tech
- landscape.\\n\\nEach of these angles opens a door to rich storytelling that
- blends technical depth, ethical inquiry, and visionary implications\u2014perfect
- for readers hungry for insight that\u2019s both sophisticated and accessible.
- Let me know which ones resonate most, or if you want me to refine any into
- full article outlines!\",\n \"refusal\": null,\n \"annotations\":
- []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
- \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 188,\n \"completion_tokens\":
- 595,\n \"total_tokens\": 783,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
- 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
+ \"assistant\",\n \"content\": \"1. **The Rise of Autonomous AI Agents:
+ Revolutionizing Everyday Tasks** \\nImagine a world where AI agents autonomously
+ manage your daily schedule, optimize your work routines, and even handle complex
+ decision-making with minimal human intervention. An article exploring the
+ rise of autonomous AI agents would captivate readers by diving into how advancements
+ in machine learning and natural language processing have matured these agents
+ from simple chatbots to intelligent collaborators. Themes could include practical
+ applications in industries like healthcare, finance, and personal productivity,
+ the challenges of trust and transparency, and a glimpse into the ethical questions
+ surrounding AI autonomy. This topic not only showcases cutting-edge technology
+ but also invites readers to envision the near future of human-AI synergy.\\n\\n2.
+ **Building Ethical AI Agents: Balancing Innovation with Responsibility** \\nAs
+ AI agents become more powerful and independent, the imperative to embed ethical
+ frameworks within their design comes sharply into focus. An insightful article
+ on this theme would engage readers by unpacking the complexities of programming
+ morality, fairness, and accountability into AI systems that influence critical
+ decisions\u2014whether in hiring processes, law enforcement, or digital content
+ moderation. Exploring real-world case studies alongside philosophical and
+ regulatory perspectives, the piece could illuminate the delicate balance between
+ technological innovation and societal values, offering a nuanced discussion
+ that appeals to technologists, ethicists, and everyday users alike.\\n\\n3.
+ **AI Agents in Startups: Accelerating Growth and Disrupting Markets** \\nStartups
+ are uniquely positioned to leverage AI agents as game-changers that turbocharge
+ growth, optimize workflows, and unlock new business models. This article could
+ enthrall readers by detailing how nimble companies integrate AI-driven agents
+ for customer engagement, market analysis, and personalized product recommendations\u2014outpacing
+ larger incumbents. It would also examine hurdles such as data privacy, scaling
+ complexities, and the human-AI collaboration dynamic, providing actionable
+ insights for entrepreneurs and investors. The story of AI agents fueling startup
+ innovation not only inspires but also outlines the practical pathways and
+ pitfalls on the frontier of modern entrepreneurship.\\n\\n4. **The Future
+ of Work with AI Agents: Redefining Roles and Skills** \\nAI agents are redefining
+ professional landscapes by automating routine tasks and augmenting human creativity
+ and decision-making. An article on this topic could engage readers by painting
+ a vivid picture of the evolving workplace, where collaboration between humans
+ and AI agents becomes the norm. Delving into emerging roles, necessary skill
+ sets, and how education and training must adapt, the piece would offer a forward-thinking
+ analysis that resonates deeply with employees, managers, and policymakers.
+ Exploring themes of workforce transformation, productivity gains, and potential
+ socioeconomic impacts, it provides a comprehensive outlook on an AI-integrated
+ work environment.\\n\\n5. **From Reactive to Proactive: How Next-Gen AI Agents
+ Anticipate Needs** \\nThe leap from reactive AI assistants to truly proactive
+ AI agents signifies one of the most thrilling advances in artificial intelligence.
+ An article centered on this evolution would captivate readers by illustrating
+ how these agents utilize predictive analytics, contextual understanding, and
+ continuous learning to anticipate user needs before they are expressed. By
+ showcasing pioneering applications in personalized healthcare management,
+ smart homes, and adaptive learning platforms, the article would highlight
+ the profound shift toward intuitive, anticipatory technology. This theme not
+ only excites with futuristic promise but also probes the technical and privacy
+ challenges that come with increased agency and foresight.\",\n \"refusal\":
+ null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
+ \ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
+ 197,\n \"completion_tokens\": 666,\n \"total_tokens\": 863,\n \"prompt_tokens_details\":
+ {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
- \"default\",\n \"system_fingerprint\": \"fp_ae0f8c9a7b\"\n}\n"
+ \"default\",\n \"system_fingerprint\": \"fp_d45f83c5fd\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- - 9db938b0489680d4-EWR
+ - 9ea3cb1cbfe2b312-TPE
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- - Fri, 13 Mar 2026 07:11:02 GMT
+ - Fri, 10 Apr 2026 18:25:28 GMT
Server:
- cloudflare
Strict-Transport-Security:
@@ -428,7 +442,7 @@ interactions:
openai-organization:
- OPENAI-ORG-XXX
openai-processing-ms:
- - '8310'
+ - '9479'
openai-project:
- OPENAI-PROJECT-XXX
openai-version:
@@ -467,91 +481,105 @@ interactions:
good an article about this topic could be. Return the list of ideas with their
paragraph and your notes.\\n\\nThis is the expected criteria for your final
answer: 5 bullet points with a paragraph for each idea.\\nyou MUST return the
- actual complete content as the final answer, not a summary.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_G2i9RJGNXKVfnd8ZTaBG8Fwi\",\"type\":\"function\",\"function\":{\"name\":\"ask_question_to_coworker\",\"arguments\":\"{\\\"question\\\":
- \\\"What are some trending topics or ideas in various fields that could be explored
- for an article?\\\", \\\"context\\\": \\\"We need to generate a list of 5 interesting
- ideas to explore for an article. These ideas should be engaging and relevant
- to current trends or captivating subjects.\\\", \\\"coworker\\\": \\\"Researcher\\\"}\"}},{\"id\":\"call_j4KH2SGZvNeioql0HcRQ9NTp\",\"type\":\"function\",\"function\":{\"name\":\"ask_question_to_coworker\",\"arguments\":\"{\\\"question\\\":
- \\\"What unique angles or perspectives could we explore to make articles more
- compelling and engaging?\\\", \\\"context\\\": \\\"Our task involves coming
- up with 5 ideas for articles, each with an exciting paragraph highlight that
- illustrates the promise and intrigue of the topic. We want them to be more than
- generic concepts, shining for readers with fresh insights or engaging twists.\\\",
- \\\"coworker\\\": \\\"Senior Writer\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_G2i9RJGNXKVfnd8ZTaBG8Fwi\",\"name\":\"ask_question_to_coworker\",\"content\":\"Here
- are five trending and engaging topics across various fields that could be explored
- for an article:\\n\\n1. **The Rise of Autonomous AI Agents and Their Impact
- on the Future of Work** \\nExplore how autonomous AI agents\u2014systems capable
- of performing complex tasks independently\u2014are transforming industries such
- as customer service, software development, and logistics. Discuss implications
- for job automation, human-AI collaboration, and ethical considerations surrounding
- decision-making autonomy.\\n\\n2. **Generative AI Beyond Text: Innovations in
- Audio, Video, and 3D Content Creation** \\nDelve into advancements in generative
- AI models that create not only text but also realistic audio, video content,
- virtual environments, and 3D models. Highlight applications in gaming, entertainment,
- education, and digital marketing, as well as challenges like misinformation
- and deepfake detection.\\n\\n3. **AI-Driven Climate Modeling: Enhancing Predictive
- Accuracy to Combat Climate Change** \\nExamine how AI and machine learning
- are improving climate models by analyzing vast datasets, uncovering patterns,
- and simulating environmental scenarios. Discuss how these advances are aiding
- policymakers in making informed decisions to address climate risks and sustainability
- goals.\\n\\n4. **The Ethical Frontiers of AI in Healthcare: Balancing Innovation
- with Patient Privacy** \\nInvestigate ethical challenges posed by AI applications
- in healthcare, including diagnosis, personalized treatment, and patient data
- management. Focus on balancing rapid technological innovation with privacy,
- bias mitigation, and regulatory frameworks to ensure equitable access and trust.\\n\\n5.
- **Quantum Computing Meets AI: Exploring the Next Leap in Computational Power**
- \ \\nCover the intersection of quantum computing and artificial intelligence,
- exploring how quantum algorithms could accelerate AI training processes and
- solve problems beyond the reach of classical computers. Outline current research,
- potential breakthroughs, and the timeline for real-world applications.\\n\\nEach
- of these topics is timely, relevant, and has the potential to engage readers
- interested in cutting-edge technology, societal impact, and future trends. Let
- me know if you want me to help develop an outline or deeper research into any
- of these areas!\"},{\"role\":\"tool\",\"tool_call_id\":\"call_j4KH2SGZvNeioql0HcRQ9NTp\",\"name\":\"ask_question_to_coworker\",\"content\":\"Absolutely!
- To create compelling and engaging AI articles that stand out, we need to go
- beyond surface-level discussions and deliver fresh perspectives that challenge
- assumptions and spark curiosity. Here are five unique angles with their highlight
- paragraphs that could really captivate our readers:\\n\\n1. **The Hidden Psychology
- of AI Agents: How They Learn Human Biases and What That Means for Our Future**
- \ \\n*Highlight:* AI agents don\u2019t just process data\u2014they absorb the
- subtle nuances and biases embedded in human language, behavior, and culture.
- This article dives deep into the psychological parallels between AI learning
- mechanisms and human cognitive biases, revealing surprising ways AI can both
- mirror and amplify our prejudices. Understanding these dynamics is crucial for
- building trustworthy AI systems and reshaping the future relationship between
- humans and machines.\\n\\n2. **From Assistants to Autonomous Creators: The Rise
- of AI Agents as Artists, Writers, and Innovators** \\n*Highlight:* What do
- we lose and gain when AI agents start producing original art, literature, and
- innovations? This piece explores groundbreaking examples where AI isn\u2019t
- just a tool but a creative partner that challenges our definition of authorship
- and genius. We\u2019ll examine ethical dilemmas, collaborative workflows, and
- the exciting frontier where human intuition meets algorithmic originality.\\n\\n3.
- **AI Agents in the Wild: How Decentralized Autonomous Organizations Could Redefine
- Economy and Governance** \\n*Highlight:* Imagine AI agents operating autonomously
- in decentralized networks, making real-time decisions that affect finances,
- resource management, and governance without human intervention. This article
- uncovers how DAOs powered by AI agents might spontaneously evolve new forms
- of organization\u2014transparent, efficient, and resistant to traditional corruption.
- We\u2019ll investigate early case studies and speculate on how this might disrupt
- centuries-old societal structures.\\n\\n4. **Beyond Chatbots: The Next Generation
- of AI Agents as Empathetic Digital Companions** \\n*Highlight:* Moving past
- scripted conversations, emerging AI agents simulate empathy and emotional intelligence
- in ways that can transform mental health care, education, and companionship.
- This article provides an insider look at the complex algorithms and biofeedback
- mechanisms enabling AI to recognize, respond to, and foster human emotions\u2014potentially
- filling gaps in underserved populations while raising profound questions about
- authenticity and connection.\\n\\n5. **The Environmental Toll of AI Agents:
- Unmasking the Ecological Cost of Intelligent Automation** \\n*Highlight:* While
- AI promises efficiency and innovation, the environmental footprint of training
- and deploying millions of AI agents is rarely discussed. This eye-opening article
- quantifies the energy demands of current models, challenges the narrative of
- AI as an unequivocal green solution, and explores emerging approaches pathing
- toward sustainable intelligent automation\u2014an urgent conversation for an
- increasingly eco-conscious tech landscape.\\n\\nEach of these angles opens a
- door to rich storytelling that blends technical depth, ethical inquiry, and
- visionary implications\u2014perfect for readers hungry for insight that\u2019s
- both sophisticated and accessible. Let me know which ones resonate most, or
- if you want me to refine any into full article outlines!\"},{\"role\":\"user\",\"content\":\"Analyze
+ actual complete content as the final answer, not a summary.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_BCh6lXsBTdixRuRh6OTBPoIJ\",\"type\":\"function\",\"function\":{\"name\":\"delegate_work_to_coworker\",\"arguments\":\"{\\\"task\\\":
+ \\\"Come up with a list of 5 interesting ideas to explore for an article.\\\",
+ \\\"context\\\": \\\"We need five intriguing ideas worth exploring for an article.
+ Each idea should have potential for in-depth exploration and appeal to a broad
+ audience, possibly touching on current trends, historical insights, future possibilities,
+ or human interest stories.\\\", \\\"coworker\\\": \\\"Researcher\\\"}\"}},{\"id\":\"call_rAQFeCrS4ogsqvIWRGAYFHGI\",\"type\":\"function\",\"function\":{\"name\":\"delegate_work_to_coworker\",\"arguments\":\"{\\\"task\\\":
+ \\\"Write one amazing paragraph highlight for each of 5 ideas that showcases
+ how good an article about this topic could be.\\\", \\\"context\\\": \\\"Upon
+ receiving five intriguing ideas from the Researcher, create a compelling paragraph
+ for each idea that highlights its potential as a fascinating article. These
+ paragraphs must capture the essence of the topic and explain why it would captivate
+ readers, incorporating possible themes and insights.\\\", \\\"coworker\\\":
+ \\\"Senior Writer\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_BCh6lXsBTdixRuRh6OTBPoIJ\",\"name\":\"delegate_work_to_coworker\",\"content\":\"Certainly!
+ Here are five intriguing article ideas that offer rich potential for deep exploration
+ and broad audience appeal, especially aligned with current trends and human
+ interest in AI and technology:\\n\\n1. **The Evolution of AI Agents: From Rule-Based
+ Bots to Autonomous Decision Makers** \\n Explore the historical development
+ of AI agents, tracing the journey from simple scripted chatbots to advanced
+ autonomous systems capable of complex decision-making and learning. Dive into
+ key technological milestones, breakthroughs in machine learning, and current
+ state-of-the-art AI agents. Discuss implications for industries such as customer
+ service, healthcare, and autonomous vehicles, highlighting both opportunities
+ and ethical concerns.\\n\\n2. **AI in Daily Life: How Intelligent Agents Are
+ Reshaping Human Routines** \\n Investigate the integration of AI agents in
+ everyday life\u2014from virtual assistants like Siri and Alexa to personalized
+ recommendation systems and smart home devices. Analyze how these AI tools influence
+ productivity, privacy, and social behavior. Include human interest elements
+ through stories of individuals or communities who have embraced or resisted
+ these technologies.\\n\\n3. **The Future of Work: AI Agents as Collaborative
+ Colleagues** \\n Examine how AI agents are transforming workplaces by acting
+ as collaborators rather than just tools. Cover applications in creative fields,
+ data analysis, and decision support, while addressing potential challenges such
+ as job displacement, new skill requirements, and the evolving definition of
+ teamwork. Use expert opinions and case studies to paint a nuanced future outlook.\\n\\n4.
+ **Ethics and Accountability in AI Agent Development** \\n Delve into the
+ ethical dilemmas posed by increasingly autonomous AI agents\u2014topics like
+ bias in algorithms, data privacy, and accountability for AI-driven decisions.
+ Explore measures being taken globally to regulate AI, frameworks for responsible
+ AI development, and the role of public awareness. Include historical context
+ about technology ethics to provide depth.\\n\\n5. **Human-AI Symbiosis: Stories
+ of Innovative Partnerships Shaping Our World** \\n Tell compelling human
+ interest stories about individuals or organizations pioneering collaborative
+ projects with AI agents that lead to breakthroughs in science, art, or social
+ good. Highlight how these partnerships transcend traditional human-machine interaction
+ and open new creative and problem-solving possibilities, inspiring readers about
+ the potential of human-AI synergy.\\n\\nThese ideas are designed to be both
+ engaging and informative, offering multiple angles\u2014technical, historical,
+ ethical, and personal\u2014to keep readers captivated while providing substantial
+ content for in-depth analysis.\"},{\"role\":\"tool\",\"tool_call_id\":\"call_rAQFeCrS4ogsqvIWRGAYFHGI\",\"name\":\"delegate_work_to_coworker\",\"content\":\"1.
+ **The Rise of Autonomous AI Agents: Revolutionizing Everyday Tasks** \\nImagine
+ a world where AI agents autonomously manage your daily schedule, optimize your
+ work routines, and even handle complex decision-making with minimal human intervention.
+ An article exploring the rise of autonomous AI agents would captivate readers
+ by diving into how advancements in machine learning and natural language processing
+ have matured these agents from simple chatbots to intelligent collaborators.
+ Themes could include practical applications in industries like healthcare, finance,
+ and personal productivity, the challenges of trust and transparency, and a glimpse
+ into the ethical questions surrounding AI autonomy. This topic not only showcases
+ cutting-edge technology but also invites readers to envision the near future
+ of human-AI synergy.\\n\\n2. **Building Ethical AI Agents: Balancing Innovation
+ with Responsibility** \\nAs AI agents become more powerful and independent,
+ the imperative to embed ethical frameworks within their design comes sharply
+ into focus. An insightful article on this theme would engage readers by unpacking
+ the complexities of programming morality, fairness, and accountability into
+ AI systems that influence critical decisions\u2014whether in hiring processes,
+ law enforcement, or digital content moderation. Exploring real-world case studies
+ alongside philosophical and regulatory perspectives, the piece could illuminate
+ the delicate balance between technological innovation and societal values, offering
+ a nuanced discussion that appeals to technologists, ethicists, and everyday
+ users alike.\\n\\n3. **AI Agents in Startups: Accelerating Growth and Disrupting
+ Markets** \\nStartups are uniquely positioned to leverage AI agents as game-changers
+ that turbocharge growth, optimize workflows, and unlock new business models.
+ This article could enthrall readers by detailing how nimble companies integrate
+ AI-driven agents for customer engagement, market analysis, and personalized
+ product recommendations\u2014outpacing larger incumbents. It would also examine
+ hurdles such as data privacy, scaling complexities, and the human-AI collaboration
+ dynamic, providing actionable insights for entrepreneurs and investors. The
+ story of AI agents fueling startup innovation not only inspires but also outlines
+ the practical pathways and pitfalls on the frontier of modern entrepreneurship.\\n\\n4.
+ **The Future of Work with AI Agents: Redefining Roles and Skills** \\nAI agents
+ are redefining professional landscapes by automating routine tasks and augmenting
+ human creativity and decision-making. An article on this topic could engage
+ readers by painting a vivid picture of the evolving workplace, where collaboration
+ between humans and AI agents becomes the norm. Delving into emerging roles,
+ necessary skill sets, and how education and training must adapt, the piece would
+ offer a forward-thinking analysis that resonates deeply with employees, managers,
+ and policymakers. Exploring themes of workforce transformation, productivity
+ gains, and potential socioeconomic impacts, it provides a comprehensive outlook
+ on an AI-integrated work environment.\\n\\n5. **From Reactive to Proactive:
+ How Next-Gen AI Agents Anticipate Needs** \\nThe leap from reactive AI assistants
+ to truly proactive AI agents signifies one of the most thrilling advances in
+ artificial intelligence. An article centered on this evolution would captivate
+ readers by illustrating how these agents utilize predictive analytics, contextual
+ understanding, and continuous learning to anticipate user needs before they
+ are expressed. By showcasing pioneering applications in personalized healthcare
+ management, smart homes, and adaptive learning platforms, the article would
+ highlight the profound shift toward intuitive, anticipatory technology. This
+ theme not only excites with futuristic promise but also probes the technical
+ and privacy challenges that come with increased agency and foresight.\"},{\"role\":\"user\",\"content\":\"Analyze
the tool result. If requirements are met, provide the Final Answer. Otherwise,
call the next tool. Deliver only the answer without meta-commentary.\"}],\"model\":\"gpt-4o\",\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"delegate_work_to_coworker\",\"description\":\"Delegate
a specific task to one of the following coworkers: Researcher, Senior Writer\\nThe
@@ -582,7 +610,7 @@ interactions:
connection:
- keep-alive
content-length:
- - '9923'
+ - '11056'
content-type:
- application/json
cookie:
@@ -598,7 +626,7 @@ interactions:
x-stainless-os:
- X-STAINLESS-OS-XXX
x-stainless-package-version:
- - 1.83.0
+ - 2.31.0
x-stainless-read-timeout:
- X-STAINLESS-READ-TIMEOUT-XXX
x-stainless-retry-count:
@@ -606,58 +634,64 @@ interactions:
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- - 3.13.3
+ - 3.13.12
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
- string: "{\n \"id\": \"chatcmpl-DIqxidsfoqQl7qXSIVHfSCyETUwlU\",\n \"object\":
- \"chat.completion\",\n \"created\": 1773385862,\n \"model\": \"gpt-4o-2024-08-06\",\n
+ string: "{\n \"id\": \"chatcmpl-DTApljTaq8nDgNMS21B319i56seCn\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1775845529,\n \"model\": \"gpt-4o-2024-08-06\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"1. **The Rise of Autonomous AI Agents
- and Their Impact on the Future of Work** \\nExplore how autonomous AI agents\u2014systems
- capable of performing complex tasks independently\u2014are transforming industries
- such as customer service, software development, and logistics. Discuss implications
- for job automation, human-AI collaboration, and ethical considerations surrounding
- decision-making autonomy.\\n\\n2. **Generative AI Beyond Text: Innovations
- in Audio, Video, and 3D Content Creation** \\nDelve into advancements in
- generative AI models that create not only text but also realistic audio, video
- content, virtual environments, and 3D models. Highlight applications in gaming,
- entertainment, education, and digital marketing, as well as challenges like
- misinformation and deepfake detection.\\n\\n3. **AI-Driven Climate Modeling:
- Enhancing Predictive Accuracy to Combat Climate Change** \\nExamine how AI
- and machine learning are improving climate models by analyzing vast datasets,
- uncovering patterns, and simulating environmental scenarios. Discuss how these
- advances are aiding policymakers in making informed decisions to address climate
- risks and sustainability goals.\\n\\n4. **The Ethical Frontiers of AI in Healthcare:
- Balancing Innovation with Patient Privacy** \\nInvestigate ethical challenges
- posed by AI applications in healthcare, including diagnosis, personalized
- treatment, and patient data management. Focus on balancing rapid technological
- innovation with privacy, bias mitigation, and regulatory frameworks to ensure
- equitable access and trust.\\n\\n5. **Quantum Computing Meets AI: Exploring
- the Next Leap in Computational Power** \\nCover the intersection of quantum
- computing and artificial intelligence, exploring how quantum algorithms could
- accelerate AI training processes and solve problems beyond the reach of classical
- computers. Outline current research, potential breakthroughs, and the timeline
- for real-world applications.\",\n \"refusal\": null,\n \"annotations\":
- []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
- \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 1748,\n \"completion_tokens\":
- 335,\n \"total_tokens\": 2083,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ \"assistant\",\n \"content\": \"- **The Evolution of AI Agents: From
+ Rule-Based Bots to Autonomous Decision Makers** \\n Explore the historical
+ development of AI agents, tracing the journey from simple scripted chatbots
+ to advanced autonomous systems capable of complex decision-making and learning.
+ Dive into key technological milestones, breakthroughs in machine learning,
+ and current state-of-the-art AI agents. Discuss implications for industries
+ such as customer service, healthcare, and autonomous vehicles, highlighting
+ both opportunities and ethical concerns.\\n\\n- **AI in Daily Life: How Intelligent
+ Agents Are Reshaping Human Routines** \\n Investigate the integration of
+ AI agents in everyday life\u2014from virtual assistants like Siri and Alexa
+ to personalized recommendation systems and smart home devices. Analyze how
+ these AI tools influence productivity, privacy, and social behavior. Include
+ human interest elements through stories of individuals or communities who
+ have embraced or resisted these technologies.\\n\\n- **The Future of Work:
+ AI Agents as Collaborative Colleagues** \\n Examine how AI agents are transforming
+ workplaces by acting as collaborators rather than just tools. Cover applications
+ in creative fields, data analysis, and decision support, while addressing
+ potential challenges such as job displacement, new skill requirements, and
+ the evolving definition of teamwork. Use expert opinions and case studies
+ to paint a nuanced future outlook.\\n\\n- **Ethics and Accountability in AI
+ Agent Development** \\n Delve into the ethical dilemmas posed by increasingly
+ autonomous AI agents\u2014topics like bias in algorithms, data privacy, and
+ accountability for AI-driven decisions. Explore measures being taken globally
+ to regulate AI, frameworks for responsible AI development, and the role of
+ public awareness. Include historical context about technology ethics to provide
+ depth.\\n\\n- **Human-AI Symbiosis: Stories of Innovative Partnerships Shaping
+ Our World** \\n Tell compelling human interest stories about individuals
+ or organizations pioneering collaborative projects with AI agents that lead
+ to breakthroughs in science, art, or social good. Highlight how these partnerships
+ transcend traditional human-machine interaction and open new creative and
+ problem-solving possibilities, inspiring readers about the potential of human-AI
+ synergy.\",\n \"refusal\": null,\n \"annotations\": []\n },\n
+ \ \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n
+ \ \"usage\": {\n \"prompt_tokens\": 1903,\n \"completion_tokens\": 399,\n
+ \ \"total_tokens\": 2302,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
- \"default\",\n \"system_fingerprint\": \"fp_b7c8e3f100\"\n}\n"
+ \"default\",\n \"system_fingerprint\": \"fp_df40ab6c25\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-Ray:
- - 9db938e60d5bc5e7-EWR
+ - 9ea3cb5a6957b301-TPE
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- - Fri, 13 Mar 2026 07:11:04 GMT
+ - Fri, 10 Apr 2026 18:25:31 GMT
Server:
- cloudflare
Strict-Transport-Security:
@@ -673,7 +707,7 @@ interactions:
openai-organization:
- OPENAI-ORG-XXX
openai-processing-ms:
- - '2009'
+ - '2183'
openai-project:
- OPENAI-PROJECT-XXX
openai-version:
diff --git a/lib/crewai/tests/cli/deploy/test_deploy_main.py b/lib/crewai/tests/cli/deploy/test_deploy_main.py
index 4b818cc58..9b6e49e1a 100644
--- a/lib/crewai/tests/cli/deploy/test_deploy_main.py
+++ b/lib/crewai/tests/cli/deploy/test_deploy_main.py
@@ -125,7 +125,7 @@ class TestDeployCommand(unittest.TestCase):
mock_response.json.return_value = {"uuid": "test-uuid"}
self.mock_client.deploy_by_uuid.return_value = mock_response
- self.deploy_command.deploy(uuid="test-uuid")
+ self.deploy_command.deploy(uuid="test-uuid", skip_validate=True)
self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid")
mock_display.assert_called_once_with({"uuid": "test-uuid"})
@@ -137,7 +137,7 @@ class TestDeployCommand(unittest.TestCase):
mock_response.json.return_value = {"uuid": "test-uuid"}
self.mock_client.deploy_by_name.return_value = mock_response
- self.deploy_command.deploy()
+ self.deploy_command.deploy(skip_validate=True)
self.mock_client.deploy_by_name.assert_called_once_with("test_project")
mock_display.assert_called_once_with({"uuid": "test-uuid"})
@@ -156,7 +156,7 @@ class TestDeployCommand(unittest.TestCase):
self.mock_client.create_crew.return_value = mock_response
with patch("sys.stdout", new=StringIO()) as fake_out:
- self.deploy_command.create_crew()
+ self.deploy_command.create_crew(skip_validate=True)
self.assertIn("Deployment created successfully!", fake_out.getvalue())
self.assertIn("new-uuid", fake_out.getvalue())
diff --git a/lib/crewai/tests/cli/deploy/test_validate.py b/lib/crewai/tests/cli/deploy/test_validate.py
new file mode 100644
index 000000000..ff8b26376
--- /dev/null
+++ b/lib/crewai/tests/cli/deploy/test_validate.py
@@ -0,0 +1,430 @@
+"""Tests for `crewai.cli.deploy.validate`.
+
+The fixtures here correspond 1:1 to the deployment-failure patterns observed
+in the #crewai-deployment-failures Slack channel that motivated this work.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from textwrap import dedent
+from typing import Iterable
+from unittest.mock import patch
+
+import pytest
+
+from crewai.cli.deploy.validate import (
+ DeployValidator,
+ Severity,
+ normalize_package_name,
+)
+
+
+def _make_pyproject(
+ name: str = "my_crew",
+ dependencies: Iterable[str] = ("crewai>=1.14.0",),
+ *,
+ hatchling: bool = False,
+ flow: bool = False,
+ extra: str = "",
+) -> str:
+ deps = ", ".join(f'"{d}"' for d in dependencies)
+ lines = [
+ "[project]",
+ f'name = "{name}"',
+ 'version = "0.1.0"',
+ f"dependencies = [{deps}]",
+ ]
+ if hatchling:
+ lines += [
+ "",
+ "[build-system]",
+ 'requires = ["hatchling"]',
+ 'build-backend = "hatchling.build"',
+ ]
+ if flow:
+ lines += ["", "[tool.crewai]", 'type = "flow"']
+ if extra:
+ lines += ["", extra]
+ return "\n".join(lines) + "\n"
+
+
+def _scaffold_standard_crew(
+ root: Path,
+ *,
+ name: str = "my_crew",
+ include_crew_py: bool = True,
+ include_agents_yaml: bool = True,
+ include_tasks_yaml: bool = True,
+ include_lockfile: bool = True,
+ pyproject: str | None = None,
+) -> Path:
+ (root / "pyproject.toml").write_text(pyproject or _make_pyproject(name=name))
+ if include_lockfile:
+ (root / "uv.lock").write_text("# dummy uv lockfile\n")
+
+ pkg_dir = root / "src" / normalize_package_name(name)
+ pkg_dir.mkdir(parents=True)
+ (pkg_dir / "__init__.py").write_text("")
+
+ if include_crew_py:
+ (pkg_dir / "crew.py").write_text(
+ dedent(
+ """
+ from crewai.project import CrewBase, crew
+
+ @CrewBase
+ class MyCrew:
+ agents_config = "config/agents.yaml"
+ tasks_config = "config/tasks.yaml"
+
+ @crew
+ def crew(self):
+ from crewai import Crew
+ return Crew(agents=[], tasks=[])
+ """
+ ).strip()
+ + "\n"
+ )
+
+ config_dir = pkg_dir / "config"
+ config_dir.mkdir()
+ if include_agents_yaml:
+ (config_dir / "agents.yaml").write_text("{}\n")
+ if include_tasks_yaml:
+ (config_dir / "tasks.yaml").write_text("{}\n")
+
+ return pkg_dir
+
+
+def _codes(validator: DeployValidator) -> set[str]:
+ return {r.code for r in validator.results}
+
+
+def _run_without_import_check(root: Path) -> DeployValidator:
+ """Run validation with the subprocess-based import check stubbed out;
+ the classifier is exercised directly in its own tests below."""
+ with patch.object(DeployValidator, "_check_module_imports", lambda self: None):
+ v = DeployValidator(project_root=root)
+ v.run()
+ return v
+
+
+@pytest.mark.parametrize(
+ "project_name, expected",
+ [
+ ("my-crew", "my_crew"),
+ ("My Cool-Project", "my_cool_project"),
+ ("crew123", "crew123"),
+ ("crew.name!with$chars", "crewnamewithchars"),
+ ],
+)
+def test_normalize_package_name(project_name: str, expected: str) -> None:
+ assert normalize_package_name(project_name) == expected
+
+
+def test_valid_standard_crew_project_passes(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path)
+ v = _run_without_import_check(tmp_path)
+ assert v.ok, f"expected clean run, got {v.results}"
+
+
+def test_missing_pyproject_errors(tmp_path: Path) -> None:
+ v = _run_without_import_check(tmp_path)
+ assert "missing_pyproject" in _codes(v)
+ assert not v.ok
+
+
+def test_invalid_pyproject_errors(tmp_path: Path) -> None:
+ (tmp_path / "pyproject.toml").write_text("this is not valid toml ====\n")
+ v = _run_without_import_check(tmp_path)
+ assert "invalid_pyproject" in _codes(v)
+
+
+def test_missing_project_name_errors(tmp_path: Path) -> None:
+ (tmp_path / "pyproject.toml").write_text(
+ '[project]\nversion = "0.1.0"\ndependencies = ["crewai>=1.14.0"]\n'
+ )
+ v = _run_without_import_check(tmp_path)
+ assert "missing_project_name" in _codes(v)
+
+
+def test_missing_lockfile_errors(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path, include_lockfile=False)
+ v = _run_without_import_check(tmp_path)
+ assert "missing_lockfile" in _codes(v)
+
+
+def test_poetry_lock_is_accepted(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path, include_lockfile=False)
+ (tmp_path / "poetry.lock").write_text("# poetry lockfile\n")
+ v = _run_without_import_check(tmp_path)
+ assert "missing_lockfile" not in _codes(v)
+
+
+def test_stale_lockfile_warns(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path)
+ # Make lockfile older than pyproject.
+ lock = tmp_path / "uv.lock"
+ pyproject = tmp_path / "pyproject.toml"
+ old_time = pyproject.stat().st_mtime - 60
+ import os
+
+ os.utime(lock, (old_time, old_time))
+ v = _run_without_import_check(tmp_path)
+ assert "stale_lockfile" in _codes(v)
+ # Stale is a warning, so the run can still be ok (no errors).
+ assert v.ok
+
+
+def test_missing_package_dir_errors(tmp_path: Path) -> None:
+ # pyproject says name=my_crew but we only create src/other_pkg/
+ (tmp_path / "pyproject.toml").write_text(_make_pyproject(name="my_crew"))
+ (tmp_path / "uv.lock").write_text("")
+ (tmp_path / "src" / "other_pkg").mkdir(parents=True)
+ v = _run_without_import_check(tmp_path)
+ codes = _codes(v)
+ assert "missing_package_dir" in codes
+ finding = next(r for r in v.results if r.code == "missing_package_dir")
+ assert "other_pkg" in finding.hint
+
+
+def test_egg_info_only_errors_with_targeted_hint(tmp_path: Path) -> None:
+ """Regression for the case where only src/.egg-info/ exists."""
+ (tmp_path / "pyproject.toml").write_text(_make_pyproject(name="odoo_pm_agents"))
+ (tmp_path / "uv.lock").write_text("")
+ (tmp_path / "src" / "odoo_pm_agents.egg-info").mkdir(parents=True)
+ v = _run_without_import_check(tmp_path)
+ finding = next(r for r in v.results if r.code == "missing_package_dir")
+ assert "egg-info" in finding.hint
+
+
+def test_stale_egg_info_sibling_warns(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path)
+ (tmp_path / "src" / "my_crew.egg-info").mkdir()
+ v = _run_without_import_check(tmp_path)
+ assert "stale_egg_info" in _codes(v)
+
+
+def test_missing_crew_py_errors(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path, include_crew_py=False)
+ v = _run_without_import_check(tmp_path)
+ assert "missing_crew_py" in _codes(v)
+
+
+def test_missing_agents_yaml_errors(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path, include_agents_yaml=False)
+ v = _run_without_import_check(tmp_path)
+ assert "missing_agents_yaml" in _codes(v)
+
+
+def test_missing_tasks_yaml_errors(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path, include_tasks_yaml=False)
+ v = _run_without_import_check(tmp_path)
+ assert "missing_tasks_yaml" in _codes(v)
+
+
+def test_flow_project_requires_main_py(tmp_path: Path) -> None:
+ (tmp_path / "pyproject.toml").write_text(
+ _make_pyproject(name="my_flow", flow=True)
+ )
+ (tmp_path / "uv.lock").write_text("")
+ (tmp_path / "src" / "my_flow").mkdir(parents=True)
+ v = _run_without_import_check(tmp_path)
+ assert "missing_flow_main" in _codes(v)
+
+
+def test_flow_project_with_main_py_passes(tmp_path: Path) -> None:
+ (tmp_path / "pyproject.toml").write_text(
+ _make_pyproject(name="my_flow", flow=True)
+ )
+ (tmp_path / "uv.lock").write_text("")
+ pkg = tmp_path / "src" / "my_flow"
+ pkg.mkdir(parents=True)
+ (pkg / "main.py").write_text("# flow entrypoint\n")
+ v = _run_without_import_check(tmp_path)
+ assert "missing_flow_main" not in _codes(v)
+
+
+def test_hatchling_without_wheel_config_passes_when_pkg_dir_matches(
+ tmp_path: Path,
+) -> None:
+ _scaffold_standard_crew(
+ tmp_path, pyproject=_make_pyproject(name="my_crew", hatchling=True)
+ )
+ v = _run_without_import_check(tmp_path)
+ # src/my_crew/ exists, so hatch default should find it — no wheel error.
+ assert "hatch_wheel_target_missing" not in _codes(v)
+
+
+def test_hatchling_with_explicit_wheel_config_passes(tmp_path: Path) -> None:
+ extra = (
+ "[tool.hatch.build.targets.wheel]\n"
+ 'packages = ["src/my_crew"]'
+ )
+ _scaffold_standard_crew(
+ tmp_path,
+ pyproject=_make_pyproject(name="my_crew", hatchling=True, extra=extra),
+ )
+ v = _run_without_import_check(tmp_path)
+ assert "hatch_wheel_target_missing" not in _codes(v)
+
+
+def test_classify_missing_openai_key_is_warning(tmp_path: Path) -> None:
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error(
+ "ImportError",
+ "Error importing native provider: 1 validation error for OpenAICompletion\n"
+ " Value error, OPENAI_API_KEY is required",
+ tb="",
+ )
+ assert len(v.results) == 1
+ result = v.results[0]
+ assert result.code == "llm_init_missing_key"
+ assert result.severity is Severity.WARNING
+ assert "OPENAI_API_KEY" in result.title
+
+
+def test_classify_azure_extra_missing_is_error(tmp_path: Path) -> None:
+ """The real message raised by the Azure provider module uses plain
+ double quotes around the install command (no backticks). Match the
+ exact string that ships in the provider source so this test actually
+ guards the regex used in production."""
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error(
+ "ImportError",
+ 'Azure AI Inference native provider not available, to install: uv add "crewai[azure-ai-inference]"',
+ tb="",
+ )
+ assert "missing_provider_extra" in _codes(v)
+ finding = next(r for r in v.results if r.code == "missing_provider_extra")
+ assert finding.title.startswith("Azure AI Inference")
+ assert 'uv add "crewai[azure-ai-inference]"' in finding.hint
+
+
+@pytest.mark.parametrize(
+ "pkg_label, install_cmd",
+ [
+ ("Anthropic", 'uv add "crewai[anthropic]"'),
+ ("AWS Bedrock", 'uv add "crewai[bedrock]"'),
+ ("Google Gen AI", 'uv add "crewai[google-genai]"'),
+ ],
+)
+def test_classify_missing_provider_extra_matches_real_messages(
+ tmp_path: Path, pkg_label: str, install_cmd: str
+) -> None:
+ """Regression for the four provider error strings verbatim."""
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error(
+ "ImportError",
+ f"{pkg_label} native provider not available, to install: {install_cmd}",
+ tb="",
+ )
+ assert "missing_provider_extra" in _codes(v)
+ finding = next(r for r in v.results if r.code == "missing_provider_extra")
+ assert install_cmd in finding.hint
+
+
+def test_classify_keyerror_at_import_is_warning(tmp_path: Path) -> None:
+ """Regression for `KeyError: 'SERPLY_API_KEY'` raised at import time."""
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error("KeyError", "'SERPLY_API_KEY'", tb="")
+ codes = _codes(v)
+ assert "env_var_read_at_import" in codes
+
+
+def test_classify_no_crewbase_class_is_error(tmp_path: Path) -> None:
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error(
+ "ValueError",
+ "Crew class annotated with @CrewBase not found.",
+ tb="",
+ )
+ assert "no_crewbase_class" in _codes(v)
+
+
+def test_classify_no_flow_subclass_is_error(tmp_path: Path) -> None:
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error("ValueError", "No Flow subclass found in the module.", tb="")
+ assert "no_flow_subclass" in _codes(v)
+
+
+def test_classify_stale_crewai_pin_attribute_error(tmp_path: Path) -> None:
+ """Regression for a stale crewai pin missing `_load_response_format`."""
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error(
+ "AttributeError",
+ "'EmploymentServiceDecisionSupportSystemCrew' object has no attribute '_load_response_format'",
+ tb="",
+ )
+ assert "stale_crewai_pin" in _codes(v)
+
+
+def test_classify_unknown_error_is_fallback(tmp_path: Path) -> None:
+ v = DeployValidator(project_root=tmp_path)
+ v._classify_import_error("RuntimeError", "something weird happened", tb="")
+ assert "import_failed" in _codes(v)
+
+
+def test_env_var_referenced_but_missing_warns(tmp_path: Path) -> None:
+ pkg = _scaffold_standard_crew(tmp_path)
+ (pkg / "tools.py").write_text(
+ 'import os\nkey = os.getenv("TAVILY_API_KEY")\n'
+ )
+ import os
+
+ # Make sure the test doesn't inherit the key from the host environment.
+ with patch.dict(os.environ, {}, clear=False):
+ os.environ.pop("TAVILY_API_KEY", None)
+ v = _run_without_import_check(tmp_path)
+ codes = _codes(v)
+ assert "env_vars_not_in_dotenv" in codes
+
+
+def test_env_var_in_dotenv_does_not_warn(tmp_path: Path) -> None:
+ pkg = _scaffold_standard_crew(tmp_path)
+ (pkg / "tools.py").write_text(
+ 'import os\nkey = os.getenv("TAVILY_API_KEY")\n'
+ )
+ (tmp_path / ".env").write_text("TAVILY_API_KEY=abc\n")
+ v = _run_without_import_check(tmp_path)
+ assert "env_vars_not_in_dotenv" not in _codes(v)
+
+
+def test_old_crewai_pin_in_uv_lock_warns(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path)
+ (tmp_path / "uv.lock").write_text(
+ 'name = "crewai"\nversion = "1.10.0"\nsource = { registry = "..." }\n'
+ )
+ v = _run_without_import_check(tmp_path)
+ assert "old_crewai_pin" in _codes(v)
+
+
+def test_modern_crewai_pin_does_not_warn(tmp_path: Path) -> None:
+ _scaffold_standard_crew(tmp_path)
+ (tmp_path / "uv.lock").write_text(
+ 'name = "crewai"\nversion = "1.14.1"\nsource = { registry = "..." }\n'
+ )
+ v = _run_without_import_check(tmp_path)
+ assert "old_crewai_pin" not in _codes(v)
+
+
+def test_create_crew_aborts_on_validation_error(tmp_path: Path) -> None:
+ """`crewai deploy create` must not contact the API when validation fails."""
+ from unittest.mock import MagicMock, patch as mock_patch
+
+ from crewai.cli.deploy.main import DeployCommand
+
+ with (
+ mock_patch("crewai.cli.command.get_auth_token", return_value="tok"),
+ mock_patch("crewai.cli.deploy.main.get_project_name", return_value="p"),
+ mock_patch("crewai.cli.command.PlusAPI") as mock_api,
+ mock_patch(
+ "crewai.cli.deploy.main.validate_project"
+ ) as mock_validate,
+ ):
+ mock_validate.return_value = MagicMock(ok=False)
+ cmd = DeployCommand()
+ cmd.create_crew()
+ assert not cmd.plus_api_client.create_crew.called
+ del mock_api # silence unused-var lint
\ No newline at end of file
diff --git a/lib/crewai/tests/cli/remote_template/__init__.py b/lib/crewai/tests/cli/remote_template/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/crewai/tests/cli/remote_template/test_main.py b/lib/crewai/tests/cli/remote_template/test_main.py
new file mode 100644
index 000000000..829e956ce
--- /dev/null
+++ b/lib/crewai/tests/cli/remote_template/test_main.py
@@ -0,0 +1,283 @@
+import io
+import os
+import zipfile
+from unittest.mock import MagicMock, patch
+
+import httpx
+import pytest
+from click.testing import CliRunner
+
+from crewai.cli.cli import template_add, template_list
+from crewai.cli.remote_template.main import TemplateCommand
+
+
+@pytest.fixture
+def runner():
+ return CliRunner()
+
+
+SAMPLE_REPOS = [
+ {"name": "template_deep_research", "description": "Deep research template", "private": False},
+ {"name": "template_pull_request_review", "description": "PR review template", "private": False},
+ {"name": "template_conversational_example", "description": "Conversational demo", "private": False},
+ {"name": "crewai", "description": "Main repo", "private": False},
+ {"name": "marketplace-crew-template", "description": "Marketplace", "private": False},
+]
+
+
+def _make_zipball(files: dict[str, str], top_dir: str = "crewAIInc-template_test-abc123") -> bytes:
+ """Create an in-memory zipball mimicking GitHub's format."""
+ buf = io.BytesIO()
+ with zipfile.ZipFile(buf, "w") as zf:
+ zf.writestr(f"{top_dir}/", "")
+ for path, content in files.items():
+ zf.writestr(f"{top_dir}/{path}", content)
+ return buf.getvalue()
+
+
+# --- CLI command tests ---
+
+
+@patch("crewai.cli.cli.TemplateCommand")
+def test_template_list_command(mock_cls, runner):
+ mock_instance = MagicMock()
+ mock_cls.return_value = mock_instance
+
+ result = runner.invoke(template_list)
+
+ assert result.exit_code == 0
+ mock_cls.assert_called_once()
+ mock_instance.list_templates.assert_called_once()
+
+
+@patch("crewai.cli.cli.TemplateCommand")
+def test_template_add_command(mock_cls, runner):
+ mock_instance = MagicMock()
+ mock_cls.return_value = mock_instance
+
+ result = runner.invoke(template_add, ["deep_research"])
+
+ assert result.exit_code == 0
+ mock_cls.assert_called_once()
+ mock_instance.add_template.assert_called_once_with("deep_research", None)
+
+
+@patch("crewai.cli.cli.TemplateCommand")
+def test_template_add_with_output_dir(mock_cls, runner):
+ mock_instance = MagicMock()
+ mock_cls.return_value = mock_instance
+
+ result = runner.invoke(template_add, ["deep_research", "-o", "my_project"])
+
+ assert result.exit_code == 0
+ mock_instance.add_template.assert_called_once_with("deep_research", "my_project")
+
+
+# --- TemplateCommand unit tests ---
+
+
+class TestTemplateCommand:
+ @pytest.fixture
+ def cmd(self):
+ with patch.object(TemplateCommand, "__init__", return_value=None):
+ instance = TemplateCommand()
+ instance._telemetry = MagicMock()
+ return instance
+
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_fetch_templates_filters_by_prefix(self, mock_get, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ # Return empty on page 2 to stop pagination
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ templates = cmd._fetch_templates()
+
+ assert len(templates) == 3
+ assert all(t["name"].startswith("template_") for t in templates)
+
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_fetch_templates_excludes_private(self, mock_get, cmd):
+ repos = [
+ {"name": "template_private_one", "description": "", "private": True},
+ {"name": "template_public_one", "description": "", "private": False},
+ ]
+ mock_response = MagicMock()
+ mock_response.json.return_value = repos
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ templates = cmd._fetch_templates()
+
+ assert len(templates) == 1
+ assert templates[0]["name"] == "template_public_one"
+
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_fetch_templates_api_error(self, mock_get, cmd):
+ mock_get.side_effect = httpx.HTTPError("connection error")
+
+ with pytest.raises(SystemExit):
+ cmd._fetch_templates()
+
+ @patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_list_templates_prints_output(self, mock_get, mock_prompt, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ with patch("crewai.cli.remote_template.main.console") as mock_console:
+ cmd.list_templates()
+ assert mock_console.print.call_count > 0
+
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_resolve_repo_name_with_prefix(self, mock_get, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ result = cmd._resolve_repo_name("template_deep_research")
+ assert result == "template_deep_research"
+
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_resolve_repo_name_without_prefix(self, mock_get, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ result = cmd._resolve_repo_name("deep_research")
+ assert result == "template_deep_research"
+
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_resolve_repo_name_not_found(self, mock_get, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ result = cmd._resolve_repo_name("nonexistent")
+ assert result is None
+
+ def test_extract_zip(self, cmd, tmp_path):
+ files = {
+ "README.md": "# Test Template",
+ "src/main.py": "print('hello')",
+ "config/settings.yaml": "key: value",
+ }
+ zip_bytes = _make_zipball(files)
+ dest = str(tmp_path / "output")
+
+ cmd._extract_zip(zip_bytes, dest)
+
+ assert os.path.isfile(os.path.join(dest, "README.md"))
+ assert os.path.isfile(os.path.join(dest, "src", "main.py"))
+ assert os.path.isfile(os.path.join(dest, "config", "settings.yaml"))
+
+ with open(os.path.join(dest, "src", "main.py")) as f:
+ assert f.read() == "print('hello')"
+
+ @patch.object(TemplateCommand, "_extract_zip")
+ @patch.object(TemplateCommand, "_download_zip")
+ @patch.object(TemplateCommand, "_resolve_repo_name")
+ def test_add_template_success(self, mock_resolve, mock_download, mock_extract, cmd, tmp_path):
+ mock_resolve.return_value = "template_deep_research"
+ mock_download.return_value = b"fake-zip-bytes"
+
+ os.chdir(tmp_path)
+ cmd.add_template("deep_research")
+
+ mock_resolve.assert_called_once_with("deep_research")
+ mock_download.assert_called_once_with("template_deep_research")
+ expected_dest = os.path.join(str(tmp_path), "deep_research")
+ mock_extract.assert_called_once_with(b"fake-zip-bytes", expected_dest)
+
+ @patch.object(TemplateCommand, "_resolve_repo_name")
+ def test_add_template_not_found(self, mock_resolve, cmd):
+ mock_resolve.return_value = None
+
+ with pytest.raises(SystemExit):
+ cmd.add_template("nonexistent")
+
+ @patch.object(TemplateCommand, "_extract_zip")
+ @patch.object(TemplateCommand, "_download_zip")
+ @patch("crewai.cli.remote_template.main.click.prompt", return_value="my_project")
+ @patch.object(TemplateCommand, "_resolve_repo_name")
+ def test_add_template_dir_exists_prompts_rename(self, mock_resolve, mock_prompt, mock_download, mock_extract, cmd, tmp_path):
+ mock_resolve.return_value = "template_deep_research"
+ mock_download.return_value = b"fake-zip-bytes"
+ existing = tmp_path / "deep_research"
+ existing.mkdir()
+
+ os.chdir(tmp_path)
+ cmd.add_template("deep_research")
+
+ expected_dest = os.path.join(str(tmp_path), "my_project")
+ mock_extract.assert_called_once_with(b"fake-zip-bytes", expected_dest)
+
+ @patch.object(TemplateCommand, "_resolve_repo_name")
+ @patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
+ def test_add_template_dir_exists_quit(self, mock_prompt, mock_resolve, cmd, tmp_path):
+ mock_resolve.return_value = "template_deep_research"
+ existing = tmp_path / "deep_research"
+ existing.mkdir()
+
+ os.chdir(tmp_path)
+ cmd.add_template("deep_research")
+ # Should return without downloading
+
+ @patch.object(TemplateCommand, "_install_repo")
+ @patch("crewai.cli.remote_template.main.click.prompt", return_value="2")
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_list_templates_selects_and_installs(self, mock_get, mock_prompt, mock_install, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ with patch("crewai.cli.remote_template.main.console"):
+ cmd.list_templates()
+
+ # Templates are sorted by name; index 1 (choice "2") = template_deep_research
+ mock_install.assert_called_once_with("template_deep_research")
+
+ @patch.object(TemplateCommand, "_install_repo")
+ @patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
+ @patch("crewai.cli.remote_template.main.httpx.get")
+ def test_list_templates_quit(self, mock_get, mock_prompt, mock_install, cmd):
+ mock_response = MagicMock()
+ mock_response.json.return_value = SAMPLE_REPOS
+ mock_response.raise_for_status = MagicMock()
+ mock_empty = MagicMock()
+ mock_empty.json.return_value = []
+ mock_empty.raise_for_status = MagicMock()
+ mock_get.side_effect = [mock_response, mock_empty]
+
+ with patch("crewai.cli.remote_template.main.console"):
+ cmd.list_templates()
+
+ mock_install.assert_not_called()
diff --git a/lib/crewai/tests/cli/test_cli.py b/lib/crewai/tests/cli/test_cli.py
index ed74a6036..b324294b1 100644
--- a/lib/crewai/tests/cli/test_cli.py
+++ b/lib/crewai/tests/cli/test_cli.py
@@ -367,7 +367,7 @@ def test_deploy_push(command, runner):
result = runner.invoke(deploy_push, ["-u", uuid])
assert result.exit_code == 0
- mock_deploy.deploy.assert_called_once_with(uuid=uuid)
+ mock_deploy.deploy.assert_called_once_with(uuid=uuid, skip_validate=False)
@mock.patch("crewai.cli.cli.DeployCommand")
@@ -376,7 +376,7 @@ def test_deploy_push_no_uuid(command, runner):
result = runner.invoke(deploy_push)
assert result.exit_code == 0
- mock_deploy.deploy.assert_called_once_with(uuid=None)
+ mock_deploy.deploy.assert_called_once_with(uuid=None, skip_validate=False)
@mock.patch("crewai.cli.cli.DeployCommand")
diff --git a/lib/crewai/tests/cli/tools/test_main.py b/lib/crewai/tests/cli/tools/test_main.py
index 31032a072..ed51db74a 100644
--- a/lib/crewai/tests/cli/tools/test_main.py
+++ b/lib/crewai/tests/cli/tools/test_main.py
@@ -161,7 +161,8 @@ def test_install_api_error(mock_get, capsys, tool_command):
@patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=False)
-def test_publish_when_not_in_sync(mock_is_synced, capsys, tool_command):
+@patch("crewai.cli.tools.main.git.Repository.__init__", return_value=None)
+def test_publish_when_not_in_sync(mock_init, mock_is_synced, capsys, tool_command):
with raises(SystemExit):
tool_command.publish(is_public=True)
diff --git a/lib/crewai/tests/events/test_llm_usage_event.py b/lib/crewai/tests/events/test_llm_usage_event.py
index f19f07b47..9be8c639f 100644
--- a/lib/crewai/tests/events/test_llm_usage_event.py
+++ b/lib/crewai/tests/events/test_llm_usage_event.py
@@ -174,3 +174,51 @@ class TestEmitCallCompletedEventPassesUsage:
event = mock_emit.call_args[1]["event"]
assert isinstance(event, LLMCallCompletedEvent)
assert event.usage is None
+
+class TestUsageMetricsNewFields:
+ def test_add_usage_metrics_aggregates_reasoning_and_cache_creation(self):
+ from crewai.types.usage_metrics import UsageMetrics
+
+ metrics1 = UsageMetrics(
+ total_tokens=100,
+ prompt_tokens=60,
+ completion_tokens=40,
+ cached_prompt_tokens=10,
+ reasoning_tokens=15,
+ cache_creation_tokens=5,
+ successful_requests=1,
+ )
+ metrics2 = UsageMetrics(
+ total_tokens=200,
+ prompt_tokens=120,
+ completion_tokens=80,
+ cached_prompt_tokens=20,
+ reasoning_tokens=25,
+ cache_creation_tokens=10,
+ successful_requests=1,
+ )
+
+ metrics1.add_usage_metrics(metrics2)
+
+ assert metrics1.total_tokens == 300
+ assert metrics1.prompt_tokens == 180
+ assert metrics1.completion_tokens == 120
+ assert metrics1.cached_prompt_tokens == 30
+ assert metrics1.reasoning_tokens == 40
+ assert metrics1.cache_creation_tokens == 15
+ assert metrics1.successful_requests == 2
+
+ def test_new_fields_default_to_zero(self):
+ from crewai.types.usage_metrics import UsageMetrics
+
+ metrics = UsageMetrics()
+ assert metrics.reasoning_tokens == 0
+ assert metrics.cache_creation_tokens == 0
+
+ def test_model_dump_includes_new_fields(self):
+ from crewai.types.usage_metrics import UsageMetrics
+
+ metrics = UsageMetrics(reasoning_tokens=10, cache_creation_tokens=5)
+ dumped = metrics.model_dump()
+ assert dumped["reasoning_tokens"] == 10
+ assert dumped["cache_creation_tokens"] == 5
diff --git a/lib/crewai/tests/hooks/test_decorators.py b/lib/crewai/tests/hooks/test_decorators.py
index ec147068d..a19a0f740 100644
--- a/lib/crewai/tests/hooks/test_decorators.py
+++ b/lib/crewai/tests/hooks/test_decorators.py
@@ -192,6 +192,38 @@ class TestToolHookDecorators:
# Should still be 1 (hook didn't execute for read_file)
assert len(execution_log) == 1
+ def test_before_tool_call_tool_filter_sanitizes_names(self):
+ """Tool filter should auto-sanitize names so users can pass BaseTool.name directly."""
+ execution_log = []
+
+ # User passes the human-readable tool name (e.g. BaseTool.name)
+ @before_tool_call(tools=["Delete File", "Execute Code"])
+ def filtered_hook(context):
+ execution_log.append(context.tool_name)
+ return None
+
+ hooks = get_before_tool_call_hooks()
+ assert len(hooks) == 1
+
+ mock_tool = Mock()
+ # Context uses the sanitized name (as set by the executor)
+ context = ToolCallHookContext(
+ tool_name="delete_file",
+ tool_input={},
+ tool=mock_tool,
+ )
+ hooks[0](context)
+ assert execution_log == ["delete_file"]
+
+ # Non-matching tool still filtered out
+ context2 = ToolCallHookContext(
+ tool_name="read_file",
+ tool_input={},
+ tool=mock_tool,
+ )
+ hooks[0](context2)
+ assert execution_log == ["delete_file"]
+
def test_before_tool_call_with_combined_filters(self):
"""Test that combined tool and agent filters work."""
execution_log = []
diff --git a/lib/crewai/tests/llms/anthropic/test_anthropic.py b/lib/crewai/tests/llms/anthropic/test_anthropic.py
index e8f16af5a..81a51c8d6 100644
--- a/lib/crewai/tests/llms/anthropic/test_anthropic.py
+++ b/lib/crewai/tests/llms/anthropic/test_anthropic.py
@@ -1463,3 +1463,45 @@ def test_tool_search_saves_input_tokens():
f"Expected tool_search ({usage_search.prompt_tokens}) to use fewer input tokens "
f"than no search ({usage_no_search.prompt_tokens})"
)
+
+
+def test_anthropic_cache_creation_tokens_extraction():
+ """Test that cache_creation_input_tokens are extracted from Anthropic responses."""
+ llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
+
+ mock_response = MagicMock()
+ mock_response.content = [MagicMock(text="test response")]
+ mock_response.usage = MagicMock(
+ input_tokens=100,
+ output_tokens=50,
+ cache_read_input_tokens=30,
+ cache_creation_input_tokens=20,
+ )
+ mock_response.stop_reason = None
+ mock_response.model = None
+
+ usage = llm._extract_anthropic_token_usage(mock_response)
+ assert usage["input_tokens"] == 100
+ assert usage["output_tokens"] == 50
+ assert usage["total_tokens"] == 150
+ assert usage["cached_prompt_tokens"] == 30
+ assert usage["cache_creation_tokens"] == 20
+
+
+def test_anthropic_missing_cache_fields_default_to_zero():
+ """Test that missing cache fields default to zero."""
+ llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
+
+ mock_response = MagicMock()
+ mock_response.content = [MagicMock(text="test response")]
+ mock_response.usage = MagicMock(
+ input_tokens=40,
+ output_tokens=20,
+ spec=["input_tokens", "output_tokens"],
+ )
+ mock_response.usage.cache_read_input_tokens = None
+ mock_response.usage.cache_creation_input_tokens = None
+
+ usage = llm._extract_anthropic_token_usage(mock_response)
+ assert usage["cached_prompt_tokens"] == 0
+ assert usage["cache_creation_tokens"] == 0
diff --git a/lib/crewai/tests/llms/anthropic/test_anthropic_async.py b/lib/crewai/tests/llms/anthropic/test_anthropic_async.py
index 3c1d01ea3..431abc6ef 100644
--- a/lib/crewai/tests/llms/anthropic/test_anthropic_async.py
+++ b/lib/crewai/tests/llms/anthropic/test_anthropic_async.py
@@ -3,13 +3,9 @@ import json
import logging
import pytest
-import tiktoken
from pydantic import BaseModel
from crewai.llm import LLM
-
-# Pre-cache tiktoken encoding so VCR doesn't intercept the download request
-tiktoken.get_encoding("cl100k_base")
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
@@ -48,9 +44,7 @@ async def test_anthropic_async_with_max_tokens():
assert result is not None
assert isinstance(result, str)
- encoder = tiktoken.get_encoding("cl100k_base")
- token_count = len(encoder.encode(result))
- assert token_count <= 10
+ assert len(result.split()) <= 10
@pytest.mark.vcr()
diff --git a/lib/crewai/tests/llms/azure/test_azure.py b/lib/crewai/tests/llms/azure/test_azure.py
index a0da30998..d42e2d7fe 100644
--- a/lib/crewai/tests/llms/azure/test_azure.py
+++ b/lib/crewai/tests/llms/azure/test_azure.py
@@ -2,6 +2,7 @@ import os
import sys
import types
from unittest.mock import patch, MagicMock, Mock
+from urllib.parse import urlparse
import pytest
from crewai.llm import LLM
@@ -378,23 +379,72 @@ def test_azure_completion_with_tools():
def test_azure_raises_error_when_endpoint_missing():
- """Test that AzureCompletion raises ValueError when endpoint is missing"""
+ """Credentials are validated lazily: construction succeeds, first
+ client build raises the descriptive error."""
from crewai.llms.providers.azure.completion import AzureCompletion
- # Clear environment variables
with patch.dict(os.environ, {}, clear=True):
+ llm = AzureCompletion(model="gpt-4", api_key="test-key")
with pytest.raises(ValueError, match="Azure endpoint is required"):
- AzureCompletion(model="gpt-4", api_key="test-key")
+ llm._get_sync_client()
def test_azure_raises_error_when_api_key_missing():
- """Test that AzureCompletion raises ValueError when API key is missing"""
+ """Credentials are validated lazily: construction succeeds, first
+ client build raises the descriptive error."""
from crewai.llms.providers.azure.completion import AzureCompletion
- # Clear environment variables
with patch.dict(os.environ, {}, clear=True):
+ llm = AzureCompletion(
+ model="gpt-4", endpoint="https://test.openai.azure.com"
+ )
with pytest.raises(ValueError, match="Azure API key is required"):
- AzureCompletion(model="gpt-4", endpoint="https://test.openai.azure.com")
+ llm._get_sync_client()
+
+
+@pytest.mark.asyncio
+async def test_azure_aclose_is_noop_when_uninitialized():
+ """`aclose` (and `async with`) on an uninstantiated-client LLM must be
+ a harmless no-op, not force lazy construction that then raises for
+ missing credentials."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ with patch.dict(os.environ, {}, clear=True):
+ llm = AzureCompletion(model="gpt-4")
+ assert llm._async_client is None
+ await llm.aclose()
+ async with llm:
+ pass
+
+
+def test_azure_lazy_build_reads_env_vars_set_after_construction():
+ """When `LLM(model="azure/...")` is constructed before env vars are set,
+ the lazy client builder must re-read `AZURE_API_KEY` / `AZURE_ENDPOINT`
+ so the LLM actually works once credentials become available, and the
+ `is_azure_openai_endpoint` routing flag must be recomputed off the
+ newly-resolved endpoint."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ with patch.dict(os.environ, {}, clear=True):
+ llm = AzureCompletion(model="gpt-4")
+ assert llm.api_key is None
+ assert llm.endpoint is None
+ assert llm.is_azure_openai_endpoint is False
+
+ with patch.dict(
+ os.environ,
+ {
+ "AZURE_API_KEY": "late-key",
+ "AZURE_ENDPOINT": "https://test.openai.azure.com/openai/deployments/gpt-4",
+ },
+ clear=True,
+ ):
+ client = llm._get_sync_client()
+ assert client is not None
+ assert llm.api_key == "late-key"
+ assert llm.endpoint is not None
+ assert urlparse(llm.endpoint).hostname == "test.openai.azure.com"
+ assert llm.is_azure_openai_endpoint is True
def test_azure_endpoint_configuration():
@@ -1403,3 +1453,44 @@ def test_azure_stop_words_still_applied_to_regular_responses():
assert "Observation:" not in result
assert "Found results" not in result
assert "I need to search for more information" in result
+
+
+def test_azure_reasoning_tokens_and_cached_tokens():
+ """Test that reasoning_tokens and cached_tokens are extracted from Azure responses."""
+ llm = LLM(model="azure/gpt-4")
+
+ mock_response = MagicMock()
+ mock_response.usage = MagicMock(
+ prompt_tokens=100,
+ completion_tokens=200,
+ total_tokens=300,
+ )
+ mock_response.usage.prompt_tokens_details = MagicMock(cached_tokens=40)
+ mock_response.usage.completion_tokens_details = MagicMock(reasoning_tokens=60)
+
+ usage = llm._extract_azure_token_usage(mock_response)
+ assert usage["prompt_tokens"] == 100
+ assert usage["completion_tokens"] == 200
+ assert usage["total_tokens"] == 300
+ assert usage["cached_prompt_tokens"] == 40
+ assert usage["reasoning_tokens"] == 60
+
+
+def test_azure_no_detail_fields():
+ """Test Azure extraction without detail fields."""
+ llm = LLM(model="azure/gpt-4")
+
+ mock_response = MagicMock()
+ mock_response.usage = MagicMock(
+ prompt_tokens=50,
+ completion_tokens=30,
+ total_tokens=80,
+ )
+ mock_response.usage.prompt_tokens_details = None
+ mock_response.usage.completion_tokens_details = None
+
+ usage = llm._extract_azure_token_usage(mock_response)
+ assert usage["prompt_tokens"] == 50
+ assert usage["completion_tokens"] == 30
+ assert usage["cached_prompt_tokens"] == 0
+ assert usage["reasoning_tokens"] == 0
diff --git a/lib/crewai/tests/llms/azure/test_azure_async.py b/lib/crewai/tests/llms/azure/test_azure_async.py
index 1bbd9cf4c..2bb1cc7f0 100644
--- a/lib/crewai/tests/llms/azure/test_azure_async.py
+++ b/lib/crewai/tests/llms/azure/test_azure_async.py
@@ -1,7 +1,6 @@
"""Tests for Azure async completion functionality."""
import pytest
-import tiktoken
from crewai import Agent, Task, Crew
from crewai.llm import LLM
@@ -57,9 +56,7 @@ async def test_azure_async_with_max_tokens():
assert result is not None
assert isinstance(result, str)
- encoder = tiktoken.get_encoding("cl100k_base")
- token_count = len(encoder.encode(result))
- assert token_count <= 10
+ assert len(result.split()) <= 10
@pytest.mark.vcr()
diff --git a/lib/crewai/tests/llms/bedrock/test_bedrock.py b/lib/crewai/tests/llms/bedrock/test_bedrock.py
index 76958bf86..959b1dbc2 100644
--- a/lib/crewai/tests/llms/bedrock/test_bedrock.py
+++ b/lib/crewai/tests/llms/bedrock/test_bedrock.py
@@ -1175,3 +1175,81 @@ def test_bedrock_tool_results_not_merged_across_assistant_messages():
)
assert tool_result_messages[0]["content"][0]["toolResult"]["toolUseId"] == "call_a"
assert tool_result_messages[1]["content"][0]["toolResult"]["toolUseId"] == "call_b"
+
+
+def test_bedrock_cached_token_tracking():
+ """Test that cached tokens (cacheReadInputTokenCount) are tracked for Bedrock."""
+ llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
+
+ with patch.object(llm._client, 'converse') as mock_converse:
+ mock_response = {
+ 'output': {
+ 'message': {
+ 'role': 'assistant',
+ 'content': [{'text': 'test response'}]
+ }
+ },
+ 'usage': {
+ 'inputTokens': 100,
+ 'outputTokens': 50,
+ 'totalTokens': 150,
+ 'cacheReadInputTokenCount': 30,
+ }
+ }
+ mock_converse.return_value = mock_response
+
+ result = llm.call("Hello")
+ assert result == "test response"
+ assert llm._token_usage['prompt_tokens'] == 100
+ assert llm._token_usage['completion_tokens'] == 50
+ assert llm._token_usage['total_tokens'] == 150
+ assert llm._token_usage['cached_prompt_tokens'] == 30
+
+
+def test_bedrock_cached_token_alternate_key():
+ """Test that the alternate key cacheReadInputTokens also works."""
+ llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
+
+ with patch.object(llm._client, 'converse') as mock_converse:
+ mock_response = {
+ 'output': {
+ 'message': {
+ 'role': 'assistant',
+ 'content': [{'text': 'test response'}]
+ }
+ },
+ 'usage': {
+ 'inputTokens': 80,
+ 'outputTokens': 40,
+ 'totalTokens': 120,
+ 'cacheReadInputTokens': 25,
+ }
+ }
+ mock_converse.return_value = mock_response
+
+ llm.call("Hello")
+ assert llm._token_usage['cached_prompt_tokens'] == 25
+
+
+def test_bedrock_no_cache_tokens_defaults_to_zero():
+ """Test that missing cache token keys default to zero."""
+ llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
+
+ with patch.object(llm._client, 'converse') as mock_converse:
+ mock_response = {
+ 'output': {
+ 'message': {
+ 'role': 'assistant',
+ 'content': [{'text': 'test response'}]
+ }
+ },
+ 'usage': {
+ 'inputTokens': 60,
+ 'outputTokens': 30,
+ 'totalTokens': 90,
+ }
+ }
+ mock_converse.return_value = mock_response
+
+ llm.call("Hello")
+ assert llm._token_usage['cached_prompt_tokens'] == 0
diff --git a/lib/crewai/tests/llms/bedrock/test_bedrock_async.py b/lib/crewai/tests/llms/bedrock/test_bedrock_async.py
index 10d6a7d3d..0bf3558b1 100644
--- a/lib/crewai/tests/llms/bedrock/test_bedrock_async.py
+++ b/lib/crewai/tests/llms/bedrock/test_bedrock_async.py
@@ -6,7 +6,6 @@ cannot be played back properly in CI.
"""
import pytest
-import tiktoken
from crewai.llm import LLM
@@ -51,9 +50,7 @@ async def test_bedrock_async_with_max_tokens():
assert result is not None
assert isinstance(result, str)
- encoder = tiktoken.get_encoding("cl100k_base")
- token_count = len(encoder.encode(result))
- assert token_count <= 10
+ assert len(result.split()) <= 10
@pytest.mark.vcr()
diff --git a/lib/crewai/tests/llms/google/test_google.py b/lib/crewai/tests/llms/google/test_google.py
index d0553c7db..f6e94f89e 100644
--- a/lib/crewai/tests/llms/google/test_google.py
+++ b/lib/crewai/tests/llms/google/test_google.py
@@ -64,6 +64,23 @@ def test_gemini_completion_module_is_imported():
assert hasattr(completion_mod, 'GeminiCompletion')
+def test_gemini_lazy_build_reads_env_vars_set_after_construction():
+ """When `LLM(model="gemini/...")` is constructed before env vars are set,
+ the lazy client builder must re-read `GOOGLE_API_KEY` / `GEMINI_API_KEY`
+ so the LLM works once credentials become available."""
+ from crewai.llms.providers.gemini.completion import GeminiCompletion
+
+ with patch.dict(os.environ, {}, clear=True):
+ llm = GeminiCompletion(model="gemini-1.5-pro")
+ assert llm.api_key is None
+ assert llm._client is None
+
+ with patch.dict(os.environ, {"GEMINI_API_KEY": "late-key"}, clear=True):
+ client = llm._get_sync_client()
+ assert client is not None
+ assert llm.api_key == "late-key"
+
+
def test_native_gemini_raises_error_when_initialization_fails():
"""
Test that LLM raises ImportError when native Gemini completion fails.
@@ -1190,3 +1207,42 @@ def test_gemini_cached_prompt_tokens_with_tools():
# cached_prompt_tokens should be populated (may be 0 if Gemini
# doesn't cache for this particular request, but the field should exist)
assert usage.cached_prompt_tokens >= 0
+
+
+def test_gemini_reasoning_tokens_extraction():
+ """Test that thoughts_token_count is extracted as reasoning_tokens from Gemini."""
+ llm = LLM(model="google/gemini-2.0-flash-001")
+
+ mock_response = MagicMock()
+ mock_response.usage_metadata = MagicMock(
+ prompt_token_count=100,
+ candidates_token_count=50,
+ total_token_count=150,
+ cached_content_token_count=10,
+ thoughts_token_count=30,
+ )
+ usage = llm._extract_token_usage(mock_response)
+ assert usage["prompt_token_count"] == 100
+ assert usage["candidates_token_count"] == 50
+ assert usage["total_tokens"] == 150
+ assert usage["cached_prompt_tokens"] == 10
+ assert usage["reasoning_tokens"] == 30
+
+
+def test_gemini_no_thinking_tokens_defaults_to_zero():
+ """Test that missing thoughts_token_count defaults to zero."""
+ llm = LLM(model="google/gemini-2.0-flash-001")
+
+ mock_response = MagicMock()
+ mock_response.usage_metadata = MagicMock(
+ prompt_token_count=80,
+ candidates_token_count=40,
+ total_token_count=120,
+ cached_content_token_count=0,
+ thoughts_token_count=None,
+ )
+ mock_response.candidates = []
+
+ usage = llm._extract_token_usage(mock_response)
+ assert usage["reasoning_tokens"] == 0
+ assert usage["cached_prompt_tokens"] == 0
diff --git a/lib/crewai/tests/llms/google/test_google_async.py b/lib/crewai/tests/llms/google/test_google_async.py
index 1385ba74e..d524f620a 100644
--- a/lib/crewai/tests/llms/google/test_google_async.py
+++ b/lib/crewai/tests/llms/google/test_google_async.py
@@ -1,7 +1,6 @@
"""Tests for Google (Gemini) async completion functionality."""
import pytest
-import tiktoken
from crewai import Agent, Task, Crew
from crewai.llm import LLM
@@ -43,9 +42,7 @@ async def test_gemini_async_with_max_tokens():
assert result is not None
assert isinstance(result, str)
- encoder = tiktoken.get_encoding("cl100k_base")
- token_count = len(encoder.encode(result))
- assert token_count <= 1000
+ assert len(result.split()) <= 1000
@pytest.mark.vcr()
diff --git a/lib/crewai/tests/llms/litellm/test_litellm_async.py b/lib/crewai/tests/llms/litellm/test_litellm_async.py
index e8d61a6a5..41707f868 100644
--- a/lib/crewai/tests/llms/litellm/test_litellm_async.py
+++ b/lib/crewai/tests/llms/litellm/test_litellm_async.py
@@ -1,7 +1,6 @@
"""Tests for LiteLLM fallback async completion functionality."""
import pytest
-import tiktoken
from crewai.llm import LLM
@@ -44,9 +43,7 @@ async def test_litellm_async_with_max_tokens():
assert result is not None
assert isinstance(result, str)
- encoder = tiktoken.get_encoding("cl100k_base")
- token_count = len(encoder.encode(result))
- assert token_count <= 10
+ assert len(result.split()) <= 10
@pytest.mark.asyncio
diff --git a/lib/crewai/tests/llms/openai/test_openai.py b/lib/crewai/tests/llms/openai/test_openai.py
index 3dada2d85..5a2a6a299 100644
--- a/lib/crewai/tests/llms/openai/test_openai.py
+++ b/lib/crewai/tests/llms/openai/test_openai.py
@@ -1929,6 +1929,47 @@ def test_openai_streaming_returns_tool_calls_without_available_functions():
assert result[0]["type"] == "function"
+def test_openai_responses_api_reasoning_tokens_extraction():
+ """Test that reasoning_tokens are extracted from Responses API responses."""
+ llm = LLM(model="openai/gpt-4o")
+
+ mock_response = MagicMock()
+ mock_response.usage = MagicMock(
+ input_tokens=100,
+ output_tokens=200,
+ total_tokens=300,
+ )
+ mock_response.usage.input_tokens_details = MagicMock(cached_tokens=25)
+ mock_response.usage.output_tokens_details = MagicMock(reasoning_tokens=80)
+
+ usage = llm._extract_responses_token_usage(mock_response)
+ assert usage["prompt_tokens"] == 100
+ assert usage["completion_tokens"] == 200
+ assert usage["total_tokens"] == 300
+ assert usage["cached_prompt_tokens"] == 25
+ assert usage["reasoning_tokens"] == 80
+
+
+def test_openai_responses_api_no_detail_fields_omitted():
+ """Test that reasoning/cached fields are omitted when Responses API details are absent."""
+ llm = LLM(model="openai/gpt-4o")
+
+ mock_response = MagicMock()
+ mock_response.usage = MagicMock(
+ input_tokens=50,
+ output_tokens=30,
+ total_tokens=80,
+ )
+ mock_response.usage.input_tokens_details = None
+ mock_response.usage.output_tokens_details = None
+
+ usage = llm._extract_responses_token_usage(mock_response)
+ assert usage["prompt_tokens"] == 50
+ assert usage["completion_tokens"] == 30
+ assert "cached_prompt_tokens" not in usage
+ assert "reasoning_tokens" not in usage
+
+
@pytest.mark.asyncio
async def test_openai_async_streaming_returns_tool_calls_without_available_functions():
"""Test that async streaming returns tool calls list when available_functions is None.
@@ -2018,3 +2059,44 @@ async def test_openai_async_streaming_returns_tool_calls_without_available_funct
assert result[0]["function"]["arguments"] == '{"expression": "1+1"}'
assert result[0]["id"] == "call_abc123"
assert result[0]["type"] == "function"
+
+
+def test_openai_reasoning_tokens_extraction():
+ """Test that reasoning_tokens are extracted from OpenAI o-series responses."""
+ llm = LLM(model="openai/gpt-4o")
+
+ mock_response = MagicMock()
+ mock_response.usage = MagicMock(
+ prompt_tokens=100,
+ completion_tokens=200,
+ total_tokens=300,
+ )
+ mock_response.usage.prompt_tokens_details = MagicMock(cached_tokens=25)
+ mock_response.usage.completion_tokens_details = MagicMock(reasoning_tokens=80)
+
+ usage = llm._extract_openai_token_usage(mock_response)
+ assert usage["prompt_tokens"] == 100
+ assert usage["completion_tokens"] == 200
+ assert usage["total_tokens"] == 300
+ assert usage["cached_prompt_tokens"] == 25
+ assert usage["reasoning_tokens"] == 80
+
+
+def test_openai_no_detail_fields_omitted():
+ """Test that reasoning/cached fields are omitted when details are absent."""
+ llm = LLM(model="openai/gpt-4o")
+
+ mock_response = MagicMock()
+ mock_response.usage = MagicMock(
+ prompt_tokens=50,
+ completion_tokens=30,
+ total_tokens=80,
+ )
+ mock_response.usage.prompt_tokens_details = None
+ mock_response.usage.completion_tokens_details = None
+
+ usage = llm._extract_openai_token_usage(mock_response)
+ assert usage["prompt_tokens"] == 50
+ assert usage["completion_tokens"] == 30
+ assert "cached_prompt_tokens" not in usage
+ assert "reasoning_tokens" not in usage
diff --git a/lib/crewai/tests/llms/openai/test_openai_async.py b/lib/crewai/tests/llms/openai/test_openai_async.py
index e6bbf11d9..e5dae7ca7 100644
--- a/lib/crewai/tests/llms/openai/test_openai_async.py
+++ b/lib/crewai/tests/llms/openai/test_openai_async.py
@@ -1,7 +1,6 @@
"""Tests for OpenAI async completion functionality."""
import pytest
-import tiktoken
from crewai import Agent, Task, Crew
from crewai.llm import LLM
@@ -42,9 +41,7 @@ async def test_openai_async_with_max_tokens():
assert result is not None
assert isinstance(result, str)
- encoder = tiktoken.get_encoding("cl100k_base")
- token_count = len(encoder.encode(result))
- assert token_count <= 10
+ assert len(result.split()) <= 10
@pytest.mark.vcr()
diff --git a/lib/crewai/tests/memory/test_unified_memory.py b/lib/crewai/tests/memory/test_unified_memory.py
index eec83f77d..cb93954d3 100644
--- a/lib/crewai/tests/memory/test_unified_memory.py
+++ b/lib/crewai/tests/memory/test_unified_memory.py
@@ -51,14 +51,13 @@ def test_memory_record_embedding_excluded_from_serialization() -> None:
dumped = r.model_dump()
assert "embedding" not in dumped
assert dumped["content"] == "hello"
-
- # model_dump_json excludes embedding
json_str = r.model_dump_json()
- assert "0.1" not in json_str
assert "embedding" not in json_str
+ rehydrated = MemoryRecord.model_validate_json(json_str)
+ assert rehydrated.embedding is None
# repr excludes embedding
- assert "0.1" not in repr(r)
+ assert "embedding=" not in repr(r)
# Direct attribute access still works for storage layer
assert r.embedding is not None
diff --git a/lib/crewai/tests/test_checkpoint.py b/lib/crewai/tests/test_checkpoint.py
index 29dc289b4..b1ad9e2df 100644
--- a/lib/crewai/tests/test_checkpoint.py
+++ b/lib/crewai/tests/test_checkpoint.py
@@ -1,8 +1,10 @@
-"""Tests for CheckpointConfig, checkpoint listener, and pruning."""
+"""Tests for CheckpointConfig, checkpoint listener, pruning, and forking."""
from __future__ import annotations
+import json
import os
+import sqlite3
import tempfile
import time
from typing import Any
@@ -21,6 +23,8 @@ from crewai.state.checkpoint_listener import (
_SENTINEL,
)
from crewai.state.provider.json_provider import JsonProvider
+from crewai.state.provider.sqlite_provider import SqliteProvider
+from crewai.state.runtime import RuntimeState
from crewai.task import Task
@@ -116,35 +120,41 @@ class TestFindCheckpoint:
class TestPrune:
def test_prune_keeps_newest(self) -> None:
with tempfile.TemporaryDirectory() as d:
+ branch_dir = os.path.join(d, "main")
+ os.makedirs(branch_dir)
for i in range(5):
- path = os.path.join(d, f"cp_{i}.json")
+ path = os.path.join(branch_dir, f"cp_{i}.json")
with open(path, "w") as f:
f.write("{}")
# Ensure distinct mtime
time.sleep(0.01)
- JsonProvider().prune(d, max_keep=2)
- remaining = os.listdir(d)
+ JsonProvider().prune(d, max_keep=2, branch="main")
+ remaining = os.listdir(branch_dir)
assert len(remaining) == 2
assert "cp_3.json" in remaining
assert "cp_4.json" in remaining
def test_prune_zero_removes_all(self) -> None:
with tempfile.TemporaryDirectory() as d:
+ branch_dir = os.path.join(d, "main")
+ os.makedirs(branch_dir)
for i in range(3):
- with open(os.path.join(d, f"cp_{i}.json"), "w") as f:
+ with open(os.path.join(branch_dir, f"cp_{i}.json"), "w") as f:
f.write("{}")
- JsonProvider().prune(d, max_keep=0)
- assert os.listdir(d) == []
+ JsonProvider().prune(d, max_keep=0, branch="main")
+ assert os.listdir(branch_dir) == []
def test_prune_more_than_existing(self) -> None:
with tempfile.TemporaryDirectory() as d:
- with open(os.path.join(d, "cp.json"), "w") as f:
+ branch_dir = os.path.join(d, "main")
+ os.makedirs(branch_dir)
+ with open(os.path.join(branch_dir, "cp.json"), "w") as f:
f.write("{}")
- JsonProvider().prune(d, max_keep=10)
- assert len(os.listdir(d)) == 1
+ JsonProvider().prune(d, max_keep=10, branch="main")
+ assert len(os.listdir(branch_dir)) == 1
# ---------- CheckpointConfig ----------
@@ -162,8 +172,393 @@ class TestCheckpointConfig:
cfg = CheckpointConfig(on_events=["*"])
assert cfg.trigger_all
+ def test_restore_from_field(self) -> None:
+ cfg = CheckpointConfig(restore_from="/path/to/checkpoint.json")
+ assert cfg.restore_from == "/path/to/checkpoint.json"
+
+ def test_restore_from_default_none(self) -> None:
+ cfg = CheckpointConfig()
+ assert cfg.restore_from is None
+
def test_trigger_events(self) -> None:
cfg = CheckpointConfig(
on_events=["task_completed", "crew_kickoff_completed"]
)
assert cfg.trigger_events == {"task_completed", "crew_kickoff_completed"}
+
+
+# ---------- RuntimeState lineage ----------
+
+
+class TestRuntimeStateLineage:
+ def _make_state(self) -> RuntimeState:
+ from crewai import Agent, Crew
+
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ crew = Crew(agents=[agent], tasks=[], verbose=False)
+ return RuntimeState(root=[crew])
+
+ def test_default_lineage_fields(self) -> None:
+ state = self._make_state()
+ assert state._checkpoint_id is None
+ assert state._parent_id is None
+ assert state._branch == "main"
+
+ def test_serialize_includes_version(self) -> None:
+ from crewai.utilities.version import get_crewai_version
+
+ state = self._make_state()
+ dumped = json.loads(state.model_dump_json())
+ assert dumped["crewai_version"] == get_crewai_version()
+
+ def test_deserialize_migrates_on_version_mismatch(self, caplog: Any) -> None:
+ import logging
+
+ state = self._make_state()
+ raw = state.model_dump_json()
+ data = json.loads(raw)
+ data["crewai_version"] = "0.1.0"
+ with caplog.at_level(logging.DEBUG):
+ RuntimeState.model_validate_json(
+ json.dumps(data), context={"from_checkpoint": True}
+ )
+ assert "Migrating checkpoint from crewAI 0.1.0" in caplog.text
+
+ def test_deserialize_warns_on_missing_version(self, caplog: Any) -> None:
+ import logging
+
+ state = self._make_state()
+ raw = state.model_dump_json()
+ data = json.loads(raw)
+ data.pop("crewai_version", None)
+ with caplog.at_level(logging.WARNING):
+ RuntimeState.model_validate_json(
+ json.dumps(data), context={"from_checkpoint": True}
+ )
+ assert "treating as 0.0.0" in caplog.text
+
+ def test_serialize_includes_lineage(self) -> None:
+ state = self._make_state()
+ state._parent_id = "parent456"
+ state._branch = "experiment"
+ dumped = json.loads(state.model_dump_json())
+ assert dumped["parent_id"] == "parent456"
+ assert dumped["branch"] == "experiment"
+ assert "checkpoint_id" not in dumped
+
+ def test_deserialize_restores_lineage(self) -> None:
+ state = self._make_state()
+ state._parent_id = "parent456"
+ state._branch = "experiment"
+ raw = state.model_dump_json()
+ restored = RuntimeState.model_validate_json(
+ raw, context={"from_checkpoint": True}
+ )
+ assert restored._parent_id == "parent456"
+ assert restored._branch == "experiment"
+
+ def test_deserialize_defaults_missing_lineage(self) -> None:
+ state = self._make_state()
+ raw = state.model_dump_json()
+ data = json.loads(raw)
+ data.pop("parent_id", None)
+ data.pop("branch", None)
+ restored = RuntimeState.model_validate_json(
+ json.dumps(data), context={"from_checkpoint": True}
+ )
+ assert restored._parent_id is None
+ assert restored._branch == "main"
+
+ def test_from_checkpoint_sets_checkpoint_id(self) -> None:
+ """from_checkpoint sets _checkpoint_id from the location, not the blob."""
+ state = self._make_state()
+ state._provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ loc = state.checkpoint(d)
+ written_id = state._checkpoint_id
+
+ cfg = CheckpointConfig(restore_from=loc)
+ restored = RuntimeState.from_checkpoint(
+ cfg, context={"from_checkpoint": True}
+ )
+ assert restored._checkpoint_id == written_id
+ assert restored._parent_id == written_id
+
+ def test_fork_sets_branch(self) -> None:
+ state = self._make_state()
+ state._checkpoint_id = "abc12345"
+ state._parent_id = "abc12345"
+ state.fork("my-experiment")
+ assert state._branch == "my-experiment"
+ assert state._parent_id == "abc12345"
+
+ def test_fork_auto_branch(self) -> None:
+ state = self._make_state()
+ state._checkpoint_id = "20260409T120000_abc12345"
+ state.fork()
+ assert state._branch.startswith("fork/20260409T120000_abc12345_")
+ assert len(state._branch) == len("fork/20260409T120000_abc12345_") + 6
+
+ def test_fork_no_checkpoint_id_unique(self) -> None:
+ state = self._make_state()
+ state.fork()
+ assert state._branch.startswith("fork/")
+ assert len(state._branch) == len("fork/") + 8
+ # Two forks without checkpoint_id produce different branches
+ first = state._branch
+ state.fork()
+ assert state._branch != first
+
+
+# ---------- JsonProvider forking ----------
+
+
+class TestJsonProviderFork:
+ def test_checkpoint_writes_to_branch_subdir(self) -> None:
+ provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ path = provider.checkpoint("{}", d, branch="main")
+ assert "/main/" in path
+ assert path.endswith(".json")
+ assert os.path.isfile(path)
+
+ def test_checkpoint_fork_branch_subdir(self) -> None:
+ provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ path = provider.checkpoint("{}", d, branch="fork/exp1")
+ assert "/fork/exp1/" in path
+ assert os.path.isfile(path)
+
+ def test_prune_branch_aware(self) -> None:
+ provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ # Write 3 checkpoints on main, 2 on fork
+ for _ in range(3):
+ provider.checkpoint("{}", d, branch="main")
+ time.sleep(0.01)
+ for _ in range(2):
+ provider.checkpoint("{}", d, branch="fork/a")
+ time.sleep(0.01)
+
+ # Prune main to 1
+ provider.prune(d, max_keep=1, branch="main")
+
+ main_dir = os.path.join(d, "main")
+ fork_dir = os.path.join(d, "fork", "a")
+ assert len(os.listdir(main_dir)) == 1
+ assert len(os.listdir(fork_dir)) == 2 # untouched
+
+ def test_extract_id(self) -> None:
+ provider = JsonProvider()
+ assert provider.extract_id("/dir/main/20260409T120000_abc12345_p-none.json") == "20260409T120000_abc12345"
+ assert provider.extract_id("/dir/main/20260409T120000_abc12345_p-20260409T115900_def67890.json") == "20260409T120000_abc12345"
+
+ def test_branch_traversal_rejected(self) -> None:
+ provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ with pytest.raises(ValueError, match="escapes checkpoint directory"):
+ provider.checkpoint("{}", d, branch="../../etc")
+ with pytest.raises(ValueError, match="escapes checkpoint directory"):
+ provider.prune(d, max_keep=1, branch="../../etc")
+
+ def test_filename_encodes_parent_id(self) -> None:
+ provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ # First checkpoint — no parent
+ path1 = provider.checkpoint("{}", d, branch="main")
+ assert "_p-none.json" in path1
+
+ # Second checkpoint — with parent
+ id1 = provider.extract_id(path1)
+ path2 = provider.checkpoint("{}", d, parent_id=id1, branch="main")
+ assert f"_p-{id1}.json" in path2
+
+ def test_checkpoint_chaining(self) -> None:
+ """RuntimeState.checkpoint() chains parent_id after each write."""
+ state = self._make_state()
+ state._provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ state.checkpoint(d)
+ id1 = state._checkpoint_id
+ assert id1 is not None
+ assert state._parent_id == id1
+
+ loc2 = state.checkpoint(d)
+ id2 = state._checkpoint_id
+ assert id2 is not None
+ assert id2 != id1
+ assert state._parent_id == id2
+
+ # Verify the second checkpoint blob has parent_id == id1
+ with open(loc2) as f:
+ data2 = json.loads(f.read())
+ assert data2["parent_id"] == id1
+
+ @pytest.mark.asyncio
+ async def test_acheckpoint_chaining(self) -> None:
+ """Async checkpoint path chains lineage identically to sync."""
+ state = self._make_state()
+ state._provider = JsonProvider()
+ with tempfile.TemporaryDirectory() as d:
+ await state.acheckpoint(d)
+ id1 = state._checkpoint_id
+ assert id1 is not None
+
+ loc2 = await state.acheckpoint(d)
+ id2 = state._checkpoint_id
+ assert id2 != id1
+ assert state._parent_id == id2
+
+ with open(loc2) as f:
+ data2 = json.loads(f.read())
+ assert data2["parent_id"] == id1
+
+ def _make_state(self) -> RuntimeState:
+ from crewai import Agent, Crew
+
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ crew = Crew(agents=[agent], tasks=[], verbose=False)
+ return RuntimeState(root=[crew])
+
+
+# ---------- SqliteProvider forking ----------
+
+
+class TestSqliteProviderFork:
+ def test_checkpoint_stores_branch_and_parent(self) -> None:
+ provider = SqliteProvider()
+ with tempfile.TemporaryDirectory() as d:
+ db = os.path.join(d, "cp.db")
+ loc = provider.checkpoint("{}", db, parent_id="p1", branch="exp")
+ cid = provider.extract_id(loc)
+
+ with sqlite3.connect(db) as conn:
+ row = conn.execute(
+ "SELECT parent_id, branch FROM checkpoints WHERE id = ?",
+ (cid,),
+ ).fetchone()
+ assert row == ("p1", "exp")
+
+ def test_prune_branch_aware(self) -> None:
+ provider = SqliteProvider()
+ with tempfile.TemporaryDirectory() as d:
+ db = os.path.join(d, "cp.db")
+ for _ in range(3):
+ provider.checkpoint("{}", db, branch="main")
+ for _ in range(2):
+ provider.checkpoint("{}", db, branch="fork/a")
+
+ provider.prune(db, max_keep=1, branch="main")
+
+ with sqlite3.connect(db) as conn:
+ main_count = conn.execute(
+ "SELECT COUNT(*) FROM checkpoints WHERE branch = 'main'"
+ ).fetchone()[0]
+ fork_count = conn.execute(
+ "SELECT COUNT(*) FROM checkpoints WHERE branch = 'fork/a'"
+ ).fetchone()[0]
+ assert main_count == 1
+ assert fork_count == 2
+
+ def test_extract_id(self) -> None:
+ provider = SqliteProvider()
+ assert provider.extract_id("/path/to/db#abc123") == "abc123"
+
+ def test_checkpoint_chaining_sqlite(self) -> None:
+ state = self._make_state()
+ state._provider = SqliteProvider()
+ with tempfile.TemporaryDirectory() as d:
+ db = os.path.join(d, "cp.db")
+ state.checkpoint(db)
+ id1 = state._checkpoint_id
+
+ state.checkpoint(db)
+ id2 = state._checkpoint_id
+ assert id2 != id1
+
+ # Second row should have parent_id == id1
+ with sqlite3.connect(db) as conn:
+ row = conn.execute(
+ "SELECT parent_id FROM checkpoints WHERE id = ?", (id2,)
+ ).fetchone()
+ assert row[0] == id1
+
+ def _make_state(self) -> RuntimeState:
+ from crewai import Agent, Crew
+
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ crew = Crew(agents=[agent], tasks=[], verbose=False)
+ return RuntimeState(root=[crew])
+
+
+# ---------- Kickoff from_checkpoint parameter ----------
+
+
+class TestKickoffFromCheckpoint:
+ def test_crew_kickoff_delegates_to_from_checkpoint(self) -> None:
+ mock_restored = MagicMock(spec=Crew)
+ mock_restored.kickoff.return_value = "result"
+
+ cfg = CheckpointConfig(restore_from="/path/to/cp.json")
+ with patch.object(Crew, "from_checkpoint", return_value=mock_restored):
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ crew = Crew(agents=[agent], tasks=[], verbose=False)
+ result = crew.kickoff(inputs={"k": "v"}, from_checkpoint=cfg)
+
+ mock_restored.kickoff.assert_called_once_with(
+ inputs={"k": "v"}, input_files=None
+ )
+ assert mock_restored.checkpoint.restore_from is None
+ assert result == "result"
+
+ def test_crew_kickoff_config_only_sets_checkpoint(self) -> None:
+ cfg = CheckpointConfig(on_events=["task_completed"])
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ crew = Crew(agents=[agent], tasks=[], verbose=False)
+ assert crew.checkpoint is None
+ with patch("crewai.crew.get_env_context"), \
+ patch("crewai.crew.prepare_kickoff", side_effect=RuntimeError("stop")):
+ with pytest.raises(RuntimeError, match="stop"):
+ crew.kickoff(from_checkpoint=cfg)
+ assert isinstance(crew.checkpoint, CheckpointConfig)
+ assert crew.checkpoint.on_events == ["task_completed"]
+
+ def test_agent_kickoff_delegates_to_from_checkpoint(self) -> None:
+ mock_restored = MagicMock(spec=Agent)
+ mock_restored.kickoff.return_value = "agent_result"
+
+ cfg = CheckpointConfig(restore_from="/path/to/agent_cp.json")
+ with patch.object(Agent, "from_checkpoint", return_value=mock_restored):
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ result = agent.kickoff(messages="hello", from_checkpoint=cfg)
+
+ mock_restored.kickoff.assert_called_once_with(
+ messages="hello", response_format=None, input_files=None
+ )
+ assert mock_restored.checkpoint.restore_from is None
+ assert result == "agent_result"
+
+ def test_agent_kickoff_config_only_sets_checkpoint(self) -> None:
+ cfg = CheckpointConfig(on_events=["lite_agent_execution_completed"])
+ agent = Agent(role="r", goal="g", backstory="b", llm="gpt-4o-mini")
+ assert agent.checkpoint is None
+ with patch.object(Agent, "_prepare_kickoff", side_effect=RuntimeError("stop")):
+ with pytest.raises(RuntimeError, match="stop"):
+ agent.kickoff(messages="hello", from_checkpoint=cfg)
+ assert isinstance(agent.checkpoint, CheckpointConfig)
+ assert agent.checkpoint.on_events == ["lite_agent_execution_completed"]
+
+ def test_flow_kickoff_delegates_to_from_checkpoint(self) -> None:
+ mock_restored = MagicMock(spec=Flow)
+ mock_restored.kickoff.return_value = "flow_result"
+
+ cfg = CheckpointConfig(restore_from="/path/to/flow_cp.json")
+ with patch.object(Flow, "from_checkpoint", return_value=mock_restored):
+ flow = Flow()
+ result = flow.kickoff(from_checkpoint=cfg)
+
+ mock_restored.kickoff.assert_called_once_with(
+ inputs=None, input_files=None
+ )
+ assert mock_restored.checkpoint.restore_from is None
+ assert result == "flow_result"
diff --git a/lib/crewai/tests/test_checkpoint_cli.py b/lib/crewai/tests/test_checkpoint_cli.py
new file mode 100644
index 000000000..38e105cce
--- /dev/null
+++ b/lib/crewai/tests/test_checkpoint_cli.py
@@ -0,0 +1,402 @@
+"""Tests for checkpoint CLI commands."""
+
+from __future__ import annotations
+
+import json
+import os
+import sqlite3
+import tempfile
+import time
+from datetime import datetime, timedelta, timezone
+from typing import Any
+from unittest.mock import MagicMock, patch
+
+import pytest
+from crewai.cli.checkpoint_cli import (
+ _parse_checkpoint_json,
+ _parse_duration,
+ _prune_json,
+ _prune_sqlite,
+ _resolve_checkpoint,
+ _task_list_from_meta,
+ diff_checkpoints,
+ prune_checkpoints,
+ resume_checkpoint,
+)
+
+
+def _make_checkpoint_data(
+ tasks_completed: int = 2,
+ tasks_total: int = 4,
+ trigger: str = "task_completed",
+ branch: str = "main",
+ parent_id: str | None = None,
+ entity_type: str = "crew",
+ name: str = "test_crew",
+ inputs: dict[str, Any] | None = None,
+) -> str:
+ tasks: list[dict[str, Any]] = []
+ for i in range(tasks_total):
+ t: dict[str, Any] = {
+ "description": f"Task {i + 1} description",
+ "expected_output": f"Output {i + 1}",
+ }
+ if i < tasks_completed:
+ t["output"] = {"raw": f"Result of task {i + 1}"}
+ else:
+ t["output"] = None
+ tasks.append(t)
+
+ data: dict[str, Any] = {
+ "entities": [
+ {
+ "entity_type": entity_type,
+ "name": name,
+ "id": "abc12345-1234-1234-1234-abcdef012345",
+ "tasks": tasks,
+ "agents": [],
+ "checkpoint_inputs": inputs or {},
+ }
+ ],
+ "event_record": {"nodes": {f"node_{i}": {} for i in range(3)}},
+ "trigger": trigger,
+ "branch": branch,
+ "parent_id": parent_id,
+ }
+ return json.dumps(data)
+
+
+def _write_json_checkpoint(
+ base_dir: str,
+ branch: str = "main",
+ name: str | None = None,
+ data: str | None = None,
+ tasks_completed: int = 2,
+ inputs: dict[str, Any] | None = None,
+) -> str:
+ branch_dir = os.path.join(base_dir, branch)
+ os.makedirs(branch_dir, exist_ok=True)
+ if name is None:
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
+ name = f"{ts}_abcd1234_p-none.json"
+ path = os.path.join(branch_dir, name)
+ if data is None:
+ data = _make_checkpoint_data(tasks_completed=tasks_completed, inputs=inputs)
+ with open(path, "w") as f:
+ f.write(data)
+ return path
+
+
+def _create_sqlite_checkpoint(
+ db_path: str,
+ checkpoint_id: str | None = None,
+ data: str | None = None,
+ tasks_completed: int = 2,
+ branch: str = "main",
+ inputs: dict[str, Any] | None = None,
+) -> str:
+ if checkpoint_id is None:
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
+ checkpoint_id = f"{ts}_abcd1234"
+ if data is None:
+ data = _make_checkpoint_data(
+ tasks_completed=tasks_completed, branch=branch, inputs=inputs
+ )
+ with sqlite3.connect(db_path) as conn:
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS checkpoints (
+ id TEXT PRIMARY KEY,
+ created_at TEXT NOT NULL,
+ parent_id TEXT,
+ branch TEXT NOT NULL DEFAULT 'main',
+ data JSONB NOT NULL
+ )"""
+ )
+ conn.execute(
+ "INSERT INTO checkpoints (id, created_at, parent_id, branch, data) "
+ "VALUES (?, ?, ?, ?, jsonb(?))",
+ (checkpoint_id, checkpoint_id.split("_")[0], None, branch, data),
+ )
+ conn.commit()
+ return checkpoint_id
+
+
+class TestParseDuration:
+ def test_days(self) -> None:
+ assert _parse_duration("7d") == timedelta(days=7)
+
+ def test_hours(self) -> None:
+ assert _parse_duration("24h") == timedelta(hours=24)
+
+ def test_minutes(self) -> None:
+ assert _parse_duration("30m") == timedelta(minutes=30)
+
+ def test_invalid_raises(self) -> None:
+ with pytest.raises(Exception):
+ _parse_duration("abc")
+
+ def test_no_unit_raises(self) -> None:
+ with pytest.raises(Exception):
+ _parse_duration("7")
+
+
+class TestResolveCheckpoint:
+ def test_json_latest(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(d, name="20260101T000000_aaaa1111_p-none.json")
+ time.sleep(0.01)
+ path2 = _write_json_checkpoint(
+ d, name="20260102T000000_bbbb2222_p-none.json", tasks_completed=3
+ )
+ meta = _resolve_checkpoint(d, None)
+ assert meta is not None
+ assert meta["path"] == path2
+
+ def test_json_by_id(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(d, name="20260101T000000_aaaa1111_p-none.json")
+ _write_json_checkpoint(d, name="20260102T000000_bbbb2222_p-none.json")
+ meta = _resolve_checkpoint(d, "aaaa1111")
+ assert meta is not None
+ assert "aaaa1111" in meta["name"]
+
+ def test_json_not_found(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(d)
+ assert _resolve_checkpoint(d, "nonexistent") is None
+
+ def test_sqlite_latest(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ db_path = os.path.join(d, "test.db")
+ _create_sqlite_checkpoint(db_path, "20260101T000000_aaaa1111")
+ _create_sqlite_checkpoint(
+ db_path, "20260102T000000_bbbb2222", tasks_completed=3
+ )
+ meta = _resolve_checkpoint(db_path, None)
+ assert meta is not None
+ assert "bbbb2222" in meta["name"]
+
+ def test_sqlite_by_id(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ db_path = os.path.join(d, "test.db")
+ _create_sqlite_checkpoint(db_path, "20260101T000000_aaaa1111")
+ _create_sqlite_checkpoint(db_path, "20260102T000000_bbbb2222")
+ meta = _resolve_checkpoint(db_path, "20260101T000000_aaaa1111")
+ assert meta is not None
+ assert "aaaa1111" in meta["name"]
+
+ def test_sqlite_partial_id(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ db_path = os.path.join(d, "test.db")
+ _create_sqlite_checkpoint(db_path, "20260101T000000_aaaa1111")
+ _create_sqlite_checkpoint(db_path, "20260102T000000_bbbb2222")
+ meta = _resolve_checkpoint(db_path, "aaaa1111")
+ assert meta is not None
+ assert "aaaa1111" in meta["name"]
+
+ def test_nonexistent(self) -> None:
+ assert _resolve_checkpoint("/nonexistent/path", None) is None
+
+
+class TestTaskListFromMeta:
+ def test_flattens_tasks(self) -> None:
+ data = _make_checkpoint_data(tasks_completed=2, tasks_total=3)
+ meta = _parse_checkpoint_json(data, "test")
+ tasks = _task_list_from_meta(meta)
+ assert len(tasks) == 3
+ assert tasks[0]["completed"] is True
+ assert tasks[2]["completed"] is False
+
+ def test_empty_entities(self) -> None:
+ assert _task_list_from_meta({"entities": []}) == []
+
+
+class TestDiffCheckpoints:
+ def test_diff_shows_status_change(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(
+ d, name="20260101T000000_aaaa1111_p-none.json", tasks_completed=1
+ )
+ _write_json_checkpoint(
+ d, name="20260102T000000_bbbb2222_p-none.json", tasks_completed=3
+ )
+ diff_checkpoints(d, "aaaa1111", "bbbb2222")
+ out = capsys.readouterr().out
+ assert "---" in out
+ assert "+++" in out
+ assert "status:" in out or "pending -> done" in out
+
+ def test_diff_shows_output_change(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ data1 = _make_checkpoint_data(tasks_completed=2)
+ data2 = json.loads(data1)
+ data2["entities"][0]["tasks"][0]["output"]["raw"] = "Updated result"
+ _write_json_checkpoint(
+ d,
+ name="20260101T000000_aaaa1111_p-none.json",
+ data=json.dumps(json.loads(data1)),
+ )
+ _write_json_checkpoint(
+ d,
+ name="20260102T000000_bbbb2222_p-none.json",
+ data=json.dumps(data2),
+ )
+ diff_checkpoints(d, "aaaa1111", "bbbb2222")
+ out = capsys.readouterr().out
+ assert "output:" in out
+
+ def test_diff_not_found(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(d, name="20260101T000000_aaaa1111_p-none.json")
+ diff_checkpoints(d, "aaaa1111", "nonexistent")
+ out = capsys.readouterr().out
+ assert "not found" in out
+
+ def test_diff_input_change(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(
+ d,
+ name="20260101T000000_aaaa1111_p-none.json",
+ inputs={"topic": "AI"},
+ )
+ _write_json_checkpoint(
+ d,
+ name="20260102T000000_bbbb2222_p-none.json",
+ inputs={"topic": "ML"},
+ )
+ diff_checkpoints(d, "aaaa1111", "bbbb2222")
+ out = capsys.readouterr().out
+ assert "Inputs:" in out
+ assert "AI" in out
+ assert "ML" in out
+
+
+class TestPruneJson:
+ def test_keep_n(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ for i in range(5):
+ _write_json_checkpoint(
+ d, name=f"2026010{i + 1}T000000_aaa{i}1111_p-none.json"
+ )
+ time.sleep(0.01)
+ deleted = _prune_json(d, keep=2, older_than=None)
+ assert deleted == 3
+ remaining = []
+ for root, _, files in os.walk(d):
+ remaining.extend(files)
+ assert len(remaining) == 2
+
+ def test_older_than(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ old_path = _write_json_checkpoint(
+ d, name="20250101T000000_old01111_p-none.json"
+ )
+ os.utime(old_path, (0, 0))
+ _write_json_checkpoint(d, name="20260417T000000_new01111_p-none.json")
+ deleted = _prune_json(d, keep=None, older_than=timedelta(days=1))
+ assert deleted == 1
+
+ def test_empty_dir(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ assert _prune_json(d, keep=2, older_than=None) == 0
+
+ def test_removes_empty_branch_dirs(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ path = _write_json_checkpoint(
+ d,
+ branch="feature",
+ name="20260101T000000_aaaa1111_p-none.json",
+ )
+ os.utime(path, (0, 0))
+ _prune_json(d, keep=None, older_than=timedelta(days=1))
+ assert not os.path.exists(os.path.join(d, "feature"))
+
+
+class TestPruneSqlite:
+ def test_keep_n(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ db_path = os.path.join(d, "test.db")
+ for i in range(5):
+ _create_sqlite_checkpoint(
+ db_path, f"2026010{i + 1}T000000_aaa{i}1111"
+ )
+ deleted = _prune_sqlite(db_path, keep=2, older_than=None)
+ assert deleted == 3
+ with sqlite3.connect(db_path) as conn:
+ count = conn.execute("SELECT COUNT(*) FROM checkpoints").fetchone()[0]
+ assert count == 2
+
+ def test_older_than(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ db_path = os.path.join(d, "test.db")
+ _create_sqlite_checkpoint(db_path, "20200101T000000_old01111")
+ _create_sqlite_checkpoint(db_path, "20260417T000000_new01111")
+ deleted = _prune_sqlite(db_path, keep=None, older_than=timedelta(days=1))
+ assert deleted >= 1
+ with sqlite3.connect(db_path) as conn:
+ count = conn.execute("SELECT COUNT(*) FROM checkpoints").fetchone()[0]
+ assert count >= 1
+
+
+class TestPruneCommand:
+ def test_no_options_shows_help(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ prune_checkpoints(d, keep=None, older_than=None)
+ out = capsys.readouterr().out
+ assert "Specify" in out
+
+ def test_dry_run_json(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ _write_json_checkpoint(d)
+ prune_checkpoints(d, keep=1, older_than=None, dry_run=True)
+ out = capsys.readouterr().out
+ assert "Would prune" in out
+
+ def test_not_found(self, capsys: pytest.CaptureFixture[str]) -> None:
+ prune_checkpoints("/nonexistent", keep=1, older_than=None)
+ out = capsys.readouterr().out
+ assert "Not a directory" in out
+
+
+class TestResumeCheckpoint:
+ def test_not_found(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ resume_checkpoint(d, "nonexistent")
+ out = capsys.readouterr().out
+ assert "not found" in out
+
+ def test_no_checkpoints(self, capsys: pytest.CaptureFixture[str]) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ resume_checkpoint(d, None)
+ out = capsys.readouterr().out
+ assert "No checkpoints" in out
+
+
+class TestDiscoverabilityMessage:
+ def test_checkpoint_listener_logs_resume_hint(self) -> None:
+ from crewai.state.checkpoint_listener import _do_checkpoint
+ from crewai.state.runtime import RuntimeState
+
+ state = MagicMock(spec=RuntimeState)
+ state.root = []
+ state.model_dump.return_value = {"entities": [], "event_record": {"nodes": {}}}
+ state._parent_id = None
+ state._branch = "main"
+
+ cfg = MagicMock()
+ cfg.location = "/tmp/cp"
+ cfg.max_checkpoints = None
+ cfg.provider.checkpoint.return_value = "/tmp/cp/main/20260101T000000_test1234_p-none.json"
+ cfg.provider.extract_id.return_value = "20260101T000000_test1234"
+
+ with (
+ patch("crewai.state.checkpoint_listener._prepare_entities"),
+ patch("crewai.state.checkpoint_listener.logger") as mock_logger,
+ ):
+ _do_checkpoint(state, cfg)
+
+ cfg.provider.extract_id.assert_called_once()
+ mock_logger.info.assert_called_once()
+ logged: str = mock_logger.info.call_args[0][0]
+ assert "crewai checkpoint resume" in logged
+ assert "20260101T000000_test1234" in logged
diff --git a/lib/crewai/tests/test_llm.py b/lib/crewai/tests/test_llm.py
index 413504f31..60ecca7f0 100644
--- a/lib/crewai/tests/test_llm.py
+++ b/lib/crewai/tests/test_llm.py
@@ -1001,6 +1001,8 @@ def test_usage_info_non_streaming_with_call():
"completion_tokens": 0,
"successful_requests": 0,
"cached_prompt_tokens": 0,
+ "reasoning_tokens": 0,
+ "cache_creation_tokens": 0,
}
assert llm.stream is False
@@ -1025,6 +1027,8 @@ def test_usage_info_streaming_with_call():
"completion_tokens": 0,
"successful_requests": 0,
"cached_prompt_tokens": 0,
+ "reasoning_tokens": 0,
+ "cache_creation_tokens": 0,
}
assert llm.stream is True
@@ -1056,6 +1060,8 @@ async def test_usage_info_non_streaming_with_acall():
"completion_tokens": 0,
"successful_requests": 0,
"cached_prompt_tokens": 0,
+ "reasoning_tokens": 0,
+ "cache_creation_tokens": 0,
}
with patch.object(
@@ -1089,6 +1095,8 @@ async def test_usage_info_non_streaming_with_acall_and_stop():
"completion_tokens": 0,
"successful_requests": 0,
"cached_prompt_tokens": 0,
+ "reasoning_tokens": 0,
+ "cache_creation_tokens": 0,
}
with patch.object(
@@ -1121,6 +1129,8 @@ async def test_usage_info_streaming_with_acall():
"completion_tokens": 0,
"successful_requests": 0,
"cached_prompt_tokens": 0,
+ "reasoning_tokens": 0,
+ "cache_creation_tokens": 0,
}
with patch.object(
diff --git a/lib/crewai/tests/test_streaming.py b/lib/crewai/tests/test_streaming.py
index 8eb63694e..9079c393f 100644
--- a/lib/crewai/tests/test_streaming.py
+++ b/lib/crewai/tests/test_streaming.py
@@ -709,6 +709,158 @@ class TestStreamingEdgeCases:
assert streaming.is_completed
+class TestStreamingCancellation:
+ """Tests for streaming cancellation and resource cleanup."""
+
+ @pytest.mark.asyncio
+ async def test_aclose_cancels_async_streaming(self) -> None:
+ """Test that aclose() stops iteration and marks as cancelled."""
+ chunks_yielded: list[str] = []
+
+ async def slow_gen() -> AsyncIterator[StreamChunk]:
+ for i in range(100):
+ await asyncio.sleep(0.01)
+ chunks_yielded.append(f"chunk-{i}")
+ yield StreamChunk(content=f"chunk-{i}")
+
+ streaming = CrewStreamingOutput(async_iterator=slow_gen())
+ collected: list[StreamChunk] = []
+
+ async for chunk in streaming:
+ collected.append(chunk)
+ if len(collected) >= 3:
+ break
+
+ await streaming.aclose()
+
+ assert streaming.is_cancelled
+ assert streaming.is_completed
+ assert len(collected) == 3
+
+ @pytest.mark.asyncio
+ async def test_aclose_idempotent(self) -> None:
+ """Test that calling aclose() multiple times is safe."""
+ async def gen() -> AsyncIterator[StreamChunk]:
+ yield StreamChunk(content="test")
+
+ streaming = CrewStreamingOutput(async_iterator=gen())
+ async for _ in streaming:
+ pass
+
+ await streaming.aclose()
+ await streaming.aclose()
+ assert not streaming.is_cancelled
+ assert streaming.is_completed
+
+ @pytest.mark.asyncio
+ async def test_async_context_manager(self) -> None:
+ """Test using streaming output as async context manager."""
+ async def gen() -> AsyncIterator[StreamChunk]:
+ yield StreamChunk(content="hello")
+ yield StreamChunk(content="world")
+
+ streaming = CrewStreamingOutput(async_iterator=gen())
+ collected: list[StreamChunk] = []
+
+ async with streaming:
+ async for chunk in streaming:
+ collected.append(chunk)
+
+ assert not streaming.is_cancelled
+ assert streaming.is_completed
+ assert len(collected) == 2
+
+ @pytest.mark.asyncio
+ async def test_async_context_manager_early_exit(self) -> None:
+ """Test context manager cleans up on early exit."""
+ async def gen() -> AsyncIterator[StreamChunk]:
+ for i in range(100):
+ await asyncio.sleep(0.01)
+ yield StreamChunk(content=f"chunk-{i}")
+
+ streaming = CrewStreamingOutput(async_iterator=gen())
+
+ async with streaming:
+ async for chunk in streaming:
+ if chunk.content == "chunk-2":
+ break
+
+ assert streaming.is_cancelled
+ assert streaming.is_completed
+
+ def test_close_cancels_sync_streaming(self) -> None:
+ """Test that close() stops sync streaming and marks as cancelled."""
+ def gen() -> Generator[StreamChunk, None, None]:
+ for i in range(100):
+ yield StreamChunk(content=f"chunk-{i}")
+
+ streaming = CrewStreamingOutput(sync_iterator=gen())
+ collected: list[StreamChunk] = []
+
+ for chunk in streaming:
+ collected.append(chunk)
+ if len(collected) >= 3:
+ break
+
+ streaming.close()
+
+ assert streaming.is_cancelled
+ assert streaming.is_completed
+
+ def test_close_idempotent(self) -> None:
+ """Test that calling close() multiple times is safe."""
+ def gen() -> Generator[StreamChunk, None, None]:
+ yield StreamChunk(content="test")
+
+ streaming = CrewStreamingOutput(sync_iterator=gen())
+ list(streaming)
+
+ streaming.close()
+ streaming.close()
+ assert not streaming.is_cancelled
+ assert streaming.is_completed
+
+ @pytest.mark.asyncio
+ async def test_flow_aclose(self) -> None:
+ """Test that FlowStreamingOutput aclose() is no-op after normal completion."""
+ async def gen() -> AsyncIterator[StreamChunk]:
+ yield StreamChunk(content="flow-chunk")
+
+ streaming = FlowStreamingOutput(async_iterator=gen())
+ async for _ in streaming:
+ pass
+
+ await streaming.aclose()
+ assert not streaming.is_cancelled
+ assert streaming.is_completed
+
+ @pytest.mark.asyncio
+ async def test_flow_async_context_manager(self) -> None:
+ """Test FlowStreamingOutput as async context manager with full consumption."""
+ async def gen() -> AsyncIterator[StreamChunk]:
+ yield StreamChunk(content="flow-chunk")
+
+ streaming = FlowStreamingOutput(async_iterator=gen())
+
+ async with streaming:
+ async for _ in streaming:
+ pass
+
+ assert not streaming.is_cancelled
+ assert streaming.is_completed
+
+ def test_flow_close(self) -> None:
+ """Test that FlowStreamingOutput close() is no-op after normal completion."""
+ def gen() -> Generator[StreamChunk, None, None]:
+ yield StreamChunk(content="flow-chunk")
+
+ streaming = FlowStreamingOutput(sync_iterator=gen())
+ list(streaming)
+
+ streaming.close()
+ assert not streaming.is_cancelled
+
+
class TestStreamingImports:
"""Tests for correct imports of streaming types."""
@@ -727,3 +879,91 @@ class TestStreamingImports:
assert StreamChunk is not None
assert StreamChunkType is not None
assert ToolCallChunk is not None
+
+
+class TestConcurrentStreamIsolation:
+ """Regression tests for concurrent streaming isolation (issue #5376)."""
+
+ def test_concurrent_streams_do_not_cross_contaminate(self) -> None:
+ """Two concurrent streaming runs must each receive only their own chunks.
+
+ Mirrors the real production path: create_streaming_state in the caller,
+ then temporarily push the stream_id into the ContextVar, copy_context,
+ and reset — exactly as create_chunk_generator does.
+ """
+ import contextvars
+ import threading
+
+ from crewai.utilities.streaming import (
+ TaskInfo,
+ _current_stream_ids,
+ _unregister_handler,
+ create_streaming_state,
+ )
+
+ task_info_a: TaskInfo = {
+ "index": 0,
+ "name": "task_a",
+ "id": "a",
+ "agent_role": "A",
+ "agent_id": "a",
+ }
+ task_info_b: TaskInfo = {
+ "index": 1,
+ "name": "task_b",
+ "id": "b",
+ "agent_role": "B",
+ "agent_id": "b",
+ }
+
+ state_a = create_streaming_state(task_info_a, [])
+ state_b = create_streaming_state(task_info_b, [])
+
+ def make_emitter_ctx(state: Any) -> contextvars.Context:
+ token = _current_stream_ids.set(
+ (*_current_stream_ids.get(), state.stream_id)
+ )
+ ctx = contextvars.copy_context()
+ _current_stream_ids.reset(token)
+ return ctx
+
+ ctx_a = make_emitter_ctx(state_a)
+ ctx_b = make_emitter_ctx(state_b)
+
+ def emit_chunks(prefix: str, call_id: str) -> None:
+ for text in [f"{prefix}1", f"{prefix}2", f"{prefix}3"]:
+ crewai_event_bus.emit(
+ None,
+ event=LLMStreamChunkEvent(
+ chunk=text, call_id=call_id, response_id="r"
+ ),
+ )
+
+ t_a = threading.Thread(target=ctx_a.run, args=(lambda: emit_chunks("A", "ca"),))
+ t_b = threading.Thread(target=ctx_b.run, args=(lambda: emit_chunks("B", "cb"),))
+ t_a.start()
+ t_b.start()
+ t_a.join()
+ t_b.join()
+
+ chunks_a: list[str] = []
+ while not state_a.sync_queue.empty():
+ item = state_a.sync_queue.get_nowait()
+ if isinstance(item, StreamChunk):
+ chunks_a.append(item.content)
+
+ chunks_b: list[str] = []
+ while not state_b.sync_queue.empty():
+ item = state_b.sync_queue.get_nowait()
+ if isinstance(item, StreamChunk):
+ chunks_b.append(item.content)
+
+ assert set(chunks_a) == {"A1", "A2", "A3"}, (
+ f"Stream A received unexpected chunks: {chunks_a}"
+ )
+ assert set(chunks_b) == {"B1", "B2", "B3"}, (
+ f"Stream B received unexpected chunks: {chunks_b}"
+ )
+
+ _unregister_handler(state_a.handler)
+ _unregister_handler(state_b.handler)
diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py
index ba2e797d9..c7754e6ac 100644
--- a/lib/crewai/tests/tools/test_tool_usage.py
+++ b/lib/crewai/tests/tools/test_tool_usage.py
@@ -308,7 +308,6 @@ def test_validate_tool_input_invalid_input():
mock_agent.key = "test_agent_key" # Must be a string
mock_agent.role = "test_agent_role" # Must be a string
mock_agent._original_role = "test_agent_role" # Must be a string
- mock_agent.i18n = MagicMock()
mock_agent.verbose = False
# Create mock action with proper string value
@@ -443,7 +442,6 @@ def test_tool_selection_error_event_direct():
mock_agent = MagicMock()
mock_agent.key = "test_key"
mock_agent.role = "test_role"
- mock_agent.i18n = MagicMock()
mock_agent.verbose = False
mock_task = MagicMock()
@@ -518,13 +516,6 @@ def test_tool_validate_input_error_event():
mock_agent.verbose = False
mock_agent._original_role = "test_role"
- # Mock i18n with error message
- mock_i18n = MagicMock()
- mock_i18n.errors.return_value = (
- "Tool input must be a valid dictionary in JSON or Python literal format"
- )
- mock_agent.i18n = mock_i18n
-
# Mock task and tools handler
mock_task = MagicMock()
mock_tools_handler = MagicMock()
@@ -590,7 +581,6 @@ def test_tool_usage_finished_event_with_result():
mock_agent.key = "test_agent_key"
mock_agent.role = "test_agent_role"
mock_agent._original_role = "test_agent_role"
- mock_agent.i18n = MagicMock()
mock_agent.verbose = False
# Create mock task
@@ -670,7 +660,6 @@ def test_tool_usage_finished_event_with_cached_result():
mock_agent.key = "test_agent_key"
mock_agent.role = "test_agent_role"
mock_agent._original_role = "test_agent_role"
- mock_agent.i18n = MagicMock()
mock_agent.verbose = False
# Create mock task
@@ -761,9 +750,6 @@ def test_tool_error_does_not_emit_finished_event():
mock_agent._original_role = "test_agent_role"
mock_agent.verbose = False
mock_agent.fingerprint = None
- mock_agent.i18n.tools.return_value = {"name": "Add Image"}
- mock_agent.i18n.errors.return_value = "Error: {error}"
- mock_agent.i18n.slice.return_value = "Available tools: {tool_names}"
mock_task = MagicMock()
mock_task.delegations = 0
diff --git a/lib/crewai/tests/utilities/test_agent_utils.py b/lib/crewai/tests/utilities/test_agent_utils.py
index 3d249906a..42de64fe6 100644
--- a/lib/crewai/tests/utilities/test_agent_utils.py
+++ b/lib/crewai/tests/utilities/test_agent_utils.py
@@ -225,16 +225,6 @@ class TestConvertToolsToOpenaiSchema:
assert max_results_prop["default"] == 10
-def _make_mock_i18n() -> MagicMock:
- """Create a mock i18n with the new structured prompt keys."""
- mock_i18n = MagicMock()
- mock_i18n.slice.side_effect = lambda key: {
- "summarizer_system_message": "You are a precise assistant that creates structured summaries.",
- "summarize_instruction": "Summarize the conversation:\n{conversation}",
- "summary": "\n{merged_summary}\n\nContinue the task.",
- }.get(key, "")
- return mock_i18n
-
class MCPStyleInput(BaseModel):
"""Input schema mimicking an MCP tool with optional fields."""
@@ -330,7 +320,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
# System message preserved + summary message = 2
@@ -361,7 +351,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
assert len(messages) == 1
@@ -387,7 +377,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
assert len(messages) == 1
@@ -410,7 +400,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
assert id(messages) == original_list_id
@@ -432,7 +422,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
assert len(messages) == 2
@@ -456,7 +446,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
# Check what was passed to llm.call
@@ -482,7 +472,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
assert "The extracted summary content." in messages[0]["content"]
@@ -506,7 +496,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
# Verify the conversation text sent to LLM contains tool labels
@@ -528,7 +518,7 @@ class TestSummarizeMessages:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
# No LLM call should have been made
@@ -733,7 +723,7 @@ class TestParallelSummarization:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
# acall should have been awaited once per chunk
@@ -757,7 +747,7 @@ class TestParallelSummarization:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
mock_llm.call.assert_called_once()
@@ -788,7 +778,7 @@ class TestParallelSummarization:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
# The final summary message should have A, B, C in order
@@ -816,7 +806,7 @@ class TestParallelSummarization:
chunks=[chunk_a, chunk_b],
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
)
@@ -843,7 +833,7 @@ class TestParallelSummarization:
messages=messages,
llm=mock_llm,
callbacks=[],
- i18n=_make_mock_i18n(),
+
)
assert mock_llm.acall.await_count == 2
@@ -940,10 +930,8 @@ class TestParallelSummarizationVCR:
def test_parallel_summarize_openai(self) -> None:
"""Test that parallel summarization with gpt-4o-mini produces a valid summary."""
from crewai.llm import LLM
- from crewai.utilities.i18n import I18N
llm = LLM(model="gpt-4o-mini", temperature=0)
- i18n = I18N()
messages = _build_long_conversation()
original_system = messages[0]["content"]
@@ -959,7 +947,6 @@ class TestParallelSummarizationVCR:
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
)
# System message preserved
@@ -975,10 +962,8 @@ class TestParallelSummarizationVCR:
def test_parallel_summarize_preserves_files(self) -> None:
"""Test that file references survive parallel summarization."""
from crewai.llm import LLM
- from crewai.utilities.i18n import I18N
llm = LLM(model="gpt-4o-mini", temperature=0)
- i18n = I18N()
messages = _build_long_conversation()
mock_file = MagicMock()
@@ -989,7 +974,6 @@ class TestParallelSummarizationVCR:
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
)
summary_msg = messages[-1]
diff --git a/lib/crewai/tests/utilities/test_llm_utils.py b/lib/crewai/tests/utilities/test_llm_utils.py
index 5d7d70b76..a32fdcbc9 100644
--- a/lib/crewai/tests/utilities/test_llm_utils.py
+++ b/lib/crewai/tests/utilities/test_llm_utils.py
@@ -119,10 +119,12 @@ def test_create_llm_with_invalid_type() -> None:
def test_create_llm_openai_missing_api_key() -> None:
- """Test that create_llm raises error when OpenAI API key is missing"""
+ """Credentials are validated lazily: `create_llm` succeeds, and the
+ descriptive error only surfaces when the client is actually built."""
with patch.dict(os.environ, {}, clear=True):
+ llm = create_llm(llm_value="gpt-4o")
with pytest.raises((ValueError, ImportError)) as exc_info:
- create_llm(llm_value="gpt-4o")
+ llm._get_sync_client()
error_message = str(exc_info.value).lower()
assert "openai_api_key" in error_message or "api_key" in error_message
diff --git a/lib/crewai/tests/utilities/test_pydantic_schema_utils.py b/lib/crewai/tests/utilities/test_pydantic_schema_utils.py
index 98a5e6aa5..70a900c7f 100644
--- a/lib/crewai/tests/utilities/test_pydantic_schema_utils.py
+++ b/lib/crewai/tests/utilities/test_pydantic_schema_utils.py
@@ -882,3 +882,110 @@ class TestEndToEndMCPSchema:
)
assert obj.filters.date_from == datetime.date(2025, 1, 1)
assert obj.filters.categories == ["news", "tech"]
+
+
+# ---------------------------------------------------------------------------
+# Recursive / circular $ref schemas (GH-5490)
+# ---------------------------------------------------------------------------
+
+RECURSIVE_NODE_SCHEMA: dict = {
+ "$defs": {
+ "Node": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "children": {
+ "type": "array",
+ "items": {"$ref": "#/$defs/Node"},
+ },
+ },
+ "required": ["name"],
+ }
+ },
+ "$ref": "#/$defs/Node",
+}
+
+MUTUAL_RECURSION_SCHEMA: dict = {
+ "$defs": {
+ "A": {
+ "type": "object",
+ "properties": {
+ "val": {"type": "string"},
+ "b": {"$ref": "#/$defs/B"},
+ },
+ "required": ["val"],
+ },
+ "B": {
+ "type": "object",
+ "properties": {
+ "val": {"type": "integer"},
+ "a": {"$ref": "#/$defs/A"},
+ },
+ "required": ["val"],
+ },
+ },
+ "$ref": "#/$defs/A",
+}
+
+
+class TestResolveRefsRecursive:
+ def test_circular_ref_preserves_type(self) -> None:
+ from crewai.utilities.pydantic_schema_utils import resolve_refs
+
+ resolved = resolve_refs(deepcopy(RECURSIVE_NODE_SCHEMA))
+ items = resolved["properties"]["children"]["items"]
+ assert items != {}, "Circular ref should not degrade to {}"
+ assert items.get("type") == "object"
+
+ def test_non_recursive_schema_still_resolves(self) -> None:
+ from crewai.utilities.pydantic_schema_utils import resolve_refs
+
+ schema = {
+ "$defs": {"Foo": {"type": "object", "properties": {"x": {"type": "integer"}}}},
+ "$ref": "#/$defs/Foo",
+ }
+ resolved = resolve_refs(schema)
+ assert resolved["properties"]["x"]["type"] == "integer"
+
+
+class TestSanitizeRecursiveSchemas:
+ def test_anthropic_strict_preserves_recursive_type(self) -> None:
+ from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_anthropic_strict
+
+ san = sanitize_tool_params_for_anthropic_strict(deepcopy(RECURSIVE_NODE_SCHEMA))
+ items = san["properties"]["children"]["items"]
+ assert items != {}
+ assert items.get("type") == "object"
+
+ def test_openai_strict_preserves_recursive_type(self) -> None:
+ from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict
+
+ san = sanitize_tool_params_for_openai_strict(deepcopy(RECURSIVE_NODE_SCHEMA))
+ items = san["properties"]["children"]["items"]
+ assert items != {}
+ assert items.get("type") == "object"
+
+
+class TestCreateModelFromSchemaRecursive:
+ def test_model_creation_succeeds(self) -> None:
+ model = create_model_from_schema(deepcopy(RECURSIVE_NODE_SCHEMA), model_name="Node")
+ assert model is not None
+ assert model.__name__ == "Node"
+
+ def test_model_accepts_valid_recursive_data(self) -> None:
+ model = create_model_from_schema(deepcopy(RECURSIVE_NODE_SCHEMA), model_name="Node")
+ instance = model(name="root", children=[{"name": "child", "children": []}])
+ assert instance.name == "root"
+ assert len(instance.children) == 1
+
+ def test_model_rejects_missing_required_field(self) -> None:
+ import pytest
+
+ model = create_model_from_schema(deepcopy(RECURSIVE_NODE_SCHEMA), model_name="Node")
+ with pytest.raises(Exception):
+ model(children=[])
+
+ def test_mutual_recursion_schema(self) -> None:
+ model = create_model_from_schema(deepcopy(MUTUAL_RECURSION_SCHEMA), model_name="A")
+ instance = model(val="hello", b={"val": 42})
+ assert instance.val == "hello"
diff --git a/lib/crewai/tests/utilities/test_structured_planning.py b/lib/crewai/tests/utilities/test_structured_planning.py
index 91bca9c0d..b76d9af5c 100644
--- a/lib/crewai/tests/utilities/test_structured_planning.py
+++ b/lib/crewai/tests/utilities/test_structured_planning.py
@@ -147,8 +147,6 @@ class TestAgentReasoningWithMockedLLM:
agent.backstory = "Test backstory"
agent.verbose = False
agent.planning_config = PlanningConfig()
- agent.i18n = MagicMock()
- agent.i18n.retrieve.return_value = "Test prompt: {description}"
# Mock the llm attribute
agent.llm = MagicMock()
agent.llm.supports_function_calling.return_value = True
diff --git a/lib/crewai/tests/utilities/test_summarize_integration.py b/lib/crewai/tests/utilities/test_summarize_integration.py
index 5b3e39d07..a5da3a108 100644
--- a/lib/crewai/tests/utilities/test_summarize_integration.py
+++ b/lib/crewai/tests/utilities/test_summarize_integration.py
@@ -14,7 +14,6 @@ from crewai.crew import Crew
from crewai.llm import LLM
from crewai.task import Task
from crewai.utilities.agent_utils import summarize_messages
-from crewai.utilities.i18n import I18N
def _build_conversation_messages(
@@ -90,7 +89,7 @@ class TestSummarizeDirectOpenAI:
def test_summarize_direct_openai(self) -> None:
"""Test summarize_messages with gpt-4o-mini preserves system messages."""
llm = LLM(model="gpt-4o-mini", temperature=0)
- i18n = I18N()
+
messages = _build_conversation_messages(include_system=True)
original_system_content = messages[0]["content"]
@@ -99,7 +98,7 @@ class TestSummarizeDirectOpenAI:
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
+
)
# System message should be preserved
@@ -122,14 +121,14 @@ class TestSummarizeDirectAnthropic:
def test_summarize_direct_anthropic(self) -> None:
"""Test summarize_messages with claude-3-5-haiku."""
llm = LLM(model="anthropic/claude-3-5-haiku-latest", temperature=0)
- i18n = I18N()
+
messages = _build_conversation_messages(include_system=True)
summarize_messages(
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
+
)
assert len(messages) >= 2
@@ -148,14 +147,14 @@ class TestSummarizeDirectGemini:
def test_summarize_direct_gemini(self) -> None:
"""Test summarize_messages with gemini-2.0-flash."""
llm = LLM(model="gemini/gemini-2.0-flash", temperature=0)
- i18n = I18N()
+
messages = _build_conversation_messages(include_system=True)
summarize_messages(
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
+
)
assert len(messages) >= 2
@@ -174,14 +173,14 @@ class TestSummarizeDirectAzure:
def test_summarize_direct_azure(self) -> None:
"""Test summarize_messages with azure/gpt-4o-mini."""
llm = LLM(model="azure/gpt-4o-mini", temperature=0)
- i18n = I18N()
+
messages = _build_conversation_messages(include_system=True)
summarize_messages(
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
+
)
assert len(messages) >= 2
@@ -261,7 +260,7 @@ class TestSummarizePreservesFiles:
def test_summarize_preserves_files_integration(self) -> None:
"""Test that file references survive a real summarization call."""
llm = LLM(model="gpt-4o-mini", temperature=0)
- i18n = I18N()
+
messages = _build_conversation_messages(
include_system=True, include_files=True
)
@@ -270,7 +269,7 @@ class TestSummarizePreservesFiles:
messages=messages,
llm=llm,
callbacks=[],
- i18n=i18n,
+
)
# System message preserved
diff --git a/lib/devtools/pyproject.toml b/lib/devtools/pyproject.toml
index 815c8392f..7eebc9ea4 100644
--- a/lib/devtools/pyproject.toml
+++ b/lib/devtools/pyproject.toml
@@ -11,7 +11,7 @@ classifiers = ["Private :: Do Not Upload"]
private = true
dependencies = [
"click~=8.1.7",
- "toml~=0.10.2",
+ "tomlkit~=0.13.2",
"openai>=1.83.0,<3",
"python-dotenv~=1.1.1",
"pygithub~=1.59.1",
@@ -25,6 +25,10 @@ release = "crewai_devtools.cli:release"
docs-check = "crewai_devtools.docs_check:docs_check"
devtools = "crewai_devtools.cli:main"
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+addopts = "--noconftest"
+
[tool.uv]
exclude-newer = "3 days"
diff --git a/lib/devtools/src/crewai_devtools/__init__.py b/lib/devtools/src/crewai_devtools/__init__.py
index 54244d24f..db857f660 100644
--- a/lib/devtools/src/crewai_devtools/__init__.py
+++ b/lib/devtools/src/crewai_devtools/__init__.py
@@ -1,3 +1,3 @@
"""CrewAI development tools."""
-__version__ = "1.14.0"
+__version__ = "1.14.2rc1"
diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py
index 9f7b469be..eca54063b 100644
--- a/lib/devtools/src/crewai_devtools/cli.py
+++ b/lib/devtools/src/crewai_devtools/cli.py
@@ -1,8 +1,8 @@
"""Development tools for version bumping and git automation."""
+from collections.abc import Mapping
import os
from pathlib import Path
-import re
import subprocess
import sys
import tempfile
@@ -18,6 +18,7 @@ from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.prompt import Confirm
+import tomlkit
from crewai_devtools.docs_check import docs_check
from crewai_devtools.prompts import RELEASE_NOTES_PROMPT, TRANSLATE_RELEASE_NOTES_PROMPT
@@ -28,6 +29,33 @@ load_dotenv()
console = Console()
+def _resume_hint(message: str) -> None:
+ """Print a boxed resume hint after a failure."""
+ console.print()
+ console.print(
+ Panel(
+ message,
+ title="[bold yellow]How to resume[/bold yellow]",
+ border_style="yellow",
+ padding=(1, 2),
+ )
+ )
+
+
+def _print_release_error(e: BaseException) -> None:
+ """Print a release error with stderr if available."""
+ if isinstance(e, KeyboardInterrupt):
+ raise
+ if isinstance(e, SystemExit):
+ return
+ if isinstance(e, subprocess.CalledProcessError):
+ console.print(f"[red]Error running command:[/red] {e}")
+ if e.stderr:
+ console.print(e.stderr)
+ else:
+ console.print(f"[red]Error:[/red] {e}")
+
+
def run_command(cmd: list[str], cwd: Path | None = None) -> str:
"""Run a shell command and return output.
@@ -169,18 +197,17 @@ def update_pyproject_version(file_path: Path, new_version: str) -> bool:
if not file_path.exists():
return False
- content = file_path.read_text()
- new_content = re.sub(
- r'^(version\s*=\s*")[^"]+(")',
- rf"\g<1>{new_version}\2",
- content,
- count=1,
- flags=re.MULTILINE,
- )
- if new_content != content:
- file_path.write_text(new_content)
- return True
- return False
+ doc = tomlkit.parse(file_path.read_text())
+ project = doc.get("project")
+ if project is None:
+ return False
+ old_version = project.get("version")
+ if old_version is None or old_version == new_version:
+ return False
+
+ project["version"] = new_version
+ file_path.write_text(tomlkit.dumps(doc))
+ return True
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
@@ -264,11 +291,9 @@ def add_docs_version(docs_json_path: Path, version: str) -> bool:
if not versions:
continue
- # Skip if this version already exists for this language
if any(v.get("version") == version_label for v in versions):
continue
- # Find the current default and copy its tabs
default_version = next(
(v for v in versions if v.get("default")),
versions[0],
@@ -280,10 +305,7 @@ def add_docs_version(docs_json_path: Path, version: str) -> bool:
"tabs": default_version.get("tabs", []),
}
- # Remove default flag from old default
default_version.pop("default", None)
-
- # Insert new version at the beginning
versions.insert(0, new_version)
updated = True
@@ -473,6 +495,14 @@ def update_changelog(
return True
+def _is_crewai_dep(spec: str) -> bool:
+ """Return True if *spec* is a ``crewai`` or ``crewai[...]`` dependency."""
+ if not spec.startswith("crewai"):
+ return False
+ rest = spec[6:]
+ return len(rest) > 0 and rest[0] in ("[", "=", ">", "<", "~", "!")
+
+
def _pin_crewai_deps(content: str, version: str) -> str:
"""Replace crewai dependency version pins in a pyproject.toml string.
@@ -486,16 +516,29 @@ def _pin_crewai_deps(content: str, version: str) -> str:
Returns:
Transformed content.
"""
- return re.sub(
- r'"crewai(\[tools\])?(==|>=)[^"]*"',
- lambda m: f'"crewai{(m.group(1) or "")!s}=={version}"',
- content,
- )
+ doc = tomlkit.parse(content)
+ for key in ("dependencies", "optional-dependencies"):
+ deps = doc.get("project", {}).get(key)
+ if deps is None:
+ continue
+ dep_lists = deps.values() if isinstance(deps, Mapping) else [deps]
+ for dep_list in dep_lists:
+ for i, dep in enumerate(dep_list):
+ s = str(dep)
+ if not _is_crewai_dep(s) or ("==" not in s and ">=" not in s):
+ continue
+ extras = s[6 : s.index("]") + 1] if "[" in s[6:7] else ""
+ dep_list[i] = f"crewai{extras}=={version}"
+ return tomlkit.dumps(doc)
def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]:
"""Update crewai dependency versions in CLI template pyproject.toml files.
+ Uses simple string replacement instead of TOML parsing because
+ template files contain Jinja placeholders (``{{folder_name}}``)
+ that are not valid TOML.
+
Args:
templates_dir: Path to the CLI templates directory.
new_version: New version string.
@@ -503,10 +546,13 @@ def update_template_dependencies(templates_dir: Path, new_version: str) -> list[
Returns:
List of paths that were updated.
"""
+ import re
+
+ pattern = re.compile(r"(crewai(?:\[[\w,]+\])?)(?:==|>=)[^\s\"']+")
updated = []
for pyproject in templates_dir.rglob("pyproject.toml"):
content = pyproject.read_text()
- new_content = _pin_crewai_deps(content, new_version)
+ new_content = pattern.sub(rf"\1=={new_version}", content)
if new_content != content:
pyproject.write_text(new_content)
updated.append(pyproject)
@@ -613,7 +659,6 @@ def get_github_contributors(commit_range: str) -> list[str]:
List of GitHub usernames sorted alphabetically.
"""
try:
- # Get GitHub token from gh CLI
try:
gh_token = run_command(["gh", "auth", "token"])
except subprocess.CalledProcessError:
@@ -655,11 +700,6 @@ def get_github_contributors(commit_range: str) -> list[str]:
return []
-# ---------------------------------------------------------------------------
-# Shared workflow helpers
-# ---------------------------------------------------------------------------
-
-
def _poll_pr_until_merged(
branch_name: str, label: str, repo: str | None = None
) -> None:
@@ -739,7 +779,6 @@ def _update_all_versions(
"[yellow]Warning:[/yellow] No __version__ attributes found to update"
)
- # Update CLI template pyproject.toml files
templates_dir = lib_dir / "crewai" / "src" / "crewai" / "cli" / "templates"
if templates_dir.exists():
if dry_run:
@@ -1049,6 +1088,11 @@ _ENTERPRISE_EXTRA_PACKAGES: Final[tuple[str, ...]] = tuple(
for p in os.getenv("ENTERPRISE_EXTRA_PACKAGES", "").split(",")
if p.strip()
)
+_ENTERPRISE_WORKFLOW_PATHS: Final[tuple[str, ...]] = tuple(
+ p.strip()
+ for p in os.getenv("ENTERPRISE_WORKFLOW_PATHS", "").split(",")
+ if p.strip()
+)
def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
@@ -1072,6 +1116,84 @@ def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
return False
+def _update_enterprise_workflows(repo_dir: Path, version: str) -> list[Path]:
+ """Update crewai version pins in enterprise CI workflow files.
+
+ Applies ``_repin_crewai_install`` line-by-line on the raw file so
+ only version numbers change and all formatting is preserved.
+
+ Args:
+ repo_dir: Root of the cloned enterprise repo.
+ version: New crewai version string.
+
+ Returns:
+ List of workflow paths that were modified.
+ """
+ updated: list[Path] = []
+ for rel_path in _ENTERPRISE_WORKFLOW_PATHS:
+ workflow = repo_dir / rel_path
+ if not workflow.exists():
+ continue
+
+ raw = workflow.read_text()
+ lines = raw.splitlines(keepends=True)
+ changed = False
+ for i, line in enumerate(lines):
+ if "crewai[" not in line:
+ continue
+ new_line = _repin_crewai_install(line, version)
+ if new_line != line:
+ lines[i] = new_line
+ changed = True
+
+ if changed:
+ new_raw = "".join(lines)
+ else:
+ new_raw = raw
+
+ if new_raw != raw:
+ workflow.write_text(new_raw)
+ updated.append(workflow)
+
+ return updated
+
+
+def _repin_crewai_install(run_value: str, version: str) -> str:
+ """Rewrite ``crewai[extras]==old`` pins in a shell command string.
+
+ Splits on the known ``crewai[`` prefix and reconstructs the pin
+ with the new version, avoiding regex.
+
+ Args:
+ run_value: The ``run:`` string from a workflow step.
+ version: New version to pin to.
+
+ Returns:
+ The updated string.
+ """
+ result: list[str] = []
+ remainder = run_value
+ marker = "crewai["
+ while marker in remainder:
+ before, _, after = remainder.partition(marker)
+ result.append(before)
+ bracket_end = after.index("]")
+ extras = after[:bracket_end]
+ rest = after[bracket_end + 1 :]
+ if rest.startswith("=="):
+ ver_start = 2
+ ver_end = ver_start
+ while ver_end < len(rest) and rest[ver_end] not in ('"', "'", " ", "\n"):
+ ver_end += 1
+ result.append(f"crewai[{extras}]=={version}")
+ remainder = rest[ver_end:]
+ else:
+ result.append(f"crewai[{extras}]")
+ remainder = rest
+ result.append(remainder)
+ return "".join(result)
+
+
_DEPLOYMENT_TEST_REPO: Final[str] = "crewAIInc/crew_deployment_test"
_PYPI_POLL_INTERVAL: Final[int] = 15
@@ -1099,11 +1221,7 @@ def _update_deployment_test_repo(version: str, is_prerelease: bool) -> None:
pyproject = repo_dir / "pyproject.toml"
content = pyproject.read_text()
- new_content = re.sub(
- r'"crewai\[tools\]==[^"]+"',
- f'"crewai[tools]=={version}"',
- content,
- )
+ new_content = _pin_crewai_deps(content, version)
if new_content == content:
console.print(
"[yellow]Warning:[/yellow] No crewai[tools] pin found to update"
@@ -1225,7 +1343,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
run_command(["gh", "repo", "clone", enterprise_repo, str(repo_dir)])
console.print(f"[green]✓[/green] Cloned {enterprise_repo}")
- # --- bump versions ---
for rel_dir in _ENTERPRISE_VERSION_DIRS:
pkg_dir = repo_dir / rel_dir
if not pkg_dir.exists():
@@ -1255,13 +1372,17 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
f"{pyproject.relative_to(repo_dir)}"
)
- # --- update crewai[tools] pin ---
enterprise_pyproject = repo_dir / enterprise_dep_path
if _update_enterprise_crewai_dep(enterprise_pyproject, version):
console.print(
f"[green]✓[/green] Updated crewai[tools] dep in {enterprise_dep_path}"
)
+ for wf in _update_enterprise_workflows(repo_dir, version):
+ console.print(
+ f"[green]✓[/green] Updated crewai pin in {wf.relative_to(repo_dir)}"
+ )
+
_wait_for_pypi("crewai", version)
console.print("\nSyncing workspace...")
@@ -1296,7 +1417,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
time.sleep(_PYPI_POLL_INTERVAL)
console.print("[green]✓[/green] Workspace synced")
- # --- branch, commit, push, PR ---
branch_name = f"feat/bump-version-{version}"
run_command(["git", "checkout", "-b", branch_name], cwd=repo_dir)
run_command(["git", "add", "."], cwd=repo_dir)
@@ -1330,7 +1450,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
_poll_pr_until_merged(branch_name, "enterprise bump PR", repo=enterprise_repo)
- # --- tag and release ---
run_command(["git", "checkout", "main"], cwd=repo_dir)
run_command(["git", "pull"], cwd=repo_dir)
@@ -1372,7 +1491,6 @@ def _trigger_pypi_publish(tag_name: str, wait: bool = False) -> None:
tag_name: The release tag to publish.
wait: Block until the workflow run completes.
"""
- # Capture the latest run ID before triggering so we can detect the new one
prev_run_id = ""
if wait:
try:
@@ -1447,11 +1565,6 @@ def _trigger_pypi_publish(tag_name: str, wait: bool = False) -> None:
console.print("[green]✓[/green] PyPI publish workflow completed")
-# ---------------------------------------------------------------------------
-# CLI commands
-# ---------------------------------------------------------------------------
-
-
@click.group()
def cli() -> None:
"""Development tools for version bumping and git automation."""
@@ -1719,62 +1832,80 @@ def release(
skip_enterprise: Skip the enterprise release phase.
skip_to_enterprise: Skip phases 1 & 2, run only the enterprise release phase.
"""
- try:
- check_gh_installed()
+ flags: list[str] = []
+ if no_edit:
+ flags.append("--no-edit")
+ if skip_enterprise:
+ flags.append("--skip-enterprise")
+ flag_suffix = (" " + " ".join(flags)) if flags else ""
+ enterprise_hint = (
+ ""
+ if skip_enterprise
+ else f"\n\nThen release enterprise:\n\n"
+ f" devtools release {version} --skip-to-enterprise"
+ )
- if skip_enterprise and skip_to_enterprise:
+ check_gh_installed()
+
+ if skip_enterprise and skip_to_enterprise:
+ console.print(
+ "[red]Error:[/red] Cannot use both --skip-enterprise "
+ "and --skip-to-enterprise"
+ )
+ sys.exit(1)
+
+ if not skip_enterprise or skip_to_enterprise:
+ missing: list[str] = []
+ if not _ENTERPRISE_REPO:
+ missing.append("ENTERPRISE_REPO")
+ if not _ENTERPRISE_VERSION_DIRS:
+ missing.append("ENTERPRISE_VERSION_DIRS")
+ if not _ENTERPRISE_CREWAI_DEP_PATH:
+ missing.append("ENTERPRISE_CREWAI_DEP_PATH")
+ if missing:
console.print(
- "[red]Error:[/red] Cannot use both --skip-enterprise "
- "and --skip-to-enterprise"
+ f"[red]Error:[/red] Missing required environment variable(s): "
+ f"{', '.join(missing)}\n"
+ f"Set them or pass --skip-enterprise to skip the enterprise release."
)
sys.exit(1)
- if not skip_enterprise or skip_to_enterprise:
- missing: list[str] = []
- if not _ENTERPRISE_REPO:
- missing.append("ENTERPRISE_REPO")
- if not _ENTERPRISE_VERSION_DIRS:
- missing.append("ENTERPRISE_VERSION_DIRS")
- if not _ENTERPRISE_CREWAI_DEP_PATH:
- missing.append("ENTERPRISE_CREWAI_DEP_PATH")
- if missing:
- console.print(
- f"[red]Error:[/red] Missing required environment variable(s): "
- f"{', '.join(missing)}\n"
- f"Set them or pass --skip-enterprise to skip the enterprise release."
- )
- sys.exit(1)
+ cwd = Path.cwd()
+ lib_dir = cwd / "lib"
- cwd = Path.cwd()
- lib_dir = cwd / "lib"
+ is_prerelease = _is_prerelease(version)
- is_prerelease = _is_prerelease(version)
-
- if skip_to_enterprise:
+ if skip_to_enterprise:
+ try:
_release_enterprise(version, is_prerelease, dry_run)
- console.print(
- f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!"
+ except BaseException as e:
+ _print_release_error(e)
+ _resume_hint(
+ f"Fix the issue, then re-run:\n\n"
+ f" devtools release {version} --skip-to-enterprise"
)
- return
-
- if not dry_run:
- console.print("Checking git status...")
- check_git_clean()
- console.print("[green]✓[/green] Working directory is clean")
- else:
- console.print("[dim][DRY RUN][/dim] Would check git status")
-
- packages = get_packages(lib_dir)
-
- console.print(f"\nFound {len(packages)} package(s) to update:")
- for pkg in packages:
- console.print(f" - {pkg.name}")
-
- # --- Phase 1: Bump versions ---
+ sys.exit(1)
console.print(
- f"\n[bold cyan]Phase 1: Bumping versions to {version}[/bold cyan]"
+ f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!"
)
+ return
+ if not dry_run:
+ console.print("Checking git status...")
+ check_git_clean()
+ console.print("[green]✓[/green] Working directory is clean")
+ else:
+ console.print("[dim][DRY RUN][/dim] Would check git status")
+
+ packages = get_packages(lib_dir)
+
+ console.print(f"\nFound {len(packages)} package(s) to update:")
+ for pkg in packages:
+ console.print(f" - {pkg.name}")
+
+ console.print(f"\n[bold cyan]Phase 1: Bumping versions to {version}[/bold cyan]")
+
+ try:
_update_all_versions(cwd, lib_dir, version, packages, dry_run)
branch_name = f"feat/bump-version-{version}"
@@ -1818,12 +1949,17 @@ def release(
console.print(
"[dim][DRY RUN][/dim] Would push branch, create PR, and wait for merge"
)
-
- # --- Phase 2: Tag and release ---
- console.print(
- f"\n[bold cyan]Phase 2: Tagging and releasing {version}[/bold cyan]"
+ except BaseException as e:
+ _print_release_error(e)
+ _resume_hint(
+ f"Phase 1 failed. Fix the issue, then re-run:\n\n"
+ f" devtools release {version}{flag_suffix}"
)
+ sys.exit(1)
+ console.print(f"\n[bold cyan]Phase 2: Tagging and releasing {version}[/bold cyan]")
+
+ try:
tag_name = version
if not dry_run:
@@ -1850,22 +1986,57 @@ def release(
if not dry_run:
_create_tag_and_release(tag_name, release_notes, is_prerelease)
+ except BaseException as e:
+ _print_release_error(e)
+ _resume_hint(
+ "Phase 2 failed before PyPI publish. The bump PR is already merged.\n"
+ "Fix the issue, then resume with:\n\n"
+ " devtools tag"
+ f"\n\nAfter tagging, publish to PyPI and update deployment test:\n\n"
+ f" gh workflow run publish.yml -f release_tag={version}"
+ f"{enterprise_hint}"
+ )
+ sys.exit(1)
+
+ try:
+ if not dry_run:
_trigger_pypi_publish(tag_name, wait=True)
+ except BaseException as e:
+ _print_release_error(e)
+ _resume_hint(
+ f"Phase 2 failed at PyPI publish. Tag and GitHub release already exist.\n"
+ f"Retry PyPI publish manually:\n\n"
+ f" gh workflow run publish.yml -f release_tag={version}"
+ f"{enterprise_hint}"
+ )
+ sys.exit(1)
+
+ try:
+ if not dry_run:
_update_deployment_test_repo(version, is_prerelease)
+ except BaseException as e:
+ _print_release_error(e)
+ _resume_hint(
+ f"Phase 2 failed updating deployment test repo. "
+ f"Tag, release, and PyPI are done.\n"
+ f"Fix the issue and update {_DEPLOYMENT_TEST_REPO} manually."
+ f"{enterprise_hint}"
+ )
+ sys.exit(1)
- if not skip_enterprise:
+ if not skip_enterprise:
+ try:
_release_enterprise(version, is_prerelease, dry_run)
+ except BaseException as e:
+ _print_release_error(e)
+ _resume_hint(
+ f"Phase 3 (enterprise) failed. Phases 1 & 2 completed successfully.\n"
+ f"Fix the issue, then resume:\n\n"
+ f" devtools release {version} --skip-to-enterprise"
+ )
+ sys.exit(1)
- console.print(f"\n[green]✓[/green] Release [bold]{version}[/bold] complete!")
-
- except subprocess.CalledProcessError as e:
- console.print(f"[red]Error running command:[/red] {e}")
- if e.stderr:
- console.print(e.stderr)
- sys.exit(1)
- except Exception as e:
- console.print(f"[red]Error:[/red] {e}")
- sys.exit(1)
+ console.print(f"\n[green]✓[/green] Release [bold]{version}[/bold] complete!")
cli.add_command(bump)
diff --git a/lib/devtools/tests/__init__.py b/lib/devtools/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/devtools/tests/test_toml_updates.py b/lib/devtools/tests/test_toml_updates.py
new file mode 100644
index 000000000..0a47283a9
--- /dev/null
+++ b/lib/devtools/tests/test_toml_updates.py
@@ -0,0 +1,274 @@
+"""Tests for TOML-based version and dependency update functions."""
+
+from pathlib import Path
+from textwrap import dedent
+
+from crewai_devtools.cli import (
+ _pin_crewai_deps,
+ _repin_crewai_install,
+ update_pyproject_version,
+ update_template_dependencies,
+)
+
+
+# --- update_pyproject_version ---
+
+
+class TestUpdatePyprojectVersion:
+ def test_updates_version(self, tmp_path: Path) -> None:
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ dedent("""\
+ [project]
+ name = "my-pkg"
+ version = "1.0.0"
+ """)
+ )
+
+ assert update_pyproject_version(pyproject, "2.0.0") is True
+ assert 'version = "2.0.0"' in pyproject.read_text()
+
+ def test_returns_false_when_already_current(self, tmp_path: Path) -> None:
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ dedent("""\
+ [project]
+ name = "my-pkg"
+ version = "1.0.0"
+ """)
+ )
+
+ assert update_pyproject_version(pyproject, "1.0.0") is False
+
+ def test_returns_false_when_no_project_section(self, tmp_path: Path) -> None:
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text("[tool.ruff]\nline-length = 88\n")
+
+ assert update_pyproject_version(pyproject, "1.0.0") is False
+
+ def test_returns_false_when_version_is_dynamic(self, tmp_path: Path) -> None:
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ dedent("""\
+ [project]
+ name = "my-pkg"
+ dynamic = ["version"]
+ """)
+ )
+
+ assert update_pyproject_version(pyproject, "1.0.0") is False
+ assert 'version = "1.0.0"' not in pyproject.read_text()
+
+ def test_returns_false_for_missing_file(self, tmp_path: Path) -> None:
+ assert update_pyproject_version(tmp_path / "nope.toml", "1.0.0") is False
+
+ def test_preserves_comments_and_formatting(self, tmp_path: Path) -> None:
+ content = dedent("""\
+ # This is important
+ [project]
+ name = "my-pkg"
+ version = "1.0.0" # current version
+ description = "A package"
+ """)
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(content)
+
+ update_pyproject_version(pyproject, "2.0.0")
+ result = pyproject.read_text()
+
+ assert "# This is important" in result
+ assert 'description = "A package"' in result
+
+
+# --- _pin_crewai_deps ---
+
+
+class TestPinCrewaiDeps:
+ def test_pins_exact_version(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai==1.0.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai==2.0.0"' in result
+
+ def test_pins_minimum_version(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai>=1.0.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai==2.0.0"' in result
+ assert ">=" not in result
+
+ def test_pins_with_tools_extra(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai[tools]==1.0.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai[tools]==2.0.0"' in result
+
+ def test_leaves_unrelated_deps_alone(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "requests>=2.0",
+ "crewai==1.0.0",
+ "click~=8.1",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"requests>=2.0"' in result
+ assert '"click~=8.1"' in result
+
+ def test_handles_optional_dependencies(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = []
+
+ [project.optional-dependencies]
+ tools = [
+ "crewai[tools]>=1.0.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "3.0.0")
+ assert '"crewai[tools]==3.0.0"' in result
+
+ def test_handles_multiple_crewai_entries(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai==1.0.0",
+ "crewai[tools]==1.0.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai==2.0.0"' in result
+ assert '"crewai[tools]==2.0.0"' in result
+
+ def test_preserves_arbitrary_extras(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai[a2a]==1.0.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai[a2a]==2.0.0"' in result
+
+ def test_no_deps_returns_unchanged(self) -> None:
+ content = dedent("""\
+ [project]
+ name = "empty"
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert "empty" in result
+
+ def test_skips_crewai_without_version_specifier(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai-tools~=1.0",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai-tools~=1.0"' in result
+
+ def test_skips_crewai_extras_without_pin(self) -> None:
+ content = dedent("""\
+ [project]
+ dependencies = [
+ "crewai[tools]",
+ ]
+ """)
+ result = _pin_crewai_deps(content, "2.0.0")
+ assert '"crewai[tools]"' in result
+ assert "==" not in result
+
+
+# --- _repin_crewai_install ---
+
+
+class TestRepinCrewaiInstall:
+ def test_repins_a2a_extra(self) -> None:
+ result = _repin_crewai_install('uv pip install "crewai[a2a]==1.14.0"', "2.0.0")
+ assert result == 'uv pip install "crewai[a2a]==2.0.0"'
+
+ def test_repins_tools_extra(self) -> None:
+ result = _repin_crewai_install('uv pip install "crewai[tools]==1.0.0"', "3.0.0")
+ assert result == 'uv pip install "crewai[tools]==3.0.0"'
+
+ def test_leaves_unrelated_commands_alone(self) -> None:
+ cmd = "uv pip install requests"
+ assert _repin_crewai_install(cmd, "2.0.0") == cmd
+
+ def test_handles_multiple_pins(self) -> None:
+ cmd = 'pip install "crewai[a2a]==1.0.0" "crewai[tools]==1.0.0"'
+ result = _repin_crewai_install(cmd, "2.0.0")
+ assert result == 'pip install "crewai[a2a]==2.0.0" "crewai[tools]==2.0.0"'
+
+ def test_preserves_surrounding_text(self) -> None:
+ cmd = 'echo hello && uv pip install "crewai[a2a]==1.14.0" && echo done'
+ result = _repin_crewai_install(cmd, "2.0.0")
+ assert (
+ result == 'echo hello && uv pip install "crewai[a2a]==2.0.0" && echo done'
+ )
+
+ def test_no_version_specifier_unchanged(self) -> None:
+ cmd = 'pip install "crewai[tools]>=1.0"'
+ assert _repin_crewai_install(cmd, "2.0.0") == cmd
+
+
+# --- update_template_dependencies ---
+
+
+class TestUpdateTemplateDependencies:
+ def test_updates_jinja_template(self, tmp_path: Path) -> None:
+ """Template pyproject.toml files with Jinja placeholders should not break."""
+ tpl = tmp_path / "crew" / "pyproject.toml"
+ tpl.parent.mkdir()
+ tpl.write_text(
+ dedent("""\
+ [project]
+ name = "{{folder_name}}"
+ version = "0.1.0"
+ dependencies = [
+ "crewai[tools]==1.14.0"
+ ]
+
+ [project.scripts]
+ {{folder_name}} = "{{folder_name}}.main:run"
+ """)
+ )
+
+ updated = update_template_dependencies(tmp_path, "2.0.0")
+
+ assert len(updated) == 1
+ content = tpl.read_text()
+ assert '"crewai[tools]==2.0.0"' in content
+ assert "{{folder_name}}" in content
+
+ def test_updates_bare_crewai(self, tmp_path: Path) -> None:
+ tpl = tmp_path / "pyproject.toml"
+ tpl.write_text('dependencies = [\n "crewai==1.0.0"\n]\n')
+
+ updated = update_template_dependencies(tmp_path, "3.0.0")
+
+ assert len(updated) == 1
+ assert '"crewai==3.0.0"' in tpl.read_text()
+
+ def test_skips_unrelated_deps(self, tmp_path: Path) -> None:
+ tpl = tmp_path / "pyproject.toml"
+ tpl.write_text('dependencies = [\n "requests>=2.0"\n]\n')
+
+ updated = update_template_dependencies(tmp_path, "2.0.0")
+
+ assert len(updated) == 0
+ assert '"requests>=2.0"' in tpl.read_text()
diff --git a/pyproject.toml b/pyproject.toml
index 44b966533..f3d5f7f8f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ dev = [
"mypy==1.19.1",
"pre-commit==4.5.1",
"bandit==1.9.2",
- "pytest==8.4.2",
+ "pytest==9.0.3",
"pytest-asyncio==1.3.0",
"pytest-subprocess==1.5.3",
"vcrpy==7.0.0", # pinned, less versions break pytest-recording
@@ -20,7 +20,7 @@ dev = [
"pytest-randomly==4.0.1",
"pytest-timeout==2.4.0",
"pytest-xdist==3.8.0",
- "pytest-split==0.10.0",
+ "pytest-split==0.11.0",
"types-requests~=2.31.0.6",
"types-pyyaml==6.0.*",
"types-regex==2026.1.15.*",
@@ -30,6 +30,7 @@ dev = [
"types-pymysql==1.1.0.20250916",
"types-aiofiles~=25.1.0",
"commitizen>=4.13.9",
+ "pip-audit==2.9.0",
]
@@ -107,6 +108,7 @@ ignore-decorators = ["typing.overload"]
"lib/crewai/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements, unnecessary assignments, and hardcoded passwords in tests
"lib/crewai-tools/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "RUF012", "N818", "E402", "RUF043", "S110", "B017"] # Allow various test-specific patterns
"lib/crewai-files/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "B017", "F841"] # Allow assert statements and blind exception assertions in tests
+"lib/devtools/tests/**/*.py" = ["S101"]
[tool.mypy]
@@ -160,18 +162,30 @@ info = "Commits must follow Conventional Commits 1.0.0."
[tool.uv]
-exclude-newer = "3 days"
+exclude-newer = "1 day"
# composio-core pins rich<14 but textual requires rich>=14.
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
# fastembed 0.7.x and docling 2.63 cap pillow<12; the removed APIs don't affect them.
-# langchain-core <1.2.11 has SSRF via image_url token counting (CVE-2026-26013).
+# langchain-core <1.2.28 has GHSA-926x-3r5x-gfhw (incomplete f-string validation).
+# transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1).
+# cryptography 46.0.6 has CVE-2026-39892; force 46.0.7+.
+# pypdf <6.10.1 has CVE-2026-40260 and GHSA-jj6c-8h6c-hppx; force 6.10.1+.
+# uv <0.11.6 has GHSA-pjjw-68hj-v9mw; force 0.11.6+.
+# python-multipart <0.0.26 has GHSA-mj87-hwqh-73pj; force 0.0.26+.
+# langsmith <0.7.31 has GHSA-rr7j-v2q5-chgv (streaming token redaction bypass); force 0.7.31+.
override-dependencies = [
"rich>=13.7.1",
"onnxruntime<1.24; python_version < '3.11'",
"pillow>=12.1.1",
- "langchain-core>=1.2.11,<2",
+ "langchain-core>=1.2.28,<2",
"urllib3>=2.6.3",
+ "transformers>=5.4.0; python_version >= '3.10'",
+ "cryptography>=46.0.7",
+ "pypdf>=6.10.1,<7",
+ "uv>=0.11.6,<1",
+ "python-multipart>=0.0.26,<1",
+ "langsmith>=0.7.31,<0.8",
]
[tool.uv.workspace]
diff --git a/uv.lock b/uv.lock
index 2f0922173..a89b53544 100644
--- a/uv.lock
+++ b/uv.lock
@@ -13,8 +13,8 @@ resolution-markers = [
]
[options]
-exclude-newer = "2026-04-04T15:11:41.651093Z"
-exclude-newer-span = "P3D"
+exclude-newer = "2026-04-15T15:14:38.695171Z"
+exclude-newer-span = "P1D"
[manifest]
members = [
@@ -24,11 +24,17 @@ members = [
"crewai-tools",
]
overrides = [
- { name = "langchain-core", specifier = ">=1.2.11,<2" },
+ { name = "cryptography", specifier = ">=46.0.7" },
+ { name = "langchain-core", specifier = ">=1.2.28,<2" },
+ { name = "langsmith", specifier = ">=0.7.31,<0.8" },
{ name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" },
{ name = "pillow", specifier = ">=12.1.1" },
+ { name = "pypdf", specifier = ">=6.10.1,<7" },
+ { name = "python-multipart", specifier = ">=0.0.26,<1" },
{ name = "rich", specifier = ">=13.7.1" },
+ { name = "transformers", marker = "python_full_version >= '3.10'", specifier = ">=5.4.0" },
{ name = "urllib3", specifier = ">=2.6.3" },
+ { name = "uv", specifier = ">=0.11.6,<1" },
]
[manifest.dependency-groups]
@@ -37,12 +43,13 @@ dev = [
{ name = "boto3-stubs", extras = ["bedrock-runtime"], specifier = "==1.42.40" },
{ name = "commitizen", specifier = ">=4.13.9" },
{ name = "mypy", specifier = "==1.19.1" },
+ { name = "pip-audit", specifier = "==2.9.0" },
{ name = "pre-commit", specifier = "==4.5.1" },
- { name = "pytest", specifier = "==8.4.2" },
+ { name = "pytest", specifier = "==9.0.3" },
{ name = "pytest-asyncio", specifier = "==1.3.0" },
{ name = "pytest-randomly", specifier = "==4.0.1" },
{ name = "pytest-recording", specifier = "==0.13.4" },
- { name = "pytest-split", specifier = "==0.10.0" },
+ { name = "pytest-split", specifier = "==0.11.0" },
{ name = "pytest-subprocess", specifier = "==1.5.3" },
{ name = "pytest-timeout", specifier = "==2.4.0" },
{ name = "pytest-xdist", specifier = "==3.8.0" },
@@ -59,7 +66,7 @@ dev = [
[[package]]
name = "a2a-sdk"
-version = "0.3.25"
+version = "0.3.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
@@ -68,9 +75,9 @@ dependencies = [
{ name = "protobuf" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/55/83/3c99b276d09656cce039464509f05bf385e5600d6dc046a131bbcf686930/a2a_sdk-0.3.25.tar.gz", hash = "sha256:afda85bab8d6af0c5d15e82f326c94190f6be8a901ce562d045a338b7127242f", size = 270638, upload-time = "2026-03-10T13:08:46.417Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/97/a6840e01795b182ce751ca165430d46459927cde9bfab838087cbb24aef7/a2a_sdk-0.3.26.tar.gz", hash = "sha256:44068e2d037afbb07ab899267439e9bc7eaa7ac2af94f1e8b239933c993ad52d", size = 274598, upload-time = "2026-04-09T15:21:13.902Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bd/f9/6a62520b7ecb945188a6e1192275f4732ff9341cd4629bc975a6c146aeab/a2a_sdk-0.3.25-py3-none-any.whl", hash = "sha256:2fce38faea82eb0b6f9f9c2bcf761b0d78612c80ef0e599b50d566db1b2654b5", size = 149609, upload-time = "2026-03-10T13:08:44.7Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d5/51f4ee1bf3b736add42a542d3c8a3fd3fa85f3d36c17972127defc46c26f/a2a_sdk-0.3.26-py3-none-any.whl", hash = "sha256:754e0573f6d33b225c1d8d51f640efa69cbbed7bdfb06ce9c3540ea9f58d4a91", size = 151016, upload-time = "2026-04-09T15:21:12.35Z" },
]
[[package]]
@@ -94,7 +101,7 @@ wheels = [
[[package]]
name = "aiobotocore"
-version = "2.25.2"
+version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -103,11 +110,12 @@ dependencies = [
{ name = "jmespath" },
{ name = "multidict" },
{ name = "python-dateutil" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/52/48/cf3c88c5e3fecdeed824f97a8a98a9fc0d7ef33e603f8f22c2fd32b9ef09/aiobotocore-2.25.2.tar.gz", hash = "sha256:ae0a512b34127097910b7af60752956254099ae54402a84c2021830768f92cda", size = 120585, upload-time = "2025-11-11T18:51:28.056Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b8/50/a48ed11b15f926ce3dbb33e7fb0f25af17dbb99bcb7ae3b30c763723eca7/aiobotocore-3.4.0.tar.gz", hash = "sha256:a918b5cb903f81feba7e26835aed4b5e6bb2d0149d7f42bb2dd7d8089e3d9000", size = 122360, upload-time = "2026-04-07T06:12:24.884Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/ad/a2f3964aa37da5a4c94c1e5f3934d6ac1333f991f675fcf08a618397a413/aiobotocore-2.25.2-py3-none-any.whl", hash = "sha256:0cec45c6ba7627dd5e5460337291c86ac38c3b512ec4054ce76407d0f7f2a48f", size = 86048, upload-time = "2025-11-11T18:51:26.139Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d8/ce9386e6d76ea79e61dee15e62aa48cff6be69e89246b0ac4a11857cb02c/aiobotocore-3.4.0-py3-none-any.whl", hash = "sha256:26290eb6830ea92d8a6f5f90b56e9f5cedd6d126074d5db63b195e281d982465", size = 88018, upload-time = "2026-04-07T06:12:22.684Z" },
]
[[package]]
@@ -611,18 +619,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/36/b7/a5cc566901af27314408b95701f8e1d9c286b0aecfa50fc76c53d73efa6f/bedrock_agentcore-1.3.2-py3-none-any.whl", hash = "sha256:3a4e7122f777916f8bd74b42f29eb881415e37fda784a5ff8fab3c813b921706", size = 121703, upload-time = "2026-02-23T20:52:55.038Z" },
]
+[[package]]
+name = "boolean-py"
+version = "5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
+]
+
[[package]]
name = "boto3"
-version = "1.40.70"
+version = "1.42.84"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/37/12/d5ac34e0536e1914dde28245f014a635056dde0427f6efa09f104d7999f4/boto3-1.40.70.tar.gz", hash = "sha256:191443707b391232ed15676bf6bba7e53caec1e71aafa12ccad2e825c5ee15cc", size = 111638, upload-time = "2025-11-10T20:29:15.199Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/89/2d647bd717da55a8cc68602b197f53a5fa36fb95a2f9e76c4aff11a9cfd1/boto3-1.42.84.tar.gz", hash = "sha256:6a84b3293a5d8b3adf827a54588e7dcffcf0a85410d7dadca615544f97d27579", size = 112816, upload-time = "2026-04-06T19:39:07.585Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/cf/e24d08b37cd318754a8e94906c8b34b88676899aad1907ff6942311f13c4/boto3-1.40.70-py3-none-any.whl", hash = "sha256:e8c2f4f4cb36297270f1023ebe5b100333e0e88ab6457a9687d80143d2e15bf9", size = 139358, upload-time = "2025-11-10T20:29:13.512Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/31/cdf4326841613d1d181a77b3038a988800fb3373ca50de1639fba9fa87de/boto3-1.42.84-py3-none-any.whl", hash = "sha256:4d03ad3211832484037337292586f71f48707141288d9ac23049c04204f4ab03", size = 140555, upload-time = "2026-04-06T19:39:06.009Z" },
]
[[package]]
@@ -646,16 +663,16 @@ bedrock-runtime = [
[[package]]
name = "botocore"
-version = "1.40.70"
+version = "1.42.84"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/35/c1/8c4c199ae1663feee579a15861e34f10b29da11ae6ea0ad7b6a847ef3823/botocore-1.40.70.tar.gz", hash = "sha256:61b1f2cecd54d1b28a081116fa113b97bf4e17da57c62ae2c2751fe4c528af1f", size = 14444592, upload-time = "2025-11-10T20:29:04.046Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b4/b7/1c03423843fb0d1795b686511c00ee63fed1234c2400f469aeedfd42212f/botocore-1.42.84.tar.gz", hash = "sha256:234064604c80d9272a5e9f6b3566d260bcaa053a5e05246db90d7eca1c2cf44b", size = 15148615, upload-time = "2026-04-06T19:38:56.673Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/55/d2/507fd0ee4dd574d2bdbdeac5df83f39d2cae1ffe97d4622cca6f6bab39f1/botocore-1.40.70-py3-none-any.whl", hash = "sha256:4a394ad25f5d9f1ef0bed610365744523eeb5c22de6862ab25d8c93f9f6d295c", size = 14106829, upload-time = "2025-11-10T20:29:01.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/37/0c0c90361c8a1b9e6c75222ca24ae12996a298c0e18822a72ab229c37207/botocore-1.42.84-py3-none-any.whl", hash = "sha256:15f3fe07dfa6545e46a60c4b049fe2bdf63803c595ae4a4eec90e8f8172764f3", size = 14827061, upload-time = "2026-04-06T19:38:53.613Z" },
]
[[package]]
@@ -672,7 +689,7 @@ wheels = [
[[package]]
name = "browserbase"
-version = "1.7.0"
+version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -682,9 +699,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d8/72/27d4ca6fec8d107f3ee905675ce7a48b47fcf7918a5ce17fdbe40846beef/browserbase-1.7.0.tar.gz", hash = "sha256:e5b7acd33fad07666c1b9c7a33acea14d46a1693adaf5620c52839a746a342b8", size = 143680, upload-time = "2026-03-16T21:01:26.837Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/07/4ab4b91921833d0fb1731940d74141396d83120821f4c85482ed80bb2457/browserbase-1.8.0.tar.gz", hash = "sha256:dc62910c2f1fab3e944f338af9fbf82f53bbffcb3aeb6382b4e435a752383011", size = 147213, upload-time = "2026-04-06T19:31:26.848Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/93/59/ae53543ca44b232f64f18413eaf5c3eb968d690ae6960ffb4b4d1a9449d9/browserbase-1.7.0-py3-none-any.whl", hash = "sha256:6ff0ad602f18a7b2034e9e564fbaee05f02954456f1709fc36061f53755356ce", size = 107840, upload-time = "2026-03-16T21:01:25.698Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/c3/a29e57566c52fdb24712dcbb93a9bc97937c0c75874d8880a41a651daa5c/browserbase-1.8.0-py3-none-any.whl", hash = "sha256:4c4215973cc99f2f6d34550ae105c3f1f83b5fe22df2845bea0920b10f809526", size = 110012, upload-time = "2026-04-06T19:31:25.765Z" },
]
[[package]]
@@ -703,6 +720,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl", hash = "sha256:7a4d8651ea877cb2a89458b1b198f2e69f536c95e89129dbf5d448045d60db88", size = 24643, upload-time = "2026-03-25T14:20:26.568Z" },
]
+[[package]]
+name = "cachecontrol"
+version = "0.14.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "msgpack" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
+]
+
+[package.optional-dependencies]
+filecache = [
+ { name = "filelock" },
+]
+
[[package]]
name = "cachetools"
version = "7.0.5"
@@ -870,7 +905,8 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "build" },
- { name = "grpcio" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "httpx" },
{ name = "importlib-resources" },
{ name = "jsonschema" },
@@ -1294,20 +1330,20 @@ watson = [
[package.metadata]
requires-dist = [
{ name = "a2a-sdk", marker = "extra == 'a2a'", specifier = "~=0.3.10" },
- { name = "aiobotocore", marker = "extra == 'aws'", specifier = "~=2.25.2" },
+ { name = "aiobotocore", marker = "extra == 'aws'", specifier = "~=3.4.0" },
{ name = "aiocache", extras = ["memcached", "redis"], marker = "extra == 'a2a'", specifier = "~=0.12.3" },
{ name = "aiofiles", specifier = "~=24.1.0" },
{ name = "aiosqlite", specifier = "~=0.21.0" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.73.0" },
{ name = "appdirs", specifier = "~=1.4.4" },
{ name = "azure-ai-inference", marker = "extra == 'azure-ai-inference'", specifier = "~=1.0.0b9" },
- { name = "boto3", marker = "extra == 'aws'", specifier = "~=1.40.38" },
- { name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.40.45" },
+ { name = "boto3", marker = "extra == 'aws'", specifier = "~=1.42.79" },
+ { name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.42.79" },
{ name = "chromadb", specifier = "~=1.1.0" },
{ name = "click", specifier = "~=8.1.7" },
{ name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" },
{ name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" },
- { name = "docling", marker = "extra == 'docling'", specifier = "~=2.75.0" },
+ { name = "docling", marker = "extra == 'docling'", specifier = "~=2.84.0" },
{ name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.65.0" },
{ name = "httpx", specifier = "~=0.28.1" },
{ name = "httpx-auth", marker = "extra == 'a2a'", specifier = "~=0.23.1" },
@@ -1321,7 +1357,7 @@ requires-dist = [
{ name = "litellm", marker = "extra == 'litellm'", specifier = "~=1.83.0" },
{ name = "mcp", specifier = "~=1.26.0" },
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = "~=0.1.94" },
- { name = "openai", specifier = ">=1.83.0,<3" },
+ { name = "openai", specifier = ">=2.0.0,<3" },
{ name = "openpyxl", specifier = "~=3.1.5" },
{ name = "openpyxl", marker = "extra == 'openpyxl'", specifier = "~=3.1.5" },
{ name = "opentelemetry-api", specifier = "~=1.34.0" },
@@ -1343,7 +1379,7 @@ requires-dist = [
{ name = "tokenizers", specifier = ">=0.21,<1" },
{ name = "tomli", specifier = "~=2.0.2" },
{ name = "tomli-w", specifier = "~=1.1.0" },
- { name = "uv", specifier = "~=0.9.13" },
+ { name = "uv", specifier = "~=0.11.6" },
{ name = "voyageai", marker = "extra == 'voyageai'", specifier = "~=0.3.5" },
]
provides-extras = ["a2a", "anthropic", "aws", "azure-ai-inference", "bedrock", "docling", "embeddings", "file-processing", "google-genai", "litellm", "mem0", "openpyxl", "pandas", "qdrant", "qdrant-edge", "tools", "voyageai", "watson"]
@@ -1357,7 +1393,7 @@ dependencies = [
{ name = "pygithub" },
{ name = "python-dotenv" },
{ name = "rich" },
- { name = "toml" },
+ { name = "tomlkit" },
]
[package.metadata]
@@ -1367,7 +1403,7 @@ requires-dist = [
{ name = "pygithub", specifier = "~=1.59.1" },
{ name = "python-dotenv", specifier = "~=1.1.1" },
{ name = "rich", specifier = ">=13.9.4" },
- { name = "toml", specifier = "~=0.10.2" },
+ { name = "tomlkit", specifier = "~=0.13.2" },
]
[[package]]
@@ -1389,7 +1425,7 @@ requires-dist = [
{ name = "aiofiles", specifier = "~=24.1.0" },
{ name = "av", specifier = "~=13.0.0" },
{ name = "pillow", specifier = "~=12.1.1" },
- { name = "pypdf", specifier = "~=6.9.1" },
+ { name = "pypdf", specifier = "~=6.10.0" },
{ name = "python-magic", specifier = ">=0.4.27" },
{ name = "tinytag", specifier = "~=2.2.1" },
]
@@ -1517,7 +1553,8 @@ tavily-python = [
{ name = "tavily-python" },
]
weaviate-client = [
- { name = "weaviate-client" },
+ { name = "weaviate-client", version = "4.16.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "weaviate-client", version = "4.18.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
]
xml = [
{ name = "unstructured", extra = ["all-docs", "local-inference"] },
@@ -1560,7 +1597,7 @@ requires-dist = [
{ name = "python-docx", marker = "extra == 'rag'", specifier = ">=1.1.0" },
{ name = "pytube", specifier = "~=15.0.0" },
{ name = "qdrant-client", marker = "extra == 'qdrant-client'", specifier = ">=1.12.1" },
- { name = "requests", specifier = "~=2.32.5" },
+ { name = "requests", specifier = ">=2.33.0,<3" },
{ name = "scrapegraph-py", marker = "extra == 'scrapegraph-py'", specifier = ">=1.9.0" },
{ name = "scrapfly-sdk", marker = "extra == 'scrapfly-sdk'", specifier = ">=0.8.19" },
{ name = "selenium", marker = "extra == 'selenium'", specifier = ">=4.27.1" },
@@ -1582,48 +1619,48 @@ provides-extras = ["apify", "beautifulsoup4", "bedrock", "browserbase", "composi
[[package]]
name = "cryptography"
-version = "46.0.6"
+version = "46.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
- { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
- { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
- { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
- { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
- { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
- { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
- { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
- { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
- { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
- { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
- { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
- { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
- { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
- { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
- { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
- { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
- { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
- { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
- { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
- { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
- { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
- { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
- { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
- { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
- { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
- { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
- { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
- { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" },
- { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
- { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
- { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
- { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
+ { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
+ { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
+ { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
+ { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
+ { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
+ { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
+ { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
+ { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
]
[[package]]
@@ -1646,10 +1683,10 @@ wheels = [
[[package]]
name = "cuda-pathfinder"
-version = "1.5.0"
+version = "1.5.2"
source = { registry = "https://pypi.org/simple" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/93/66/0c02bd330e7d976f83fa68583d6198d76f23581bcbb5c0e98a6148f326e5/cuda_pathfinder-1.5.0-py3-none-any.whl", hash = "sha256:498f90a9e9de36044a7924742aecce11c50c49f735f1bc53e05aa46de9ea4110", size = 49739, upload-time = "2026-03-24T21:14:30.869Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f9/1b9b60a30fc463c14cdea7a77228131a0ccc89572e8df9cb86c9648271ab/cuda_pathfinder-1.5.2-py3-none-any.whl", hash = "sha256:0c5f160a7756c5b072723cbbd6d861e38917ef956c68150b02f0b6e9271c71fa", size = 49988, upload-time = "2026-04-06T23:01:05.17Z" },
]
[[package]]
@@ -1704,6 +1741,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
+[[package]]
+name = "cyclonedx-python-lib"
+version = "9.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "license-expression" },
+ { name = "packageurl-python" },
+ { name = "py-serializable" },
+ { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/fc/abaad5482f7b59c9a0a9d8f354ce4ce23346d582a0d85730b559562bbeb4/cyclonedx_python_lib-9.1.0.tar.gz", hash = "sha256:86935f2c88a7b47a529b93c724dbd3e903bc573f6f8bd977628a7ca1b5dadea1", size = 1048735, upload-time = "2025-02-27T17:23:40.367Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/f1/f3be2e9820a2c26fa77622223e91f9c504e1581830930d477e06146073f4/cyclonedx_python_lib-9.1.0-py3-none-any.whl", hash = "sha256:55693fca8edaecc3363b24af14e82cc6e659eb1e8353e58b587c42652ce0fb52", size = 374968, upload-time = "2025-02-27T17:23:37.766Z" },
+]
+
[[package]]
name = "databricks-sdk"
version = "0.102.0"
@@ -1820,7 +1872,7 @@ wheels = [
[[package]]
name = "docling"
-version = "2.75.0"
+version = "2.84.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accelerate" },
@@ -1851,17 +1903,19 @@ dependencies = [
{ name = "rtree" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "torch" },
+ { name = "torchvision" },
{ name = "tqdm" },
{ name = "typer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/77/0b/8ea363fd3c8bb4facb8d3c37aebfe7ad5265fecc1c6bd40f979d1f6179ba/docling-2.75.0.tar.gz", hash = "sha256:1b0a77766e201e5e2d118e236c006f3814afcea2e13726fb3c7389d666a56622", size = 364929, upload-time = "2026-02-24T20:18:04.896Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/1f/85560d7ba90a20f46c65396b45990fad34b7c95da23ca6e547456631d0e6/docling-2.84.0.tar.gz", hash = "sha256:007b0bad3c0ec45dc91af6083cbe1f0a93ddef1686304f466e8a168a1fb1dccb", size = 425470, upload-time = "2026-04-01T18:36:31.377Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b8/85/5c6885547ce5cde33af43201e3b2b04cf2360e6854abc07485f54b8d265d/docling-2.75.0-py3-none-any.whl", hash = "sha256:6e156f0326edb6471fc076e978ac64f902f54aac0da13cf89df456013e377bcc", size = 396243, upload-time = "2026-02-24T20:18:03.57Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e1/054e6ddf45e5760d51053b93b1a4f8be1568882b50c5ceeb88e6adaa6918/docling-2.84.0-py3-none-any.whl", hash = "sha256:ee431e5bb20cbebdd957f6173918f133d769340462814f3479df3446743d240e", size = 451391, upload-time = "2026-04-01T18:36:29.379Z" },
]
[[package]]
name = "docling-core"
-version = "2.71.0"
+version = "2.73.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "defusedxml" },
@@ -1876,9 +1930,9 @@ dependencies = [
{ name = "typer" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/5e/0e5463bcbb2de3ae8f35f76a1e98b201b373b71783120f57daa4d5bc4683/docling_core-2.71.0.tar.gz", hash = "sha256:4caa9f50c68b9dd332584ae16170b36db05d773532b14d7078b580d89d8bd2a4", size = 302901, upload-time = "2026-03-30T15:48:20.997Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/e3/b9c3b1a1ea62e5e03d9e844a5cff2f89b7a3e960725a862f009e8553ca3d/docling_core-2.73.0.tar.gz", hash = "sha256:33ffc2b2bf736ed0e079bba296081a26885f6cb08081c828d630ca85a51e22e0", size = 308895, upload-time = "2026-04-09T08:08:51.573Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/50/5d/604cd8d076cacea11018e20c461bad6df1b769e1aa901b70d06bca33b0f6/docling_core-2.71.0-py3-none-any.whl", hash = "sha256:4761857816853b2b35263b5b4518e1ea6214e0565db0bbf1d929fb976665d1a0", size = 268049, upload-time = "2026-03-30T15:48:18.998Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/c3/08143b7e8fe1b9230ce15e54926859f8c40ec2622fb612f0b2ff13169696/docling_core-2.73.0-py3-none-any.whl", hash = "sha256:4366fab8f4422fbde090ed87d9b091bd25b3b37cdd284dc0b02c9a5e24caaa22", size = 271518, upload-time = "2026-04-09T08:08:49.838Z" },
]
[package.optional-dependencies]
@@ -1919,7 +1973,7 @@ wheels = [
[[package]]
name = "docling-parse"
-version = "5.7.0"
+version = "5.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docling-core" },
@@ -1928,24 +1982,24 @@ dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "tabulate" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/22/ce/2dff1c13dffd5557833b83697556126cbe78ad3d60adfbd9c915e6b8b464/docling_parse-5.7.0.tar.gz", hash = "sha256:c77209c2e093ca5f8266952bd13b95aef09dfa38e6995ecf855971819786c93d", size = 64359331, upload-time = "2026-04-01T08:46:45.447Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/57/7b98e3ccf1ed40977bf832f028c68c248b0df1c25a5a33a50c2b2943ea72/docling_parse-5.8.0.tar.gz", hash = "sha256:cbb1d591dd94edab4ab3b81e9e42a3e4c7fe9ab3c3e690dccd498602aae63c5a", size = 65990181, upload-time = "2026-04-08T09:41:39.651Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/22/7b/79a3aadb6b58b1e29660db833202d40a648a032475f52dadd994bc6a778e/docling_parse-5.7.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e4d218e0983cdf447eb994b657fed7ba9b324ab2544b7a004ef97736b3b44b7c", size = 8531704, upload-time = "2026-04-01T08:46:04.047Z" },
- { url = "https://files.pythonhosted.org/packages/16/ff/08d6c25131e1dc8ab9cc745ea7b86168be9367c094389c98b29ed62152d0/docling_parse-5.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78631d7a9dafe716fb92af00199a585e9959454dd87d178d82ad583cc62af68c", size = 9303534, upload-time = "2026-04-01T08:46:06.096Z" },
- { url = "https://files.pythonhosted.org/packages/a8/20/ecd4da5492d6fafae8402d79251c389ac74e428bcab98c9c32a5d7439157/docling_parse-5.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f78f8a570bb33e9557ec3c93e4939bec8bf4d9d96032e34616a877a3bda84f", size = 9544737, upload-time = "2026-04-01T08:46:08.458Z" },
- { url = "https://files.pythonhosted.org/packages/54/cb/175436f1fb29a5338bc6cc32a88ab319910dec55bf873f35cf4f8221cc2f/docling_parse-5.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:8acf03df37e475c523d3e2fd9101ec21f4f7de532adc4dd7b9394890dcc0547c", size = 10349252, upload-time = "2026-04-01T08:46:10.559Z" },
- { url = "https://files.pythonhosted.org/packages/61/90/164b10d24064e3186ba679b80f118a09644f67e938a90324d3a9b1294d64/docling_parse-5.7.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4a4df3a79b413e2fcaa9f4494c355045778b18fd71db070e6f9166e19d00b193", size = 8533116, upload-time = "2026-04-01T08:46:12.367Z" },
- { url = "https://files.pythonhosted.org/packages/44/c1/5181c34b2c6841222fff3a4a4ad082b4441c33a7e47227d21582021e7ed6/docling_parse-5.7.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fcab1f5c4a82925305897d198ad19a27e05a6859fe0c917c321040490d968dc", size = 9261386, upload-time = "2026-04-01T08:46:14.248Z" },
- { url = "https://files.pythonhosted.org/packages/62/1a/8dd86721b8dc653e750e1531359abb0548568a92c08d781348fafb17ff29/docling_parse-5.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af12d1a011687cb46a0879d4b6dcb8534be393cb70de5d7428a335706af53dcc", size = 9592996, upload-time = "2026-04-01T08:46:15.881Z" },
- { url = "https://files.pythonhosted.org/packages/bf/c4/744e9f6150c7373d6ffa61ebed7957819f4c0e00c6794ea1473f9a11c799/docling_parse-5.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:660bbcc1fe7736289cb1e57ea8f770266e7095c3708e40b35b3c0e7d9ca08d81", size = 10350448, upload-time = "2026-04-01T08:46:17.948Z" },
- { url = "https://files.pythonhosted.org/packages/97/9d/14269974385ae0b1d6fb31df0224e0ae83aefb9931288282222f908fd704/docling_parse-5.7.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a645b47bc637a63e87b86b3995fe319b63be116e1b7bc9ec1fd44edb00356f6d", size = 8534658, upload-time = "2026-04-01T08:46:19.878Z" },
- { url = "https://files.pythonhosted.org/packages/1b/d1/f2a7815da9c8df51306fe941b4c829fa53bdaf866331caa0917508c1bade/docling_parse-5.7.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7503f5321ef94b455c4cd56e3d437699205d2150f2f3c93889dd64309b34d342", size = 9262244, upload-time = "2026-04-01T08:46:21.623Z" },
- { url = "https://files.pythonhosted.org/packages/3b/e6/17d7c19e4e4193aec5219ebbb4a8baf0afafa6d82c11df04a05e8483c759/docling_parse-5.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92e819292ab3ee2852a296b0189dfa972916446518fe977eefdfb2ea6823d86e", size = 9595224, upload-time = "2026-04-01T08:46:25.001Z" },
- { url = "https://files.pythonhosted.org/packages/e5/b1/9f9a1006de94e6775b2a332fd72a5d91478e4a9eda878a369d33e0ab23a6/docling_parse-5.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:763b53a30ea171e3a58f92d2892682692ae6a34001dfcad4f01806c18cbd021b", size = 10351618, upload-time = "2026-04-01T08:46:26.878Z" },
- { url = "https://files.pythonhosted.org/packages/c9/da/d781ee9da13b4d952e3baf5d7d01f429d60afe30ef90b1d70afc5960613c/docling_parse-5.7.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d480fff217fc62183ca97259347c09f46e7539fcacedfb860ecdae628c0247a0", size = 8534712, upload-time = "2026-04-01T08:46:28.887Z" },
- { url = "https://files.pythonhosted.org/packages/a6/23/4205b2d8e0007d18d2bef7c67257272594f23a26882acdec06b13aabe858/docling_parse-5.7.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2247152e4438d01cc51bc9d5d6524a8da06362d3a80ec84397f6b3b414b577f", size = 9263031, upload-time = "2026-04-01T08:46:30.859Z" },
- { url = "https://files.pythonhosted.org/packages/01/61/8fbe76e34cd6715a5974f599ca1524f730847d6eebe73f7a230f391fab9b/docling_parse-5.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41785ee7b472d7a688f183e33c927c6b364ac8432898ff4616b99de1b1ae518d", size = 9595643, upload-time = "2026-04-01T08:46:32.819Z" },
- { url = "https://files.pythonhosted.org/packages/ee/62/6607673219fa157628f5c2ccb7e8bf1715f36c54cebaf46f031cc1bd6727/docling_parse-5.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:f122a81390e2869e03cf110de0ff4db6f5c57ce7d95def82fe0c5f1c3838fdf7", size = 10351630, upload-time = "2026-04-01T08:46:35.132Z" },
+ { url = "https://files.pythonhosted.org/packages/06/38/02a686660fe89a6f6775618ae43f9d4b76f615edc7374a1e8e1bf648fb73/docling_parse-5.8.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:241d09a904d8e4b70a2c040252a75a088e971a7926a46973389cb3235a5cab74", size = 8539476, upload-time = "2026-04-08T09:40:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/38/ebd2fd850eef60d9c201cfb28b24bc3c8a27efeb34e817c12f544453a3c2/docling_parse-5.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e81da134baff612ea38ff0af3bf17deef196195d2415bfcf4f531bc7d0dd84", size = 9311993, upload-time = "2026-04-08T09:40:55.362Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/ba/c05c35a75b358ddaafdf0cd1e3f3737091722c6547b692cd66a99071159a/docling_parse-5.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b149bd7eeb91a5c6bdbc4a9bd87055a2a06d9ea959bf34d309580c1722d2e2b9", size = 9553650, upload-time = "2026-04-08T09:40:57.636Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/3670258908f6e5cf04251b9547967ebbf28211e29ede30eb5da41e0b509a/docling_parse-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac2c03347de9a0f02cdd46385ee4ae05f91eefc72aeac4749389d17f661dd7d5", size = 10357004, upload-time = "2026-04-08T09:40:59.921Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/09/57e47cc861f4e98201d6b881c6a7683e84f8ad20e2c1d619fe94c39ab7f2/docling_parse-5.8.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:fd1ae1cc22a96ccef76f82756ff7958d2a1eb38804e7cd9eed6ae951e2480c30", size = 8540650, upload-time = "2026-04-08T09:41:01.933Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/55/0265703d03377ad7ad3c4d482b00265275061ac15470dc815815944637cf/docling_parse-5.8.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3908496e6949d2e56e361fc743a8f9248cb0f76807a1860027dde02be14f854", size = 9269550, upload-time = "2026-04-08T09:41:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/96/03/962449ed1b6692e16c3cae0cf00fd60145d620dd1886aedacd1636727dec/docling_parse-5.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:860fbd5f2d30774d1c739d373aec14b7e074fdace191e5ac16750e7b14f136f4", size = 9601965, upload-time = "2026-04-08T09:41:06.807Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/18/5bee07b6ef6451b71904e0d21d7721af964fd92f3465305ef791d7a3cf56/docling_parse-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:854630f6ef7889d1757611194330d88fbbe53c0b202b5a010a467bf059f715da", size = 10358059, upload-time = "2026-04-08T09:41:09.049Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/61/3038e3a759df3aff0f02628eaeb71f6068b428ddd62981e639c5acf1eca8/docling_parse-5.8.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a37c8c0aab730a9857c726420925cccc304a16abd91f054b25726394ee1ac836", size = 8541739, upload-time = "2026-04-08T09:41:11.525Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/98/b9307f84a7753cc369bbdd81f0183f308e8be1efeb2998193a494f8a8f44/docling_parse-5.8.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2c7455b058525cdd46d4c6b7c429871f096aa7718ce1b8481dae426358cf29", size = 9269677, upload-time = "2026-04-08T09:41:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/a6/686adf6ed39d9de9912b233b8d0bd4f5e8113023aef47630ffde12ff0ba4/docling_parse-5.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:987d8eacb0f515f53a860329acc5c826487a9d2ff4430f08bd37498854cdab42", size = 9604016, upload-time = "2026-04-08T09:41:15.762Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/1b/90c5447a00a652a81e2b4fea86b33a694b1e0fec3b9fb1862f9b6f48f54a/docling_parse-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f72b0fdd370e825777f7a9989c390c630774870390c7277b7f016bfae395d6a", size = 10360133, upload-time = "2026-04-08T09:41:18.085Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c9/799cc497b71537bafb6b8bf66fcccf303f8a84684503e8783d489db03aab/docling_parse-5.8.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:292b82a9773c66a76e5ee376cfdde4a4d6a8edae6a4493aba4013d939e7a213f", size = 8541804, upload-time = "2026-04-08T09:41:20.358Z" },
+ { url = "https://files.pythonhosted.org/packages/93/29/1030c13b257be7a4317bc7837c22366eff6d961ca6d6604b426dc8a9adcd/docling_parse-5.8.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85c896983aaa7b95f409ed52014da59a945f2b914291c0782740e6a5b6d39028", size = 9269366, upload-time = "2026-04-08T09:41:22.437Z" },
+ { url = "https://files.pythonhosted.org/packages/54/22/40990653103c2eb83b073d2aca47aa95b767f1360214fca4c6339df105c3/docling_parse-5.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d9139f8da5e6553a36afb40dba614011ebd1bf97e5d17896ace07191a289c4b", size = 9604422, upload-time = "2026-04-08T09:41:24.619Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/9e/4ab1b16f6ba17f9695df79faa08a332b09a2d333d609036a7d0106538d57/docling_parse-5.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7343ee48b0480593ed08b04ed0b09421724a6dec63d82c23fac436129b32c66a", size = 10360242, upload-time = "2026-04-08T09:41:27.132Z" },
]
[[package]]
@@ -2011,7 +2065,7 @@ wheels = [
[[package]]
name = "exa-py"
-version = "2.10.2"
+version = "2.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpcore" },
@@ -2022,9 +2076,9 @@ dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fe/4f/f06a6f277d668f143e330fe503b0027cc5fed753b22c3e161f8cbbccdf65/exa_py-2.10.2.tar.gz", hash = "sha256:f781f30b199f1102333384728adae64bb15a6bbcabfa97e91fd705f90acffc45", size = 53792, upload-time = "2026-03-26T20:29:35.764Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c5/08/af21dace845b5cd67d728e9d7747e4d1024ec90bd83e007d78f969dc6e19/exa_py-2.11.0.tar.gz", hash = "sha256:989103cbd83aae6dbe88cb70e11522a4bb06026fdb54b8659e3a7922da41fc93", size = 54905, upload-time = "2026-04-04T00:04:32.455Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e2/bc/7a34e904a415040ba626948d0b0a36a08cd073f12b13342578a68331be3c/exa_py-2.10.2-py3-none-any.whl", hash = "sha256:ecb2a7581f4b7a8aeb6b434acce1bbc40f92ed1d4126b2aa6029913acd904a47", size = 72248, upload-time = "2026-03-26T20:29:37.306Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/c9/129dd486505e3c0dadda0d6c83c560060f76d4cf14ef4b7b93053846598a/exa_py-2.11.0-py3-none-any.whl", hash = "sha256:3b0070a6ce98e02895755f0f81752dff64e2e121cf9d9a82facf715a4b9a5238", size = 73424, upload-time = "2026-04-04T00:04:33.699Z" },
]
[[package]]
@@ -2050,14 +2104,14 @@ wheels = [
[[package]]
name = "faker"
-version = "40.12.0"
+version = "40.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c1/f8224fe97fea2f98d455c22438c1b09b10e14ef2cb95ae4f7cec9aa59659/faker-40.12.0.tar.gz", hash = "sha256:58b5a9054c367bd5fb2e948634105364cc570e78a98a8e5161a74691c45f158f", size = 1962003, upload-time = "2026-03-30T18:00:56.596Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/95/4822ffe94723553789aef783104f4f18fc20d7c4c68e1bbd633e11d09758/faker-40.13.0.tar.gz", hash = "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", size = 1962043, upload-time = "2026-04-06T16:44:55.68Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/5c/39452a6b6aa76ffa518fa7308e1975b37e9ba77caa6172a69d61e7180221/faker-40.12.0-py3-none-any.whl", hash = "sha256:6238a4058a8b581892e3d78fe5fdfa7568739e1c8283e4ede83f1dde0bfc1a3b", size = 1994601, upload-time = "2026-03-30T18:00:54.804Z" },
+ { url = "https://files.pythonhosted.org/packages/da/8a/708103325edff16a0b0e004de0d37db8ba216a32713948c64d71f6d4a4c2/faker-40.13.0-py3-none-any.whl", hash = "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019", size = 1994597, upload-time = "2026-04-06T16:44:53.698Z" },
]
[[package]]
@@ -2182,7 +2236,7 @@ wheels = [
[[package]]
name = "firecrawl-py"
-version = "4.21.1"
+version = "4.22.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -2193,9 +2247,9 @@ dependencies = [
{ name = "requests" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9c/3e/a2426b461e57f10327ba8ec56511a0ab0a817a433c933380c61b80e9b5c3/firecrawl_py-4.21.1.tar.gz", hash = "sha256:e82eab65ee4d46f38293c30d43e065a78d40ec9efd2872dd543c58e03ea58b54", size = 174335, upload-time = "2026-04-02T18:29:01.975Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/64/87/08cd440a3b942be5983c1a2db921d55697bdb91f7ead9a925b75715039a0/firecrawl_py-4.22.1.tar.gz", hash = "sha256:fb44d4c63ba91c076ae2f0b688f1556327c971baea45e7fb67d6ed5d393542a2", size = 174394, upload-time = "2026-04-07T01:54:19.682Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/ee/b1030a89dc28f0f54bb8bf387333bc67d905920b2165cb0fa94692f3c6b3/firecrawl_py-4.21.1-py3-none-any.whl", hash = "sha256:b54c645ae7cb73f2a683c4448cc0dfc195eea6948ef529be5ba52f0ec2210366", size = 217676, upload-time = "2026-04-02T18:29:00.433Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a7/54199470a5bf8e09bdf9511f80e766a11b20daafc3b0e1e638ec04e24fc9/firecrawl_py-4.22.1-py3-none-any.whl", hash = "sha256:3df92a7888f9d5907a6fbbe50ade330d2925f5bf51f8efa507c2ab9891df9a0a", size = 217741, upload-time = "2026-04-07T01:54:18.403Z" },
]
[[package]]
@@ -2380,7 +2434,7 @@ wheels = [
[[package]]
name = "google-api-core"
-version = "2.30.2"
+version = "2.30.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
@@ -2389,28 +2443,29 @@ dependencies = [
{ name = "protobuf" },
{ name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1a/2e/83ca41eb400eb228f9279ec14ed66f6475218b59af4c6daec2d5a509fe83/google_api_core-2.30.2.tar.gz", hash = "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", size = 176862, upload-time = "2026-04-02T21:23:44.876Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/84/e1/ebd5100cbb202e561c0c8b59e485ef3bd63fa9beb610f3fdcaea443f0288/google_api_core-2.30.2-py3-none-any.whl", hash = "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594", size = 173236, upload-time = "2026-04-02T21:23:06.395Z" },
+ { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" },
]
[package.optional-dependencies]
grpc = [
- { name = "grpcio" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "grpcio-status" },
]
[[package]]
name = "google-auth"
-version = "2.49.1"
+version = "2.49.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1-modules" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" },
+ { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" },
]
[package.optional-dependencies]
@@ -2425,7 +2480,8 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
{ name = "google-auth" },
- { name = "grpcio" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "proto-plus" },
{ name = "protobuf" },
]
@@ -2469,53 +2525,64 @@ wheels = [
[[package]]
name = "greenlet"
-version = "3.3.2"
+version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" },
- { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" },
- { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" },
- { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" },
- { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" },
- { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" },
- { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" },
- { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" },
- { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" },
- { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" },
- { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" },
- { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" },
- { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" },
- { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" },
- { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" },
- { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" },
- { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" },
- { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
- { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
- { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
- { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
- { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
- { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
- { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
- { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" },
- { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" },
- { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
- { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
- { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
- { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" },
- { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
- { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
- { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
- { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" },
- { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/bc/e30e1e3d5e8860b0e0ce4d2b16b2681b77fd13542fc0d72f7e3c22d16eff/greenlet-3.4.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d18eae9a7fb0f499efcd146b8c9750a2e1f6e0e93b5a382b3481875354a430e6", size = 284315, upload-time = "2026-04-08T17:02:52.322Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/cc/e023ae1967d2a26737387cac083e99e47f65f58868bd155c4c80c01ec4e0/greenlet-3.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636d2f95c309e35f650e421c23297d5011716be15d966e6328b367c9fc513a82", size = 601916, upload-time = "2026-04-08T16:24:35.533Z" },
+ { url = "https://files.pythonhosted.org/packages/67/32/5be1677954b6d8810b33abe94e3eb88726311c58fa777dc97e390f7caf5a/greenlet-3.4.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234582c20af9742583c3b2ddfbdbb58a756cfff803763ffaae1ac7990a9fac31", size = 616399, upload-time = "2026-04-08T16:30:54.536Z" },
+ { url = "https://files.pythonhosted.org/packages/82/0a/3a4af092b09ea02bcda30f33fd7db397619132fe52c6ece24b9363130d34/greenlet-3.4.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ac6a5f618be581e1e0713aecec8e54093c235e5fa17d6d8eb7ffc487e2300508", size = 621077, upload-time = "2026-04-08T16:40:34.946Z" },
+ { url = "https://files.pythonhosted.org/packages/74/bf/2d58d5ea515704f83e34699128c9072a34bea27d2b6a556e102105fe62a5/greenlet-3.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:523677e69cd4711b5a014e37bc1fb3a29947c3e3a5bb6a527e1cc50312e5a398", size = 611978, upload-time = "2026-04-08T15:56:31.335Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/39/3786520a7d5e33ee87b3da2531f589a3882abf686a42a3773183a41ef010/greenlet-3.4.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:d336d46878e486de7d9458653c722875547ac8d36a1cff9ffaf4a74a3c1f62eb", size = 416893, upload-time = "2026-04-08T16:43:02.392Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/69/6525049b6c179d8a923256304d8387b8bdd4acab1acf0407852463c6d514/greenlet-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b45e45fe47a19051a396abb22e19e7836a59ee6c5a90f3be427343c37908d65b", size = 1571957, upload-time = "2026-04-08T16:26:17.041Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/6c/bbfb798b05fec736a0d24dc23e81b45bcee87f45a83cfb39db031853bddc/greenlet-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5434271357be07f3ad0936c312645853b7e689e679e29310e2de09a9ea6c3adf", size = 1637223, upload-time = "2026-04-08T15:57:27.556Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7d/981fe0e7c07bd9d5e7eb18decb8590a11e3955878291f7a7de2e9c668eb7/greenlet-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a19093fbad824ed7c0f355b5ff4214bffda5f1a7f35f29b31fcaa240cc0135ab", size = 237902, upload-time = "2026-04-08T17:03:14.16Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c6/dba32cab7e3a625b011aa5647486e2d28423a48845a2998c126dd69c85e1/greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58", size = 285504, upload-time = "2026-04-08T15:52:14.071Z" },
+ { url = "https://files.pythonhosted.org/packages/54/f4/7cb5c2b1feb9a1f50e038be79980dfa969aa91979e5e3a18fdbcfad2c517/greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6", size = 605476, upload-time = "2026-04-08T16:24:37.064Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/af/b66ab0b2f9a4c5a867c136bf66d9599f34f21a1bcca26a2884a29c450bd9/greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875", size = 618336, upload-time = "2026-04-08T16:30:56.59Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/31/56c43d2b5de476f77d36ceeec436328533bff960a4cba9a07616e93063ab/greenlet-3.4.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c5696c42e6bb5cfb7c6ff4453789081c66b9b91f061e5e9367fa15792644e76", size = 625045, upload-time = "2026-04-08T16:40:37.111Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/5c/8c5633ece6ba611d64bf2770219a98dd439921d6424e4e8cf16b0ac74ea5/greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83", size = 613515, upload-time = "2026-04-08T15:56:32.478Z" },
+ { url = "https://files.pythonhosted.org/packages/80/ca/704d4e2c90acb8bdf7ae593f5cbc95f58e82de95cc540fb75631c1054533/greenlet-3.4.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:89995ce5ddcd2896d89615116dd39b9703bfa0c07b583b85b89bf1b5d6eddf81", size = 419745, upload-time = "2026-04-08T16:43:04.022Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/df/950d15bca0d90a0e7395eb777903060504cdb509b7b705631e8fb69ff415/greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2", size = 1574623, upload-time = "2026-04-08T16:26:18.596Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/e7/0839afab829fcb7333c9ff6d80c040949510055d2d4d63251f0d1c7c804e/greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71", size = 1639579, upload-time = "2026-04-08T15:57:29.231Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/2b/b4482401e9bcaf9f5c97f67ead38db89c19520ff6d0d6699979c6efcc200/greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711", size = 238233, upload-time = "2026-04-08T17:02:54.286Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/4d/d8123a4e0bcd583d5cfc8ddae0bbe29c67aab96711be331a7cc935a35966/greenlet-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:04403ac74fe295a361f650818de93be11b5038a78f49ccfb64d3b1be8fbf1267", size = 235045, upload-time = "2026-04-08T17:04:05.072Z" },
+ { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" },
+ { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" },
+ { url = "https://files.pythonhosted.org/packages/44/57/eae2cac10421feae6c0987e3dc106c6d86262b1cb379e171b017aba893a6/greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d", size = 624901, upload-time = "2026-04-08T16:40:38.981Z" },
+ { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/8a/0e73c9b94f31d1cc257fe79a0eff621674141cdae7d6d00f40de378a1e42/greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077", size = 423927, upload-time = "2026-04-08T16:43:05.293Z" },
+ { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" },
+ { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" },
+ { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455, upload-time = "2026-04-08T16:40:40.698Z" },
+ { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238, upload-time = "2026-04-08T16:43:06.865Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" },
]
[[package]]
name = "grpcio"
version = "1.78.0"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine == 's390x'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x'",
+]
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" }
wheels = [
@@ -2561,13 +2628,83 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" },
]
+[[package]]
+name = "grpcio"
+version = "1.80.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x'",
+ "python_full_version < '3.11' and platform_machine != 's390x'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" },
+ { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" },
+ { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" },
+ { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" },
+ { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" },
+ { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" },
+ { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" },
+ { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" },
+ { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" },
+ { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" },
+ { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" },
+ { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" },
+ { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" },
+]
+
+[[package]]
+name = "grpcio-health-checking"
+version = "1.71.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "protobuf", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/53/86/20994347ef36b7626fb74539f13128100dd8b7eaac67efc063264e6cdc80/grpcio_health_checking-1.71.2.tar.gz", hash = "sha256:1c21ece88c641932f432b573ef504b20603bdf030ad4e1ec35dd7fdb4ea02637", size = 16770, upload-time = "2025-06-28T04:24:08.768Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/74/7bc6ab96bf1083cab2684f9c3ae434caa638de3d5c5574e8435e2c146598/grpcio_health_checking-1.71.2-py3-none-any.whl", hash = "sha256:f91db41410d6bd18a7828c5b6ac2bebd77a63483263cbe42bf3c0c9b86cece33", size = 18918, upload-time = "2025-06-28T04:23:56.923Z" },
+]
+
[[package]]
name = "grpcio-status"
version = "1.71.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
- { name = "grpcio" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/d1/b6e9877fedae3add1afdeae1f89d1927d296da9cf977eca0eb08fb8a460e/grpcio_status-1.71.2.tar.gz", hash = "sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50", size = 13677, upload-time = "2025-06-28T04:24:05.426Z" }
@@ -2735,21 +2872,22 @@ wheels = [
[[package]]
name = "huggingface-hub"
-version = "0.36.2"
+version = "1.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "fsspec" },
- { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
{ name = "packaging" },
{ name = "pyyaml" },
- { name = "requests" },
{ name = "tqdm" },
+ { name = "typer" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/28/baf5d745559503ce8d28cf5bc9551f5ac59158eafd7b6a6afff0bcdb0f50/huggingface_hub-1.10.1.tar.gz", hash = "sha256:696c53cf9c2ac9befbfb5dd41d05392a031c69fc6930d1ed9671debd405b6fff", size = 758094, upload-time = "2026-04-09T15:01:18.928Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
+ { url = "https://files.pythonhosted.org/packages/83/8c/c7a33f3efaa8d6a5bc40e012e5ecc2d72c2e6124550ca9085fe0ceed9993/huggingface_hub-1.10.1-py3-none-any.whl", hash = "sha256:6b981107a62fbe68c74374418983399c632e35786dcd14642a9f2972633c8b5a", size = 642630, upload-time = "2026-04-09T15:01:17.35Z" },
]
[[package]]
@@ -2766,7 +2904,7 @@ wheels = [
[[package]]
name = "hyperbrowser"
-version = "0.89.2"
+version = "0.90.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -2774,9 +2912,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e0/45/b31a6cd1a7db3ca41b986174013653893a1947af348835c23f22f997aac6/hyperbrowser-0.89.2.tar.gz", hash = "sha256:3f97f392b5394124fd8424fcf274c69a37317fc4c773923ef8fdf78f5b5406e0", size = 64130, upload-time = "2026-03-30T17:13:53.581Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/60/b651865b7154feb571980c7f3341c75275a7330d3980c6a328bd875eb1dc/hyperbrowser-0.90.1.tar.gz", hash = "sha256:987259a99a8fe740274bc87b9cd64430476588fb5467313537d746881703fe4c", size = 65524, upload-time = "2026-04-07T23:56:44.951Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/65/abbc6cc3446e174ba17aa145de85dc740fc241296e5e0fd443f7bf263922/hyperbrowser-0.89.2-py3-none-any.whl", hash = "sha256:7ae4a9eb155b2d984748224ded0e33bf5bd910779a326021f3060a2024e9e82c", size = 109655, upload-time = "2026-03-30T17:13:52.397Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/49/cca92edcbace09135bf6c13a15c1856357c1cf68185d09088937b0bfe1f2/hyperbrowser-0.90.1-py3-none-any.whl", hash = "sha256:831c4e9b3143d713b64dd69034936763c5d92dfbf18f2936bc33d72c066b6551", size = 110792, upload-time = "2026-04-07T23:56:43.626Z" },
]
[[package]]
@@ -3033,11 +3171,11 @@ wheels = [
[[package]]
name = "invoke"
-version = "2.2.1"
+version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" },
]
[[package]]
@@ -3419,7 +3557,7 @@ wheels = [
[[package]]
name = "langchain-core"
-version = "1.2.25"
+version = "1.2.28"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -3431,9 +3569,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/86/2a/d65de24fc9b7989137253da8973f850f3e39b4ce3e0377bc8200d6b3c189/langchain_core-1.2.25.tar.gz", hash = "sha256:77e032b96509d0eb1f6875042fdf97b7e2334a815314700c6894d9d078909b9c", size = 842347, upload-time = "2026-04-02T22:39:11.528Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/317a1a3ac1df33a64adb3670bf88bbe3b3d5baa274db6863a979db472897/langchain_core-1.2.28.tar.gz", hash = "sha256:271a3d8bd618f795fdeba112b0753980457fc90537c46a0c11998516a74dc2cb", size = 846119, upload-time = "2026-04-08T18:19:34.867Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3d/0e/7b31b0249f9b9b0fc7829d5b0ee484b8f8d43c78e376e9951e2ef3eac70c/langchain_core-1.2.25-py3-none-any.whl", hash = "sha256:0c05bf395aec6d2dfa14488fd006f7bcd0540e7e89287e04f92203532a82c828", size = 506866, upload-time = "2026-04-02T22:39:10.137Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/92/32f785f077c7e898da97064f113c73fbd9ad55d1e2169cf3a391b183dedb/langchain_core-1.2.28-py3-none-any.whl", hash = "sha256:80764232581eaf8057bcefa71dbf8adc1f6a28d257ebd8b95ba9b8b452e8c6ac", size = 508727, upload-time = "2026-04-08T18:19:32.823Z" },
]
[[package]]
@@ -3459,7 +3597,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2
[[package]]
name = "langsmith"
-version = "0.7.25"
+version = "0.7.31"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -3472,9 +3610,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7e/d7/21ffae5ccdc3c9b8de283e8f8bf48a92039681df0d39f15133d8ff8965bd/langsmith-0.7.25.tar.gz", hash = "sha256:d17da71f156ca69eafd28ac9627c8e0e93170260ec37cd27cedc83205a067598", size = 1145410, upload-time = "2026-04-03T13:11:42.36Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/29/13/67889d41baf7dbaf13ffd0b334a0f284e107fad1cc8782a1abb1e56e5eeb/langsmith-0.7.25-py3-none-any.whl", hash = "sha256:55ecc24c547f6c79b5a684ff8685c669eec34e52fcac5d2c0af7d613aef5a632", size = 359417, upload-time = "2026-04-03T13:11:40.729Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" },
]
[[package]]
@@ -3488,61 +3626,73 @@ wheels = [
[[package]]
name = "librt"
-version = "0.8.1"
+version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" },
- { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" },
- { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" },
- { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" },
- { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" },
- { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" },
- { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" },
- { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" },
- { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" },
- { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" },
- { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" },
- { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" },
- { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" },
- { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" },
- { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" },
- { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" },
- { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" },
- { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" },
- { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" },
- { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" },
- { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" },
- { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" },
- { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" },
- { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" },
- { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
- { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
- { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
- { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
- { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
- { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
- { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
- { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
- { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
- { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
- { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
- { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
- { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
- { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
- { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
- { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
- { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
- { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
- { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
- { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
- { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
- { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
- { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
- { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/4a/c64265d71b84030174ff3ac2cd16d8b664072afab8c41fccd8e2ee5a6f8d/librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443", size = 67529, upload-time = "2026-04-09T16:04:27.373Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b1/30ca0b3a8bdac209a00145c66cf42e5e7da2cc056ffc6ebc5c7b430ddd34/librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c", size = 70248, upload-time = "2026-04-09T16:04:28.758Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/fc/c6018dc181478d6ac5aa24a5846b8185101eb90894346db239eb3ea53209/librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e", size = 202184, upload-time = "2026-04-09T16:04:29.893Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/58/d69629f002203370ef41ea69ff71c49a2c618aec39b226ff49986ecd8623/librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285", size = 212926, upload-time = "2026-04-09T16:04:31.126Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/55/01d859f57824e42bd02465c77bec31fa5ef9d8c2bcee702ccf8ef1b9f508/librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2", size = 225664, upload-time = "2026-04-09T16:04:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/02/32f63ad0ef085a94a70315291efe1151a48b9947af12261882f8445b2a30/librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce", size = 219534, upload-time = "2026-04-09T16:04:33.667Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/5a/9d77111a183c885acf3b3b6e4c00f5b5b07b5817028226499a55f1fedc59/librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f", size = 227322, upload-time = "2026-04-09T16:04:34.945Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e7/05d700c93063753e12ab230b972002a3f8f3b9c95d8a980c2f646c8b6963/librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236", size = 223407, upload-time = "2026-04-09T16:04:36.22Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/26/26c3124823c67c987456977c683da9a27cc874befc194ddcead5f9988425/librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38", size = 221302, upload-time = "2026-04-09T16:04:37.62Z" },
+ { url = "https://files.pythonhosted.org/packages/50/2b/c7cc2be5cf4ff7b017d948a789256288cb33a517687ff1995e72a7eea79f/librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b", size = 243893, upload-time = "2026-04-09T16:04:38.909Z" },
+ { url = "https://files.pythonhosted.org/packages/62/d3/da553d37417a337d12660450535d5fd51373caffbedf6962173c87867246/librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774", size = 55375, upload-time = "2026-04-09T16:04:40.148Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/5a/46fa357bab8311b6442a83471591f2f9e5b15ecc1d2121a43725e0c529b8/librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8", size = 62581, upload-time = "2026-04-09T16:04:41.452Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" },
+ { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" },
+ { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" },
+ { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" },
+ { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" },
+ { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" },
+ { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" },
+ { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" },
+ { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" },
+ { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" },
+ { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" },
+]
+
+[[package]]
+name = "license-expression"
+version = "30.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "boolean-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" },
]
[[package]]
@@ -4121,11 +4271,11 @@ wheels = [
[[package]]
name = "more-itertools"
-version = "11.0.1"
+version = "11.0.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/24/e0acc4bf54cba50c1d432c70a72a3df96db4a321b2c4c68432a60759044f/more_itertools-11.0.1.tar.gz", hash = "sha256:fefaf25b7ab08f0b45fa9f1892cae93b9fc0089ef034d39213bce15f1cc9e199", size = 144739, upload-time = "2026-04-02T16:17:45.061Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/f4/5e52c7319b8087acef603ed6e50dc325c02eaa999355414830468611f13c/more_itertools-11.0.1-py3-none-any.whl", hash = "sha256:eaf287826069452a8f61026c597eae2428b2d1ba2859083abbf240b46842ce6d", size = 72182, upload-time = "2026-04-02T16:17:43.724Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" },
]
[[package]]
@@ -4156,6 +4306,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
+[[package]]
+name = "msgpack"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
+]
+
[[package]]
name = "msoffcrypto-tool"
version = "6.0.0"
@@ -4863,7 +5056,7 @@ wheels = [
[[package]]
name = "openai"
-version = "2.30.0"
+version = "2.31.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -4875,9 +5068,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/fe/64b3d035780b3188f86c4f6f1bc202e7bb74757ef028802112273b9dcacf/openai-2.31.0.tar.gz", hash = "sha256:43ca59a88fc973ad1848d86b98d7fac207e265ebbd1828b5e4bdfc85f79427a5", size = 684772, upload-time = "2026-04-08T21:01:41.797Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload-time = "2026-04-08T21:01:39.217Z" },
]
[[package]]
@@ -4955,7 +5148,8 @@ version = "1.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
- { name = "grpcio" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
@@ -5124,6 +5318,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/c1/88bf70a327c86f8529ad3a4ae35e92fcebf05295668fca7973279e189afe/oxylabs-2.0.0-py3-none-any.whl", hash = "sha256:3848d53bc47acdcea16ea829dc52416cdf96edae130e17bb3ac7146b012387d7", size = 34274, upload-time = "2025-03-28T13:54:15.188Z" },
]
+[[package]]
+name = "packageurl-python"
+version = "0.17.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" },
+]
+
[[package]]
name = "packaging"
version = "26.0"
@@ -5455,12 +5658,66 @@ wheels = [
]
[[package]]
-name = "platformdirs"
-version = "4.9.4"
+name = "pip"
+version = "26.0.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
+ { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
+]
+
+[[package]]
+name = "pip-api"
+version = "0.0.34"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pip" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" },
+]
+
+[[package]]
+name = "pip-audit"
+version = "2.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cachecontrol", extra = ["filecache"] },
+ { name = "cyclonedx-python-lib" },
+ { name = "packaging" },
+ { name = "pip-api" },
+ { name = "pip-requirements-parser" },
+ { name = "platformdirs" },
+ { name = "requests" },
+ { name = "rich" },
+ { name = "toml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/7f/28fad19a9806f796f13192ab6974c07c4a04d9cbb8e30dd895c3c11ce7ee/pip_audit-2.9.0.tar.gz", hash = "sha256:0b998410b58339d7a231e5aa004326a294e4c7c6295289cdc9d5e1ef07b1f44d", size = 52089, upload-time = "2025-04-07T16:45:23.679Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/9e/f4dfd9d3dadb6d6dc9406f1111062f871e2e248ed7b584cca6020baf2ac1/pip_audit-2.9.0-py3-none-any.whl", hash = "sha256:348b16e60895749a0839875d7cc27ebd692e1584ebe5d5cb145941c8e25a80bd", size = 58634, upload-time = "2025-04-07T16:45:22.056Z" },
+]
+
+[[package]]
+name = "pip-requirements-parser"
+version = "32.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "pyparsing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.9.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
]
[[package]]
@@ -5796,6 +6053,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/15/b1894b9741f7a48f0b4cbea458f7d4141a6df6a1b26bec05fcde96703ce1/py_rust_stemmers-0.1.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57b061c3b4af9e409d009d729b21bc53dabe47116c955ccf0b642a5a2d438f93", size = 324879, upload-time = "2025-02-19T13:56:27.462Z" },
]
+[[package]]
+name = "py-serializable"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "defusedxml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" },
+]
+
[[package]]
name = "pyarrow"
version = "23.0.1"
@@ -6460,43 +6729,43 @@ wheels = [
[[package]]
name = "pypdf"
-version = "6.9.2"
+version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/31/83/691bdb309306232362503083cb15777491045dd54f45393a317dc7d8082f/pypdf-6.9.2.tar.gz", hash = "sha256:7f850faf2b0d4ab936582c05da32c52214c2b089d61a316627b5bfb5b0dab46c", size = 5311837, upload-time = "2026-03-23T14:53:27.983Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/79/f2730c42ec7891a75a2fcea2eb4f356872bcbc671b711418060424796612/pypdf-6.10.1.tar.gz", hash = "sha256:62e6ca7f65aaa28b3d192addb44f97296e4be1748f57ed0f4efb2d4915841880", size = 5315704, upload-time = "2026-04-14T12:55:20.996Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a5/7e/c85f41243086a8fe5d1baeba527cb26a1918158a565932b41e0f7c0b32e9/pypdf-6.9.2-py3-none-any.whl", hash = "sha256:662cf29bcb419a36a1365232449624ab40b7c2d0cfc28e54f42eeecd1fd7e844", size = 333744, upload-time = "2026-03-23T14:53:26.573Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/04/e3aa7f1f14dbc53429cae34666261eb935d99bd61d24756ab94d7e0309da/pypdf-6.10.1-py3-none-any.whl", hash = "sha256:6331940d3bfe75b7e6601d35db7adabab5fc1d716efaeb384e3c0c3957d033de", size = 335606, upload-time = "2026-04-14T12:55:18.941Z" },
]
[[package]]
name = "pypdfium2"
-version = "5.6.0"
+version = "5.7.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3b/01/be763b9081c7eb823196e7d13d9c145bf75ac43f3c1466de81c21c24b381/pypdfium2-5.6.0.tar.gz", hash = "sha256:bcb9368acfe3547054698abbdae68ba0cbd2d3bda8e8ee437e061deef061976d", size = 270714, upload-time = "2026-03-08T01:05:06.5Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/76/19aacfff78d328a700ca34b5b1dff891e587aac2fd6b928b035ed366cc37/pypdfium2-5.7.0.tar.gz", hash = "sha256:9febb09f532555485f064c1f6442f46d31e27be5981359cb06b5826695906a06", size = 265935, upload-time = "2026-04-08T19:58:16.831Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/b1/129ed0177521a93a892f8a6a215dd3260093e30e77ef7035004bb8af7b6c/pypdfium2-5.6.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:fb7858c9707708555b4a719b5548a6e7f5d26bc82aef55ae4eb085d7a2190b11", size = 3346059, upload-time = "2026-03-08T01:04:21.37Z" },
- { url = "https://files.pythonhosted.org/packages/86/34/cbdece6886012180a7f2c7b2c360c415cf5e1f83f1973d2c9201dae3506a/pypdfium2-5.6.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:6a7e1f4597317786f994bfb947eef480e53933f804a990193ab89eef8243f805", size = 2804418, upload-time = "2026-03-08T01:04:23.384Z" },
- { url = "https://files.pythonhosted.org/packages/6e/f6/9f9e190fe0e5a6b86b82f83bd8b5d3490348766062381140ca5cad8e00b1/pypdfium2-5.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e468c38997573f0e86f03273c2c1fbdea999de52ba43fee96acaa2f6b2ad35f7", size = 3412541, upload-time = "2026-03-08T01:04:25.45Z" },
- { url = "https://files.pythonhosted.org/packages/ee/8d/e57492cb2228ba56ed57de1ff044c8ac114b46905f8b1445c33299ba0488/pypdfium2-5.6.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:ad3abddc5805424f962e383253ccad6a0d1d2ebd86afa9a9e1b9ca659773cd0d", size = 3592320, upload-time = "2026-03-08T01:04:27.509Z" },
- { url = "https://files.pythonhosted.org/packages/f9/8a/8ab82e33e9c551494cbe1526ea250ca8cc4e9e98d6a4fc6b6f8d959aa1d1/pypdfium2-5.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b5eb9eae5c45076395454522ca26add72ba8bd1fe473e1e4721aa58521470c", size = 3596450, upload-time = "2026-03-08T01:04:29.183Z" },
- { url = "https://files.pythonhosted.org/packages/f5/b5/602a792282312ccb158cc63849528079d94b0a11efdc61f2a359edfb41e9/pypdfium2-5.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:258624da8ef45cdc426e11b33e9d83f9fb723c1c201c6e0f4ab5a85966c6b876", size = 3325442, upload-time = "2026-03-08T01:04:30.886Z" },
- { url = "https://files.pythonhosted.org/packages/81/1f/9e48ec05ed8d19d736c2d1f23c1bd0f20673f02ef846a2576c69e237f15d/pypdfium2-5.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9367451c8a00931d6612db0822525a18c06f649d562cd323a719e46ac19c9bb", size = 3727434, upload-time = "2026-03-08T01:04:33.619Z" },
- { url = "https://files.pythonhosted.org/packages/33/90/0efd020928b4edbd65f4f3c2af0c84e20b43a3ada8fa6d04f999a97afe7a/pypdfium2-5.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a757869f891eac1cc1372e38a4aa01adac8abc8fe2a8a4e2ebf50595e3bf5937", size = 4139029, upload-time = "2026-03-08T01:04:36.08Z" },
- { url = "https://files.pythonhosted.org/packages/ff/49/a640b288a48dab1752281dd9b72c0679fccea107874e80a65a606b00efa9/pypdfium2-5.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515be355222cc57ae9e62cd5c7c350b8e0c863efc539f80c7d75e2811ba45cb6", size = 3646387, upload-time = "2026-03-08T01:04:38.151Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3b/a344c19c01021eeb5d830c102e4fc9b1602f19c04aa7d11abbe2d188fd8e/pypdfium2-5.6.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c4753c7caf7d004211d7f57a21f10d127f5e0e5510a14d24bc073e7220a3ea", size = 3097212, upload-time = "2026-03-08T01:04:40.776Z" },
- { url = "https://files.pythonhosted.org/packages/50/96/e48e13789ace22aeb9b7510904a1b1493ec588196e11bbacc122da330b3d/pypdfium2-5.6.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49729090281fdd85775fb8912c10bd19e99178efaa98f145ab06e7ce68554d2", size = 2965026, upload-time = "2026-03-08T01:04:42.857Z" },
- { url = "https://files.pythonhosted.org/packages/cb/06/3100e44d4935f73af8f5d633d3bd40f0d36d606027085a0ef1f0566a6320/pypdfium2-5.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a4a1749a8d4afd62924a8d95cfa4f2e26fc32957ce34ac3b674be6f127ed252e", size = 4131431, upload-time = "2026-03-08T01:04:44.982Z" },
- { url = "https://files.pythonhosted.org/packages/64/ef/d8df63569ce9a66c8496057782eb8af78e0d28667922d62ec958434e3d4b/pypdfium2-5.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:36469ebd0fdffb7130ce45ed9c44f8232d91571c89eb851bd1633c64b6f6114f", size = 3747469, upload-time = "2026-03-08T01:04:46.702Z" },
- { url = "https://files.pythonhosted.org/packages/a6/47/fd2c6a67a49fade1acd719fbd11f7c375e7219912923ef2de0ea0ac1544e/pypdfium2-5.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da900df09be3cf546b637a127a7b6428fb22d705951d731269e25fd3adef457", size = 4337578, upload-time = "2026-03-08T01:04:49.007Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f5/836c83e54b01e09478c4d6bf4912651d6053c932250fcee953f5c72d8e4a/pypdfium2-5.6.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:45fccd5622233c5ec91a885770ae7dd4004d4320ac05a4ad8fa03a66dea40244", size = 4376104, upload-time = "2026-03-08T01:04:51.04Z" },
- { url = "https://files.pythonhosted.org/packages/6e/7f/b940b6a1664daf8f9bad87c6c99b84effa3611615b8708d10392dc33036c/pypdfium2-5.6.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:282dc030e767cd61bd0299f9d581052b91188e2b87561489057a8e7963e7e0cb", size = 3929824, upload-time = "2026-03-08T01:04:53.544Z" },
- { url = "https://files.pythonhosted.org/packages/88/79/00267d92a6a58c229e364d474f5698efe446e0c7f4f152f58d0138715e99/pypdfium2-5.6.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a1c1dfe950382c76a7bba1ba160ec5e40df8dd26b04a1124ae268fda55bc4cbe", size = 4270201, upload-time = "2026-03-08T01:04:55.81Z" },
- { url = "https://files.pythonhosted.org/packages/e1/ab/b127f38aba41746bdf9ace15ba08411d7ef6ecba1326d529ba414eb1ed50/pypdfium2-5.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:43b0341ca6feb6c92e4b7a9eb4813e5466f5f5e8b6baeb14df0a94d5f312c00b", size = 4180793, upload-time = "2026-03-08T01:04:57.961Z" },
- { url = "https://files.pythonhosted.org/packages/0e/8c/a01c8e4302448b614d25a85c08298b0d3e9dfbdac5bd1b2f32c9b02e83d9/pypdfium2-5.6.0-py3-none-win32.whl", hash = "sha256:9dfcd4ff49a2b9260d00e38539ab28190d59e785e83030b30ffaf7a29c42155d", size = 3596753, upload-time = "2026-03-08T01:05:00.566Z" },
- { url = "https://files.pythonhosted.org/packages/9b/5f/2d871adf46761bb002a62686545da6348afe838d19af03df65d1ece786a2/pypdfium2-5.6.0-py3-none-win_amd64.whl", hash = "sha256:c6bc8dd63d0568f4b592f3e03de756afafc0e44aa1fe8878cc4aba1b11ae7374", size = 3716526, upload-time = "2026-03-08T01:05:02.433Z" },
- { url = "https://files.pythonhosted.org/packages/3a/80/0d9b162098597fbe3ac2b269b1682c0c3e8db9ba87679603fdd9b19afaa6/pypdfium2-5.6.0-py3-none-win_arm64.whl", hash = "sha256:5538417b199bdcb3207370c88df61f2ba3dac7a3253f82e1aa2708e6376b6f90", size = 3515049, upload-time = "2026-03-08T01:05:04.587Z" },
+ { url = "https://files.pythonhosted.org/packages/81/a5/7e6d9532e7753a1dc439412b38dda5943c692d3ab3f1e01826f9b5527c67/pypdfium2-5.7.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:9e815e75498a03a3049baf68ff00b90459bead0d9eee65b1860142529faba81d", size = 3343748, upload-time = "2026-04-08T19:57:40.293Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ea/9d4a0b41f86d342dfb6529c31789e70d1123cc6521b29979e02ec2b267b6/pypdfium2-5.7.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:405bb3c6d0e7a5a32e98eb45a3343da1ad847d6d6eef77bf6f285652a250e0b7", size = 2805480, upload-time = "2026-04-08T19:57:42.109Z" },
+ { url = "https://files.pythonhosted.org/packages/34/dc/ce1c8e94082a84d1669606f90c4f694acbdcabd359d92db7302d16b5938b/pypdfium2-5.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:609b34d91871c185f399b1a503513c03a9de83597f55404de00c3d31a8037544", size = 3420156, upload-time = "2026-04-08T19:57:43.672Z" },
+ { url = "https://files.pythonhosted.org/packages/51/84/6d859ce82a3723ba7cd70d88ad87eca3cb40553c68db182976fd2b0febe1/pypdfium2-5.7.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:6ae6c6bba0cde30c9293c3f525778c229466de7782e8f7d99e7c2a1b8f9c7a6f", size = 3601560, upload-time = "2026-04-08T19:57:45.148Z" },
+ { url = "https://files.pythonhosted.org/packages/66/0c/8bc2258d1e7ba971d05241a049cd3100c75df6bcf930423de7d0c6265a30/pypdfium2-5.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b518d78211cb2912139d10d7f4e39669231eb155e8258159e3413e9e5e4baef", size = 3588134, upload-time = "2026-04-08T19:57:47.379Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f7/3248cc569a92ff25f1fe0a4a1790807e6e05df60563e39e74c9b723d5620/pypdfium2-5.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aaa8e7681ebcaa042ac8adc152521fd5f16a4ceee1e9b9b582e148519528aa9", size = 3323100, upload-time = "2026-04-08T19:57:49.243Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ee/6f004509df77ce963ed5a0f2e090ea0c43036e49cc72c321ce90f3d328bf/pypdfium2-5.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d2284f799adbae755b66ce1a579834e487337d89bbb34ee749ecfa68322425", size = 3719217, upload-time = "2026-04-08T19:57:50.708Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f0/bb61601aa1c2990d4a5d194440281941781250f6a438813a13fe20eb95cf/pypdfium2-5.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08e9e9576eefbc085ba9a63feede4bcaf93d9fa0d9b17cb549aba6f065a8750e", size = 4147676, upload-time = "2026-04-08T19:57:52.292Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/27/a119e0519049afcfca51e9834b67949ffaba5b9afe7e74ed04d6c39b0285/pypdfium2-5.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ace647320bae562903097977b83449f91d30e045dd19ce62939d3100869f180", size = 3635469, upload-time = "2026-04-08T19:57:53.948Z" },
+ { url = "https://files.pythonhosted.org/packages/70/0b/4bcb67b039f057aca01ddbe692ae7666b630ad42b91a3aca3cb4d4f01222/pypdfium2-5.7.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7bb7555fe613cd76fff871a12299f902b80443f90b49e2001338718c758f6f4", size = 3091818, upload-time = "2026-04-08T19:57:55.471Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/c9/31490ab7cecaf433195683ff5c750f4111c7347f1fef9131d3d8704618eb/pypdfium2-5.7.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7c0ef5ae35d40daa1883f3993b3b7ecf3fb06993bcc46651e28cf058d9da992", size = 2959579, upload-time = "2026-04-08T19:57:57.238Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/1e/bf5fe52f007130c0b1b38786ef82c98b4ac06f77e7ca001a17cda6ce76b6/pypdfium2-5.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:423c749e8cab22ddaf833041498ec5ad477c1c2abbff0a8ec00b99663c284592", size = 4126033, upload-time = "2026-04-08T19:57:59.111Z" },
+ { url = "https://files.pythonhosted.org/packages/18/7d/46dcebf4eb9ccf9b5fafe79702c31863b4c127e9c3140c0f335c375d3818/pypdfium2-5.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f48f453f848a90ec7786bcc84a4c0ee42eb84c2d8af3ca9004f7c18648939838", size = 3742063, upload-time = "2026-04-08T19:58:00.643Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/29/cfec37942f13a1dfe3ab059cf8d130609143d33ca1dd554b017a30bffe97/pypdfium2-5.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e84bfa61f0243ed4b33bfe2492946ba761007b7feb5e7e0a086c635436d47906", size = 4332177, upload-time = "2026-04-08T19:58:02.425Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/da/07812153eff746bbc548d50129ada699765036674ff94065d538015c9556/pypdfium2-5.7.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:e3f4d7f4473b5ef762560cd5971cad3b51a77da3a25af479ef5aae4611709bb8", size = 4370704, upload-time = "2026-04-08T19:58:04.379Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/df/07a6a038ccb6fae6a1a06708c98d00aa03f2ca720b02cd3b75248dc5da70/pypdfium2-5.7.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:9e0b6c9be8c92b63ce0a00a94f6635eec22831e253811d6692824a1244e21780", size = 3924428, upload-time = "2026-04-08T19:58:06.406Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a8/70ce4f997fef4186098c032fb3dd2c39193027a92a23b5a94d7a4c85e068/pypdfium2-5.7.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:3e4974a8545f726fc97a7443507713007e177f22058cd1ca0b28cb0e8e2d7dc2", size = 4264817, upload-time = "2026-04-08T19:58:08.003Z" },
+ { url = "https://files.pythonhosted.org/packages/02/42/03779e61ca40120f87839b4693899c72031b7a9e23676dcd8914d92e460c/pypdfium2-5.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2fe12d57a0b413d42bdba435a608b2435a921a5f6a9d78fd8091b6266b63901a", size = 4175393, upload-time = "2026-04-08T19:58:09.858Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f1/19bea36b354f2407c6ffdc60ad8564d95eb515badec457043ff57ad636f0/pypdfium2-5.7.0-py3-none-win32.whl", hash = "sha256:23958aec5c28c52e71f183a647fcc9fcec96ef703cc60a3ade44e55f4701678f", size = 3606308, upload-time = "2026-04-08T19:58:11.672Z" },
+ { url = "https://files.pythonhosted.org/packages/70/aa/fb333c1912a019de26e2395afd3dbef09e8118a59d70f1e5886fc90aa565/pypdfium2-5.7.0-py3-none-win_amd64.whl", hash = "sha256:a33d2c190042ae09c5512f599a540f88b07be956f18c4bb49c027e8c5118ce44", size = 3726429, upload-time = "2026-04-08T19:58:13.374Z" },
+ { url = "https://files.pythonhosted.org/packages/86/cf/6d4bc1ae4466a1f223abfe27210dce218da307e921961cd687f6e5a795a0/pypdfium2-5.7.0-py3-none-win_arm64.whl", hash = "sha256:8233fd06b0b8c22a5ea0bccbd7c4f73d6e9d0388040ea51909a5b2b1f63157e8", size = 3519317, upload-time = "2026-04-08T19:58:15.261Z" },
]
[[package]]
@@ -6550,7 +6819,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/12/a0/d0638470df605ce26
[[package]]
name = "pytest"
-version = "8.4.2"
+version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -6561,9 +6830,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -6607,14 +6876,14 @@ wheels = [
[[package]]
name = "pytest-split"
-version = "0.10.0"
+version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/46/d7/e30ba44adf83f15aee3f636daea54efadf735769edc0f0a7d98163f61038/pytest_split-0.10.0.tar.gz", hash = "sha256:adf80ba9fef7be89500d571e705b4f963dfa05038edf35e4925817e6b34ea66f", size = 13903, upload-time = "2024-10-16T15:45:19.783Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2f/16/8af4c5f2ceb3640bb1f78dfdf5c184556b10dfe9369feaaad7ff1c13f329/pytest_split-0.11.0.tar.gz", hash = "sha256:8ebdb29cc72cc962e8eb1ec07db1eeb98ab25e215ed8e3216f6b9fc7ce0ec2b5", size = 13421, upload-time = "2026-02-03T09:14:31.469Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d6/a7/cad88e9c1109a5c2a320d608daa32e5ee008ccbc766310f54b1cd6b3d69c/pytest_split-0.10.0-py3-none-any.whl", hash = "sha256:466096b086a7147bcd423c6e6c2e57fc62af1c5ea2e256b4ed50fc030fc3dddc", size = 11961, upload-time = "2024-10-16T15:45:18.289Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/a1/d4423657caaa8be9b31e491592b49cebdcfd434d3e74512ce71f6ec39905/pytest_split-0.11.0-py3-none-any.whl", hash = "sha256:899d7c0f5730da91e2daf283860eb73b503259cb416851a65599368849c7f382", size = 11911, upload-time = "2026-02-03T09:14:33.708Z" },
]
[[package]]
@@ -6668,15 +6937,15 @@ wheels = [
[[package]]
name = "python-discovery"
-version = "1.2.1"
+version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
]
[[package]]
@@ -6721,11 +6990,11 @@ wheels = [
[[package]]
name = "python-multipart"
-version = "0.0.22"
+version = "0.0.26"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
]
[[package]]
@@ -6845,7 +7114,8 @@ name = "qdrant-client"
version = "1.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "grpcio" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
{ name = "httpx", extra = ["http2"] },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@@ -6899,75 +7169,75 @@ wheels = [
[[package]]
name = "rapidfuzz"
-version = "3.14.3"
+version = "3.14.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" },
- { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" },
- { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" },
- { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" },
- { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" },
- { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" },
- { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" },
- { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" },
- { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" },
- { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" },
- { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" },
- { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" },
- { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" },
- { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" },
- { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" },
- { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" },
- { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" },
- { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" },
- { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" },
- { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" },
- { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" },
- { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" },
- { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" },
- { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" },
- { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" },
- { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" },
- { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" },
- { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" },
- { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" },
- { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" },
- { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" },
- { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" },
- { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" },
- { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" },
- { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" },
- { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" },
- { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" },
- { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" },
- { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" },
- { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" },
- { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" },
- { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" },
- { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" },
- { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" },
- { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" },
- { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" },
- { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" },
- { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" },
- { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" },
- { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" },
- { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" },
- { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" },
- { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" },
- { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" },
- { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" },
- { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" },
- { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" },
- { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" },
- { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" },
- { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b1/d6d6e7737fe3d0eb2ac2ac337686420d538f83f28495acc3cc32201c0dbf/rapidfuzz-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:071d96b957a33b9296b9284b6350a0fb6d030b154a04efd7c15e56b98b79a517", size = 1953508, upload-time = "2026-04-07T11:13:37.733Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/7b/94c1c953ac818bdd88b43213a9d38e4a41e953b786af3c3b2444d4a8f96d/rapidfuzz-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667f40fe9c81ad129b198d236881b00dd9e8314d9cc72d03c3e16bdfe5879051", size = 1160895, upload-time = "2026-04-07T11:13:39.278Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/60/a67a7ca7c2532c6c1a4b5cd797917780eed43798b82c98b6df734a086c95/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9fff308486bbd2c8c24f25e8e152c7594d3fe8db265a2d6a1ce24d58671127f", size = 1382245, upload-time = "2026-04-07T11:13:41.054Z" },
+ { url = "https://files.pythonhosted.org/packages/95/ff/a42c9ce9f9e90ceb5b51136e0b8e8e6e5113ba0b45d986effbd671e7dddf/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dfa552338f51aec280f17b02d28bace1e162d1a84ccd80e3339a57f98aedb56b", size = 3163974, upload-time = "2026-04-07T11:13:42.662Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/3c/11e2d41075e6e48b7dad373631b379b7e40491f71d5412c5a98d3c58f60f/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:068b3e965ca9d9ee4debe40001ae7c3938ba646308afd33cf0c66618147db65c", size = 1475540, upload-time = "2026-04-07T11:13:44.687Z" },
+ { url = "https://files.pythonhosted.org/packages/29/fa/09be143dcc22c79f09cf90168a574725dbda49f02cbbd55d0447da8bec86/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88b7d31ff1cc5e9bc0e4406e6b1fa00b6d37163d50bb58091e9b976ff1129faa", size = 2404128, upload-time = "2026-04-07T11:13:46.641Z" },
+ { url = "https://files.pythonhosted.org/packages/32/f9/1aeb504cdcfde42881825e9c86f48238d4e01ba8a1530491e82eb17e5689/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eacb434410b8d9ca99a8d42352ef085cf423e3c76c1f0b86be2fcba3bff2952c", size = 2508455, upload-time = "2026-04-07T11:13:48.726Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8e/b1b5eed8d887a29b0e18fd3222c46ca60fddfb528e7e1c41267ce42d5522/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:649712823f3abcdc48427147a5384fac15623ba435d0013959b52e6462521397", size = 4274060, upload-time = "2026-04-07T11:13:50.805Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/c4/7e5b0353693d4f47b8b0f96e941efc377cfb2034b67ef92d082ac4441a0f/rapidfuzz-3.14.5-cp310-cp310-win32.whl", hash = "sha256:13cb79c23ef5516e4c4e3830877be8b19aa75203636be1163d690d37803f6504", size = 1727457, upload-time = "2026-04-07T11:13:52.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/6e/f530a39b946fa71c009bc9c81fdb6b48a77bbc57ee8572ac0302b3bf6308/rapidfuzz-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:f2073495a7f9b75e57e600747ac09510d67683fd64d3228e009740b7ef88f9fe", size = 1544657, upload-time = "2026-04-07T11:13:54.952Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/01/02fa075f9f59ff766d374fecbd042b3ac9782dcd5abc52d909a54f587eeb/rapidfuzz-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:8166efddea49fdbc61185559f47593239e4794fd7c9044dd5a789d1a90af852d", size = 816587, upload-time = "2026-04-07T11:13:56.418Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/f9/3c41a7be8855803f4f6c713b472226a98d31d41869d98f64f4ca790510d6/rapidfuzz-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e251126d48615e1f02b4a178f2cd0cd4f0332b8a019c01a2e10480f7552554b4", size = 1952372, upload-time = "2026-04-07T11:13:58.32Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/89/c2557e37531d03465193bff0ab9de70b468420a807d71a26a65100635459/rapidfuzz-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ab449c9abd0d4e1f8145dce0798a4c822a1a1933d613c764a641bea88b8bdab", size = 1159782, upload-time = "2026-04-07T11:14:00.127Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b2/ffeeb7eca1a897d51b998f4c0ef0281696c3b06abcca4f88f9def708ffe1/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb2829fedd672dd7107267189dabe2bbe07972801d636014417c6861eb89e358", size = 1383677, upload-time = "2026-04-07T11:14:01.696Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/d0/4539e42a2d596e068f7738f279638a4a74edd1fbb6f8594e2458058979c6/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d50e5861872935fece391351cbb5ba21d1bced277cf5e1143d207a0a35f1925", size = 3168906, upload-time = "2026-04-07T11:14:03.29Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/1c/3ec897eb9d8b05308aa8ef6ae4ed64b088ad521a3f9d8ff469e7e97bc2b0/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:7092a216728f80c960bd6b3807275d1ee318b168986bd5dc523349581d4890b8", size = 1478176, upload-time = "2026-04-07T11:14:04.94Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ba/970c03a12ce20a5399e22afe9f8932fd4cd1265b8a8461d0e63b00eb4eae/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9669753caef7fdc6529f6adcc5883ed98d65976445d9322e7dbdb6b697feee13", size = 2402441, upload-time = "2026-04-07T11:14:07.228Z" },
+ { url = "https://files.pythonhosted.org/packages/81/93/61d351cae60c1d0e21ba5ff1a1015ad045539ed215da9d6e302204ed887a/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:823b1b9d9230809d8edcc18872770764bfe8ef4357995e16744047c8ccf0e489", size = 2511628, upload-time = "2026-04-07T11:14:09.234Z" },
+ { url = "https://files.pythonhosted.org/packages/87/52/374d2d4f60fd98155142a869323aa221e30868cfa1f15171a0f64070c247/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0b2af76b7e7060c09e1a0dfa9410eb19369cbe6164509bff2ef94094b54d2b6", size = 4275480, upload-time = "2026-04-07T11:14:11.332Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/04/82e7989bc9ec20a15b720a335c5cb6b0724bf6582013898f90a3280cfccd/rapidfuzz-3.14.5-cp311-cp311-win32.whl", hash = "sha256:c5801a89604c65ab4cc9e91b23bc4076d0ca80efd8c976fb63843d7879a85d7f", size = 1725627, upload-time = "2026-04-07T11:14:13.217Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b5/eca8ac5609bc9bcb02bb6ff87fa5983cc92b8772d66a431556ab8a8c178f/rapidfuzz-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:d7ca16637c0ede8243f84074044bd0b2335a0341421f8227c85756de2d18c819", size = 1545977, upload-time = "2026-04-07T11:14:14.766Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e1/dbf318de28f65fa2cdd0a9dfbdee380f8199eb83b19259bc4f8592551b4e/rapidfuzz-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:8c90cdf8516d9057e502aa6003cea71cf5ec27cc44699ca52412b502a04761bb", size = 816827, upload-time = "2026-04-07T11:14:16.788Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/e3/574435c6aafb80254c191ef40d7aca2cb2bb97a095ec9395e9fa59ac307a/rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638", size = 1944601, upload-time = "2026-04-07T11:14:18.771Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/1f/fbad3102a255ecc112ce9a7e779bacab7fd14398217be8868dc9082ba363/rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48", size = 1164293, upload-time = "2026-04-07T11:14:20.534Z" },
+ { url = "https://files.pythonhosted.org/packages/88/37/a3eb7ff6121ed3a5f199a8c38cc86c8e481816f879cb0e0b738b078c9a7e/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1", size = 1371999, upload-time = "2026-04-07T11:14:22.63Z" },
+ { url = "https://files.pythonhosted.org/packages/79/72/97a9728c711c7c1b06e107d3f0623880fb4ef90e147ed13c551a1730e7cc/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6", size = 3145715, upload-time = "2026-04-07T11:14:24.508Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/54/d5caabbea233ac90c286c87c260e49d7641467e87438a18d858e41c82e91/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741", size = 1456304, upload-time = "2026-04-07T11:14:26.515Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/a7/2d1a81250ac8c01a0100c026018e76f0e7a097ff63e4c553e02a6938c6fb/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646", size = 2389089, upload-time = "2026-04-07T11:14:28.635Z" },
+ { url = "https://files.pythonhosted.org/packages/65/0d/c47c3872203ae88e6506997c0b576ad731f5261daa25d559be09c9756658/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10", size = 2493404, upload-time = "2026-04-07T11:14:30.577Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/2f/71e0a5a3130792146c8a200a2dd1e52aa16f7c1074012e17f2601eea9a90/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9", size = 4251709, upload-time = "2026-04-07T11:14:32.451Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/d39874901abacef325adb5b34ae416817c8486dfb4fb87c7a9b74ec5b072/rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5", size = 1710069, upload-time = "2026-04-07T11:14:34.37Z" },
+ { url = "https://files.pythonhosted.org/packages/85/0b/f65572c53de8a1c704bda707f63a447b67bdbe95d7cdc70d18885e191df5/rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9", size = 1540630, upload-time = "2026-04-07T11:14:36.287Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/c3/143be3a578f989758cae516f3270d5cbb49783a7bfdf57cc27a670e00456/rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8", size = 813137, upload-time = "2026-04-07T11:14:38.289Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/252803f2010ba699618cdc048b6e1f7cc1f433c08b4a9a17579b92ab0142/rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6", size = 1940205, upload-time = "2026-04-07T11:14:40.319Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/59/b2afd98e41af9cd54554a4c1c423d84cdd60e6b1c0a09496f033b55f60ec/rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609", size = 1159639, upload-time = "2026-04-07T11:14:42.52Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/7aa7e62c4c516a7af322ed0c4f0774208b72d457d0cfec808bad0df12f4a/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f", size = 1367194, upload-time = "2026-04-07T11:14:44.25Z" },
+ { url = "https://files.pythonhosted.org/packages/90/79/2fc252a63bc91d3c3b234d0a3a6ad4ebc460037a23cdcdaf9285f986e6c9/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7", size = 3151805, upload-time = "2026-04-07T11:14:46.21Z" },
+ { url = "https://files.pythonhosted.org/packages/17/54/0c83508f2683ea70e2d05f8527eb07328acf7bb1e9d97a3bece5702378e7/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e", size = 1455667, upload-time = "2026-04-07T11:14:47.991Z" },
+ { url = "https://files.pythonhosted.org/packages/71/1b/070175e873177814d58850a01ebe80e20ae11e93eb4da894d563988660fa/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610", size = 2388246, upload-time = "2026-04-07T11:14:50.098Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/77caf7aaf9c2be050ad1f128d7c24ff0f59079aa62c5f62f9df41c0af45e/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8", size = 2494333, upload-time = "2026-04-07T11:14:52.303Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/dd7e1f2aa31a8fbbfc16b0610af1d770ffaf1287490f3c8c5b1c52da264f/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98", size = 4258579, upload-time = "2026-04-07T11:14:54.538Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/0a/ac99e1ba347ba0e85e0bb60b74231d55fb93c0eff43f2920ccb413d0be08/rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc", size = 1709231, upload-time = "2026-04-07T11:14:56.524Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/cb/0e251d731b3166378644238e8f0cf9e89858c024e19f75ca9f7e3ae83fd5/rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35", size = 1538519, upload-time = "2026-04-07T11:14:58.635Z" },
+ { url = "https://files.pythonhosted.org/packages/30/6f/4548132acc947db6d5346a248e44a8b3a22d608ef30e770fb578caaf2d00/rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd", size = 812628, upload-time = "2026-04-07T11:15:00.552Z" },
+ { url = "https://files.pythonhosted.org/packages/00/60/69b177577290c5eab892c6f75fe89c3aff3f9ae80298a78d9372b1cecb9a/rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8", size = 1970231, upload-time = "2026-04-07T11:15:02.603Z" },
+ { url = "https://files.pythonhosted.org/packages/48/38/2fd790052659cc4e2907b63c25433f0987864b445c1aeec1a302ef5ad948/rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9", size = 1194394, upload-time = "2026-04-07T11:15:04.572Z" },
+ { url = "https://files.pythonhosted.org/packages/80/f4/28430ad8472fc3536e8ebd51a864a226e979cfe924c6e3f83d111373aa74/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d", size = 1377051, upload-time = "2026-04-07T11:15:06.728Z" },
+ { url = "https://files.pythonhosted.org/packages/77/7e/9aeacabcfd1e77397968362e5b98fe14248b8307011136b17daf99752a8e/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074", size = 3160565, upload-time = "2026-04-07T11:15:08.667Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f4/db4dd7be0cd2f2022117ac5407d905f435d60e48baaea313a567ad27e865/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3", size = 1442113, upload-time = "2026-04-07T11:15:11.138Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/99/0e9f6aa57f3e32a767216f797e56dc96b720fcecfb9d8ee907ecc82f8d66/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09", size = 2396618, upload-time = "2026-04-07T11:15:13.154Z" },
+ { url = "https://files.pythonhosted.org/packages/60/94/44a78e39ffce17cbdd3e2b53b696acc751d5d153be0f499d052b07a4d904/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa", size = 2478220, upload-time = "2026-04-07T11:15:15.193Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/df/454311469a09a507e9d784a35796742bec22e4cebe75551e2da4e0e290fd/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1", size = 4265027, upload-time = "2026-04-07T11:15:17.28Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/01/175465a9ab3e3b70ba669058372f009d1d49c1746e2dcd56b69df188d3a5/rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f", size = 1766814, upload-time = "2026-04-07T11:15:19.687Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a0/a9b84a47af06ebed94a1439eb2f02adebfb8628bcd30af1fe3e02f5ef56c/rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a", size = 1582448, upload-time = "2026-04-07T11:15:21.98Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/f1/5937800238b3f8248e70860d79f69ba8f73e764fff47e36bc9e2f26dbcc6/rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895", size = 832932, upload-time = "2026-04-07T11:15:24.358Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/ee/e71853bf82846c5c2174b924b71d8e8099fb05ff87c958a720380b434ba3/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:578e6051f6d5e6200c259b47a103cf06bb875ab5814d17333fc0b5c290b22f4c", size = 1888603, upload-time = "2026-04-07T11:16:18.223Z" },
+ { url = "https://files.pythonhosted.org/packages/36/82/40f67b730f32be2ebad9f62add1571c754f52249254b2e88af094b907eee/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbf1b8bb2695415b347f3727da1addca2acb82c9b97ac86bebf8b1bead1eb12d", size = 1120599, upload-time = "2026-04-07T11:16:20.682Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/9f/a3635cc4ec8fc6e14b46e7db1f7f8763d8c4bef33dcc124eea2e6cb2c8f3/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4a8f5cc84c7ad6bffa0e9947b33eb343ad66e6b53e94fe54378a5508c5ed53", size = 1348524, upload-time = "2026-04-07T11:16:23.451Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/1b/2b229520f0b48464cfcd7aa758f74551d12c9bc4ab544022a60210aab064/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c6d85283629646fa87acc22c66b30ea9d4de7f6fdf887daa2e30fa041829b5", size = 3099302, upload-time = "2026-04-07T11:16:25.858Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b5/363906b1064fc6fe611783a61764927bbd91919aaaabe8cba82151ca93ef/rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf", size = 1509889, upload-time = "2026-04-07T11:16:28.487Z" },
]
[[package]]
name = "rapidocr"
-version = "3.7.0"
+version = "3.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorlog" },
@@ -6984,7 +7254,7 @@ dependencies = [
{ name = "tqdm" },
]
wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/b8/011338eec8aea40cf9b82da7481f3e65e100537cff4c866b3c1b1e719b97/rapidocr-3.7.0-py3-none-any.whl", hash = "sha256:ace47f037956fa3780875f8556a0f27ab20d91962d36a9a2816aa367bb48718f", size = 15080131, upload-time = "2026-03-04T15:38:20.339Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1f/5f815e17c0b02b8f937b5b680b85d0ec5f34b195314dfa8f11ed14a6de03/rapidocr-3.8.0-py3-none-any.whl", hash = "sha256:54abb10883d588120a3390bc447566f1590aea641e127f63a4ca44415fecd18a", size = 15082360, upload-time = "2026-04-08T13:42:15.89Z" },
]
[[package]]
@@ -7104,7 +7374,7 @@ wheels = [
[[package]]
name = "requests"
-version = "2.32.5"
+version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -7112,9 +7382,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
]
[[package]]
@@ -7291,14 +7561,14 @@ wheels = [
[[package]]
name = "s3transfer"
-version = "0.14.0"
+version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
]
[[package]]
@@ -7473,7 +7743,7 @@ wheels = [
[[package]]
name = "scrapfly-sdk"
-version = "0.8.27"
+version = "0.8.28"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
@@ -7483,14 +7753,14 @@ dependencies = [
{ name = "requests" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fb/49/c9c13113630ea38653b784f3511779e191152aa6afb44cf7e148d99ad345/scrapfly_sdk-0.8.27.tar.gz", hash = "sha256:affce316fecfabe444685779fc61b28a9e7a36344819701339637a96272831c6", size = 82753, upload-time = "2026-02-26T19:00:32.638Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/3e/a881968b866ed77cb8a5013aeb100a5a3dd2b502e9a9f955615e15157ad0/scrapfly_sdk-0.8.28.tar.gz", hash = "sha256:051f734ae10fd9b136527f3dc3344abb68ed64822c108b1caff6dc8399c197e0", size = 104208, upload-time = "2026-04-09T16:18:51.793Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/70/9a/f9367c504710f0fc06654adef079b3e020318bf0c6beccb8291ecf26b9fe/scrapfly_sdk-0.8.27-py3-none-any.whl", hash = "sha256:c0cb76fd65e95a6221b3f4531af363f2dcd2dc2e5b18641be9554bb2f60e001c", size = 95229, upload-time = "2026-02-26T19:00:31.227Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/c6/97a5fbc9ff952c45783303add4c4e431b7a34a020f6dc3adb8f878af0c2a/scrapfly_sdk-0.8.28-py3-none-any.whl", hash = "sha256:116198df90cdbea224d6b0c92d4d74c9ee585fa63c1c5ec9f021b5fc9638fe3f", size = 117920, upload-time = "2026-04-09T16:18:50.356Z" },
]
[[package]]
name = "selenium"
-version = "4.41.0"
+version = "4.42.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -7500,9 +7770,9 @@ dependencies = [
{ name = "urllib3" },
{ name = "websocket-client" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/04/7c/133d00d6d013a17d3f39199f27f1a780ec2e95d7b9aa997dc1b8ac2e62a7/selenium-4.41.0.tar.gz", hash = "sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa", size = 937872, upload-time = "2026-02-20T03:42:06.216Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/46/fb93d37749ecf13853739c31c70bd95704310a7defbc57e7101dc4ab2513/selenium-4.42.0.tar.gz", hash = "sha256:4c8ebd84ff96505db4277223648f12e2799e92e13169bc69633a6b24eb066c72", size = 956304, upload-time = "2026-04-09T08:31:20.268Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/d6/e4160989ef6b272779af6f3e5c43c3ba9be6687bdc21c68c3fb220e555b3/selenium-4.41.0-py3-none-any.whl", hash = "sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1", size = 9532858, upload-time = "2026-02-20T03:42:03.218Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/47/9f094f1cffdb54b01da75b45cc29673869458a504b30002797c0c47ac985/selenium-4.42.0-py3-none-any.whl", hash = "sha256:bb29eababf54fa479c95d5fa3fba73889db5d532f3a76addc5b526bbff14fca7", size = 9559171, upload-time = "2026-04-09T08:31:17.38Z" },
]
[[package]]
@@ -7763,49 +8033,49 @@ wheels = [
[[package]]
name = "sqlalchemy"
-version = "2.0.48"
+version = "2.0.49"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/67/1235676e93dd3b742a4a8eddfae49eea46c85e3eed29f0da446a8dd57500/sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89", size = 2157384, upload-time = "2026-03-02T15:38:26.781Z" },
- { url = "https://files.pythonhosted.org/packages/4d/d7/fa728b856daa18c10e1390e76f26f64ac890c947008284387451d56ca3d0/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0", size = 3236981, upload-time = "2026-03-02T15:58:53.53Z" },
- { url = "https://files.pythonhosted.org/packages/5c/ad/6c4395649a212a6c603a72c5b9ab5dce3135a1546cfdffa3c427e71fd535/sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd", size = 3235232, upload-time = "2026-03-02T15:52:25.654Z" },
- { url = "https://files.pythonhosted.org/packages/01/f4/58f845e511ac0509765a6f85eb24924c1ef0d54fb50de9d15b28c3601458/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29", size = 3188106, upload-time = "2026-03-02T15:58:55.193Z" },
- { url = "https://files.pythonhosted.org/packages/3f/f9/6dcc7bfa5f5794c3a095e78cd1de8269dfb5584dfd4c2c00a50d3c1ade44/sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0", size = 3209522, upload-time = "2026-03-02T15:52:27.407Z" },
- { url = "https://files.pythonhosted.org/packages/d7/5a/b632875ab35874d42657f079529f0745410604645c269a8c21fb4272ff7a/sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018", size = 2117695, upload-time = "2026-03-02T15:46:51.389Z" },
- { url = "https://files.pythonhosted.org/packages/de/03/9752eb2a41afdd8568e41ac3c3128e32a0a73eada5ab80483083604a56d1/sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76", size = 2140928, upload-time = "2026-03-02T15:46:52.992Z" },
- { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" },
- { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" },
- { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" },
- { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" },
- { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" },
- { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" },
- { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" },
- { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" },
- { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" },
- { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" },
- { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" },
- { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" },
- { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" },
- { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" },
- { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" },
- { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" },
- { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" },
- { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" },
- { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" },
- { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" },
- { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" },
- { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" },
- { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" },
- { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" },
- { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" },
- { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" },
+ { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" },
+ { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" },
+ { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" },
+ { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" },
+ { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" },
+ { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" },
+ { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" },
+ { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" },
+ { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" },
+ { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" },
+ { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" },
]
[[package]]
@@ -7832,7 +8102,7 @@ wheels = [
[[package]]
name = "stagehand"
-version = "3.19.1"
+version = "3.19.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -7842,12 +8112,12 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/86/81/da1fc0e559708f6d3c722e2a231209e4f0bcd235e5f7864467598a046b94/stagehand-3.19.1.tar.gz", hash = "sha256:79f90149617c66b52f3d5ef98eec670084576ced21adfc5047f0287f1825bd0a", size = 279625, upload-time = "2026-03-31T22:05:48.01Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/f8/ccd2bb2758a4eaf0af3846e097ff206e0aa76c8d3b5aa2bded77fb47825e/stagehand-3.19.5.tar.gz", hash = "sha256:3cb8279ac82051e584b34d26e87dc764f0ccad766a01625198ca578eb35f0b6c", size = 281033, upload-time = "2026-04-03T20:21:09.792Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/f5/691c3e050b059fdb949dcb34e6e692e22bf2bb5913d595a7142afa33fa9d/stagehand-3.19.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:ec7e2f0ed5a33c8374ce29cc0be96d5bd79f3b0912c862268df9a87cba3abee0", size = 34492500, upload-time = "2026-03-31T22:05:49.574Z" },
- { url = "https://files.pythonhosted.org/packages/42/c8/4d40169828de0ed9f9d108aa8c8a5a4e2ee42d13c4a5f2612cec6acec63e/stagehand-3.19.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:002989a7730c91c51ea38c1b11b0b07b25610ccf823779e709951568ae0e65de", size = 33190159, upload-time = "2026-03-31T22:05:55.079Z" },
- { url = "https://files.pythonhosted.org/packages/25/4d/787a3a5a4a0a0661dba24d8904734d48934c28b20ba9af842c8c84892487/stagehand-3.19.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:baea3a6cff498e096c3d1933751aa1879838d5e308b479d50e60cdd4fe104d99", size = 37269772, upload-time = "2026-03-31T22:05:45.856Z" },
- { url = "https://files.pythonhosted.org/packages/f1/65/03072d2d5e8178fd4cfa7cbb51088ea4248c1a29ad0cb84c4ab813e5e416/stagehand-3.19.1-py3-none-win_amd64.whl", hash = "sha256:1cd73285d80517a674aaaa40a3bf40cf9b85da50e8f29a01d7f5f2ef2f11d70e", size = 30754952, upload-time = "2026-03-31T22:05:52.481Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/6f/a47bad258bfafc193ebb8e0e8c440e8028c9ab28b54a333b46aa3c0cff53/stagehand-3.19.5-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:14f39a4f8d30d77c089166185c705f66aade25432b903a663a937b3747439c26", size = 34495874, upload-time = "2026-04-03T20:21:07.366Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f7/e39868903121f1a80ae6eda088383362cd2d3a578c04493a2f83c1aac1da/stagehand-3.19.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80ed0d732cb9c3e952ad851e071dad5775a9ea88d2787c006289d61097fd2609", size = 33193535, upload-time = "2026-04-03T20:21:18.536Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/0b/35cb92bb53e9539c0147892dbd0a227b43bf0d8adcd0a8e867dc5f2bf7fd/stagehand-3.19.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:aa947a5f6241f5953ac238cd9b0ab72e0cb87f559f97e5ee875f83dbc0c351d1", size = 37273148, upload-time = "2026-04-03T20:21:11.939Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/c7/dccf63cba1941b5710dc9968218e2883a937cf6534d644bb0c5222d3f40a/stagehand-3.19.5-py3-none-win_amd64.whl", hash = "sha256:e37bf630b99b4a9b7d95f151c56b296940db88b3049b68f0abb56f9e31cc6095", size = 30758357, upload-time = "2026-04-03T20:21:15.121Z" },
]
[[package]]
@@ -7927,7 +8197,7 @@ wheels = [
[[package]]
name = "textual"
-version = "8.2.2"
+version = "8.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py", extra = ["linkify"] },
@@ -7937,9 +8207,9 @@ dependencies = [
{ name = "rich" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/69/b0/a9aedf13af1bfb1bf01cbc645ea5d5a4151b5d77ac1748b85c4f0d777d7d/textual-8.2.2.tar.gz", hash = "sha256:94e85267650cf679ac16ade5ac929055e836dc00798a0e6e3925926a5beee303", size = 1848623, upload-time = "2026-04-03T13:19:06.057Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/2f/d44f0f12b3ddb1f0b88f7775652e99c6b5a43fd733badf4ce064bdbfef4a/textual-8.2.3.tar.gz", hash = "sha256:beea7b86b03b03558a2224f0cc35252e60ef8b0c4353b117b2f40972902d976a", size = 1848738, upload-time = "2026-04-05T09:12:45.338Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/18/4d59eb3f2241db6d346a90f2452fc47a19d61090a38b9cf331afe23e8431/textual-8.2.2-py3-none-any.whl", hash = "sha256:35a8f439875dc6e5b4dc7ee72dc9698a40bd13091c2de5bd5b2d4318522af8df", size = 724078, upload-time = "2026-04-03T13:19:08.115Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/28/a81d6ce9f4804818bd1231a9a6e4d56ea84ebbe8385c49591444f0234fa2/textual-8.2.3-py3-none-any.whl", hash = "sha256:5008ac581bebf1f6fa0520404261844a231e5715fdbddd10ca73916a3af48ca2", size = 724231, upload-time = "2026-04-05T09:12:48.747Z" },
]
[[package]]
@@ -8062,11 +8332,11 @@ wheels = [
[[package]]
name = "tomlkit"
-version = "0.14.0"
+version = "0.13.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
]
[[package]]
@@ -8172,24 +8442,23 @@ wheels = [
[[package]]
name = "transformers"
-version = "4.57.6"
+version = "5.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "filelock" },
{ name = "huggingface-hub" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "regex" },
- { name = "requests" },
{ name = "safetensors" },
{ name = "tokenizers" },
{ name = "tqdm" },
+ { name = "typer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/35/cd5b0d1288e65d2c12db4ce84c1ec1074f7ee9bced040de6c9d69e70d620/transformers-5.5.3.tar.gz", hash = "sha256:3f60128e840b40d352655903552e1eed4f94ed49369a4d43e1bc067bd32d3f50", size = 8226047, upload-time = "2026-04-09T15:52:56.231Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/0b/f8524551ab2d896dfaca74ddb70a4453d515bbf4ab5451c100c7788ae155/transformers-5.5.3-py3-none-any.whl", hash = "sha256:e48f3ec31dd96505e96e66b63a1e43e1ad7a65749e108d9227caaf51051cdb02", size = 10236257, upload-time = "2026-04-09T15:52:52.866Z" },
]
[[package]]
@@ -8357,11 +8626,11 @@ wheels = [
[[package]]
name = "types-aiofiles"
-version = "25.1.0.20251011"
+version = "25.1.0.20260409"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/84/6c/6d23908a8217e36704aa9c79d99a620f2fdd388b66a4b7f72fbc6b6ff6c6/types_aiofiles-25.1.0.20251011.tar.gz", hash = "sha256:1c2b8ab260cb3cd40c15f9d10efdc05a6e1e6b02899304d80dfa0410e028d3ff", size = 14535, upload-time = "2025-10-11T02:44:51.237Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/66/9e62a2692792bc96c0f423f478149f4a7b84720704c546c8960b0a047c89/types_aiofiles-25.1.0.20260409.tar.gz", hash = "sha256:49e67d72bdcf9fe406f5815758a78dc34a1249bb5aa2adba78a80aec0a775435", size = 14812, upload-time = "2026-04-09T04:22:35.308Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/71/0f/76917bab27e270bb6c32addd5968d69e558e5b6f7fb4ac4cbfa282996a96/types_aiofiles-25.1.0.20251011-py3-none-any.whl", hash = "sha256:8ff8de7f9d42739d8f0dadcceeb781ce27cd8d8c4152d4a7c52f6b20edb8149c", size = 14338, upload-time = "2025-10-11T02:44:50.054Z" },
+ { url = "https://files.pythonhosted.org/packages/27/d0/28236f869ba4dfb223ecdbc267eb2bdb634b81a561dd992230a4f9ec48fa/types_aiofiles-25.1.0.20260409-py3-none-any.whl", hash = "sha256:923fedb532c772cc0f62e0ce4282725afa82ca5b41cabd9857f06b55e5eee8de", size = 14372, upload-time = "2026-04-09T04:22:34.328Z" },
]
[[package]]
@@ -8402,11 +8671,11 @@ wheels = [
[[package]]
name = "types-pyyaml"
-version = "6.0.12.20250915"
+version = "6.0.12.20260408"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" },
]
[[package]]
@@ -8545,7 +8814,8 @@ all-docs = [
{ name = "pypdf" },
{ name = "python-docx" },
{ name = "python-pptx" },
- { name = "unstructured-inference" },
+ { name = "unstructured-inference", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
+ { name = "unstructured-inference", version = "1.6.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "unstructured-pytesseract" },
{ name = "xlrd" },
]
@@ -8568,7 +8838,8 @@ local-inference = [
{ name = "pypdf" },
{ name = "python-docx" },
{ name = "python-pptx" },
- { name = "unstructured-inference" },
+ { name = "unstructured-inference", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
+ { name = "unstructured-inference", version = "1.6.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "unstructured-pytesseract" },
{ name = "xlrd" },
]
@@ -8596,31 +8867,67 @@ wheels = [
name = "unstructured-inference"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.11.*' and platform_machine != 's390x'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x'",
+ "python_full_version < '3.11' and platform_machine != 's390x'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
dependencies = [
- { name = "accelerate" },
- { name = "huggingface-hub" },
- { name = "matplotlib" },
+ { name = "accelerate", marker = "python_full_version < '3.12'" },
+ { name = "huggingface-hub", marker = "python_full_version < '3.12'" },
+ { name = "matplotlib", marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "onnx" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
+ { name = "onnx", marker = "python_full_version < '3.12'" },
{ name = "onnxruntime", marker = "python_full_version < '3.11'" },
- { name = "opencv-python" },
- { name = "pandas" },
- { name = "pdfminer-six" },
- { name = "pypdfium2" },
- { name = "python-multipart" },
- { name = "rapidfuzz" },
+ { name = "opencv-python", marker = "python_full_version < '3.12'" },
+ { name = "pandas", marker = "python_full_version < '3.12'" },
+ { name = "pdfminer-six", marker = "python_full_version < '3.12'" },
+ { name = "pypdfium2", marker = "python_full_version < '3.12'" },
+ { name = "python-multipart", marker = "python_full_version < '3.12'" },
+ { name = "rapidfuzz", marker = "python_full_version < '3.12'" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "timm" },
- { name = "torch" },
- { name = "transformers" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
+ { name = "timm", marker = "python_full_version < '3.12'" },
+ { name = "torch", marker = "python_full_version < '3.12'" },
+ { name = "transformers", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/10/8f3bccfa9f1e0101a402ae1f529e07876541c6b18004747f0e793ed41f9e/unstructured_inference-1.2.0.tar.gz", hash = "sha256:19ca28512f3649c70a759cf2a4e98663e942a1b83c1acdb9506b0445f4862f23", size = 45732, upload-time = "2026-01-30T20:57:58.019Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/3b/349cd091b590a6f1dbfebcb5fee0ea7b0b6ef6520df58794c9582567a24f/unstructured_inference-1.2.0-py3-none-any.whl", hash = "sha256:60a1635aa8e97a9e7daed1a129836f51c26588e0d2062c9cc6a5a17e6d40cb6a", size = 49443, upload-time = "2026-01-30T20:57:56.617Z" },
]
+[[package]]
+name = "unstructured-inference"
+version = "1.6.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x'",
+ "python_full_version >= '3.13' and platform_machine == 's390x'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "accelerate", marker = "python_full_version >= '3.12'" },
+ { name = "huggingface-hub", marker = "python_full_version >= '3.12'" },
+ { name = "matplotlib", marker = "python_full_version >= '3.12'" },
+ { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+ { name = "onnx", marker = "python_full_version >= '3.12'" },
+ { name = "opencv-python", marker = "python_full_version >= '3.12'" },
+ { name = "pandas", marker = "python_full_version >= '3.12'" },
+ { name = "pypdfium2", marker = "python_full_version >= '3.12'" },
+ { name = "rapidfuzz", marker = "python_full_version >= '3.12'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
+ { name = "timm", marker = "python_full_version >= '3.12'" },
+ { name = "torch", marker = "python_full_version >= '3.12'" },
+ { name = "transformers", marker = "python_full_version >= '3.12'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d3/e3/6c98caf4965e07eb0153dc2b4457ec6fb1cfef336411add4acd3b28c697c/unstructured_inference-1.6.6.tar.gz", hash = "sha256:f14745daef4c37f785d4edb6c3d3834c7414d9d5abd47ca0e377ca60c624d225", size = 47024, upload-time = "2026-04-09T19:58:52.292Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/5b/bd4aa4d16446fbc79bea07b22c19c8f8b578c8f1dd73745d152511c17a5a/unstructured_inference-1.6.6-py3-none-any.whl", hash = "sha256:ac472f341407b2ea14d1b63074080af840b9badeefdcd90ea38feb22b4928e5a", size = 54286, upload-time = "2026-04-09T19:58:50.858Z" },
+]
+
[[package]]
name = "unstructured-pytesseract"
version = "0.3.15"
@@ -8674,42 +8981,42 @@ wheels = [
[[package]]
name = "uv"
-version = "0.9.30"
+version = "0.11.6"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/a0/63cea38fe839fb89592728b91928ee6d15705f1376a7940fee5bbc77fea0/uv-0.9.30.tar.gz", hash = "sha256:03ebd4b22769e0a8d825fa09d038e31cbab5d3d48edf755971cb0cec7920ab95", size = 3846526, upload-time = "2026-02-04T21:45:37.58Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/3c/71be72f125f0035348b415468559cc3b335ec219376d17a3d242d2bd9b23/uv-0.9.30-py3-none-linux_armv6l.whl", hash = "sha256:a5467dddae1cd5f4e093f433c0f0d9a0df679b92696273485ec91bbb5a8620e6", size = 21927585, upload-time = "2026-02-04T21:46:14.935Z" },
- { url = "https://files.pythonhosted.org/packages/0f/fd/8070b5423a77d4058d14e48a970aa075762bbff4c812dda3bb3171543e44/uv-0.9.30-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ec38ae29aa83a37c6e50331707eac8ecc90cf2b356d60ea6382a94de14973be", size = 21050392, upload-time = "2026-02-04T21:45:55.649Z" },
- { url = "https://files.pythonhosted.org/packages/42/5f/3ccc9415ef62969ed01829572338ea7bdf4c5cf1ffb9edc1f8cb91b571f3/uv-0.9.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:777ecd117cf1d8d6bb07de8c9b7f6c5f3e802415b926cf059d3423699732eb8c", size = 19817085, upload-time = "2026-02-04T21:45:40.881Z" },
- { url = "https://files.pythonhosted.org/packages/8b/3f/76b44e2a224f4c4a8816fc92686ef6d4c2656bc5fc9d4f673816162c994d/uv-0.9.30-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:93049ba3c41fa2cc38b467cb78ef61b2ddedca34b6be924a5481d7750c8111c6", size = 21620537, upload-time = "2026-02-04T21:45:47.846Z" },
- { url = "https://files.pythonhosted.org/packages/60/2a/50f7e8c6d532af8dd327f77bdc75ce4652322ac34f5e29f79a8e04ea3cc8/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:f295604fee71224ebe2685a0f1f4ff7a45c77211a60bd57133a4a02056d7c775", size = 21550855, upload-time = "2026-02-04T21:46:26.269Z" },
- { url = "https://files.pythonhosted.org/packages/0e/10/f823d4af1125fae559194b356757dc7d4a8ac79d10d11db32c2d4c9e2f63/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2faf84e1f3b6fc347a34c07f1291d11acf000b0dd537a61d541020f22b17ccd9", size = 21516576, upload-time = "2026-02-04T21:46:03.494Z" },
- { url = "https://files.pythonhosted.org/packages/91/f3/64b02db11f38226ed34458c7fbdb6f16b6d4fd951de24c3e51acf02b30f8/uv-0.9.30-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b3b3700ecf64a09a07fd04d10ec35f0973ec15595d38bbafaa0318252f7e31f", size = 22718097, upload-time = "2026-02-04T21:45:51.875Z" },
- { url = "https://files.pythonhosted.org/packages/28/21/a48d1872260f04a68bb5177b0f62ddef62ab892d544ed1922f2d19fd2b00/uv-0.9.30-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b176fc2937937dd81820445cb7e7e2e3cd1009a003c512f55fa0ae10064c8a38", size = 24107844, upload-time = "2026-02-04T21:46:19.032Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c6/d7e5559bfe1ab7a215a7ad49c58c8a5701728f2473f7f436ef00b4664e88/uv-0.9.30-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:180e8070b8c438b9a3fb3fde8a37b365f85c3c06e17090f555dc68fdebd73333", size = 23685378, upload-time = "2026-02-04T21:46:07.166Z" },
- { url = "https://files.pythonhosted.org/packages/a8/bf/b937bbd50d14c6286e353fd4c7bdc09b75f6b3a26bd4e2f3357e99891f28/uv-0.9.30-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4125a9aa2a751e1589728f6365cfe204d1be41499148ead44b6180b7df576f27", size = 22848471, upload-time = "2026-02-04T21:45:18.728Z" },
- { url = "https://files.pythonhosted.org/packages/6a/57/12a67c569e69b71508ad669adad266221f0b1d374be88eaf60109f551354/uv-0.9.30-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4366dd740ac9ad3ec50a58868a955b032493bb7d7e6ed368289e6ced8bbc70f3", size = 22774258, upload-time = "2026-02-04T21:46:10.798Z" },
- { url = "https://files.pythonhosted.org/packages/3d/b8/a26cc64685dddb9fb13f14c3dc1b12009f800083405f854f84eb8c86b494/uv-0.9.30-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:33e50f208e01a0c20b3c5f87d453356a5cbcfd68f19e47a28b274cd45618881c", size = 21699573, upload-time = "2026-02-04T21:45:44.365Z" },
- { url = "https://files.pythonhosted.org/packages/c8/59/995af0c5f0740f8acb30468e720269e720352df1d204e82c2d52d9a8c586/uv-0.9.30-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5e7a6fa7a3549ce893cf91fe4b06629e3e594fc1dca0a6050aba2ea08722e964", size = 22460799, upload-time = "2026-02-04T21:45:26.658Z" },
- { url = "https://files.pythonhosted.org/packages/bb/0b/6affe815ecbaebf38b35d6230fbed2f44708c67d5dd5720f81f2ec8f96ff/uv-0.9.30-py3-none-musllinux_1_1_i686.whl", hash = "sha256:62d7e408d41e392b55ffa4cf9b07f7bbd8b04e0929258a42e19716c221ac0590", size = 22001777, upload-time = "2026-02-04T21:45:34.656Z" },
- { url = "https://files.pythonhosted.org/packages/f3/b6/47a515171c891b0d29f8e90c8a1c0e233e4813c95a011799605cfe04c74c/uv-0.9.30-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6dc65c24f5b9cdc78300fa6631368d3106e260bbffa66fb1e831a318374da2df", size = 22968416, upload-time = "2026-02-04T21:45:22.863Z" },
- { url = "https://files.pythonhosted.org/packages/3d/3a/c1df8615385138bb7c43342586431ca32b77466c5fb086ac0ed14ab6ca28/uv-0.9.30-py3-none-win32.whl", hash = "sha256:74e94c65d578657db94a753d41763d0364e5468ec0d368fb9ac8ddab0fb6e21f", size = 20889232, upload-time = "2026-02-04T21:46:22.617Z" },
- { url = "https://files.pythonhosted.org/packages/f2/a8/e8761c8414a880d70223723946576069e042765475f73b4436d78b865dba/uv-0.9.30-py3-none-win_amd64.whl", hash = "sha256:88a2190810684830a1ba4bb1cf8fb06b0308988a1589559404259d295260891c", size = 23432208, upload-time = "2026-02-04T21:45:30.85Z" },
- { url = "https://files.pythonhosted.org/packages/49/e8/6f2ebab941ec559f97110bbbae1279cd0333d6bc352b55f6fa3fefb020d9/uv-0.9.30-py3-none-win_arm64.whl", hash = "sha256:7fde83a5b5ea027315223c33c30a1ab2f2186910b933d091a1b7652da879e230", size = 21887273, upload-time = "2026-02-04T21:45:59.787Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" },
+ { url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" },
+ { url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" },
+ { url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" },
+ { url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" },
+ { url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" },
+ { url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" },
+ { url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" },
+ { url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" },
]
[[package]]
name = "uvicorn"
-version = "0.42.0"
+version = "0.44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
]
[package.optional-dependencies]
@@ -8781,7 +9088,7 @@ wheels = [
[[package]]
name = "virtualenv"
-version = "21.2.0"
+version = "21.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
@@ -8790,9 +9097,9 @@ dependencies = [
{ name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" },
+ { url = "https://files.pythonhosted.org/packages/20/0e/f083a76cb590e60dff3868779558eefefb8dfb7c9ed020babc7aa014ccbf/virtualenv-21.2.1-py3-none-any.whl", hash = "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2", size = 5828326, upload-time = "2026-04-09T18:47:09.331Z" },
]
[[package]]
@@ -8906,18 +9213,48 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
]
+[[package]]
+name = "weaviate-client"
+version = "4.16.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine != 's390x'",
+ "python_full_version < '3.11' and platform_machine != 's390x'",
+ "python_full_version < '3.11' and platform_machine == 's390x'",
+]
+dependencies = [
+ { name = "authlib", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "deprecation", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "grpcio-health-checking", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "httpx", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "pydantic", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+ { name = "validators", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/b9/7b9e05cf923743aa1479afcd85c48ebca82d031c3c3a5d02b1b3fcb52eb9/weaviate_client-4.16.2.tar.gz", hash = "sha256:eb7107a3221a5ad68d604cafc65195bd925a9709512ea0b6fe0dd212b0678fab", size = 681321, upload-time = "2025-07-22T09:10:48.79Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/c8/8a8c7ddbdd2c7fc73782056310666736a36a7d860f9935ce1d21f5f6c02e/weaviate_client-4.16.2-py3-none-any.whl", hash = "sha256:c236adca30d18667943544ad89fcd9157947af95dfc6de4a8ecf9e7619f1c979", size = 451475, upload-time = "2025-07-22T09:10:46.941Z" },
+]
+
[[package]]
name = "weaviate-client"
version = "4.18.3"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13' and platform_machine == 's390x'",
+ "python_full_version == '3.12.*' and platform_machine != 's390x'",
+ "python_full_version == '3.12.*' and platform_machine == 's390x'",
+ "python_full_version == '3.11.*' and platform_machine != 's390x'",
+ "python_full_version == '3.11.*' and platform_machine == 's390x'",
+]
dependencies = [
- { name = "authlib" },
- { name = "deprecation" },
- { name = "grpcio" },
- { name = "httpx" },
- { name = "protobuf" },
- { name = "pydantic" },
- { name = "validators" },
+ { name = "authlib", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "deprecation", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "grpcio", version = "1.78.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "httpx", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "protobuf", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "pydantic", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
+ { name = "validators", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/76/14e07761c5fb7e8573e3cff562e2d9073c65f266db0e67511403d10435b1/weaviate_client-4.18.3.tar.gz", hash = "sha256:9d889246d62be36641a7f2b8cedf5fb665b804d46f7a53ae37e02d297a11f119", size = 783634, upload-time = "2025-12-03T09:38:28.261Z" }
wheels = [