mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-28 12:28:10 +00:00
Compare commits
10 Commits
1.12.0
...
devin/1774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e18477235b | ||
|
|
a4890e5626 | ||
|
|
6193e082e1 | ||
|
|
33f33c6fcc | ||
|
|
74976b157d | ||
|
|
bd03f6cf64 | ||
|
|
a91cd1a7d7 | ||
|
|
66dee3195f | ||
|
|
034f576dc0 | ||
|
|
918654318b |
50
.github/security.md
vendored
50
.github/security.md
vendored
@@ -1,50 +1,12 @@
|
||||
## CrewAI Security Policy
|
||||
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the CrewAI ecosystem. This policy explains how to report potential vulnerabilities and what you can expect from us when you do.
|
||||
|
||||
### Scope
|
||||
|
||||
We welcome reports for vulnerabilities that could impact:
|
||||
|
||||
- CrewAI-maintained source code and repositories
|
||||
- CrewAI-operated infrastructure and services
|
||||
- Official CrewAI releases, packages, and distributions
|
||||
|
||||
Issues affecting clearly unaffiliated third-party services or user-generated content are out of scope, unless you can demonstrate a direct impact on CrewAI systems or customers.
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the
|
||||
CrewAI ecosystem.
|
||||
|
||||
### How to Report
|
||||
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests, or social media.
|
||||
- Email detailed reports to **security@crewai.com** with the subject line `Security Report`.
|
||||
- If you need to share large files or sensitive artifacts, mention it in your email and we will coordinate a secure transfer method.
|
||||
Please submit reports to **crewai-vdp-ess@submit.bugcrowd.com**
|
||||
|
||||
### What to Include
|
||||
|
||||
Providing comprehensive information enables us to validate the issue quickly:
|
||||
|
||||
- **Vulnerability overview** — a concise description and classification (e.g., RCE, privilege escalation)
|
||||
- **Affected components** — repository, branch, tag, or deployed service along with relevant file paths or endpoints
|
||||
- **Reproduction steps** — detailed, step-by-step instructions; include logs, screenshots, or screen recordings when helpful
|
||||
- **Proof-of-concept** — exploit details or code that demonstrates the impact (if available)
|
||||
- **Impact analysis** — severity assessment, potential exploitation scenarios, and any prerequisites or special configurations
|
||||
|
||||
### Our Commitment
|
||||
|
||||
- **Acknowledgement:** We aim to acknowledge your report within two business days.
|
||||
- **Communication:** We will keep you informed about triage results, remediation progress, and planned release timelines.
|
||||
- **Resolution:** Confirmed vulnerabilities will be prioritized based on severity and fixed as quickly as possible.
|
||||
- **Recognition:** We currently do not run a bug bounty program; any rewards or recognition are issued at CrewAI's discretion.
|
||||
|
||||
### Coordinated Disclosure
|
||||
|
||||
We ask that you allow us a reasonable window to investigate and remediate confirmed issues before any public disclosure. We will coordinate publication timelines with you whenever possible.
|
||||
|
||||
### Safe Harbor
|
||||
|
||||
We will not pursue or support legal action against individuals who, in good faith:
|
||||
|
||||
- Follow this policy and refrain from violating any applicable laws
|
||||
- Avoid privacy violations, data destruction, or service disruption
|
||||
- Limit testing to systems in scope and respect rate limits and terms of service
|
||||
|
||||
If you are unsure whether your testing is covered, please contact us at **security@crewai.com** before proceeding.
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests,
|
||||
or social media
|
||||
- Reports submitted via channels other than this Bugcrowd submission email will not be reviewed and will be dismissed
|
||||
|
||||
@@ -4,6 +4,69 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة مرحلة إصدار المؤسسات إلى إصدار أدوات المطورين
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- الحفاظ على قيمة إرجاع الطريقة كإخراج تدفق لـ @human_feedback مع emit
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.1
|
||||
- مراجعة سياسة الأمان وتعليمات الإبلاغ
|
||||
|
||||
## المساهمون
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة request_id إلى HumanFeedbackRequestedEvent
|
||||
- إضافة Qdrant Edge كخلفية تخزين لنظام الذاكرة
|
||||
- إضافة أمر docs-check لتحليل التغييرات وتوليد الوثائق مع الترجمات
|
||||
- إضافة دعم اللغة العربية إلى سجل التغييرات وأدوات الإصدار
|
||||
- إضافة ترجمة باللغة العربية الفصحى لجميع الوثائق
|
||||
- إضافة أمر تسجيل الخروج في واجهة سطر الأوامر
|
||||
- إضافة مهارات الوكيل
|
||||
- تنفيذ root_scope تلقائيًا لعزل الذاكرة الهيكلية
|
||||
- تنفيذ مزودين متوافقين مع OpenAI (OpenRouter، DeepSeek، Ollama، vLLM، Cerebras، Dashscope)
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح بيانات اعتماد غير صحيحة لدفع دفعات التتبع (404)
|
||||
- حل العديد من الأخطاء في نظام تدفق HITL
|
||||
- إصلاح حفظ ذاكرة الوكيل
|
||||
- حل جميع أخطاء mypy الصارمة عبر حزمة crewai
|
||||
- إصلاح استخدام __router_paths__ لطرق المستمع + الموجه في FlowMeta
|
||||
- إصلاح خطأ القيمة عند عدم دعم الملفات
|
||||
- تصحيح صياغة الحجر الصحي لـ litellm في الوثائق
|
||||
- إصلاح جميع أخطاء mypy في crewai-files وإضافة جميع الحزم إلى فحوصات النوع في CI
|
||||
- تثبيت الحد الأعلى لـ litellm على آخر إصدار تم اختباره (1.82.6)
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.0
|
||||
- إضافة CONTRIBUTING.md
|
||||
- إضافة دليل لاستخدام CrewAI بدون LiteLLM
|
||||
|
||||
## المساهمون
|
||||
|
||||
@akaKuruma، @alex-clawd، @greysonlalonde، @iris-clawd، @joaomdmoura، @lorenzejay، @lucasgomide، @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
3718
docs/docs.json
3718
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,69 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add enterprise release phase to devtools release
|
||||
|
||||
### Bug Fixes
|
||||
- Preserve method return value as flow output for @human_feedback with emit
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.1
|
||||
- Revise security policy and reporting instructions
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add request_id to HumanFeedbackRequestedEvent
|
||||
- Add Qdrant Edge storage backend for memory system
|
||||
- Add docs-check command to analyze changes and generate docs with translations
|
||||
- Add Arabic language support to changelog and release tooling
|
||||
- Add modern standard Arabic translation of all documentation
|
||||
- Add logout command in CLI
|
||||
- Add agent skills
|
||||
- Implement automatic root_scope for hierarchical memory isolation
|
||||
- Implement native OpenAI-compatible providers (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix bad credentials for traces batch push (404)
|
||||
- Resolve multiple bugs in HITL flow system
|
||||
- Fix agent memory saving
|
||||
- Resolve all strict mypy errors across crewai package
|
||||
- Fix use of __router_paths__ for listener+router methods in FlowMeta
|
||||
- Fix value error on no file support
|
||||
- Correct litellm quarantine wording in docs
|
||||
- Fix all mypy errors in crewai-files and add all packages to CI type checks
|
||||
- Pin litellm upper bound to last tested version (1.82.6)
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.0
|
||||
- Add CONTRIBUTING.md
|
||||
- Add guide for using CrewAI without LiteLLM
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
@@ -4,6 +4,69 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- devtools 릴리스에 기업 릴리스 단계 추가
|
||||
|
||||
### 버그 수정
|
||||
- @human_feedback과 함께 emit을 사용할 때 메서드 반환 값을 흐름 출력으로 유지
|
||||
|
||||
### 문서
|
||||
- v1.12.1에 대한 변경 로그 및 버전 업데이트
|
||||
- 보안 정책 및 보고 지침 수정
|
||||
|
||||
## 기여자
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- HumanFeedbackRequestedEvent에 request_id 추가
|
||||
- 메모리 시스템을 위한 Qdrant Edge 저장소 백엔드 추가
|
||||
- 변경 사항을 분석하고 번역된 문서와 함께 문서를 생성하는 docs-check 명령어 추가
|
||||
- 변경 로그 및 릴리스 도구에 아랍어 지원 추가
|
||||
- 모든 문서에 대한 현대 표준 아랍어 번역 추가
|
||||
- CLI에 로그아웃 명령어 추가
|
||||
- 에이전트 기술 추가
|
||||
- 계층적 메모리 격리를 위한 자동 root_scope 구현
|
||||
- OpenAI 호환 네이티브 제공자 구현 (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### 버그 수정
|
||||
- 트레이스 배치 푸시에 대한 잘못된 자격 증명 수정 (404)
|
||||
- HITL 흐름 시스템의 여러 버그 해결
|
||||
- 에이전트 메모리 저장 수정
|
||||
- crewai 패키지 전반에 걸쳐 모든 엄격한 mypy 오류 해결
|
||||
- FlowMeta의 listener+router 메서드에 대한 __router_paths__ 사용 수정
|
||||
- 파일 지원이 없는 경우 값 오류 수정
|
||||
- 문서에서 litellm 격리 단어 수정
|
||||
- crewai-files의 모든 mypy 오류 수정 및 모든 패키지를 CI 유형 검사에 추가
|
||||
- litellm의 상한을 마지막 테스트된 버전 (1.82.6)으로 고정
|
||||
|
||||
### 문서
|
||||
- v1.12.0에 대한 변경 로그 및 버전 업데이트
|
||||
- CONTRIBUTING.md 추가
|
||||
- LiteLLM 없이 CrewAI를 사용하는 가이드 추가
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
@@ -4,6 +4,69 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar fase de lançamento empresarial ao lançamento do devtools
|
||||
|
||||
### Correções de Bugs
|
||||
- Preservar o valor de retorno do método como saída de fluxo para @human_feedback com emit
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.1
|
||||
- Revisar política de segurança e instruções de relatório
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar request_id ao HumanFeedbackRequestedEvent
|
||||
- Adicionar backend de armazenamento Qdrant Edge para sistema de memória
|
||||
- Adicionar comando docs-check para analisar mudanças e gerar documentação com traduções
|
||||
- Adicionar suporte ao idioma árabe para changelog e ferramentas de lançamento
|
||||
- Adicionar tradução em árabe padrão moderno de toda a documentação
|
||||
- Adicionar comando de logout na CLI
|
||||
- Adicionar habilidades de agente
|
||||
- Implementar root_scope automático para isolamento hierárquico de memória
|
||||
- Implementar provedores nativos compatíveis com OpenAI (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir credenciais incorretas para envio em lote de traces (404)
|
||||
- Resolver múltiplos bugs no sistema de fluxo HITL
|
||||
- Corrigir salvamento de memória do agente
|
||||
- Resolver todos os erros estritos do mypy no pacote crewai
|
||||
- Corrigir uso de __router_paths__ para métodos listener+router em FlowMeta
|
||||
- Corrigir erro de valor em caso de suporte a nenhum arquivo
|
||||
- Corrigir redação da quarentena do litellm na documentação
|
||||
- Corrigir todos os erros do mypy em crewai-files e adicionar todos os pacotes às verificações de tipo do CI
|
||||
- Fixar limite superior do litellm na última versão testada (1.82.6)
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.0
|
||||
- Adicionar CONTRIBUTING.md
|
||||
- Adicionar guia para usar CrewAI sem LiteLLM
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Greyson LaLonde", email = "greyson@crewai.com" }
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
requires-python = ">=3.10, <3.15"
|
||||
dependencies = [
|
||||
"Pillow~=12.1.1",
|
||||
"pypdf~=6.9.1",
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.2"
|
||||
|
||||
@@ -6,12 +6,12 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "João Moura", email = "joaomdmoura@gmail.com" },
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
requires-python = ">=3.10, <3.15"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.12.0a3",
|
||||
"crewai==1.12.2",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.2"
|
||||
|
||||
@@ -47,7 +47,7 @@ class BrowserSessionManager:
|
||||
Returns:
|
||||
An async browser instance specific to the thread
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
with self._lock:
|
||||
if thread_id in self._async_sessions:
|
||||
|
||||
@@ -94,11 +94,9 @@ class BrowserBaseTool(BaseTool):
|
||||
try:
|
||||
import nest_asyncio # type: ignore[import-untyped]
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
nest_asyncio.apply(loop)
|
||||
result: str = asyncio.get_event_loop().run_until_complete(
|
||||
self._arun(*args, **kwargs)
|
||||
)
|
||||
result: str = loop.run_until_complete(self._arun(*args, **kwargs))
|
||||
return result
|
||||
except Exception as e:
|
||||
return f"Error in patched _run: {e!s}"
|
||||
@@ -118,7 +116,7 @@ class BrowserBaseTool(BaseTool):
|
||||
def _is_in_asyncio_loop(self) -> bool:
|
||||
"""Check if we're currently in an asyncio event loop."""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
return loop.is_running()
|
||||
except RuntimeError:
|
||||
return False
|
||||
@@ -544,14 +542,13 @@ class BrowserToolkit:
|
||||
def _nest_current_loop(self) -> None:
|
||||
"""Apply nest_asyncio if we're in an asyncio loop."""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
try:
|
||||
import nest_asyncio
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
import nest_asyncio
|
||||
|
||||
nest_asyncio.apply(loop)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to apply nest_asyncio: {e!s}")
|
||||
nest_asyncio.apply(loop)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to apply nest_asyncio: {e!s}")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class SnowflakeSearchTool(BaseTool):
|
||||
with self._pool_lock:
|
||||
if self._connection_pool:
|
||||
return self._connection_pool.pop()
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
self._thread_pool, self._create_connection
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Joao Moura", email = "joao@crewai.com" }
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
requires-python = ">=3.10, <3.15"
|
||||
dependencies = [
|
||||
# Core Dependencies
|
||||
"pydantic~=2.11.9",
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.12.0a3",
|
||||
"crewai-tools==1.12.2",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -42,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.2"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ class MemoryTUI(App[None]):
|
||||
panel.loading = True
|
||||
try:
|
||||
scope = self._selected_scope if self._selected_scope != "/" else None
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
matches = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._memory.recall(query, scope=scope, limit=10, depth="deep"),
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.12.0a3"
|
||||
"crewai[tools]==1.12.2"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.12.0a3"
|
||||
"crewai[tools]==1.12.2"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.12.0a3"
|
||||
"crewai[tools]==1.12.2"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -178,12 +178,15 @@ class HumanFeedbackRequestedEvent(FlowEvent):
|
||||
output: The method output shown to the human for review.
|
||||
message: The message displayed when requesting feedback.
|
||||
emit: Optional list of possible outcomes for routing.
|
||||
request_id: Platform-assigned identifier for this feedback request,
|
||||
used for correlating the request across system boundaries.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
output: Any
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
request_id: str | None = None
|
||||
type: str = "human_feedback_requested"
|
||||
|
||||
|
||||
@@ -198,9 +201,12 @@ class HumanFeedbackReceivedEvent(FlowEvent):
|
||||
method_name: Name of the method that received feedback.
|
||||
feedback: The raw text feedback provided by the human.
|
||||
outcome: The collapsed outcome string (if emit was specified).
|
||||
request_id: Platform-assigned identifier for this feedback request,
|
||||
used for correlating the response back to its originating request.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
feedback: str
|
||||
outcome: str | None = None
|
||||
request_id: str | None = None
|
||||
type: str = "human_feedback_received"
|
||||
|
||||
@@ -883,6 +883,9 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self.human_feedback_history: list[HumanFeedbackResult] = []
|
||||
self.last_human_feedback: HumanFeedbackResult | None = None
|
||||
self._pending_feedback_context: PendingFeedbackContext | None = None
|
||||
# Per-method stash for real @human_feedback output (keyed by method name)
|
||||
# Used to decouple routing outcome from method return value when emit is set
|
||||
self._human_feedback_method_outputs: dict[str, Any] = {}
|
||||
self.suppress_flow_events: bool = suppress_flow_events
|
||||
|
||||
# User input history (for self.ask())
|
||||
@@ -2290,6 +2293,17 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
result = await result
|
||||
|
||||
self._method_outputs.append(result)
|
||||
|
||||
# For @human_feedback methods with emit, the result is the collapsed outcome
|
||||
# (e.g., "approved") used for routing. But we want the actual method output
|
||||
# to be the stored result (for final flow output). Replace the last entry
|
||||
# if a stashed output exists. Dict-based stash is concurrency-safe and
|
||||
# handles None return values (presence in dict = stashed, not value).
|
||||
if method_name in self._human_feedback_method_outputs:
|
||||
self._method_outputs[-1] = self._human_feedback_method_outputs.pop(
|
||||
method_name
|
||||
)
|
||||
|
||||
self._method_execution_counts[method_name] = (
|
||||
self._method_execution_counts.get(method_name, 0) + 1
|
||||
)
|
||||
|
||||
@@ -591,6 +591,13 @@ def human_feedback(
|
||||
):
|
||||
_distill_and_store_lessons(self, method_output, raw_feedback)
|
||||
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
|
||||
return result
|
||||
|
||||
wrapper: Any = async_wrapper
|
||||
@@ -615,6 +622,13 @@ def human_feedback(
|
||||
):
|
||||
_distill_and_store_lessons(self, method_output, raw_feedback)
|
||||
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
|
||||
return result
|
||||
|
||||
wrapper = sync_wrapper
|
||||
|
||||
@@ -86,7 +86,7 @@ class ChromaDBClient(BaseClient):
|
||||
yield
|
||||
return
|
||||
lock_cm = store_lock(self._lock_name)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, lock_cm.__enter__)
|
||||
try:
|
||||
yield
|
||||
|
||||
@@ -266,7 +266,7 @@ class CrewStructuredTool:
|
||||
# Run sync functions in a thread pool
|
||||
import asyncio
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
None, lambda: self.func(**parsed_args, **kwargs)
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -184,7 +184,7 @@ def create_streaming_state(
|
||||
|
||||
if use_async:
|
||||
async_queue = asyncio.Queue()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
handler = _create_stream_handler(current_task_info, sync_queue, async_queue, loop)
|
||||
crewai_event_bus.register_handler(LLMStreamChunkEvent, handler)
|
||||
|
||||
@@ -246,7 +246,7 @@ class TestHumanFeedbackExecution:
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_empty_feedback_with_default_outcome(self, mock_print, mock_input):
|
||||
"""Test empty feedback uses default_outcome."""
|
||||
"""Test empty feedback uses default_outcome for routing, but flow returns method output."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@@ -264,14 +264,16 @@ class TestHumanFeedbackExecution:
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "needs_work"
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "Content"
|
||||
assert flow.last_human_feedback is not None
|
||||
# But the outcome is still correctly set for routing purposes
|
||||
assert flow.last_human_feedback.outcome == "needs_work"
|
||||
|
||||
@patch("builtins.input", return_value="Approved!")
|
||||
@patch("builtins.print")
|
||||
def test_feedback_collapsing(self, mock_print, mock_input):
|
||||
"""Test that feedback is collapsed to an outcome."""
|
||||
"""Test that feedback is collapsed to an outcome for routing, but flow returns method output."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@@ -291,8 +293,10 @@ class TestHumanFeedbackExecution:
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "approved"
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "Content"
|
||||
assert flow.last_human_feedback is not None
|
||||
# But the outcome is still correctly set for routing purposes
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
|
||||
@@ -591,3 +595,162 @@ class TestHumanFeedbackLearn:
|
||||
assert config.learn is True
|
||||
# llm defaults to "gpt-4o-mini" at the function level
|
||||
assert config.llm == "gpt-4o-mini"
|
||||
|
||||
|
||||
class TestHumanFeedbackFinalOutputPreservation:
|
||||
"""Tests for preserving method return value as flow's final output when @human_feedback with emit is terminal.
|
||||
|
||||
This addresses the bug where the flow's final output was the collapsed outcome string (e.g., 'approved')
|
||||
instead of the method's actual return value when a @human_feedback method with emit is the final method.
|
||||
"""
|
||||
|
||||
@patch("builtins.input", return_value="Looks good!")
|
||||
@patch("builtins.print")
|
||||
def test_final_output_is_method_return_not_collapsed_outcome(
|
||||
self, mock_print, mock_input
|
||||
):
|
||||
"""When @human_feedback with emit is the final method, flow output is the method's return value."""
|
||||
|
||||
class FinalHumanFeedbackFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review this content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def generate_and_review(self):
|
||||
# This dict should be the final output, NOT the string 'approved'
|
||||
return {"title": "My Article", "content": "Article content here", "status": "ready"}
|
||||
|
||||
flow = FinalHumanFeedbackFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="Looks great, approved!"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# The final output should be the actual method return value, not the collapsed outcome
|
||||
assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}: {result}"
|
||||
assert result == {"title": "My Article", "content": "Article content here", "status": "ready"}
|
||||
# But the outcome should still be tracked in last_human_feedback
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
@patch("builtins.input", return_value="approved")
|
||||
@patch("builtins.print")
|
||||
def test_routing_still_works_with_downstream_listener(self, mock_print, mock_input):
|
||||
"""When @human_feedback has a downstream listener, routing still triggers the listener."""
|
||||
publish_called = []
|
||||
|
||||
class RoutingFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def review(self):
|
||||
return {"content": "original content"}
|
||||
|
||||
@listen("approved")
|
||||
def publish(self):
|
||||
publish_called.append(True)
|
||||
return {"published": True, "timestamp": "2024-01-01"}
|
||||
|
||||
flow = RoutingFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="LGTM"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# The downstream listener should have been triggered
|
||||
assert len(publish_called) == 1, "publish() should have been called"
|
||||
# The final output should be from the listener, not the human_feedback method
|
||||
assert result == {"published": True, "timestamp": "2024-01-01"}
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_human_feedback_final_output_preserved(self, mock_print, mock_input):
|
||||
"""Async @human_feedback methods also preserve the real return value."""
|
||||
|
||||
class AsyncFinalFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review async content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="approved",
|
||||
)
|
||||
async def async_generate(self):
|
||||
return {"async_data": "value", "computed": 42}
|
||||
|
||||
flow = AsyncFinalFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value=""),
|
||||
):
|
||||
result = await flow.kickoff_async()
|
||||
|
||||
# The final output should be the dict, not "approved"
|
||||
assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}: {result}"
|
||||
assert result == {"async_data": "value", "computed": 42}
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
def test_method_outputs_contains_real_output(self, mock_print, mock_input):
|
||||
"""The _method_outputs list should contain the real method output, not the collapsed outcome."""
|
||||
|
||||
class OutputTrackingFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def generate(self):
|
||||
return {"data": "real output"}
|
||||
|
||||
flow = OutputTrackingFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="approved"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
flow.kickoff()
|
||||
|
||||
# _method_outputs should contain the real output
|
||||
assert len(flow._method_outputs) == 1
|
||||
assert flow._method_outputs[0] == {"data": "real output"}
|
||||
|
||||
@patch("builtins.input", return_value="looks good")
|
||||
@patch("builtins.print")
|
||||
def test_none_return_value_is_preserved(self, mock_print, mock_input):
|
||||
"""A method returning None should preserve None as flow output, not the outcome string."""
|
||||
|
||||
class NoneReturnFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def process(self):
|
||||
# Method does work but returns None (implicit)
|
||||
pass
|
||||
|
||||
flow = NoneReturnFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value=""),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# Final output should be None (the method's real return), not "approved"
|
||||
assert result is None, f"Expected None, got {result!r}"
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
@@ -708,7 +708,7 @@ class TestEdgeCases:
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_empty_feedback_first_outcome_fallback(self, mock_print, mock_input):
|
||||
"""Test that empty feedback without default uses first outcome."""
|
||||
"""Test that empty feedback without default uses first outcome for routing, but returns method output."""
|
||||
|
||||
class FallbackFlow(Flow):
|
||||
@start()
|
||||
@@ -726,12 +726,15 @@ class TestEdgeCases:
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "first" # Falls back to first outcome
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "content"
|
||||
# But outcome is still set to first for routing purposes
|
||||
assert flow.last_human_feedback.outcome == "first"
|
||||
|
||||
@patch("builtins.input", return_value="whitespace only ")
|
||||
@patch("builtins.print")
|
||||
def test_whitespace_only_feedback_treated_as_empty(self, mock_print, mock_input):
|
||||
"""Test that whitespace-only feedback is treated as empty."""
|
||||
"""Test that whitespace-only feedback is treated as empty for routing, but returns method output."""
|
||||
|
||||
class WhitespaceFlow(Flow):
|
||||
@start()
|
||||
@@ -749,7 +752,10 @@ class TestEdgeCases:
|
||||
with patch.object(flow, "_request_human_feedback", return_value=" "):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "reject" # Uses default because feedback is empty after strip
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "content"
|
||||
# But outcome is set to default because feedback is empty after strip
|
||||
assert flow.last_human_feedback.outcome == "reject"
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
|
||||
210
lib/crewai/tests/test_python314_compat.py
Normal file
210
lib/crewai/tests/test_python314_compat.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""Tests for Python 3.14 compatibility.
|
||||
|
||||
Python 3.14 changed asyncio.get_event_loop() to raise RuntimeError when no
|
||||
running event loop exists instead of creating one. All async code paths must
|
||||
use asyncio.get_running_loop() instead.
|
||||
|
||||
See: https://github.com/crewAIInc/crewAI/issues/5109
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.utilities.streaming import create_streaming_state
|
||||
|
||||
|
||||
class TestStructuredToolAsyncCompat:
|
||||
"""Test that CrewStructuredTool.ainvoke uses get_running_loop correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ainvoke_sync_func_uses_running_loop(self) -> None:
|
||||
"""ainvoke() with a sync function must use the running event loop."""
|
||||
|
||||
def sync_func(x: int) -> int:
|
||||
"""A sync function."""
|
||||
return x * 2
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=sync_func, name="double", description="Doubles a number"
|
||||
)
|
||||
result = await tool.ainvoke({"x": 5})
|
||||
assert result == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ainvoke_async_func(self) -> None:
|
||||
"""ainvoke() with an async function should call it directly."""
|
||||
|
||||
async def async_func(x: int) -> int:
|
||||
"""An async function."""
|
||||
return x * 3
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=async_func, name="triple", description="Triples a number"
|
||||
)
|
||||
result = await tool.ainvoke({"x": 4})
|
||||
assert result == 12
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ainvoke_sync_func_runs_in_executor(self) -> None:
|
||||
"""Verify ainvoke offloads sync functions to an executor via the running loop."""
|
||||
import threading
|
||||
|
||||
call_thread_ids: list[int] = []
|
||||
|
||||
def sync_func(x: int) -> int:
|
||||
"""A sync function that records its thread."""
|
||||
call_thread_ids.append(threading.current_thread().ident or 0)
|
||||
return x + 1
|
||||
|
||||
tool = CrewStructuredTool.from_function(
|
||||
func=sync_func, name="inc", description="Increment"
|
||||
)
|
||||
|
||||
result = await tool.ainvoke({"x": 1})
|
||||
assert result == 2
|
||||
assert len(call_thread_ids) == 1
|
||||
# Sync func should run in a different thread (executor)
|
||||
assert call_thread_ids[0] != threading.current_thread().ident
|
||||
|
||||
|
||||
class TestStreamingStateAsyncCompat:
|
||||
"""Test that create_streaming_state uses get_running_loop correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_streaming_state_async_uses_running_loop(self) -> None:
|
||||
"""create_streaming_state(use_async=True) must use the running loop."""
|
||||
task_info = {
|
||||
"index": 0,
|
||||
"name": "test",
|
||||
"id": "test-id",
|
||||
"agent_role": "tester",
|
||||
"agent_id": "agent-id",
|
||||
}
|
||||
state = create_streaming_state(
|
||||
current_task_info=task_info,
|
||||
result_holder=[],
|
||||
use_async=True,
|
||||
)
|
||||
assert state.loop is not None
|
||||
assert state.async_queue is not None
|
||||
assert state.loop is asyncio.get_running_loop()
|
||||
|
||||
def test_create_streaming_state_sync_no_loop_needed(self) -> None:
|
||||
"""create_streaming_state(use_async=False) should not require a loop."""
|
||||
task_info = {
|
||||
"index": 0,
|
||||
"name": "test",
|
||||
"id": "test-id",
|
||||
"agent_role": "tester",
|
||||
"agent_id": "agent-id",
|
||||
}
|
||||
state = create_streaming_state(
|
||||
current_task_info=task_info,
|
||||
result_holder=[],
|
||||
use_async=False,
|
||||
)
|
||||
assert state.loop is None
|
||||
assert state.async_queue is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_streaming_state_async_uses_get_running_loop_not_get_event_loop(
|
||||
self,
|
||||
) -> None:
|
||||
"""Verify create_streaming_state does not call asyncio.get_event_loop()."""
|
||||
task_info = {
|
||||
"index": 0,
|
||||
"name": "test",
|
||||
"id": "test-id",
|
||||
"agent_role": "tester",
|
||||
"agent_id": "agent-id",
|
||||
}
|
||||
|
||||
with patch("crewai.utilities.streaming.asyncio") as mock_asyncio:
|
||||
mock_asyncio.Queue = asyncio.Queue
|
||||
mock_asyncio.get_running_loop.return_value = asyncio.get_running_loop()
|
||||
|
||||
create_streaming_state(
|
||||
current_task_info=task_info,
|
||||
result_holder=[],
|
||||
use_async=True,
|
||||
)
|
||||
|
||||
mock_asyncio.get_running_loop.assert_called_once()
|
||||
mock_asyncio.get_event_loop.assert_not_called()
|
||||
|
||||
|
||||
class TestChromaDBClientAsyncCompat:
|
||||
"""Test that ChromaDBClient._alocked uses get_running_loop correctly."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alocked_without_lock_name(self) -> None:
|
||||
"""_alocked should yield immediately when no lock name is set."""
|
||||
from crewai.rag.chromadb.client import ChromaDBClient
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_ef = MagicMock()
|
||||
client = ChromaDBClient(
|
||||
client=mock_client,
|
||||
embedding_function=mock_ef,
|
||||
lock_name=None,
|
||||
)
|
||||
|
||||
async with client._alocked():
|
||||
pass # Should not raise
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alocked_uses_get_running_loop_not_get_event_loop(self) -> None:
|
||||
"""Verify _alocked does not call asyncio.get_event_loop()."""
|
||||
from crewai.rag.chromadb.client import ChromaDBClient
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_ef = MagicMock()
|
||||
client = ChromaDBClient(
|
||||
client=mock_client,
|
||||
embedding_function=mock_ef,
|
||||
lock_name="test-lock",
|
||||
)
|
||||
|
||||
with patch("crewai.rag.chromadb.client.asyncio") as mock_asyncio:
|
||||
loop = asyncio.get_running_loop()
|
||||
mock_asyncio.get_running_loop.return_value = loop
|
||||
|
||||
mock_cm = MagicMock()
|
||||
with patch("crewai.rag.chromadb.client.store_lock", return_value=mock_cm):
|
||||
async with client._alocked():
|
||||
pass
|
||||
|
||||
mock_asyncio.get_running_loop.assert_called()
|
||||
mock_asyncio.get_event_loop.assert_not_called()
|
||||
|
||||
|
||||
class TestGetRunningLoopInAsyncContext:
|
||||
"""General tests ensuring get_running_loop works in async contexts."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_running_loop_available_in_async_context(self) -> None:
|
||||
"""asyncio.get_running_loop() should work in an async context."""
|
||||
loop = asyncio.get_running_loop()
|
||||
assert loop is not None
|
||||
assert loop.is_running()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_in_executor_with_running_loop(self) -> None:
|
||||
"""run_in_executor should work with get_running_loop()."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def sync_work() -> str:
|
||||
return "done"
|
||||
|
||||
result = await loop.run_in_executor(None, sync_work)
|
||||
assert result == "done"
|
||||
|
||||
def test_get_running_loop_raises_outside_async(self) -> None:
|
||||
"""get_running_loop() should raise RuntimeError outside async context."""
|
||||
with pytest.raises(RuntimeError):
|
||||
asyncio.get_running_loop()
|
||||
@@ -271,7 +271,7 @@ async def test_mixed_sync_async_handler_execution():
|
||||
timeout=5
|
||||
)
|
||||
|
||||
await asyncio.get_event_loop().run_in_executor(None, wait_for_completion)
|
||||
await asyncio.get_running_loop().run_in_executor(None, wait_for_completion)
|
||||
|
||||
assert len(sync_executed) == 5
|
||||
assert len(async_executed) == 5
|
||||
|
||||
@@ -8,18 +8,22 @@ Installed automatically via the workspace (`uv sync`). Requires:
|
||||
|
||||
- [GitHub CLI](https://cli.github.com/) (`gh`) — authenticated
|
||||
- `OPENAI_API_KEY` env var — for release note generation and translation
|
||||
- `ENTERPRISE_REPO` env var — GitHub repo for enterprise releases
|
||||
- `ENTERPRISE_VERSION_DIRS` env var — comma-separated directories to bump in the enterprise repo
|
||||
- `ENTERPRISE_CREWAI_DEP_PATH` env var — path to the pyproject.toml with the `crewai[tools]` pin in the enterprise repo
|
||||
|
||||
## Commands
|
||||
|
||||
### `devtools release <version>`
|
||||
|
||||
Full end-to-end release. Bumps versions, creates PRs, tags, and publishes a GitHub release.
|
||||
Full end-to-end release. Bumps versions, creates PRs, tags, publishes a GitHub release, and releases the enterprise repo.
|
||||
|
||||
```
|
||||
devtools release 1.10.3
|
||||
devtools release 1.10.3a1 # pre-release
|
||||
devtools release 1.10.3 --no-edit # skip editing release notes
|
||||
devtools release 1.10.3 --dry-run # preview without changes
|
||||
devtools release 1.10.3a1 # pre-release
|
||||
devtools release 1.10.3 --no-edit # skip editing release notes
|
||||
devtools release 1.10.3 --dry-run # preview without changes
|
||||
devtools release 1.10.3 --skip-enterprise # skip enterprise release phase
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
@@ -31,6 +35,10 @@ devtools release 1.10.3 --dry-run # preview without changes
|
||||
5. Updates changelogs (en, pt-BR, ko) and docs version switcher
|
||||
6. Creates docs PR against main, polls until merged
|
||||
7. Tags main and creates GitHub release
|
||||
8. Triggers PyPI publish workflow
|
||||
9. Clones enterprise repo, bumps versions and `crewai[tools]` dep, runs `uv sync`
|
||||
10. Creates enterprise bump PR, polls until merged
|
||||
11. Tags and creates GitHub release on enterprise repo
|
||||
|
||||
### `devtools bump <version>`
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.2"
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Final, Literal
|
||||
from urllib.request import urlopen
|
||||
|
||||
import click
|
||||
from dotenv import load_dotenv
|
||||
@@ -153,12 +156,24 @@ def update_version_in_file(file_path: Path, new_version: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def update_pyproject_dependencies(file_path: Path, new_version: str) -> bool:
|
||||
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
|
||||
"crewai",
|
||||
"crewai-tools",
|
||||
"crewai-devtools",
|
||||
]
|
||||
|
||||
|
||||
def update_pyproject_dependencies(
|
||||
file_path: Path,
|
||||
new_version: str,
|
||||
extra_packages: list[str] | None = None,
|
||||
) -> bool:
|
||||
"""Update workspace dependency versions in pyproject.toml.
|
||||
|
||||
Args:
|
||||
file_path: Path to pyproject.toml file.
|
||||
new_version: New version string.
|
||||
extra_packages: Additional package names to update beyond the defaults.
|
||||
|
||||
Returns:
|
||||
True if any dependencies were updated, False otherwise.
|
||||
@@ -170,7 +185,7 @@ def update_pyproject_dependencies(file_path: Path, new_version: str) -> bool:
|
||||
lines = content.splitlines()
|
||||
updated = False
|
||||
|
||||
workspace_packages = ["crewai", "crewai-tools", "crewai-devtools"]
|
||||
workspace_packages = _DEFAULT_WORKSPACE_PACKAGES + (extra_packages or [])
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
for pkg in workspace_packages:
|
||||
@@ -431,12 +446,29 @@ def update_changelog(
|
||||
return True
|
||||
|
||||
|
||||
def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]:
|
||||
"""Update crewai dependency versions in CLI template pyproject.toml files.
|
||||
def _pin_crewai_deps(content: str, version: str) -> str:
|
||||
"""Replace crewai dependency version pins in a pyproject.toml string.
|
||||
|
||||
Handles both pinned (==) and minimum (>=) version specifiers,
|
||||
as well as extras like [tools].
|
||||
|
||||
Args:
|
||||
content: File content to transform.
|
||||
version: New version string.
|
||||
|
||||
Returns:
|
||||
Transformed content.
|
||||
"""
|
||||
return re.sub(
|
||||
r'"crewai(\[tools\])?(==|>=)[^"]*"',
|
||||
lambda m: f'"crewai{(m.group(1) or "")!s}=={version}"',
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]:
|
||||
"""Update crewai dependency versions in CLI template pyproject.toml files.
|
||||
|
||||
Args:
|
||||
templates_dir: Path to the CLI templates directory.
|
||||
new_version: New version string.
|
||||
@@ -444,16 +476,10 @@ def update_template_dependencies(templates_dir: Path, new_version: str) -> list[
|
||||
Returns:
|
||||
List of paths that were updated.
|
||||
"""
|
||||
import re
|
||||
|
||||
updated = []
|
||||
for pyproject in templates_dir.rglob("pyproject.toml"):
|
||||
content = pyproject.read_text()
|
||||
new_content = re.sub(
|
||||
r'"crewai(\[tools\])?(==|>=)[^"]*"',
|
||||
lambda m: f'"crewai{(m.group(1) or "")!s}=={new_version}"',
|
||||
content,
|
||||
)
|
||||
new_content = _pin_crewai_deps(content, new_version)
|
||||
if new_content != content:
|
||||
pyproject.write_text(new_content)
|
||||
updated.append(pyproject)
|
||||
@@ -607,24 +633,26 @@ def get_github_contributors(commit_range: str) -> list[str]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _poll_pr_until_merged(branch_name: str, label: str) -> None:
|
||||
"""Poll a GitHub PR until it is merged. Exit if closed without merging."""
|
||||
def _poll_pr_until_merged(
|
||||
branch_name: str, label: str, repo: str | None = None
|
||||
) -> None:
|
||||
"""Poll a GitHub PR until it is merged. Exit if closed without merging.
|
||||
|
||||
Args:
|
||||
branch_name: Branch name to look up the PR.
|
||||
label: Human-readable label for status messages.
|
||||
repo: Optional GitHub repo (owner/name) for cross-repo PRs.
|
||||
"""
|
||||
console.print(f"[cyan]Waiting for {label} to be merged...[/cyan]")
|
||||
cmd = ["gh", "pr", "view", branch_name]
|
||||
if repo:
|
||||
cmd.extend(["--repo", repo])
|
||||
cmd.extend(["--json", "state", "--jq", ".state"])
|
||||
|
||||
while True:
|
||||
time.sleep(10)
|
||||
try:
|
||||
state = run_command(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"view",
|
||||
branch_name,
|
||||
"--json",
|
||||
"state",
|
||||
"--jq",
|
||||
".state",
|
||||
]
|
||||
)
|
||||
state = run_command(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
state = ""
|
||||
|
||||
@@ -984,8 +1012,252 @@ def _create_tag_and_release(
|
||||
console.print(f"[green]✓[/green] Created GitHub {release_type} for {tag_name}")
|
||||
|
||||
|
||||
def _trigger_pypi_publish(tag_name: str) -> None:
|
||||
"""Trigger the PyPI publish GitHub Actions workflow."""
|
||||
_ENTERPRISE_REPO: Final[str | None] = os.getenv("ENTERPRISE_REPO")
|
||||
_ENTERPRISE_VERSION_DIRS: Final[tuple[str, ...]] = tuple(
|
||||
d.strip() for d in os.getenv("ENTERPRISE_VERSION_DIRS", "").split(",") if d.strip()
|
||||
)
|
||||
_ENTERPRISE_CREWAI_DEP_PATH: Final[str | None] = os.getenv("ENTERPRISE_CREWAI_DEP_PATH")
|
||||
_ENTERPRISE_EXTRA_PACKAGES: Final[tuple[str, ...]] = tuple(
|
||||
p.strip()
|
||||
for p in os.getenv("ENTERPRISE_EXTRA_PACKAGES", "").split(",")
|
||||
if p.strip()
|
||||
)
|
||||
|
||||
|
||||
def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
|
||||
"""Update the crewai[tools] pin in an enterprise pyproject.toml.
|
||||
|
||||
Args:
|
||||
pyproject_path: Path to the pyproject.toml file.
|
||||
version: New crewai version string.
|
||||
|
||||
Returns:
|
||||
True if the file was modified.
|
||||
"""
|
||||
if not pyproject_path.exists():
|
||||
return False
|
||||
|
||||
content = pyproject_path.read_text()
|
||||
new_content = _pin_crewai_deps(content, version)
|
||||
if new_content != content:
|
||||
pyproject_path.write_text(new_content)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
_PYPI_POLL_INTERVAL: Final[int] = 15
|
||||
_PYPI_POLL_TIMEOUT: Final[int] = 600
|
||||
|
||||
|
||||
def _wait_for_pypi(package: str, version: str) -> None:
|
||||
"""Poll PyPI until a specific package version is available.
|
||||
|
||||
Args:
|
||||
package: PyPI package name.
|
||||
version: Version string to wait for.
|
||||
"""
|
||||
url = f"https://pypi.org/pypi/{package}/{version}/json"
|
||||
deadline = time.monotonic() + _PYPI_POLL_TIMEOUT
|
||||
|
||||
console.print(f"[cyan]Waiting for {package}=={version} to appear on PyPI...[/cyan]")
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with urlopen(url) as resp: # noqa: S310
|
||||
if resp.status == 200:
|
||||
console.print(
|
||||
f"[green]✓[/green] {package}=={version} is available on PyPI"
|
||||
)
|
||||
return
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
time.sleep(_PYPI_POLL_INTERVAL)
|
||||
|
||||
console.print(
|
||||
f"[red]Error:[/red] Timed out waiting for {package}=={version} on PyPI"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> None:
|
||||
"""Clone the enterprise repo, bump versions, and create a release PR.
|
||||
|
||||
Expects ENTERPRISE_REPO, ENTERPRISE_VERSION_DIRS, and
|
||||
ENTERPRISE_CREWAI_DEP_PATH to be validated before calling.
|
||||
|
||||
Args:
|
||||
version: New version string.
|
||||
is_prerelease: Whether this is a pre-release version.
|
||||
dry_run: Show what would be done without making changes.
|
||||
"""
|
||||
if (
|
||||
not _ENTERPRISE_REPO
|
||||
or not _ENTERPRISE_VERSION_DIRS
|
||||
or not _ENTERPRISE_CREWAI_DEP_PATH
|
||||
):
|
||||
console.print("[red]Error:[/red] Enterprise env vars not configured")
|
||||
sys.exit(1)
|
||||
|
||||
enterprise_repo: str = _ENTERPRISE_REPO
|
||||
enterprise_dep_path: str = _ENTERPRISE_CREWAI_DEP_PATH
|
||||
|
||||
console.print(
|
||||
f"\n[bold cyan]Phase 3: Releasing {enterprise_repo} {version}[/bold cyan]"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
console.print(f"[dim][DRY RUN][/dim] Would clone {enterprise_repo}")
|
||||
for d in _ENTERPRISE_VERSION_DIRS:
|
||||
console.print(f"[dim][DRY RUN][/dim] Would update versions in {d}")
|
||||
console.print(
|
||||
f"[dim][DRY RUN][/dim] Would update crewai[tools] dep in "
|
||||
f"{enterprise_dep_path}"
|
||||
)
|
||||
console.print(
|
||||
"[dim][DRY RUN][/dim] Would create bump PR, wait for merge, "
|
||||
"then tag and release"
|
||||
)
|
||||
return
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
repo_dir = Path(tmp) / enterprise_repo.split("/")[-1]
|
||||
console.print(f"Cloning {enterprise_repo}...")
|
||||
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():
|
||||
console.print(
|
||||
f"[yellow]Warning:[/yellow] {rel_dir} not found, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
for vfile in find_version_files(pkg_dir):
|
||||
if update_version_in_file(vfile, version):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated: {vfile.relative_to(repo_dir)}"
|
||||
)
|
||||
|
||||
pyproject = pkg_dir / "pyproject.toml"
|
||||
if pyproject.exists():
|
||||
if update_pyproject_dependencies(
|
||||
pyproject, version, extra_packages=list(_ENTERPRISE_EXTRA_PACKAGES)
|
||||
):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated deps in: "
|
||||
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}"
|
||||
)
|
||||
|
||||
_wait_for_pypi("crewai", version)
|
||||
|
||||
console.print("\nSyncing workspace...")
|
||||
run_command(["uv", "sync"], cwd=repo_dir)
|
||||
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)
|
||||
run_command(
|
||||
["git", "commit", "-m", f"feat: bump versions to {version}"],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
console.print("[green]✓[/green] Changes committed")
|
||||
|
||||
run_command(["git", "push", "-u", "origin", branch_name], cwd=repo_dir)
|
||||
console.print("[green]✓[/green] Branch pushed")
|
||||
|
||||
run_command(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"create",
|
||||
"--repo",
|
||||
enterprise_repo,
|
||||
"--base",
|
||||
"main",
|
||||
"--title",
|
||||
f"feat: bump versions to {version}",
|
||||
"--body",
|
||||
"",
|
||||
],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
console.print("[green]✓[/green] Enterprise bump PR created")
|
||||
|
||||
_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)
|
||||
|
||||
tag_name = version
|
||||
run_command(
|
||||
["git", "tag", "-a", tag_name, "-m", f"Release {version}"],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
run_command(["git", "push", "origin", tag_name], cwd=repo_dir)
|
||||
console.print(f"[green]✓[/green] Pushed tag {tag_name}")
|
||||
|
||||
gh_cmd = [
|
||||
"gh",
|
||||
"release",
|
||||
"create",
|
||||
tag_name,
|
||||
"--repo",
|
||||
enterprise_repo,
|
||||
"--title",
|
||||
tag_name,
|
||||
"--notes",
|
||||
f"Release {version}",
|
||||
]
|
||||
if is_prerelease:
|
||||
gh_cmd.append("--prerelease")
|
||||
|
||||
run_command(gh_cmd)
|
||||
release_type = "prerelease" if is_prerelease else "release"
|
||||
console.print(
|
||||
f"[green]✓[/green] Created GitHub {release_type} for "
|
||||
f"{enterprise_repo} {tag_name}"
|
||||
)
|
||||
|
||||
|
||||
def _trigger_pypi_publish(tag_name: str, wait: bool = False) -> None:
|
||||
"""Trigger the PyPI publish GitHub Actions workflow.
|
||||
|
||||
Args:
|
||||
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:
|
||||
prev_run_id = run_command(
|
||||
[
|
||||
"gh",
|
||||
"run",
|
||||
"list",
|
||||
"--workflow=publish.yml",
|
||||
"--limit=1",
|
||||
"--json=databaseId",
|
||||
"--jq=.[0].databaseId",
|
||||
]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
console.print(
|
||||
"[yellow]Note:[/yellow] Could not determine previous workflow run; "
|
||||
"continuing without previous run ID"
|
||||
)
|
||||
|
||||
with console.status("[cyan]Triggering PyPI publish workflow..."):
|
||||
try:
|
||||
run_command(
|
||||
@@ -1003,6 +1275,42 @@ def _trigger_pypi_publish(tag_name: str) -> None:
|
||||
sys.exit(1)
|
||||
console.print("[green]✓[/green] Triggered PyPI publish workflow")
|
||||
|
||||
if wait:
|
||||
console.print("[cyan]Waiting for PyPI publish workflow to complete...[/cyan]")
|
||||
run_id = ""
|
||||
deadline = time.monotonic() + 120
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(5)
|
||||
try:
|
||||
run_id = run_command(
|
||||
[
|
||||
"gh",
|
||||
"run",
|
||||
"list",
|
||||
"--workflow=publish.yml",
|
||||
"--limit=1",
|
||||
"--json=databaseId",
|
||||
"--jq=.[0].databaseId",
|
||||
]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
if run_id and run_id != prev_run_id:
|
||||
break
|
||||
|
||||
if not run_id or run_id == prev_run_id:
|
||||
console.print(
|
||||
"[red]Error:[/red] Could not find the PyPI publish workflow run"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
run_command(["gh", "run", "watch", run_id, "--exit-status"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
console.print(f"[red]✗[/red] PyPI publish workflow failed: {e}")
|
||||
sys.exit(1)
|
||||
console.print("[green]✓[/green] PyPI publish workflow completed")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI commands
|
||||
@@ -1032,6 +1340,15 @@ def bump(version: str, dry_run: bool, no_push: bool, no_commit: bool) -> None:
|
||||
no_push: Don't push changes to remote.
|
||||
no_commit: Don't commit changes (just update files).
|
||||
"""
|
||||
console.print(
|
||||
f"\n[yellow]Note:[/yellow] [bold]devtools bump[/bold] only bumps versions "
|
||||
f"in this repo. It will not tag, publish to PyPI, or release enterprise.\n"
|
||||
f"If you want a full end-to-end release, run "
|
||||
f"[bold]devtools release {version}[/bold] instead."
|
||||
)
|
||||
if not Confirm.ask("Continue with bump only?", default=True):
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
check_gh_installed()
|
||||
|
||||
@@ -1136,6 +1453,16 @@ def tag(dry_run: bool, no_edit: bool) -> None:
|
||||
dry_run: Show what would be done without making changes.
|
||||
no_edit: Skip editing release notes.
|
||||
"""
|
||||
console.print(
|
||||
"\n[yellow]Note:[/yellow] [bold]devtools tag[/bold] only tags and creates "
|
||||
"a GitHub release for this repo. It will not bump versions, publish to "
|
||||
"PyPI, or release enterprise.\n"
|
||||
"If you want a full end-to-end release, run "
|
||||
"[bold]devtools release <version>[/bold] instead."
|
||||
)
|
||||
if not Confirm.ask("Continue with tag only?", default=True):
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
cwd = Path.cwd()
|
||||
lib_dir = cwd / "lib"
|
||||
@@ -1226,21 +1553,44 @@ def tag(dry_run: bool, no_edit: bool) -> None:
|
||||
"--dry-run", is_flag=True, help="Show what would be done without making changes"
|
||||
)
|
||||
@click.option("--no-edit", is_flag=True, help="Skip editing release notes")
|
||||
def release(version: str, dry_run: bool, no_edit: bool) -> None:
|
||||
@click.option(
|
||||
"--skip-enterprise",
|
||||
is_flag=True,
|
||||
help="Skip the enterprise release phase",
|
||||
)
|
||||
def release(version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool) -> None:
|
||||
"""Full release: bump versions, tag, and publish a GitHub release.
|
||||
|
||||
Combines bump and tag into a single workflow. Creates a version bump PR,
|
||||
waits for it to be merged, then generates release notes, updates docs,
|
||||
creates the tag, and publishes a GitHub release.
|
||||
creates the tag, and publishes a GitHub release. Then bumps versions and
|
||||
releases the enterprise repo.
|
||||
|
||||
Args:
|
||||
version: New version to set (e.g., 1.0.0, 1.0.0a1).
|
||||
dry_run: Show what would be done without making changes.
|
||||
no_edit: Skip editing release notes.
|
||||
skip_enterprise: Skip the enterprise release phase.
|
||||
"""
|
||||
try:
|
||||
check_gh_installed()
|
||||
|
||||
if not skip_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"
|
||||
|
||||
@@ -1337,7 +1687,10 @@ def release(version: str, dry_run: bool, no_edit: bool) -> None:
|
||||
|
||||
if not dry_run:
|
||||
_create_tag_and_release(tag_name, release_notes, is_prerelease)
|
||||
_trigger_pypi_publish(tag_name)
|
||||
_trigger_pypi_publish(tag_name, wait=not skip_enterprise)
|
||||
|
||||
if not skip_enterprise:
|
||||
_release_enterprise(version, is_prerelease, dry_run)
|
||||
|
||||
console.print(f"\n[green]✓[/green] Release [bold]{version}[/bold] complete!")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "crewai-workspace"
|
||||
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
requires-python = ">=3.10,<3.15"
|
||||
authors = [
|
||||
{ name = "Joao Moura", email = "joao@crewai.com" }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user