mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-28 12:28:10 +00:00
Compare commits
15 Commits
1.14.0
...
docs/organ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf49dd4972 | ||
|
|
fe028ef400 | ||
|
|
52c227ab17 | ||
|
|
8bae740899 | ||
|
|
1c784695c1 | ||
|
|
1ae237a287 | ||
|
|
0e8ed75947 | ||
|
|
98e0d1054f | ||
|
|
fc9280ccf6 | ||
|
|
f4c0667d34 | ||
|
|
0450d06a65 | ||
|
|
b23b2696fe | ||
|
|
8700e3db33 | ||
|
|
75f162fd3c | ||
|
|
c0f3151e13 |
@@ -4,6 +4,40 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="9 أبريل 2026">
|
||||
## 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
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="7 أبريل 2026">
|
||||
## v1.14.0
|
||||
|
||||
|
||||
@@ -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 لجميع الوكلاء في الطاقم
|
||||
|
||||
@@ -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 لأي أطقم مستخدمة داخل التدفق
|
||||
|
||||
@@ -472,6 +472,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -944,6 +945,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -1416,6 +1418,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -1886,6 +1889,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -2356,6 +2360,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -2827,6 +2832,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -3299,6 +3305,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -3769,6 +3776,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
@@ -4242,6 +4250,7 @@
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/custom-mcp-server",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
"en/enterprise/guides/organization-management",
|
||||
"en/enterprise/guides/team-management",
|
||||
"en/enterprise/guides/human-in-the-loop",
|
||||
"en/enterprise/guides/webhook-automation"
|
||||
|
||||
@@ -4,6 +4,40 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Apr 09, 2026">
|
||||
## 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
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Apr 07, 2026">
|
||||
## v1.14.0
|
||||
|
||||
|
||||
163
docs/en/enterprise/guides/organization-management.mdx
Normal file
163
docs/en/enterprise/guides/organization-management.mdx
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
title: "Organization Management"
|
||||
description: "Learn how to manage organizations via the CrewAI CLI — list, switch, and verify your active org before publishing or deploying."
|
||||
icon: "building"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
When you sign up for [CrewAI AMP](https://app.crewai.com), you may belong to multiple organizations — for example, a personal org and one or more team/company orgs. The **active organization** determines where your tools are published, where deployments are created, and which resources you can access.
|
||||
|
||||
Managing your active organization is important because actions like `crewai tool publish` and `crewai deploy create` target whatever org is currently active in your CLI session.
|
||||
|
||||
## Authentication and Organizations
|
||||
|
||||
When you run `crewai login`, the CLI:
|
||||
|
||||
1. Authenticates you via a secure device code flow (OAuth2)
|
||||
2. Logs you in to the Tool Repository
|
||||
3. Sets your **active organization** to the default organization returned by the server
|
||||
|
||||
<Warning>
|
||||
If you belong to multiple organizations, the default org set after login may not be the one you intend to use. Always verify your active organization before publishing tools or creating deployments.
|
||||
</Warning>
|
||||
|
||||
### Logging In
|
||||
|
||||
```shell Terminal
|
||||
crewai login
|
||||
```
|
||||
|
||||
After running this command:
|
||||
- A verification URL and code are displayed in your terminal
|
||||
- Your browser opens for you to confirm authentication
|
||||
- On success, your active org is automatically set
|
||||
|
||||
## Checking Your Current Organization
|
||||
|
||||
To see which organization is currently active:
|
||||
|
||||
```shell Terminal
|
||||
crewai org current
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
Currently logged in to organization My Company (a1b2c3d4-e5f6-7890-abcd-ef1234567890)
|
||||
```
|
||||
|
||||
If no organization is set:
|
||||
|
||||
```
|
||||
You're not currently logged in to any organization.
|
||||
Use 'crewai org list' to see available organizations.
|
||||
Use 'crewai org switch <id>' to switch to an organization.
|
||||
```
|
||||
|
||||
## Listing Your Organizations
|
||||
|
||||
To see all organizations you belong to:
|
||||
|
||||
```shell Terminal
|
||||
crewai org list
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Name ┃ ID ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Personal │ 11111111-1111-1111-1111-111111111111 │
|
||||
│ My Company │ 22222222-2222-2222-2222-222222222222 │
|
||||
└────────────────────┴──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Switching Organizations
|
||||
|
||||
To switch your active organization:
|
||||
|
||||
```shell Terminal
|
||||
crewai org switch <organization_id>
|
||||
```
|
||||
|
||||
For example, to switch to "My Company":
|
||||
|
||||
```shell Terminal
|
||||
crewai org switch 22222222-2222-2222-2222-222222222222
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Successfully switched to My Company (22222222-2222-2222-2222-222222222222)
|
||||
```
|
||||
|
||||
The organization ID is the UUID shown in `crewai org list`.
|
||||
|
||||
<Note>
|
||||
You must be authenticated (`crewai login`) before using any `crewai org` commands.
|
||||
</Note>
|
||||
|
||||
## Practical Example: Publishing Tools to the Right Organization
|
||||
|
||||
A common scenario is when you belong to both a personal org and a team org. If you run `crewai login` and immediately publish a tool, the tool may end up in your personal org rather than your team org.
|
||||
|
||||
To avoid this:
|
||||
|
||||
<Steps>
|
||||
<Step title="Log in to CrewAI AMP">
|
||||
```shell Terminal
|
||||
crewai login
|
||||
```
|
||||
</Step>
|
||||
<Step title="Check your current organization">
|
||||
```shell Terminal
|
||||
crewai org current
|
||||
```
|
||||
</Step>
|
||||
<Step title="List available organizations (if needed)">
|
||||
```shell Terminal
|
||||
crewai org list
|
||||
```
|
||||
</Step>
|
||||
<Step title="Switch to the correct organization">
|
||||
```shell Terminal
|
||||
crewai org switch <your-team-org-id>
|
||||
```
|
||||
</Step>
|
||||
<Step title="Publish your tool">
|
||||
```shell Terminal
|
||||
crewai tool publish
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
Make it a habit to run `crewai org current` before publishing tools or creating deployments, especially if you belong to multiple organizations.
|
||||
</Tip>
|
||||
|
||||
## How Settings Are Stored
|
||||
|
||||
Organization settings are stored in `~/.config/crewai/settings.json`. The relevant fields are:
|
||||
|
||||
- `org_name`: Name of the currently active organization
|
||||
- `org_uuid`: UUID of the currently active organization
|
||||
|
||||
These fields are managed automatically by `crewai login` and `crewai org switch` — you should not edit them manually.
|
||||
|
||||
<Note>
|
||||
Running `crewai login` clears your current organization settings before re-authenticating. After login, the active org is set based on the server's default for your account.
|
||||
</Note>
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | Description |
|
||||
| :--- | :--- |
|
||||
| `crewai login` | Authenticate to CrewAI AMP (sets active org to default) |
|
||||
| `crewai org current` | Show the currently active organization |
|
||||
| `crewai org list` | List all organizations you belong to |
|
||||
| `crewai org switch <id>` | Switch to a specific organization by UUID |
|
||||
| `crewai logout` | Log out and clear organization settings |
|
||||
@@ -82,6 +82,10 @@ crewai uv remove requests
|
||||
|
||||
This will add the package to your project and update `pyproject.toml` accordingly.
|
||||
|
||||
<Tip>
|
||||
If you belong to multiple organizations, make sure you are publishing to the correct one. Run `crewai org current` to verify your active organization, and `crewai org switch <org_id>` to change it if needed. See the [Organization Management](/en/enterprise/guides/organization-management) guide for details.
|
||||
</Tip>
|
||||
|
||||
## Creating and Publishing Tools
|
||||
|
||||
To create a new tool project:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,40 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 4월 9일">
|
||||
## 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
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 4월 7일">
|
||||
## v1.14.0
|
||||
|
||||
|
||||
@@ -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 스트리밍을 활성화합니다
|
||||
|
||||
@@ -4,6 +4,40 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="09 abr 2026">
|
||||
## 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
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="07 abr 2026">
|
||||
## v1.14.0
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.0"
|
||||
__version__ = "1.14.1rc1"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"crewai==1.14.0",
|
||||
"crewai==1.14.1rc1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -305,4 +305,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.14.0"
|
||||
__version__ = "1.14.1rc1"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.0",
|
||||
"crewai-tools==1.14.1rc1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
@@ -68,7 +68,7 @@ openpyxl = [
|
||||
]
|
||||
mem0 = ["mem0ai~=0.1.94"]
|
||||
docling = [
|
||||
"docling~=2.75.0",
|
||||
"docling~=2.84.0",
|
||||
]
|
||||
qdrant = [
|
||||
"qdrant-client[fastembed]~=1.14.3",
|
||||
|
||||
@@ -46,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.14.0"
|
||||
__version__ = "1.14.1rc1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from crewai.memory.unified_memory import Memory
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.security.security_config import SecurityConfig
|
||||
from crewai.skills.models import Skill
|
||||
from crewai.state.checkpoint_config import CheckpointConfig
|
||||
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
|
||||
@@ -300,7 +300,10 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default_factory=SecurityConfig,
|
||||
description="Security configuration for the agent, including fingerprinting.",
|
||||
)
|
||||
checkpoint: CheckpointConfig | bool | None = Field(
|
||||
checkpoint: Annotated[
|
||||
CheckpointConfig | bool | None,
|
||||
BeforeValidator(_coerce_checkpoint),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Automatic checkpointing configuration. "
|
||||
"True for defaults, False to opt out, None to inherit.",
|
||||
|
||||
@@ -6,7 +6,6 @@ from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
from crewai.agents.parser import AgentFinish
|
||||
from crewai.memory.utils import sanitize_scope_name
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
@@ -30,7 +29,6 @@ class BaseAgentExecutor(BaseModel):
|
||||
messages: list[LLMMessage] = Field(default_factory=list)
|
||||
_resuming: bool = PrivateAttr(default=False)
|
||||
_i18n: I18N | None = PrivateAttr(default=None)
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
|
||||
def _save_to_memory(self, output: AgentFinish) -> None:
|
||||
"""Save task result to unified memory (memory or crew._memory)."""
|
||||
|
||||
@@ -68,6 +68,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.printer import PRINTER
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
from crewai.utilities.tool_utils import (
|
||||
@@ -212,13 +213,13 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
formatted_answer = self._invoke_loop()
|
||||
except AssertionError:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="Agent failed to reach a final answer. This is likely a bug - please report it.",
|
||||
color="red",
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise
|
||||
|
||||
if self.ask_for_human_input:
|
||||
@@ -326,7 +327,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if has_reached_max_iterations(self.iterations, self.max_iter):
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
i18n=self._i18n,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
@@ -341,7 +342,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
messages=self.messages,
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
response_model=self.response_model,
|
||||
@@ -422,7 +423,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
messages=self.messages,
|
||||
iterations=self.iterations,
|
||||
log_error_after=self.log_error_after,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
|
||||
@@ -433,7 +434,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
callbacks=self.callbacks,
|
||||
@@ -441,7 +442,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
continue
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise e
|
||||
finally:
|
||||
self.iterations += 1
|
||||
@@ -482,7 +483,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if has_reached_max_iterations(self.iterations, self.max_iter):
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
None,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
i18n=self._i18n,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
@@ -502,7 +503,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
messages=self.messages,
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
tools=openai_tools,
|
||||
available_functions=None,
|
||||
from_task=self.task,
|
||||
@@ -570,7 +571,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
callbacks=self.callbacks,
|
||||
@@ -578,7 +579,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
continue
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise e
|
||||
finally:
|
||||
self.iterations += 1
|
||||
@@ -595,7 +596,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
messages=self.messages,
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
response_model=self.response_model,
|
||||
@@ -965,7 +966,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
break
|
||||
except Exception as hook_error:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in before_tool_call hook: {hook_error}",
|
||||
color="red",
|
||||
)
|
||||
@@ -1031,7 +1032,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
after_hook_context.tool_result = result
|
||||
except Exception as hook_error:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in after_tool_call hook: {hook_error}",
|
||||
color="red",
|
||||
)
|
||||
@@ -1078,7 +1079,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
if self.agent and self.agent.verbose:
|
||||
cache_info = " (from cache)" if from_cache else ""
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool {func_name} executed with result{cache_info}: {result[:200]}...",
|
||||
color="green",
|
||||
)
|
||||
@@ -1118,13 +1119,13 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
formatted_answer = await self._ainvoke_loop()
|
||||
except AssertionError:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="Agent failed to reach a final answer. This is likely a bug - please report it.",
|
||||
color="red",
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise
|
||||
|
||||
if self.ask_for_human_input:
|
||||
@@ -1168,7 +1169,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if has_reached_max_iterations(self.iterations, self.max_iter):
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
i18n=self._i18n,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
@@ -1183,7 +1184,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
messages=self.messages,
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
response_model=self.response_model,
|
||||
@@ -1263,7 +1264,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
messages=self.messages,
|
||||
iterations=self.iterations,
|
||||
log_error_after=self.log_error_after,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
|
||||
@@ -1273,7 +1274,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
callbacks=self.callbacks,
|
||||
@@ -1281,7 +1282,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
continue
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise e
|
||||
finally:
|
||||
self.iterations += 1
|
||||
@@ -1316,7 +1317,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if has_reached_max_iterations(self.iterations, self.max_iter):
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
None,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
i18n=self._i18n,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
@@ -1336,7 +1337,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
messages=self.messages,
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
tools=openai_tools,
|
||||
available_functions=None,
|
||||
from_task=self.task,
|
||||
@@ -1403,7 +1404,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
messages=self.messages,
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
callbacks=self.callbacks,
|
||||
@@ -1411,7 +1412,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
continue
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise e
|
||||
finally:
|
||||
self.iterations += 1
|
||||
@@ -1428,7 +1429,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
llm=cast("BaseLLM", self.llm),
|
||||
messages=self.messages,
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
response_model=self.response_model,
|
||||
@@ -1576,7 +1577,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
if train_iteration is None or not isinstance(train_iteration, int):
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="Invalid or missing train iteration. Cannot save training data.",
|
||||
color="red",
|
||||
)
|
||||
@@ -1600,7 +1601,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
agent_training_data[train_iteration]["improved_output"] = result.output
|
||||
else:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"No existing training data for agent {agent_id} and iteration "
|
||||
f"{train_iteration}. Cannot save improved output."
|
||||
|
||||
@@ -40,7 +40,7 @@ from crewai.utilities.agent_utils import (
|
||||
)
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.planning_types import TodoItem
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
@@ -109,7 +109,6 @@ class StepExecutor:
|
||||
self.request_within_rpm_limit = request_within_rpm_limit
|
||||
self.callbacks = callbacks or []
|
||||
self._i18n: I18N = i18n or get_i18n()
|
||||
self._printer: Printer = Printer()
|
||||
|
||||
# Native tool support — set up once
|
||||
self._use_native_tools = check_native_tool_support(
|
||||
@@ -585,7 +584,7 @@ class StepExecutor:
|
||||
task=self.task,
|
||||
crew=self.crew,
|
||||
event_source=self,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
verbose=bool(self.agent and self.agent.verbose),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,17 +3,14 @@ from pathlib import Path
|
||||
import click
|
||||
|
||||
from crewai.cli.utils import copy_template
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
_printer = Printer()
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
|
||||
def add_crew_to_flow(crew_name: str) -> None:
|
||||
"""Add a new crew to the current flow."""
|
||||
# Check if pyproject.toml exists in the current directory
|
||||
if not Path("pyproject.toml").exists():
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
"This command must be run from the root of a flow project.", color="red"
|
||||
)
|
||||
raise click.ClickException(
|
||||
@@ -25,7 +22,7 @@ def add_crew_to_flow(crew_name: str) -> None:
|
||||
crews_folder = flow_folder / "src" / flow_folder.name / "crews"
|
||||
|
||||
if not crews_folder.exists():
|
||||
_printer.print("Crews folder does not exist in the current flow.", color="red")
|
||||
PRINTER.print("Crews folder does not exist in the current flow.", color="red")
|
||||
raise click.ClickException("Crews folder does not exist in the current flow.")
|
||||
|
||||
# Create the crew within the flow's crews directory
|
||||
|
||||
366
lib/crewai/src/crewai/cli/checkpoint_tui.py
Normal file
366
lib/crewai/src/crewai/cli/checkpoint_tui.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""Textual TUI for browsing checkpoint files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Footer, Header, OptionList, Static
|
||||
from textual.widgets.option_list import Option
|
||||
|
||||
from crewai.cli.checkpoint_cli import (
|
||||
_entity_summary,
|
||||
_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 _format_list_label(entry: dict[str, Any]) -> str:
|
||||
"""Format a checkpoint entry for the list panel."""
|
||||
name = entry.get("name", "")
|
||||
ts = entry.get("ts") or ""
|
||||
trigger = entry.get("trigger") or ""
|
||||
summary = _entity_summary(entry.get("entities", []))
|
||||
|
||||
line1 = f"[bold]{name}[/]"
|
||||
parts = []
|
||||
if ts:
|
||||
parts.append(f"[dim]{ts}[/]")
|
||||
if "size" in entry:
|
||||
parts.append(f"[dim]{_format_size(entry['size'])}[/]")
|
||||
if trigger:
|
||||
parts.append(f"[{_PRIMARY}]{trigger}[/]")
|
||||
line2 = " ".join(parts)
|
||||
line3 = f" [{_DIM}]{summary}[/]"
|
||||
|
||||
return f"{line1}\n{line2}\n{line3}"
|
||||
|
||||
|
||||
def _format_detail(entry: dict[str, Any]) -> str:
|
||||
"""Format checkpoint details for the right panel."""
|
||||
lines: list[str] = []
|
||||
|
||||
# Header
|
||||
name = entry.get("name", "")
|
||||
lines.append(f"[bold {_PRIMARY}]{name}[/]")
|
||||
lines.append(f"[{_DIM}]{'─' * 50}[/]")
|
||||
lines.append("")
|
||||
|
||||
# Metadata table
|
||||
ts = entry.get("ts") or "unknown"
|
||||
trigger = entry.get("trigger") or ""
|
||||
lines.append(f" [bold]Time[/] {ts}")
|
||||
if "size" in entry:
|
||||
lines.append(f" [bold]Size[/] {_format_size(entry['size'])}")
|
||||
lines.append(f" [bold]Events[/] {entry.get('event_count', 0)}")
|
||||
if trigger:
|
||||
lines.append(f" [bold]Trigger[/] [{_PRIMARY}]{trigger}[/]")
|
||||
if "path" in entry:
|
||||
lines.append(f" [bold]Path[/] [{_DIM}]{entry['path']}[/]")
|
||||
if "db" in entry:
|
||||
lines.append(f" [bold]Database[/] [{_DIM}]{entry['db']}[/]")
|
||||
|
||||
# Entities
|
||||
for ent in entry.get("entities", []):
|
||||
eid = str(ent.get("id", ""))[:8]
|
||||
etype = ent.get("type", "unknown")
|
||||
ename = ent.get("name", "unnamed")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f" [{_DIM}]{'─' * 50}[/]")
|
||||
lines.append(f" [bold {_SECONDARY}]{etype}[/]: {ename} [{_DIM}]{eid}[/]")
|
||||
|
||||
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}%)")
|
||||
lines.append("")
|
||||
|
||||
for i, task in enumerate(tasks):
|
||||
if task.get("completed"):
|
||||
icon = "[green]✓[/]"
|
||||
else:
|
||||
icon = "[yellow]○[/]"
|
||||
desc = str(task.get("description", ""))
|
||||
if len(desc) > 55:
|
||||
desc = desc[:52] + "..."
|
||||
lines.append(f" {icon} {i + 1}. {desc}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class ConfirmResumeScreen(ModalScreen[bool]):
|
||||
"""Modal confirmation before resuming from a checkpoint."""
|
||||
|
||||
CSS = f"""
|
||||
ConfirmResumeScreen {{
|
||||
align: center middle;
|
||||
}}
|
||||
#confirm-dialog {{
|
||||
width: 60;
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
background: {_BG_PANEL};
|
||||
border: round {_PRIMARY};
|
||||
}}
|
||||
#confirm-label {{
|
||||
width: 100%;
|
||||
content-align: center middle;
|
||||
margin-bottom: 1;
|
||||
}}
|
||||
#confirm-name {{
|
||||
width: 100%;
|
||||
content-align: center middle;
|
||||
color: {_PRIMARY};
|
||||
text-style: bold;
|
||||
margin-bottom: 1;
|
||||
}}
|
||||
#confirm-buttons {{
|
||||
width: 100%;
|
||||
height: 3;
|
||||
layout: horizontal;
|
||||
align: center middle;
|
||||
}}
|
||||
Button {{
|
||||
margin: 0 2;
|
||||
min-width: 12;
|
||||
}}
|
||||
"""
|
||||
|
||||
def __init__(self, checkpoint_name: str) -> None:
|
||||
super().__init__()
|
||||
self._checkpoint_name = checkpoint_name
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="confirm-dialog"):
|
||||
yield Static("Resume from this checkpoint?", id="confirm-label")
|
||||
yield Static(self._checkpoint_name, id="confirm-name")
|
||||
with Horizontal(id="confirm-buttons"):
|
||||
yield Button("Resume", variant="success", id="btn-yes")
|
||||
yield Button("Cancel", variant="default", id="btn-no")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.dismiss(event.button.id == "btn-yes")
|
||||
|
||||
def on_key(self, event: Any) -> None:
|
||||
if event.key == "y":
|
||||
self.dismiss(True)
|
||||
elif event.key in ("n", "escape"):
|
||||
self.dismiss(False)
|
||||
|
||||
|
||||
class CheckpointTUI(App[str | None]):
|
||||
"""TUI to browse and inspect checkpoints.
|
||||
|
||||
Returns the checkpoint location string to resume from, 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};
|
||||
}}
|
||||
Horizontal {{
|
||||
height: 1fr;
|
||||
}}
|
||||
#cp-list {{
|
||||
width: 38%;
|
||||
background: {_BG_PANEL};
|
||||
border: round {_SECONDARY};
|
||||
padding: 0 1;
|
||||
scrollbar-color: {_PRIMARY};
|
||||
}}
|
||||
#cp-list:focus {{
|
||||
border: round {_PRIMARY};
|
||||
}}
|
||||
#cp-list > .option-list--option-highlighted {{
|
||||
background: {_SECONDARY};
|
||||
color: {_TERTIARY};
|
||||
text-style: none;
|
||||
}}
|
||||
#cp-list > .option-list--option-highlighted * {{
|
||||
color: {_TERTIARY};
|
||||
}}
|
||||
#detail-container {{
|
||||
width: 62%;
|
||||
padding: 0 1;
|
||||
}}
|
||||
#detail {{
|
||||
height: 1fr;
|
||||
background: {_BG_PANEL};
|
||||
border: round {_SECONDARY};
|
||||
padding: 1 2;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: {_PRIMARY};
|
||||
}}
|
||||
#detail:focus {{
|
||||
border: round {_PRIMARY};
|
||||
}}
|
||||
#status {{
|
||||
height: 1;
|
||||
padding: 0 2;
|
||||
color: {_DIM};
|
||||
}}
|
||||
"""
|
||||
|
||||
BINDINGS: ClassVar[list[Binding | tuple[str, str] | tuple[str, str, str]]] = [
|
||||
("q", "quit", "Quit"),
|
||||
("r", "refresh", "Refresh"),
|
||||
("j", "cursor_down", "Down"),
|
||||
("k", "cursor_up", "Up"),
|
||||
]
|
||||
|
||||
def __init__(self, location: str = "./.checkpoints") -> None:
|
||||
super().__init__()
|
||||
self._location = location
|
||||
self._entries: list[dict[str, Any]] = []
|
||||
self._selected_idx: int = 0
|
||||
self._pending_location: str = ""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=False)
|
||||
with Horizontal():
|
||||
yield OptionList(id="cp-list")
|
||||
with Vertical(id="detail-container"):
|
||||
yield Static("", id="status")
|
||||
yield Static(
|
||||
f"\n [{_DIM}]Select a checkpoint from the list[/]", # noqa: S608
|
||||
id="detail",
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
self.query_one("#cp-list", OptionList).border_title = "Checkpoints"
|
||||
self.query_one("#detail", Static).border_title = "Detail"
|
||||
self._refresh_list()
|
||||
|
||||
def _refresh_list(self) -> None:
|
||||
self._entries = _load_entries(self._location)
|
||||
option_list = self.query_one("#cp-list", OptionList)
|
||||
option_list.clear_options()
|
||||
|
||||
if not self._entries:
|
||||
self.query_one("#detail", Static).update(
|
||||
f"\n [{_DIM}]No checkpoints in {self._location}[/]"
|
||||
)
|
||||
self.query_one("#status", Static).update("")
|
||||
self.sub_title = self._location
|
||||
return
|
||||
|
||||
for entry in self._entries:
|
||||
option_list.add_option(Option(_format_list_label(entry)))
|
||||
|
||||
count = len(self._entries)
|
||||
storage = "SQLite" if _is_sqlite(self._location) else "JSON"
|
||||
self.sub_title = f"{self._location}"
|
||||
self.query_one("#status", Static).update(f" {count} checkpoint(s) | {storage}")
|
||||
|
||||
async def on_option_list_option_highlighted(
|
||||
self,
|
||||
event: OptionList.OptionHighlighted,
|
||||
) -> None:
|
||||
idx = event.option_index
|
||||
if idx is None:
|
||||
return
|
||||
if idx < len(self._entries):
|
||||
self._selected_idx = idx
|
||||
entry = self._entries[idx]
|
||||
self.query_one("#detail", Static).update(_format_detail(entry))
|
||||
|
||||
def action_cursor_down(self) -> None:
|
||||
self.query_one("#cp-list", OptionList).action_cursor_down()
|
||||
|
||||
def action_cursor_up(self) -> None:
|
||||
self.query_one("#cp-list", OptionList).action_cursor_up()
|
||||
|
||||
async def on_option_list_option_selected(
|
||||
self,
|
||||
event: OptionList.OptionSelected,
|
||||
) -> None:
|
||||
idx = event.option_index
|
||||
if idx is None or idx >= len(self._entries):
|
||||
return
|
||||
entry = self._entries[idx]
|
||||
if "path" in entry:
|
||||
loc = entry["path"]
|
||||
elif _is_sqlite(self._location):
|
||||
loc = f"{self._location}#{entry['name']}"
|
||||
else:
|
||||
loc = entry.get("name", "")
|
||||
self._pending_location = loc
|
||||
name = entry.get("name", loc)
|
||||
self.push_screen(ConfirmResumeScreen(name), self._on_confirm)
|
||||
|
||||
def _on_confirm(self, confirmed: bool | None) -> None:
|
||||
if confirmed:
|
||||
self.exit(self._pending_location)
|
||||
else:
|
||||
self._pending_location = ""
|
||||
|
||||
def action_refresh(self) -> None:
|
||||
self._refresh_list()
|
||||
|
||||
|
||||
async def _run_checkpoint_tui_async(location: str) -> None:
|
||||
"""Async implementation of the checkpoint TUI flow."""
|
||||
import click
|
||||
|
||||
app = CheckpointTUI(location=location)
|
||||
selected = await app.run_async()
|
||||
|
||||
if selected is None:
|
||||
return
|
||||
|
||||
click.echo(f"\nResuming from: {selected}\n")
|
||||
|
||||
from crewai.crew import Crew
|
||||
|
||||
crew = Crew.from_checkpoint(selected)
|
||||
result = await crew.akickoff()
|
||||
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))
|
||||
@@ -786,9 +786,19 @@ 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."""
|
||||
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")
|
||||
|
||||
@@ -19,12 +19,10 @@ from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
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.printer import PRINTER
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
_printer = Printer()
|
||||
|
||||
MIN_REQUIRED_VERSION: Final[Literal["0.98.0"]] = "0.98.0"
|
||||
|
||||
|
||||
@@ -121,9 +119,9 @@ def run_chat() -> None:
|
||||
def show_loading(event: threading.Event) -> None:
|
||||
"""Display animated loading dots while processing."""
|
||||
while not event.is_set():
|
||||
_printer.print(".", end="")
|
||||
PRINTER.print(".", end="")
|
||||
time.sleep(1)
|
||||
_printer.print("")
|
||||
PRINTER.print("")
|
||||
|
||||
|
||||
def initialize_chat_llm(crew: Crew) -> LLM | BaseLLM | None:
|
||||
|
||||
@@ -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.1rc1"
|
||||
]
|
||||
|
||||
[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.14.0"
|
||||
"crewai[tools]==1.14.1rc1"
|
||||
]
|
||||
|
||||
[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.14.0"
|
||||
"crewai[tools]==1.14.1rc1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -104,7 +104,7 @@ 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
|
||||
from crewai.state.checkpoint_config import CheckpointConfig, _coerce_checkpoint
|
||||
from crewai.task import Task
|
||||
from crewai.tasks.conditional_task import ConditionalTask
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
@@ -134,6 +134,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,
|
||||
)
|
||||
@@ -341,7 +342,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default_factory=SecurityConfig,
|
||||
description="Security configuration for the crew, including fingerprinting.",
|
||||
)
|
||||
checkpoint: CheckpointConfig | bool | None = Field(
|
||||
checkpoint: Annotated[
|
||||
CheckpointConfig | bool | None,
|
||||
BeforeValidator(_coerce_checkpoint),
|
||||
] = Field(
|
||||
default=None,
|
||||
description="Automatic checkpointing configuration. "
|
||||
"True for defaults, False to opt out, None to inherit.",
|
||||
@@ -376,8 +380,12 @@ class Crew(FlowTrackable, BaseModel):
|
||||
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.provider.utils import detect_provider
|
||||
from crewai.state.runtime import RuntimeState
|
||||
|
||||
if provider is None:
|
||||
provider = detect_provider(path)
|
||||
|
||||
state = RuntimeState.from_checkpoint(
|
||||
path,
|
||||
provider=provider or JsonProvider(),
|
||||
@@ -879,6 +887,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
|
||||
|
||||
@@ -1004,6 +1013,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
|
||||
@@ -1075,6 +1085,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -98,7 +98,7 @@ from crewai.utilities.planning_types import (
|
||||
TodoItem,
|
||||
TodoList,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
from crewai.utilities.step_execution_context import StepExecutionContext, StepResult
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
@@ -199,7 +199,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
)
|
||||
|
||||
_i18n: I18N = PrivateAttr(default_factory=get_i18n)
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
_console: Console = PrivateAttr(default_factory=Console)
|
||||
_last_parser_error: OutputParserError | None = PrivateAttr(default=None)
|
||||
_last_context_error: Exception | None = PrivateAttr(default=None)
|
||||
@@ -503,7 +502,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
)
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Observe] Step {current_todo.step_number} "
|
||||
f"(effort={effort}): "
|
||||
@@ -553,7 +552,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
current_todo.step_number, result=current_todo.result
|
||||
)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Low] Step {current_todo.step_number} hard-failed "
|
||||
f"— triggering replan: {observation.replan_reason}"
|
||||
@@ -572,7 +571,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
if self.agent.verbose:
|
||||
completed = self.state.todos.completed_count
|
||||
total = len(self.state.todos.items)
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"[Low] Step {current_todo.step_number} done ({completed}/{total}) — continuing",
|
||||
color="green",
|
||||
)
|
||||
@@ -605,7 +604,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
if self.agent.verbose:
|
||||
completed = self.state.todos.completed_count
|
||||
total = len(self.state.todos.items)
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"[Medium] Step {current_todo.step_number} succeeded ({completed}/{total}) — continuing",
|
||||
color="green",
|
||||
)
|
||||
@@ -618,7 +617,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
current_todo.step_number, result=current_todo.result
|
||||
)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Medium] Step {current_todo.step_number} failed + replan required "
|
||||
f"— triggering replan: {observation.replan_reason}"
|
||||
@@ -638,7 +637,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
if self.agent.verbose:
|
||||
failed = len(self.state.todos.get_failed_todos())
|
||||
total = len(self.state.todos.items)
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Medium] Step {current_todo.step_number} failed but no replan needed "
|
||||
f"({failed} failed/{total} total) — continuing"
|
||||
@@ -680,7 +679,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
current_todo.step_number, result=current_todo.result
|
||||
)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="[Decide] Goal achieved early — finalizing",
|
||||
color="green",
|
||||
)
|
||||
@@ -692,7 +691,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
current_todo.step_number, result=current_todo.result
|
||||
)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"[Decide] Full replan needed: {observation.replan_reason}",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -705,7 +704,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
current_todo.step_number, result=current_todo.result
|
||||
)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="[Decide] Step failed — triggering replan",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -718,7 +717,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
current_todo.step_number, result=current_todo.result
|
||||
)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="[Decide] Plan valid but refining upcoming steps",
|
||||
color="cyan",
|
||||
)
|
||||
@@ -731,7 +730,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
if self.agent.verbose:
|
||||
completed = self.state.todos.completed_count
|
||||
total = len(self.state.todos.items)
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"[Decide] Continue plan ({completed}/{total} done)",
|
||||
color="green",
|
||||
)
|
||||
@@ -776,7 +775,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
)
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"[Refine] Updated {len(remaining)} pending step(s)",
|
||||
color="cyan",
|
||||
)
|
||||
@@ -811,7 +810,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
)
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="Goal achieved early — skipping remaining steps",
|
||||
color="green",
|
||||
)
|
||||
@@ -829,7 +828,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
|
||||
if self.state.replan_count >= max_replans:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Max replans ({max_replans}) reached — finalizing with current results",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -936,7 +935,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
# Plan-and-Execute path: use StepExecutor for isolated execution
|
||||
if getattr(self.agent, "planning_enabled", False):
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Execute] Step {current.step_number}: "
|
||||
f"{current.description[:60]}..."
|
||||
@@ -971,7 +970,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
|
||||
if self.agent.verbose:
|
||||
status = "success" if result.success else "failed"
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Execute] Step {current.step_number} {status} "
|
||||
f"({result.execution_time:.1f}s, "
|
||||
@@ -1080,7 +1079,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
todo.result = error_msg
|
||||
self.state.todos.mark_failed(todo.step_number, result=error_msg)
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Todo {todo.step_number} failed: {error_msg}",
|
||||
color="red",
|
||||
)
|
||||
@@ -1105,7 +1104,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
|
||||
if self.agent.verbose:
|
||||
status = "success" if step_result.success else "failed"
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Execute] Step {todo.step_number} {status} "
|
||||
f"({step_result.execution_time:.1f}s, "
|
||||
@@ -1152,7 +1151,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
self.state.todos.mark_failed(todo.step_number, result=todo.result)
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=(
|
||||
f"[Observe] Step {todo.step_number} "
|
||||
f"(effort={effort}): "
|
||||
@@ -1203,7 +1202,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
"""Force agent to provide final answer when max iterations exceeded."""
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer=None,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
i18n=self._i18n,
|
||||
messages=list(self.state.messages),
|
||||
llm=self.llm,
|
||||
@@ -1232,7 +1231,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
llm=self.llm,
|
||||
messages=list(self.state.messages),
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
from_task=self.task,
|
||||
from_agent=self.agent,
|
||||
response_model=self.response_model,
|
||||
@@ -1282,7 +1281,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
return "context_error"
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
raise e
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise
|
||||
|
||||
@router("continue_reasoning_native")
|
||||
@@ -1318,7 +1317,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
llm=self.llm,
|
||||
messages=list(self.state.messages),
|
||||
callbacks=self.callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
tools=self._openai_tools,
|
||||
available_functions=None,
|
||||
from_task=self.task,
|
||||
@@ -1373,7 +1372,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
return "context_error"
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
raise e
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise
|
||||
|
||||
def _route_finish_with_todos(
|
||||
@@ -1442,9 +1441,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
)
|
||||
except Exception as e:
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(
|
||||
content=f"Error in tool execution: {e}", color="red"
|
||||
)
|
||||
PRINTER.print(content=f"Error in tool execution: {e}", color="red")
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
|
||||
@@ -1598,7 +1595,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
# Log the tool execution
|
||||
if self.agent and self.agent.verbose:
|
||||
cache_info = " (from cache)" if from_cache else ""
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool {func_name} executed with result{cache_info}: {result[:200]}...",
|
||||
color="green",
|
||||
)
|
||||
@@ -1636,7 +1633,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
# Log the tool execution
|
||||
if self.agent and self.agent.verbose:
|
||||
cache_info = " (from cache)" if from_cache else ""
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool {func_name} executed with result{cache_info}: {result[:200]}...",
|
||||
color="green",
|
||||
)
|
||||
@@ -1800,7 +1797,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
break
|
||||
except Exception as hook_error:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in before_tool_call hook: {hook_error}",
|
||||
color="red",
|
||||
)
|
||||
@@ -1875,7 +1872,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
after_hook_context.tool_result = result
|
||||
except Exception as hook_error:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in after_tool_call hook: {hook_error}",
|
||||
color="red",
|
||||
)
|
||||
@@ -2033,7 +2030,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
if self.agent.verbose:
|
||||
completed = self.state.todos.completed_count
|
||||
total = len(self.state.todos.items)
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"✓ Todo {step_number} completed ({completed}/{total})",
|
||||
color="green",
|
||||
)
|
||||
@@ -2100,7 +2097,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
self._finalize_called = True
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"[Finalize] todos_count={len(self.state.todos.items)}, todos_with_results={sum(1 for t in self.state.todos.items if t.result)}",
|
||||
color="magenta",
|
||||
)
|
||||
@@ -2263,7 +2260,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
|
||||
except Exception as e:
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Synthesis LLM call failed ({e}), falling back to concatenation",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -2348,7 +2345,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
self.state.last_replan_reason = reason
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Triggering replan (attempt {self.state.replan_count}): {reason}",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -2408,7 +2405,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
self.state.todos.replace_pending_todos(new_todos)
|
||||
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Replan: {len(new_todos)} new steps (completed history preserved)",
|
||||
color="green",
|
||||
)
|
||||
@@ -2492,7 +2489,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
|
||||
if self.state.replan_count >= max_replans:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Max replans ({max_replans}) reached — finalizing with current results",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -2518,7 +2515,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
messages=list(self.state.messages),
|
||||
iterations=self.state.iterations,
|
||||
log_error_after=self.log_error_after,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
verbose=self.agent.verbose,
|
||||
)
|
||||
|
||||
@@ -2534,7 +2531,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
"""Recover from context length errors and retry."""
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
messages=self.state.messages,
|
||||
llm=self.llm,
|
||||
callbacks=self.callbacks,
|
||||
@@ -2637,7 +2634,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
self._console.print(fail_text)
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise
|
||||
finally:
|
||||
self._is_executing = False
|
||||
@@ -2728,7 +2725,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
self._console.print(fail_text)
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_unknown_error(self._printer, e, verbose=self.agent.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.agent.verbose)
|
||||
raise
|
||||
finally:
|
||||
self._is_executing = False
|
||||
@@ -2793,7 +2790,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): # type: ignor
|
||||
task.result()
|
||||
except Exception as e:
|
||||
if self.agent.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in async step_callback task: {e!s}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
DarkGray = Literal["#333333"]
|
||||
CrewAIOrange = Literal["#FF5A50"]
|
||||
Gray = Literal["#666666"]
|
||||
White = Literal["#FFFFFF"]
|
||||
Black = Literal["#000000"]
|
||||
|
||||
|
||||
DARK_GRAY: Literal["#333333"] = "#333333"
|
||||
CREWAI_ORANGE: Literal["#FF5A50"] = "#FF5A50"
|
||||
GRAY: Literal["#666666"] = "#666666"
|
||||
WHITE: Literal["#FFFFFF"] = "#FFFFFF"
|
||||
BLACK: Literal["#000000"] = "#000000"
|
||||
|
||||
|
||||
class FlowColors(TypedDict):
|
||||
bg: White
|
||||
start: CrewAIOrange
|
||||
method: DarkGray
|
||||
router: DarkGray
|
||||
router_border: CrewAIOrange
|
||||
edge: Gray
|
||||
router_edge: CrewAIOrange
|
||||
text: White
|
||||
|
||||
|
||||
class FontStyles(TypedDict, total=False):
|
||||
color: DarkGray | CrewAIOrange | Gray | White | Black
|
||||
multi: Literal["html"]
|
||||
|
||||
|
||||
class StartNodeStyle(TypedDict):
|
||||
color: CrewAIOrange
|
||||
shape: Literal["box"]
|
||||
font: FontStyles
|
||||
label: NotRequired[str]
|
||||
margin: dict[str, int]
|
||||
|
||||
|
||||
class MethodNodeStyle(TypedDict):
|
||||
color: DarkGray
|
||||
shape: Literal["box"]
|
||||
font: FontStyles
|
||||
label: NotRequired[str]
|
||||
margin: dict[str, int]
|
||||
|
||||
|
||||
class RouterNodeStyle(TypedDict):
|
||||
color: dict[str, Any]
|
||||
shape: Literal["box"]
|
||||
font: FontStyles
|
||||
label: NotRequired[str]
|
||||
borderWidth: int
|
||||
borderWidthSelected: int
|
||||
shapeProperties: dict[str, list[int] | bool]
|
||||
margin: dict[str, int]
|
||||
|
||||
|
||||
class CrewNodeStyle(TypedDict):
|
||||
color: dict[str, CrewAIOrange | White]
|
||||
shape: Literal["box"]
|
||||
font: FontStyles
|
||||
label: NotRequired[str]
|
||||
borderWidth: int
|
||||
borderWidthSelected: int
|
||||
shapeProperties: dict[str, bool]
|
||||
margin: dict[str, int]
|
||||
|
||||
|
||||
class NodeStyles(TypedDict):
|
||||
start: StartNodeStyle
|
||||
method: MethodNodeStyle
|
||||
router: RouterNodeStyle
|
||||
crew: CrewNodeStyle
|
||||
|
||||
|
||||
COLORS: FlowColors = {
|
||||
"bg": WHITE,
|
||||
"start": CREWAI_ORANGE,
|
||||
"method": DARK_GRAY,
|
||||
"router": DARK_GRAY,
|
||||
"router_border": CREWAI_ORANGE,
|
||||
"edge": GRAY,
|
||||
"router_edge": CREWAI_ORANGE,
|
||||
"text": WHITE,
|
||||
}
|
||||
|
||||
NODE_STYLES: NodeStyles = {
|
||||
"start": {
|
||||
"color": CREWAI_ORANGE,
|
||||
"shape": "box",
|
||||
"font": {"color": WHITE},
|
||||
"margin": {"top": 10, "bottom": 8, "left": 10, "right": 10},
|
||||
},
|
||||
"method": {
|
||||
"color": DARK_GRAY,
|
||||
"shape": "box",
|
||||
"font": {"color": WHITE},
|
||||
"margin": {"top": 10, "bottom": 8, "left": 10, "right": 10},
|
||||
},
|
||||
"router": {
|
||||
"color": {
|
||||
"background": DARK_GRAY,
|
||||
"border": CREWAI_ORANGE,
|
||||
"highlight": {
|
||||
"border": CREWAI_ORANGE,
|
||||
"background": DARK_GRAY,
|
||||
},
|
||||
},
|
||||
"shape": "box",
|
||||
"font": {"color": WHITE},
|
||||
"borderWidth": 3,
|
||||
"borderWidthSelected": 4,
|
||||
"shapeProperties": {"borderDashes": [5, 5]},
|
||||
"margin": {"top": 10, "bottom": 8, "left": 10, "right": 10},
|
||||
},
|
||||
"crew": {
|
||||
"color": {
|
||||
"background": WHITE,
|
||||
"border": CREWAI_ORANGE,
|
||||
},
|
||||
"shape": "box",
|
||||
"font": {"color": BLACK},
|
||||
"borderWidth": 3,
|
||||
"borderWidthSelected": 4,
|
||||
"shapeProperties": {"borderDashes": False},
|
||||
"margin": {"top": 10, "bottom": 8, "left": 10, "right": 10},
|
||||
},
|
||||
}
|
||||
@@ -113,7 +113,7 @@ 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
|
||||
from crewai.state.checkpoint_config import CheckpointConfig, _coerce_checkpoint
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -132,6 +132,7 @@ from crewai.utilities.streaming import (
|
||||
create_async_chunk_generator,
|
||||
create_chunk_generator,
|
||||
create_streaming_state,
|
||||
register_cleanup,
|
||||
signal_end,
|
||||
signal_error,
|
||||
)
|
||||
@@ -921,7 +922,10 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
max_method_calls: int = Field(default=100)
|
||||
|
||||
execution_context: ExecutionContext | None = Field(default=None)
|
||||
checkpoint: CheckpointConfig | bool | None = Field(default=None)
|
||||
checkpoint: Annotated[
|
||||
CheckpointConfig | bool | None,
|
||||
BeforeValidator(_coerce_checkpoint),
|
||||
] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_checkpoint(
|
||||
@@ -1959,6 +1963,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
|
||||
@@ -2032,6 +2037,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
|
||||
|
||||
@@ -28,13 +28,13 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Final, TypeVar, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.flow.persistence.base import FlowPersistence
|
||||
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -56,8 +56,6 @@ LOG_MESSAGES: Final[dict[str, str]] = {
|
||||
class PersistenceDecorator:
|
||||
"""Class to handle flow state persistence with consistent logging."""
|
||||
|
||||
_printer: ClassVar[Printer] = Printer()
|
||||
|
||||
@classmethod
|
||||
def persist_state(
|
||||
cls,
|
||||
@@ -104,7 +102,7 @@ class PersistenceDecorator:
|
||||
|
||||
# Log state saving only if verbose is True
|
||||
if verbose:
|
||||
cls._printer.print(
|
||||
PRINTER.print(
|
||||
LOG_MESSAGES["save_state"].format(flow_uuid), color="cyan"
|
||||
)
|
||||
logger.info(LOG_MESSAGES["save_state"].format(flow_uuid))
|
||||
@@ -119,19 +117,19 @@ class PersistenceDecorator:
|
||||
except Exception as e:
|
||||
error_msg = LOG_MESSAGES["save_error"].format(method_name, str(e))
|
||||
if verbose:
|
||||
cls._printer.print(error_msg, color="red")
|
||||
PRINTER.print(error_msg, color="red")
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(f"State persistence failed: {e!s}") from e
|
||||
except AttributeError as e:
|
||||
error_msg = LOG_MESSAGES["state_missing"]
|
||||
if verbose:
|
||||
cls._printer.print(error_msg, color="red")
|
||||
PRINTER.print(error_msg, color="red")
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
except (TypeError, ValueError) as e:
|
||||
error_msg = LOG_MESSAGES["id_missing"]
|
||||
if verbose:
|
||||
cls._printer.print(error_msg, color="red")
|
||||
PRINTER.print(error_msg, color="red")
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
|
||||
@@ -32,14 +32,12 @@ from crewai.flow.flow_wrappers import (
|
||||
SimpleFlowCondition,
|
||||
)
|
||||
from crewai.flow.types import FlowMethodCallable, FlowMethodName
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.flow import Flow
|
||||
|
||||
_printer = Printer()
|
||||
|
||||
|
||||
def _extract_string_literals_from_type_annotation(
|
||||
node: ast.expr,
|
||||
@@ -181,7 +179,7 @@ def get_possible_return_constants(
|
||||
return None
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"Error retrieving source code for function {function.__name__}: {e}",
|
||||
color="red",
|
||||
)
|
||||
@@ -194,27 +192,27 @@ def get_possible_return_constants(
|
||||
code_ast = ast.parse(source)
|
||||
except IndentationError as e:
|
||||
if verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"IndentationError while parsing source code of {function.__name__}: {e}",
|
||||
color="red",
|
||||
)
|
||||
_printer.print(f"Source code:\n{source}", color="yellow")
|
||||
PRINTER.print(f"Source code:\n{source}", color="yellow")
|
||||
return None
|
||||
except SyntaxError as e:
|
||||
if verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"SyntaxError while parsing source code of {function.__name__}: {e}",
|
||||
color="red",
|
||||
)
|
||||
_printer.print(f"Source code:\n{source}", color="yellow")
|
||||
PRINTER.print(f"Source code:\n{source}", color="yellow")
|
||||
return None
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"Unexpected error while parsing source code of {function.__name__}: {e}",
|
||||
color="red",
|
||||
)
|
||||
_printer.print(f"Source code:\n{source}", color="yellow")
|
||||
PRINTER.print(f"Source code:\n{source}", color="yellow")
|
||||
return None
|
||||
|
||||
return_values: set[str] = set()
|
||||
@@ -395,13 +393,13 @@ def get_possible_return_constants(
|
||||
StateAttributeVisitor().visit(class_ast)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"Could not analyze class context for {function.__name__}: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"Could not introspect class for {function.__name__}: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from crewai.hooks.types import (
|
||||
BeforeLLMCallHookCallable,
|
||||
BeforeLLMCallHookType,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -138,16 +138,15 @@ class LLMCallHookContext:
|
||||
... print("LLM call skipped by user")
|
||||
"""
|
||||
|
||||
printer = Printer()
|
||||
event_listener.formatter.pause_live_updates()
|
||||
|
||||
try:
|
||||
printer.print(content=f"\n{prompt}", color="bold_yellow")
|
||||
printer.print(content=default_message, color="cyan")
|
||||
PRINTER.print(content=f"\n{prompt}", color="bold_yellow")
|
||||
PRINTER.print(content=default_message, color="cyan")
|
||||
response = input().strip()
|
||||
|
||||
if response:
|
||||
printer.print(content="\nProcessing your input...", color="cyan")
|
||||
PRINTER.print(content="\nProcessing your input...", color="cyan")
|
||||
|
||||
return response
|
||||
finally:
|
||||
|
||||
@@ -9,7 +9,7 @@ from crewai.hooks.types import (
|
||||
BeforeToolCallHookCallable,
|
||||
BeforeToolCallHookType,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -100,16 +100,15 @@ class ToolCallHookContext:
|
||||
... return None # Allow execution
|
||||
"""
|
||||
|
||||
printer = Printer()
|
||||
event_listener.formatter.pause_live_updates()
|
||||
|
||||
try:
|
||||
printer.print(content=f"\n{prompt}", color="bold_yellow")
|
||||
printer.print(content=default_message, color="cyan")
|
||||
PRINTER.print(content=f"\n{prompt}", color="bold_yellow")
|
||||
PRINTER.print(content=default_message, color="cyan")
|
||||
response = input().strip()
|
||||
|
||||
if response:
|
||||
printer.print(content="\nProcessing your input...", color="cyan")
|
||||
PRINTER.print(content="\nProcessing your input...", color="cyan")
|
||||
|
||||
return response
|
||||
finally:
|
||||
|
||||
@@ -91,7 +91,7 @@ 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.llm_utils import create_llm
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
from crewai.utilities.pydantic_schema_utils import generate_model_description
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
@@ -270,7 +270,6 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
_key: str = PrivateAttr(default_factory=lambda: str(uuid.uuid4()))
|
||||
_messages: list[LLMMessage] = PrivateAttr(default_factory=list)
|
||||
_iterations: int = PrivateAttr(default=0)
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
_guardrail: GuardrailCallable | None = PrivateAttr(default=None)
|
||||
_guardrail_retry_count: int = PrivateAttr(default=0)
|
||||
_callbacks: list[TokenCalcHandler] = PrivateAttr(default_factory=list)
|
||||
@@ -528,11 +527,11 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="Agent failed to reach a final answer. This is likely a bug - please report it.",
|
||||
color="red",
|
||||
)
|
||||
handle_unknown_error(self._printer, e, verbose=self.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.verbose)
|
||||
# Emit error event
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
@@ -609,7 +608,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
self._memory.remember_many(extracted, agent_role=self.role)
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Failed to save to memory: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -661,7 +660,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
formatted_result = result
|
||||
except ConverterError as e:
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Failed to parse output into response format after retries: {e.message}",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -704,7 +703,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
)
|
||||
self._guardrail_retry_count += 1
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
f"Guardrail failed. Retrying ({self._guardrail_retry_count}/{self.guardrail_max_retries})..."
|
||||
f"\n{guardrail_result.error}"
|
||||
)
|
||||
@@ -875,7 +874,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
if has_reached_max_iterations(self._iterations, self.max_iterations):
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
i18n=self.i18n,
|
||||
messages=self._messages,
|
||||
llm=cast(LLM, self.llm),
|
||||
@@ -890,7 +889,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
llm=cast(LLM, self.llm),
|
||||
messages=self._messages,
|
||||
callbacks=self._callbacks,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
from_agent=self, # type: ignore[arg-type]
|
||||
executor_context=self,
|
||||
response_model=response_model,
|
||||
@@ -933,7 +932,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
self._append_message(formatted_answer.text, role="assistant")
|
||||
except OutputParserError as e:
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content="Failed to parse LLM output. Retrying...",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -942,7 +941,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
messages=self._messages,
|
||||
iterations=self._iterations,
|
||||
log_error_after=3,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
verbose=self.verbose,
|
||||
)
|
||||
|
||||
@@ -953,7 +952,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
printer=PRINTER,
|
||||
messages=self._messages,
|
||||
llm=cast(LLM, self.llm),
|
||||
callbacks=self._callbacks,
|
||||
@@ -961,7 +960,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
verbose=self.verbose,
|
||||
)
|
||||
continue
|
||||
handle_unknown_error(self._printer, e, verbose=self.verbose)
|
||||
handle_unknown_error(PRINTER, e, verbose=self.verbose)
|
||||
raise e
|
||||
|
||||
finally:
|
||||
|
||||
@@ -3,18 +3,14 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TextIO,
|
||||
TypedDict,
|
||||
cast,
|
||||
)
|
||||
@@ -102,72 +98,6 @@ if LITELLM_AVAILABLE:
|
||||
litellm.suppress_debug_info = True
|
||||
|
||||
|
||||
class FilteredStream(io.TextIOBase):
|
||||
_lock = None
|
||||
|
||||
def __init__(self, original_stream: TextIO):
|
||||
self._original_stream = original_stream
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def write(self, s: str) -> int:
|
||||
if not self._lock:
|
||||
self._lock = threading.Lock()
|
||||
|
||||
with self._lock:
|
||||
lower_s = s.lower()
|
||||
|
||||
# Skip common noisy LiteLLM banners and any other lines that contain "litellm"
|
||||
if (
|
||||
"litellm.info:" in lower_s
|
||||
or "Consider using a smaller input or implementing a text splitting strategy"
|
||||
in lower_s
|
||||
):
|
||||
return 0
|
||||
|
||||
return self._original_stream.write(s)
|
||||
|
||||
def flush(self) -> None:
|
||||
if self._lock:
|
||||
with self._lock:
|
||||
return self._original_stream.flush()
|
||||
return None
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Delegate attribute access to the wrapped original stream.
|
||||
|
||||
This ensures compatibility with libraries (e.g., Rich) that rely on
|
||||
attributes such as `encoding`, `isatty`, `buffer`, etc., which may not
|
||||
be explicitly defined on this proxy class.
|
||||
"""
|
||||
return getattr(self._original_stream, name)
|
||||
|
||||
# Delegate common properties/methods explicitly so they aren't shadowed by
|
||||
# the TextIOBase defaults (e.g., .encoding returns None by default, which
|
||||
# confuses Rich). These explicit pass-throughs ensure the wrapped Console
|
||||
# still sees a fully-featured stream.
|
||||
@property
|
||||
def encoding(self) -> str | Any: # type: ignore[override]
|
||||
return getattr(self._original_stream, "encoding", "utf-8")
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return self._original_stream.isatty()
|
||||
|
||||
def fileno(self) -> int:
|
||||
return self._original_stream.fileno()
|
||||
|
||||
def writable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# Apply the filtered stream globally so that any subsequent writes containing the filtered
|
||||
# keywords (e.g., "litellm") are hidden from terminal output. We guard against double
|
||||
# wrapping to ensure idempotency in environments where this module might be reloaded.
|
||||
if not isinstance(sys.stdout, FilteredStream):
|
||||
sys.stdout = FilteredStream(sys.stdout)
|
||||
if not isinstance(sys.stderr, FilteredStream):
|
||||
sys.stderr = FilteredStream(sys.stderr)
|
||||
|
||||
|
||||
MIN_CONTEXT: Final[int] = 1024
|
||||
MAX_CONTEXT: Final[int] = 2097152 # Current max from gemini-1.5-pro
|
||||
ANTHROPIC_PREFIXES: Final[tuple[str, str, str]] = ("anthropic/", "claude-", "claude/")
|
||||
|
||||
@@ -857,7 +857,7 @@ class BaseLLM(BaseModel, ABC):
|
||||
LLMCallHookContext,
|
||||
get_before_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
before_hooks = get_before_llm_call_hooks()
|
||||
if not before_hooks:
|
||||
@@ -872,21 +872,20 @@ class BaseLLM(BaseModel, ABC):
|
||||
crew=None,
|
||||
)
|
||||
verbose = getattr(from_agent, "verbose", True) if from_agent else True
|
||||
printer = Printer()
|
||||
|
||||
try:
|
||||
for hook in before_hooks:
|
||||
result = hook(hook_context)
|
||||
if result is False:
|
||||
if verbose:
|
||||
printer.print(
|
||||
PRINTER.print(
|
||||
content="LLM call blocked by before_llm_call hook",
|
||||
color="yellow",
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in before_llm_call hook: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -927,7 +926,7 @@ class BaseLLM(BaseModel, ABC):
|
||||
LLMCallHookContext,
|
||||
get_after_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
after_hooks = get_after_llm_call_hooks()
|
||||
if not after_hooks:
|
||||
@@ -943,7 +942,6 @@ class BaseLLM(BaseModel, ABC):
|
||||
response=response,
|
||||
)
|
||||
verbose = getattr(from_agent, "verbose", True) if from_agent else True
|
||||
printer = Printer()
|
||||
modified_response = response
|
||||
|
||||
try:
|
||||
@@ -954,7 +952,7 @@ class BaseLLM(BaseModel, ABC):
|
||||
hook_context.response = modified_response
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Error in after_llm_call hook: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import sqlite3
|
||||
from typing import Any
|
||||
|
||||
from crewai.task import Task
|
||||
from crewai.utilities import Printer
|
||||
from crewai.utilities.crew_json_encoder import CrewJSONEncoder
|
||||
from crewai.utilities.errors import DatabaseError, DatabaseOperationError
|
||||
from crewai.utilities.lock_store import lock as store_lock
|
||||
@@ -27,7 +26,6 @@ class KickoffTaskOutputsSQLiteStorage:
|
||||
db_path = str(Path(db_storage_path()) / "latest_kickoff_task_outputs.db")
|
||||
self.db_path = db_path
|
||||
self._lock_name = f"sqlite:{os.path.realpath(self.db_path)}"
|
||||
self._printer: Printer = Printer()
|
||||
self._initialize_db()
|
||||
|
||||
def _initialize_db(self) -> None:
|
||||
|
||||
@@ -6,10 +6,7 @@ from chromadb.api.types import Documents, EmbeddingFunction, Embeddings
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from crewai.rag.embeddings.providers.ibm.types import WatsonXProviderConfig
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
_printer = Printer()
|
||||
from crewai.utilities.printer import PRINTER
|
||||
|
||||
|
||||
class WatsonXEmbeddingFunction(EmbeddingFunction[Documents]):
|
||||
@@ -164,5 +161,5 @@ class WatsonXEmbeddingFunction(EmbeddingFunction[Documents]):
|
||||
return cast(Embeddings, embeddings)
|
||||
except Exception as e:
|
||||
if self._verbose:
|
||||
_printer.print(f"Error during WatsonX embedding: {e}", color="red")
|
||||
PRINTER.print(f"Error during WatsonX embedding: {e}", color="red")
|
||||
raise
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from crewai.state.provider.core import BaseProvider
|
||||
from crewai.state.provider.json_provider import JsonProvider
|
||||
from crewai.state.provider.sqlite_provider import SqliteProvider
|
||||
|
||||
|
||||
CheckpointEventType = Literal[
|
||||
@@ -158,6 +158,20 @@ CheckpointEventType = Literal[
|
||||
]
|
||||
|
||||
|
||||
def _coerce_checkpoint(v: Any) -> Any:
|
||||
"""BeforeValidator for checkpoint fields on Crew/Flow/Agent.
|
||||
|
||||
Converts True to CheckpointConfig and triggers handler registration.
|
||||
"""
|
||||
if v is True:
|
||||
v = CheckpointConfig()
|
||||
if isinstance(v, CheckpointConfig):
|
||||
from crewai.state.checkpoint_listener import _ensure_handlers_registered
|
||||
|
||||
_ensure_handlers_registered()
|
||||
return v
|
||||
|
||||
|
||||
class CheckpointConfig(BaseModel):
|
||||
"""Configuration for automatic checkpointing.
|
||||
|
||||
@@ -175,7 +189,10 @@ class CheckpointConfig(BaseModel):
|
||||
description="Event types that trigger a checkpoint write. "
|
||||
'Use ["*"] to checkpoint on every event.',
|
||||
)
|
||||
provider: BaseProvider = Field(
|
||||
provider: Annotated[
|
||||
JsonProvider | SqliteProvider,
|
||||
Field(discriminator="provider_type"),
|
||||
] = Field(
|
||||
default_factory=JsonProvider,
|
||||
description="Storage backend. Defaults to JsonProvider.",
|
||||
)
|
||||
@@ -185,6 +202,13 @@ class CheckpointConfig(BaseModel):
|
||||
"each write. None means keep all.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _register_handlers(self) -> CheckpointConfig:
|
||||
from crewai.state.checkpoint_listener import _ensure_handlers_registered
|
||||
|
||||
_ensure_handlers_registered()
|
||||
return self
|
||||
|
||||
@property
|
||||
def trigger_all(self) -> bool:
|
||||
return "*" in self.on_events
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
"""Base protocol for state providers."""
|
||||
"""Base class for state providers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class BaseProvider(Protocol):
|
||||
"""Interface for persisting and restoring runtime state checkpoints.
|
||||
class BaseProvider(BaseModel, ABC):
|
||||
"""Base class for persisting and restoring runtime state checkpoints.
|
||||
|
||||
Implementations handle the storage backend — filesystem, cloud, database,
|
||||
etc. — while ``RuntimeState`` handles serialization.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source_type: Any, handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
"""Allow Pydantic to validate any ``BaseProvider`` instance."""
|
||||
|
||||
def _validate(v: Any) -> BaseProvider:
|
||||
if isinstance(v, BaseProvider):
|
||||
return v
|
||||
raise TypeError(f"Expected a BaseProvider instance, got {type(v)}")
|
||||
|
||||
return core_schema.no_info_plain_validator_function(
|
||||
_validate,
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
lambda v: type(v).__name__, info_arg=False
|
||||
),
|
||||
)
|
||||
provider_type: str = "base"
|
||||
|
||||
@abstractmethod
|
||||
def checkpoint(self, data: str, location: str) -> str:
|
||||
"""Persist a snapshot synchronously.
|
||||
|
||||
@@ -46,6 +29,7 @@ class BaseProvider(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def acheckpoint(self, data: str, location: str) -> str:
|
||||
"""Persist a snapshot asynchronously.
|
||||
|
||||
@@ -58,6 +42,7 @@ class BaseProvider(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def prune(self, location: str, max_keep: int) -> None:
|
||||
"""Remove old checkpoints, keeping at most *max_keep*.
|
||||
|
||||
@@ -67,6 +52,7 @@ class BaseProvider(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def from_checkpoint(self, location: str) -> str:
|
||||
"""Read a snapshot synchronously.
|
||||
|
||||
@@ -78,6 +64,7 @@ class BaseProvider(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def afrom_checkpoint(self, location: str) -> str:
|
||||
"""Read a snapshot asynchronously.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import glob
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
@@ -21,6 +22,8 @@ logger = logging.getLogger(__name__)
|
||||
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:
|
||||
"""Write a JSON checkpoint file.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
from typing import Literal
|
||||
import uuid
|
||||
|
||||
import aiosqlite
|
||||
@@ -47,6 +48,8 @@ class SqliteProvider(BaseProvider):
|
||||
used as the database file path.
|
||||
"""
|
||||
|
||||
provider_type: Literal["sqlite"] = "sqlite"
|
||||
|
||||
def checkpoint(self, data: str, location: str) -> str:
|
||||
"""Write a checkpoint to the SQLite database.
|
||||
|
||||
|
||||
34
lib/crewai/src/crewai/state/provider/utils.py
Normal file
34
lib/crewai/src/crewai/state/provider/utils.py
Normal file
@@ -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()
|
||||
@@ -81,13 +81,10 @@ from crewai.utilities.guardrail_types import (
|
||||
GuardrailsType,
|
||||
)
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
from crewai.utilities.string_utils import interpolate_only
|
||||
|
||||
|
||||
_printer = Printer()
|
||||
|
||||
|
||||
class Task(BaseModel):
|
||||
"""Class that represents a task to be executed.
|
||||
|
||||
@@ -981,7 +978,7 @@ Follow these guidelines:
|
||||
crew_chat_messages = json.loads(crew_chat_messages_json)
|
||||
except json.JSONDecodeError as e:
|
||||
if self.agent and self.agent.verbose:
|
||||
_printer.print(
|
||||
PRINTER.print(
|
||||
f"An error occurred while parsing crew chat messages: {e}",
|
||||
color="red",
|
||||
)
|
||||
@@ -1227,8 +1224,7 @@ Follow these guidelines:
|
||||
task_output=task_output.raw,
|
||||
)
|
||||
if agent and agent.verbose:
|
||||
printer = Printer()
|
||||
printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Guardrail {guardrail_index if guardrail_index is not None else ''} blocked (attempt {attempt + 1}/{max_attempts}), retrying due to: {guardrail_result.error}\n",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -1325,8 +1321,7 @@ Follow these guidelines:
|
||||
task_output=task_output.raw,
|
||||
)
|
||||
if agent and agent.verbose:
|
||||
printer = Printer()
|
||||
printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Guardrail {guardrail_index if guardrail_index is not None else ''} blocked (attempt {attempt + 1}/{max_attempts}), retrying due to: {guardrail_result.error}\n",
|
||||
color="yellow",
|
||||
)
|
||||
|
||||
@@ -38,13 +38,10 @@ from crewai.tools.structured_tool import (
|
||||
build_schema_hint,
|
||||
)
|
||||
from crewai.types.callback import SerializableCallable, _resolve_dotted_path
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.pydantic_schema_utils import generate_model_description
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
|
||||
|
||||
_printer = Printer()
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R", covariant=True)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from crewai.utilities.agent_utils import (
|
||||
)
|
||||
from crewai.utilities.converter import Converter
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ class ToolUsage:
|
||||
fingerprint_context: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
self._i18n: I18N = agent.i18n if agent else get_i18n()
|
||||
self._printer: Printer = Printer()
|
||||
self._telemetry: Telemetry = Telemetry()
|
||||
self._run_attempts: int = 1
|
||||
self._max_parsing_attempts: int = 3
|
||||
@@ -129,7 +128,7 @@ class ToolUsage:
|
||||
if isinstance(calling, ToolUsageError):
|
||||
error = calling.message
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
PRINTER.print(content=f"\n\n{error}\n", color="red")
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
return error
|
||||
@@ -141,7 +140,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
PRINTER.print(content=f"\n\n{error}\n", color="red")
|
||||
return error
|
||||
|
||||
if (
|
||||
@@ -157,7 +156,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
PRINTER.print(content=f"\n\n{error}\n", color="red")
|
||||
return error
|
||||
|
||||
return f"{self._use(tool_string=tool_string, tool=tool, calling=calling)}"
|
||||
@@ -177,7 +176,7 @@ class ToolUsage:
|
||||
if isinstance(calling, ToolUsageError):
|
||||
error = calling.message
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
PRINTER.print(content=f"\n\n{error}\n", color="red")
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
return error
|
||||
@@ -189,7 +188,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
PRINTER.print(content=f"\n\n{error}\n", color="red")
|
||||
return error
|
||||
|
||||
if (
|
||||
@@ -206,7 +205,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{error}\n", color="red")
|
||||
PRINTER.print(content=f"\n\n{error}\n", color="red")
|
||||
return error
|
||||
|
||||
return (
|
||||
@@ -391,7 +390,7 @@ class ToolUsage:
|
||||
and self.agent
|
||||
and self.agent.verbose
|
||||
):
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
|
||||
color="blue",
|
||||
)
|
||||
@@ -405,7 +404,7 @@ class ToolUsage:
|
||||
and self.agent
|
||||
and self.agent.verbose
|
||||
):
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
|
||||
color="blue",
|
||||
)
|
||||
@@ -429,9 +428,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(
|
||||
content=f"\n\n{error_message}\n", color="red"
|
||||
)
|
||||
PRINTER.print(content=f"\n\n{error_message}\n", color="red")
|
||||
else:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
@@ -626,7 +623,7 @@ class ToolUsage:
|
||||
and self.agent
|
||||
and self.agent.verbose
|
||||
):
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
|
||||
color="blue",
|
||||
)
|
||||
@@ -640,7 +637,7 @@ class ToolUsage:
|
||||
and self.agent
|
||||
and self.agent.verbose
|
||||
):
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
content=f"Tool '{sanitize_tool_name(available_tool.name)}' usage: {available_tool.current_usage_count}/{available_tool.max_usage_count}",
|
||||
color="blue",
|
||||
)
|
||||
@@ -664,9 +661,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(
|
||||
content=f"\n\n{error_message}\n", color="red"
|
||||
)
|
||||
PRINTER.print(content=f"\n\n{error_message}\n", color="red")
|
||||
else:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
@@ -859,7 +854,7 @@ class ToolUsage:
|
||||
if self.task:
|
||||
self.task.increment_tools_errors()
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=f"\n\n{e}\n", color="red")
|
||||
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)}"
|
||||
)
|
||||
@@ -903,16 +898,14 @@ class ToolUsage:
|
||||
try:
|
||||
repaired_input = str(repair_json(tool_input, skip_json_loads=True))
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(
|
||||
content=f"Repaired JSON: {repaired_input}", color="blue"
|
||||
)
|
||||
PRINTER.print(content=f"Repaired JSON: {repaired_input}", color="blue")
|
||||
arguments = json.loads(repaired_input)
|
||||
if isinstance(arguments, dict):
|
||||
return arguments
|
||||
except Exception as e:
|
||||
error = f"Failed to repair JSON: {e}"
|
||||
if self.agent and self.agent.verbose:
|
||||
self._printer.print(content=error, color="red")
|
||||
PRINTER.print(content=error, color="red")
|
||||
|
||||
error_message = (
|
||||
"Tool input must be a valid dictionary in JSON or Python literal format"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededError,
|
||||
)
|
||||
from crewai.utilities.i18n import I18N
|
||||
from crewai.utilities.printer import ColoredText, Printer
|
||||
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
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
@@ -946,7 +946,7 @@ def summarize_messages(
|
||||
summarized_contents: list[SummaryContent] = []
|
||||
for idx, chunk in enumerate(chunks, 1):
|
||||
if verbose:
|
||||
Printer().print(
|
||||
PRINTER.print(
|
||||
content=f"Summarizing {idx}/{total_chunks}...",
|
||||
color="yellow",
|
||||
)
|
||||
@@ -967,7 +967,7 @@ def summarize_messages(
|
||||
else:
|
||||
# Multiple chunks — summarize in parallel via asyncio
|
||||
if verbose:
|
||||
Printer().print(
|
||||
PRINTER.print(
|
||||
content=f"Summarizing {total_chunks} chunks in parallel...",
|
||||
color="yellow",
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ 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.internal_instructor import InternalInstructor
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.printer import PRINTER
|
||||
from crewai.utilities.pydantic_schema_utils import generate_model_description
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ def convert_to_model(
|
||||
|
||||
except Exception as e:
|
||||
if agent and getattr(agent, "verbose", True):
|
||||
Printer().print(
|
||||
PRINTER.print(
|
||||
content=f"Unexpected error during model conversion: {type(e).__name__}: {e}. Returning original result.",
|
||||
color="red",
|
||||
)
|
||||
@@ -267,7 +267,7 @@ def handle_partial_json(
|
||||
raise
|
||||
except Exception as e:
|
||||
if agent and getattr(agent, "verbose", True):
|
||||
Printer().print(
|
||||
PRINTER.print(
|
||||
content=f"Unexpected error during partial JSON handling: {type(e).__name__}: {e}. Attempting alternative conversion method.",
|
||||
color="red",
|
||||
)
|
||||
@@ -329,7 +329,7 @@ def convert_with_instructions(
|
||||
|
||||
if isinstance(exported_result, ConverterError):
|
||||
if agent and getattr(agent, "verbose", True):
|
||||
Printer().print(
|
||||
PRINTER.print(
|
||||
content=f"Failed to convert result to model: {exported_result}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.utilities.printer import ColoredText, Printer, PrinterColor
|
||||
from crewai.utilities.printer import PRINTER, ColoredText, PrinterColor
|
||||
|
||||
|
||||
class Logger(BaseModel):
|
||||
@@ -14,7 +14,6 @@ class Logger(BaseModel):
|
||||
default="bold_yellow",
|
||||
description="Default color for log messages",
|
||||
)
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
|
||||
def log(self, level: str, message: str, color: PrinterColor | None = None) -> None:
|
||||
"""Log a message with timestamp if verbose mode is enabled.
|
||||
@@ -26,7 +25,7 @@ class Logger(BaseModel):
|
||||
"""
|
||||
if self.verbose:
|
||||
timestamp: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self._printer.print(
|
||||
PRINTER.print(
|
||||
[
|
||||
ColoredText(f"\n[{timestamp}]", "cyan"),
|
||||
ColoredText(f"[{level.upper()}]: ", "yellow"),
|
||||
|
||||
@@ -93,3 +93,6 @@ class Printer:
|
||||
file=file,
|
||||
flush=flush,
|
||||
)
|
||||
|
||||
|
||||
PRINTER: Printer = Printer()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Iterator
|
||||
import contextvars
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
from typing import Any, NamedTuple
|
||||
@@ -22,6 +23,9 @@ from crewai.types.streaming import (
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskInfo(TypedDict):
|
||||
"""Task context information for streaming."""
|
||||
|
||||
@@ -159,10 +163,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],
|
||||
@@ -294,7 +311,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:
|
||||
|
||||
@@ -96,7 +96,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 +120,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,
|
||||
@@ -216,7 +216,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 +240,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,
|
||||
|
||||
@@ -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.printer import Printer
|
||||
executor._printer = Printer()
|
||||
from crewai.utilities.i18n import get_i18n
|
||||
executor._i18n = kwargs.get("i18n") or get_i18n()
|
||||
return executor
|
||||
@@ -1491,7 +1489,6 @@ class TestReasoningEffort:
|
||||
executor.handle_step_observed_medium = (
|
||||
AgentExecutor.handle_step_observed_medium.__get__(executor)
|
||||
)
|
||||
executor._printer = Mock()
|
||||
|
||||
# --- Case 1: step succeeded → should return "continue_plan" ---
|
||||
success_todo = TodoItem(
|
||||
@@ -1562,7 +1559,6 @@ class TestReasoningEffort:
|
||||
executor.handle_step_observed_low = (
|
||||
AgentExecutor.handle_step_observed_low.__get__(executor)
|
||||
)
|
||||
executor._printer = Mock()
|
||||
|
||||
todo = TodoItem(
|
||||
step_number=1,
|
||||
|
||||
@@ -1060,27 +1060,13 @@ def test_lite_agent_verbose_false_suppresses_printer_output():
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
result = agent.kickoff("Say hello")
|
||||
mock_printer = Mock()
|
||||
with patch("crewai.lite_agent.PRINTER", mock_printer):
|
||||
result = agent.kickoff("Say hello")
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, LiteAgentOutput)
|
||||
# Verify the printer was never called
|
||||
agent._printer.print = Mock()
|
||||
# For a clean verification, patch printer before execution
|
||||
with pytest.warns(DeprecationWarning):
|
||||
agent2 = LiteAgent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm=mock_llm,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_printer = Mock()
|
||||
agent2._printer = mock_printer
|
||||
|
||||
agent2.kickoff("Say hello")
|
||||
|
||||
# Verify the printer was never called when verbose=False
|
||||
mock_printer.print.assert_not_called()
|
||||
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -529,9 +529,6 @@ def test_tool_validate_input_error_event():
|
||||
mock_task = MagicMock()
|
||||
mock_tools_handler = MagicMock()
|
||||
|
||||
# Mock printer
|
||||
mock_printer = MagicMock()
|
||||
|
||||
# Create test tool
|
||||
class TestTool(BaseTool):
|
||||
name: str = "Test Tool"
|
||||
@@ -551,8 +548,6 @@ def test_tool_validate_input_error_event():
|
||||
agent=mock_agent,
|
||||
action=MagicMock(tool="test_tool"),
|
||||
)
|
||||
tool_usage._printer = mock_printer
|
||||
|
||||
# Mock all parsing attempts to fail
|
||||
with (
|
||||
patch("json.loads", side_effect=json.JSONDecodeError("Test Error", "", 0)),
|
||||
|
||||
@@ -207,10 +207,10 @@ def test_convert_with_instructions_failure(
|
||||
mock_create_converter.return_value = mock_converter
|
||||
|
||||
result = "Some text to convert"
|
||||
with patch("crewai.utilities.converter.Printer") as mock_printer:
|
||||
with patch("crewai.utilities.converter.PRINTER") as mock_printer:
|
||||
output = convert_with_instructions(result, SimpleModel, False, mock_agent)
|
||||
assert output == result
|
||||
mock_printer.return_value.print.assert_called_once()
|
||||
mock_printer.print.assert_called_once()
|
||||
|
||||
|
||||
# Tests for get_conversion_instructions
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.14.0"
|
||||
__version__ = "1.14.1rc1"
|
||||
|
||||
@@ -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
|
||||
@@ -169,18 +170,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]] = [
|
||||
@@ -473,6 +473,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:] # after "crewai"
|
||||
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 +494,30 @@ 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
|
||||
# optional-dependencies is a table of lists; dependencies is a list
|
||||
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 +525,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)
|
||||
@@ -1049,6 +1074,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 +1102,86 @@ 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)
|
||||
# after looks like: a2a]==1.14.0" ...
|
||||
bracket_end = after.index("]")
|
||||
extras = after[:bracket_end]
|
||||
rest = after[bracket_end + 1 :]
|
||||
if rest.startswith("=="):
|
||||
# Find end of version — next quote or whitespace
|
||||
ver_start = 2 # len("==")
|
||||
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 +1209,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"
|
||||
@@ -1262,6 +1368,12 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
f"[green]✓[/green] Updated crewai[tools] dep in {enterprise_dep_path}"
|
||||
)
|
||||
|
||||
# --- update crewai pins in CI workflows ---
|
||||
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...")
|
||||
|
||||
0
lib/devtools/tests/__init__.py
Normal file
0
lib/devtools/tests/__init__.py
Normal file
274
lib/devtools/tests/test_toml_updates.py
Normal file
274
lib/devtools/tests/test_toml_updates.py
Normal file
@@ -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()
|
||||
@@ -107,6 +107,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]
|
||||
@@ -166,12 +167,14 @@ exclude-newer = "3 days"
|
||||
# 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).
|
||||
# transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1).
|
||||
override-dependencies = [
|
||||
"rich>=13.7.1",
|
||||
"onnxruntime<1.24; python_version < '3.11'",
|
||||
"pillow>=12.1.1",
|
||||
"langchain-core>=1.2.11,<2",
|
||||
"urllib3>=2.6.3",
|
||||
"transformers>=5.4.0; python_version >= '3.10'",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
|
||||
52
uv.lock
generated
52
uv.lock
generated
@@ -13,7 +13,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-04-04T15:11:41.651093Z"
|
||||
exclude-newer = "2026-04-05T11:09:48.9111Z"
|
||||
exclude-newer-span = "P3D"
|
||||
|
||||
[manifest]
|
||||
@@ -28,6 +28,7 @@ overrides = [
|
||||
{ name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" },
|
||||
{ name = "pillow", specifier = ">=12.1.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" },
|
||||
]
|
||||
|
||||
@@ -1307,7 +1308,7 @@ requires-dist = [
|
||||
{ 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" },
|
||||
@@ -1357,7 +1358,7 @@ dependencies = [
|
||||
{ name = "pygithub" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "rich" },
|
||||
{ name = "toml" },
|
||||
{ name = "tomlkit" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1367,7 +1368,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]]
|
||||
@@ -1820,7 +1821,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "docling"
|
||||
version = "2.75.0"
|
||||
version = "2.84.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "accelerate" },
|
||||
@@ -1851,12 +1852,14 @@ 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]]
|
||||
@@ -2735,21 +2738,22 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.36.2"
|
||||
version = "1.9.0"
|
||||
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/88/bb/62c7aa86f63a05e2f9b96642fdef9b94526a23979820b09f5455deff4983/huggingface_hub-1.9.0.tar.gz", hash = "sha256:0ea5be7a56135c91797cae6ad726e38eaeb6eb4b77cefff5c9d38ba0ecf874f7", size = 750326, upload-time = "2026-04-03T08:35:55.888Z" }
|
||||
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/73/37/0d15d16150e1829f3e90962c99f28257f6de9e526a680b4c6f5acdb54fd2/huggingface_hub-1.9.0-py3-none-any.whl", hash = "sha256:2999328c058d39fd19ab748dd09bd4da2fbaa4f4c1ddea823eab103051e14a1f", size = 637355, upload-time = "2026-04-03T08:35:53.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8033,15 +8037,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.2"
|
||||
@@ -8062,11 +8057,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 +8167,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "transformers"
|
||||
version = "4.57.6"
|
||||
version = "5.5.0"
|
||||
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/ff/9d/fb46e729b461985f41a5740167688b924a4019141e5c164bea77548d3d9e/transformers-5.5.0.tar.gz", hash = "sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd", size = 8237745, upload-time = "2026-04-02T16:13:08.113Z" }
|
||||
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/e7/28/35f7411ff80a3640c1f4fc907dcbb6a65061ebb82f66950e38bfc9f7f740/transformers-5.5.0-py3-none-any.whl", hash = "sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944", size = 10245591, upload-time = "2026-04-02T16:13:03.462Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user